Merge branch 'feat/wiki' into 'main'

Feat/wiki

See merge request jjimenez/safekat!581
This commit is contained in:
Alvaro
2025-03-02 09:25:35 +00:00
54 changed files with 14061 additions and 7 deletions

View File

@ -27,6 +27,7 @@ class Routing extends BaseRouting
*/
public array $routeFiles = [
APPPATH . 'Config/Routes.php',
APPPATH . 'Config/Wiki/WikiRoutes.php',
];
/**

View File

@ -0,0 +1,20 @@
<?php
use CodeIgniter\Router\RouteCollection;
$routes->group('wiki', ['namespace' => 'App\Controllers\Wiki'], function ($routes) {
$routes->get('','WikiController::index',["as" => "wikiIndex"]);
$routes->get('view/(:segment)','WikiController::show_page/$1',["as" => "showWikiPage"]);
$routes->get('section/(:num)','WikiController::get_section/$1',["as" => "getWikiSection"]);
$routes->post('section','WikiController::store_section',["as" => "storeWikiSection"]);
$routes->delete('section/(:num)','WikiController::delete_section/$1',["as" => "deleteWikiSection"]);
$routes->post('update/section','WikiController::update_section',["as" => "updateWikiSection"]);
$routes->post('save/(:num)','WikiController::store_save_page/$1',["as" => "storeWikiSavePage"]);
$routes->post('publish/(:num)','WikiController::store_publish_page/$1',["as" => "storeWikiPublishPage"]);
$routes->post('file/upload/(:num)','WikiController::wiki_file_upload/$1',["as" => "storeWikiFileUpload"]);
$routes->get('file/(:num)','WikiController::get_wiki_file/$1',["as" => "getWikiFile"]);
$routes->post('section/update/order','WikiController::wiki_section_update_order',["as" => "updateWikiSectionOrder"]);
});

View File

@ -0,0 +1,219 @@
<?php
namespace App\Controllers\Wiki;
use App\Controllers\BaseController;
use App\Models\Wiki\WikiContentModel;
use App\Models\Wiki\WikiFileModel;
use App\Models\Wiki\WikiPageModel;
use App\Models\Wiki\WikiSectionModel;
use App\Models\Wiki\WikiSectionRoleModel;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\I18n\Time;
use Psr\Log\LoggerInterface;
use Throwable;
class WikiController extends BaseController
{
protected WikiSectionModel $wikiSectionModel;
protected WikiContentModel $wikiContentModel;
protected WikiPageModel $wikiPageModel;
protected WikiFileModel $wikiFileModel;
protected WikiSectionRoleModel $wikiSectionRoleModel;
protected string $locale;
protected array $viewData;
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
{
$this->wikiSectionModel = model(WikiSectionModel::class);
$this->wikiPageModel = model(WikiPageModel::class);
$this->wikiContentModel = model(WikiContentModel::class);
$this->wikiFileModel = model(WikiFileModel::class);
$this->wikiSectionRoleModel = model(WikiSectionRoleModel::class);
$sections = $this->wikiSectionModel->sections();
$this->locale = session()->get('lang');
$this->viewData['wiki_sections'] = $sections;
parent::initController($request, $response, $logger);
}
public function index()
{
return view('themes/vuexy/wiki/pages/render', $this->viewData);
}
public function show_page(string $slug)
{
$section = $this->wikiSectionModel->where('slug', $slug)->first();
if($section){
if(auth()->user()->inGroup(...$section->roles_array()) || auth()->user()->inGroup('admin')){
$this->viewData['slug'] = $slug;
$this->viewData['section'] = $section->withAll($this->locale);
$this->viewData['breadcrumb'] = [
['title' => lang("Wiki." . $section->slug), 'route' => route_to('showWikiPage', $slug), 'active' => true],
];
return view('themes/vuexy/wiki/pages/render', $this->viewData);
}else{
return $this->response->setStatusCode(403);
}
}else{
return redirect_to('/');
}
}
public function get_section(int $section_id)
{
$section = $this->wikiSectionModel->find($section_id)->withAll($this->locale);
if(auth()->user()->inGroup(...$section->roles_array()) || auth()->user()->inGroup('admin')){
return $this->response->setJSON(["data" => $section, "message" => lang("App.global_alert_fetch_success")]);
}else{
return $this->response->setStatusCode(403);
}
}
public function store_save_page(int $section_id)
{
$bodyData = $this->request->getPost();
$wikiSectionPage = $this->wikiSectionModel->find($section_id)->page();
if ($wikiSectionPage) {
$wikiPageId = $wikiSectionPage->id;
} else {
$wikiPageId = $this->wikiPageModel->insert(['section_id' => $section_id]);
}
$content = $this->wikiContentModel->where('locale', $this->locale)->where('page_id', $wikiPageId)->countAllResults();
if ($content > 0) {
$this->wikiContentModel->where('locale', $this->locale)->where('page_id', $wikiPageId)->delete();
}
$this->wikiContentModel->insert([
"locale" => $this->locale,
"page_id" => $wikiPageId,
"last_edit_by" => auth()->user()->id,
"editor_data" => json_encode($bodyData)
]);
return $this->response->setJSON(["data" => [], "message" => lang("App.global_alert_save_success")]);
}
public function store_publish_page(int $section_id)
{
$bodyData = $this->request->getPost();
$wikiSectionPage = $this->wikiSectionModel->find($section_id)->page();
if ($wikiSectionPage) {
$wikiPageId = $wikiSectionPage->id;
$wikiContentId = $this->wikiContentModel->where("page_id", $wikiPageId)->where('locale', $this->locale)->first()->id;
$this->wikiContentModel->update($wikiContentId, [
"published_by" => auth()->user()->id,
"last_edit_by" => auth()->user()->id,
"editor_data" => json_encode($bodyData),
"published_data" => json_encode($bodyData),
"published_at" => Time::now()->format('Y-m-d H:i:s'),
]);
$response = $this->response->setJSON(["data" => [], "message" => lang("App.global_alert_save_success")]);
} else {
$response = $this->response->setStatusCode(400)->setJSON(["data" => [], "error" => lang('Wiki.errors.publish_before_save')]);
}
return $response;
}
public function wiki_file_upload(int $section_id)
{
try {
$file = $this->request->getFile('image');
$section = $this->wikiSectionModel->find($section_id);
$content = $section->content();
$r = null;
$fullpath = null;
if ($file->isValid() && !$file->hasMoved()) {
$fullpath = $file->store('wiki_images/' . $section->slug);
$r = $this->wikiFileModel->insert(["content_id" => $content->id, "path" => $fullpath]);
return $this->response->setJSON(["success" => 1, "file" => [
"url" => '/wiki/file/' . $r
]]);
} else {
return $this->response->setJSON(["success" => 0, "file" => [
"url" => null
]]);
}
} catch (\Throwable $th) {
return $this->response->setJSON(["success" => 0, "error" => $th->getMessage()])->setStatusCode(403);
}
}
public function get_wiki_file(int $wiki_file_id)
{
$wikiFile = $this->wikiFileModel->find($wiki_file_id);
if ($wikiFile->path) {
$filePath = WRITEPATH . 'uploads/' . $wikiFile->path;
$mimeType = mime_content_type($filePath);
return $this->response
->setHeader('Content-Type', $mimeType)
->setHeader('Content-Length', filesize($filePath))
->setBody(file_get_contents($filePath));
} else {
return $this->response->setJSON(["success" => 0, "error" => lang('Wiki.file_dont_exist')])->setStatusCode(400);
}
}
public function wiki_section_update_order()
{
try {
$bodyData = $this->request->getPost();
$orders = $bodyData['orders'];
foreach ($orders as $key => $section_id) {
$this->wikiSectionModel->update($section_id, ['order' => $key]);
}
return $this->response->setJSON(["status" => 1, "message" => lang('Wiki.order_success')]);
} catch (\Throwable $th) {
return $this->response->setJSON(["status" => 0, "message" => lang('Wiki.order_error')])->setStatusCode(400);
}
}
public function store_section()
{
try {
$bodyData = $this->request->getPost();
$sectionName = $bodyData['name'];
$roles = $bodyData['roles'];
$bodyData["slug"] = implode('-', array_map(fn($e) => strtolower($e), explode(' ', $sectionName)));
$bodyData["order"] = $this->wikiSectionModel->selectMax('order')->first()->order + 1;
$wikiSectionId = $this->wikiSectionModel->insert($bodyData);
if(count($roles) > 0){
foreach ($roles as $key => $role) {
$this->wikiSectionRoleModel->insert(['wiki_section_id' => $wikiSectionId , 'role' => $role]);
}
}
return $this->response->setJSON(["status" => 1, "message" => lang('Wiki.section_new_success')]);
} catch (\Throwable $th) {
return $this->response->setJSON(["status" => 0, "message" => $th->getMessage()])->setStatusCode(400);
}
}
public function update_section()
{
try {
$bodyData = $this->request->getPost();
$wikiSectionId = $bodyData['wiki_section_id'];
$roles = $bodyData['roles'];
$this->wikiSectionModel->update($wikiSectionId, [
"name" => $bodyData['name'],
"icon" => $bodyData['icon']
]);
$this->wikiSectionRoleModel->where('wiki_section_id',$wikiSectionId)->delete();
if(count($roles) > 0){
foreach ($roles as $key => $role) {
$this->wikiSectionRoleModel->insert(['wiki_section_id' => $wikiSectionId , 'role' => $role]);
}
}
return $this->response->setJSON(["status" => 1, "message" => lang('Wiki.section_edit_success')]);
} catch (\Throwable $th) {
return $this->response->setJSON(["status" => 0, "message" => $th->getMessage()])->setStatusCode(400);
}
}
public function delete_section(int $section_id)
{
try{
$this->wikiSectionModel->delete($section_id);
return $this->response->setJSON(["status" => 1, "message" => lang('Wiki.section_delete_success')]);
}catch(Throwable $th){
return $this->response->setJSON(["status" => 0, "message" => $th->getMessage()])->setStatusCode(400);
}
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use CodeIgniter\Database\RawSql;
class WikiSectionsMigration extends Migration
{
protected array $COLUMNS = [
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'order' => [
'type' => 'INT',
'unsigned' => true,
'null'=> true,
],
'name' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'icon' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'parent_section_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
];
public function up()
{
$this->forge->addField($this->COLUMNS);
$currenttime = new RawSql('CURRENT_TIMESTAMP');
$this->forge->addField([
'created_at' => [
'type' => 'TIMESTAMP',
'default' => $currenttime,
],
'updated_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
'deleted_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('parent_section_id','wiki_sections','id');
$this->forge->createTable("wiki_sections");
}
public function down()
{
$this->forge->dropTable("wiki_sections");
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use CodeIgniter\Database\RawSql;
class WikiPagesMigration extends Migration
{
protected array $COLUMNS = [
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'section_id' => [
'type' => 'INT',
'unsigned' => true,
],
];
public function up()
{
$this->forge->addField($this->COLUMNS);
$currenttime = new RawSql('CURRENT_TIMESTAMP');
$this->forge->addField([
'created_at' => [
'type' => 'TIMESTAMP',
'default' => $currenttime,
],
'updated_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
'deleted_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('section_id','wiki_sections','id');
$this->forge->createTable("wiki_pages");
}
public function down()
{
$this->forge->dropTable("wiki_pages");
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use CodeIgniter\Database\RawSql;
class WikiContentsMigration extends Migration
{
protected array $COLUMNS = [
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'locale' => [
'type' => 'VARCHAR',
'constraint' => 255,
'default' => 'es',
],
'page_id' => [
'type' => 'INT',
'unsigned' => true,
],
'editor_data' => [
'type' => 'LONGTEXT',
'null' => true,
],
'published_data' => [
'type' => 'LONGTEXT',
'null' => true,
],
'published_by' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
'constraint' => 10,
],
'last_edit_by' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
'constraint' => 10,
],
];
public function up()
{
$this->forge->addField($this->COLUMNS);
$currenttime = new RawSql('CURRENT_TIMESTAMP');
$this->forge->addField([
'published_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
'created_at' => [
'type' => 'TIMESTAMP',
'default' => $currenttime,
],
'updated_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
'deleted_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('page_id','wiki_pages','id');
$this->forge->addForeignKey('published_by','users','id');
$this->forge->addForeignKey('last_edit_by','users','id');
$this->forge->createTable("wiki_contents");
}
public function down()
{
$this->forge->dropTable("wiki_contents");
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use CodeIgniter\Database\RawSql;
class WikiFilesMigration extends Migration
{
protected array $COLUMNS = [
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'content_id' => [
'type' => 'INT',
'unsigned' => true,
],
'path' => [
'type' => 'LONGTEXT',
'null' => true,
],
];
public function up()
{
$this->forge->addField($this->COLUMNS);
$currenttime = new RawSql('CURRENT_TIMESTAMP');
$this->forge->addField([
'created_at' => [
'type' => 'TIMESTAMP',
'default' => $currenttime,
],
'updated_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
'deleted_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('content_id','wiki_contents','id');
$this->forge->createTable("wiki_files");
}
public function down()
{
$this->forge->dropTable("wiki_files");
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use CodeIgniter\Database\RawSql;
class WikiSectionRolesMigration extends Migration
{
protected array $COLUMNS = [
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'wiki_section_id' => [
'type' => 'INT',
'unsigned' => true,
],
'role' => [
'type' => 'VARCHAR',
'constraint' => 255,
'default' => 'admin'
],
];
public function up()
{
$this->forge->addField($this->COLUMNS);
$currenttime = new RawSql('CURRENT_TIMESTAMP');
$this->forge->addField([
'created_at' => [
'type' => 'TIMESTAMP',
'default' => $currenttime,
],
'updated_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
'deleted_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('wiki_section_id','wiki_sections','id');
$this->forge->createTable("wiki_section_roles");
}
public function down()
{
$this->forge->dropTable("wiki_section_roles");
}
}

View File

@ -0,0 +1,165 @@
<?php
namespace App\Database\Seeds;
use App\Models\Configuracion\ConfigVariableModel;
use App\Models\Wiki\WikiSectionModel;
use App\Models\Wiki\WikiSectionRoleModel;
use CodeIgniter\Database\Seeder;
class WikiSectionSeeder extends Seeder
{
protected array $dataAdmin = [
[
"name" => 'Introducción',
"slug" => 'intro-admin',
"icon" => 'ti ti-home-2',
"roles" => [
"admin",
],
],
[
"name" => 'Presupuesto',
"slug" => 'presupuesto-admin',
"icon" => 'ti ti-currency-dollar',
"roles" => [
"admin",
],
],
[
"name" => 'Pedidos',
"slug" => 'pedidos-admin',
"icon" => 'ti ti-file-description',
"roles" => [
"admin",
],
],
[
"name" => 'Facturación',
"slug" => 'facturacion-admin',
"icon" => 'ti ti-file-dollar',
"roles" => [
"admin",
],
],
[
"name" => 'Logística',
"slug" => 'logistica-admin',
"icon" => 'ti ti-truck',
"roles" => [
"admin",
],
],
[
"name" => 'Tarifas',
"slug" => 'tarifas-admin',
"icon" => 'ti ti-receipt',
"roles" => [
"admin",
],
],
[
"name" => 'Configuración',
"slug" => 'config-admin',
"icon" => 'ti ti-adjustments-horizontal',
"roles" => [
"admin",
],
],
[
"name" => 'Mensajería',
"slug" => 'messages-admin',
"icon" => 'ti ti-message',
"roles" => [
"admin",
],
]
];
protected array $dataCliente = [
[
"name" => 'Introducción',
"slug" => 'intro-cliente',
"icon" => 'ti ti-home-2',
"roles" => [
"cliente-admin",
"cliente-editor",
]
],
[
"name" => 'Presupuesto(Cliente)',
"slug" => 'presupuesto-cliente',
"role" => 'cliente',
"icon" => 'ti ti-currency-dollar',
"roles" => [
"cliente-admin",
"cliente-editor",
]
],
[
"name" => 'Pedidos(Cliente)',
"slug" => 'pedidos-cliente',
"icon" => 'ti ti-file-description',
"roles" => [
"cliente-admin",
"cliente-editor",
]
],
[
"name" => 'Facturación (Cliente)',
"slug" => 'facturacion-cliente',
"icon" => 'ti ti-file-dollar',
"roles" => [
"cliente-admin",
"cliente-editor",
]
],
[
"name" => 'Tarifas (Cliente)',
"slug" => 'tarifas-cliente',
"icon" => 'ti ti-receipt',
"roles" => [
"cliente-admin",
"cliente-editor",
]
],
[
"name" => 'Mensajería (Cliente)',
"slug" => 'messages-cliente',
"icon" => 'ti ti-message',
"roles" => [
"cliente-admin",
"cliente-editor",
]
]
];
public function run()
{
$wikiSectionModel = model(WikiSectionModel::class);
$wikiSectionRoleModel = model(WikiSectionRoleModel::class);
$section_order = 0;
foreach ($this->dataAdmin as $key => $row) {
$row['order'] = $section_order;
$wikiSectionId = $wikiSectionModel->insert($row);
$section_order++;
foreach ($row['roles'] as $key => $role) {
$wikiSectionRoleModel->insert(['wiki_section_id' => $wikiSectionId,"role" => $role]);
}
}
foreach ($this->dataCliente as $key => $row) {
$row['order'] = $section_order;
$wikiSectionId = $wikiSectionModel->insert($row);
$section_order++;
foreach ($row['roles'] as $key => $role) {
$wikiSectionRoleModel->insert(['wiki_section_id' => $wikiSectionId,"role" => $role]);
}
}
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Entities\Wiki;
use App\Models\Usuarios\UserModel;
use CodeIgniter\Entity\Entity;
class WikiContentEntity extends Entity
{
protected $attributes = [
"locale" => null,
"page_id" => null,
"editor_data" => null,
"published_data" => null,
"last_edit_by" => null,
"published_by" => null,
"published_at" => null,
];
protected $casts = [
"locale" => "string",
"page_id" => "int",
"editor_data" => "string",
"published_data" => "string",
"last_edit_by" => "int",
"published_by" => "int",
"published_at" => "string",
];
public function publish_by() : string
{
$m = model(UserModel::class);
$user = $m->find($this->attributes['published_by']);
return $user->first_name." ".$user->last_name;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Entities\Wiki;
use CodeIgniter\Entity\Entity;
class WikiFileEntity extends Entity
{
protected $attributes = [
"content_id" => null,
"path" => null,
];
protected $casts = [
"content_id" => "int",
"path" => "string",
];
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Entities\Wiki;
use App\Models\Usuarios\UserModel;
use CodeIgniter\Entity\Entity;
class WikiPageEntity extends Entity
{
protected $attributes = [
"section_id" => null,
];
protected $casts = [
"section_id" => "int",
];
}

View File

@ -0,0 +1,94 @@
<?php
namespace App\Entities\Wiki;
use App\Models\Wiki\WikiContentModel;
use App\Models\Wiki\WikiPageModel;
use App\Models\Wiki\WikiSectionRoleModel;
use CodeIgniter\Entity\Entity;
class WikiSectionEntity extends Entity
{
protected $attributes = [
"name" => null,
"slug" => null,
"role" => null,
"order" => null,
"icon" => null,
"parent_id" => null
];
protected $casts = [
"name" => "string",
"slug" => "string",
"role" => "string",
"icon" => "string",
"order" => "int",
"parent_id" => "int"
];
public function withPage(): self
{
$this->attributes['pages'] = $this->page();
return $this;
}
public function withContents(string $locale = "es"): self
{
$m = model(WikiContentModel::class);
$this->attributes['contents'] = $this->content($locale);
return $this;
}
public function withRoles(): self
{
$m = model(WikiSectionRoleModel::class);
$this->attributes['roles'] = $this->roles();
return $this;
}
public function withRolesArray(): self
{
$m = model(WikiSectionRoleModel::class);
$this->attributes['roles_array'] = $this->roles_array();
return $this;
}
public function withAll(string $locale = "es") : self
{
$this->withPage();
$this->withContents($locale);
$this->withRoles();
$this->withRolesArray();
return $this;
}
public function page(): ?WikiPageEntity
{
$m = model(WikiPageModel::class);
return $m->where('section_id',$this->attributes['id'])->first();
}
public function roles_array(): ?array
{
$m = model(WikiSectionRoleModel::class);
$section_roles = $m->where('wiki_section_id',$this->attributes['id'])->findAll();
$roles = array_map(fn($r) => $r->role,$section_roles);
return $roles;
}
public function roles(): ?array
{
$m = model(WikiSectionRoleModel::class);
$section_roles = $m->where('wiki_section_id',$this->attributes['id'])->findAll();
return $section_roles;
}
public function content(string $locale = "es"): ?WikiContentEntity
{
$page = $this->page();
$content = null;
$m = model(WikiContentModel::class);
if($page){
$content = $m->where('page_id',$page->id)
->where('locale',$locale)
->first();
}
return $content;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Entities\Wiki;
use App\Models\Wiki\WikiContentModel;
use App\Models\Wiki\WikiPageModel;
use App\Models\Wiki\WikiSectionModel;
use CodeIgniter\Entity\Entity;
class WikiSectionRoleEntity extends Entity
{
protected $attributes = [
"wiki_section_id" => null,
"role" => null,
];
protected $casts = [
"wiki_section_id" => "int",
"role" => "string",
];
public function page(): ?WikiSectionEntity
{
$m = model(WikiSectionModel::class);
return $m->where('id',$this->attributes['section_id'])->first();
}
}

View File

@ -0,0 +1,44 @@
<?php
return [
'help' => "Help",
'intro-admin' => "Introduction",
'presupuesto-cliente' => "Client budget",
'presupuesto-admin' => "Admin budget",
'pedidos-cliente' => "Orders",
'pedidos-admin' => "Orders",
'facturacion-admin' => "Invoice",
'facturacion-cliente' => "Invoice (Client)",
'logistica-admin' => "Logistic",
'tarifas-admin' => "Tariff",
'tarifas-cliente' => "Tariff (Client)",
'config-admin' => "Configuration",
'messages-admin' => "Messages",
'messages-cliente' => "Messages (Client)",
'save' => "Save",
'release' => "Release",
'preview' => "Preview",
'edit' => "Edit",
'new_section' => "New section",
'edit_section' => "Edit section",
'header-placeholder' => "Start writing a header ...",
'errors' => [
'publish_before_save' => "You have to save before publish the content"
],
'alt' => [
"sort" => "Drag to set the section order"
],
'order_success' => 'Order updated',
'order_error' => 'An error has ocurred',
'published' => 'Released',
'not_published' => 'Not released',
'section_new_success' => 'Section created successfully',
'section_edit_success' => 'Section updated successfully',
'need_reload' => 'You need to refresh the page after creating/updating a section to see the changes.',
'file_dont_exist' => "File doesn't exist",
'need_editor_to_save' => 'Need to be in edit mode to save the content.',
'no_content' => 'Page is empty',
'dropdown_roles' => 'Roles that can see this section',
];

View File

@ -19,6 +19,7 @@ return [
"global_come_back" => "Volver",
"global_save" => "Guardar",
"global_alert_save_success" => "¡Guardado exitosamente!",
"global_alert_fetch_success" => "Obtenido exitosamente!",
"global_alert_save_error" => "¡Error al guardar!",
"global_activate" => "Activar",
"global_disable" => "Desactivar",

View File

@ -0,0 +1,50 @@
<?php
return [
'help' => "Ayuda",
'intro-admin' => "Introducción",
'intro-cliente' => "Introducción",
'presupuesto-cliente' => "Presupuesto (Cliente)",
'presupuesto-admin' => "Presupuesto admin",
'pedidos-admin' => "Pedidos",
'pedidos-cliente' => "Pedidos (Cliente)",
'facturacion-admin' => "Facturación",
'facturacion-cliente' => "Facturación(Cliente)",
'logistica-admin' => "Logística",
'tarifas-admin' => "Tarifas",
'tarifas-cliente' => "Tarifas (Cliente)",
'config-admin' => "Configuración",
'messages-admin' => "Mensajería",
'messages-cliente' => "Mensajería (Cliente)",
'save' => "Guardar",
'release' => "Publicar",
'preview' => "Vista previa",
'edit' => "Editar",
'name' => "Nombre sección",
'icon' => "Icono sección",
'section_placeholder' => "Introduce el nombre de la sección",
'section_order' => "Orden",
'section_icon_placeholder' => "Introduce el nombre de la sección",
'edit_section' => "Editar sección",
'new_section' => "Nueva sección",
'header-placeholder' => "Escribe un encabezado ...",
'errors' => [
'publish_before_save' => "Es necesario guardar antes de publicar"
],
'alt' => [
"sort" => "Arrastra para establecer el orden de la sección"
],
'order_success' => 'Orden actualizado correctamente',
'order_error' => 'Ha ocurriendo un error al ordenar',
'published' => 'Publicado',
'not_published' => 'Sin publicar',
'section_new_success' => 'Sección creada correctamente',
'section_edit_success' => 'Sección actualizada correctamente',
'need_reload' => 'Es necesario recargar la página para ver la nueva sección creada o editada.',
'file_dont_exist' => 'El fichero no existe',
'need_editor_to_save' => 'Tienes que estar en modo editar para guardar.',
'no_content' => 'No hay contenido en la página',
'dropdown_roles' => 'Roles que pueden ver la sección',
];

View File

@ -0,0 +1,58 @@
<?php
namespace App\Models\Wiki;
use App\Entities\Wiki\WikiContentEntity;
use CodeIgniter\Model;
class WikiContentModel extends Model
{
protected $table = 'wiki_contents';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = WikiContentEntity::class;
protected $useSoftDeletes = true;
protected $protectFields = true;
protected $allowedFields = [
"locale",
"page_id",
"editor_data",
"published_data",
"last_edit_by",
"published_by",
"published_at"
];
protected bool $allowEmptyInserts = false;
protected bool $updateOnlyChanged = true;
protected array $casts = [];
protected array $castHandlers = [];
// Dates
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
// Validation
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
protected $cleanValidationRules = true;
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = [];
protected $afterInsert = [];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Models\Wiki;
use App\Entities\Wiki\WikiFileEntity;
use CodeIgniter\Model;
class WikiFileModel extends Model
{
protected $table = 'wiki_files';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = WikiFileEntity::class;
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = [
"content_id",
"path"
];
protected bool $allowEmptyInserts = false;
protected bool $updateOnlyChanged = true;
protected array $casts = [];
protected array $castHandlers = [];
// Dates
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
// Validation
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
protected $cleanValidationRules = true;
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = [];
protected $afterInsert = [];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Models\Wiki;
use App\Entities\Wiki\WikiPageEntity;
use CodeIgniter\Model;
class WikiPageModel extends Model
{
protected $table = 'wiki_pages';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = WikiPageEntity::class;
protected $useSoftDeletes = true;
protected $protectFields = true;
protected $allowedFields = [
"section_id",
];
protected bool $allowEmptyInserts = false;
protected bool $updateOnlyChanged = true;
protected array $casts = [];
protected array $castHandlers = [];
// Dates
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
// Validation
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
protected $cleanValidationRules = true;
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = [];
protected $afterInsert = [];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Models\Wiki;
use App\Entities\Wiki\WikiSectionEntity;
use CodeIgniter\Model;
class WikiSectionModel extends Model
{
protected $table = 'wiki_sections';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = WikiSectionEntity::class;
protected $useSoftDeletes = true;
protected $protectFields = true;
protected $allowedFields = [
"name",
"slug",
"icon",
"order",
"parent_id"
];
protected bool $allowEmptyInserts = false;
protected bool $updateOnlyChanged = true;
protected array $casts = [];
protected array $castHandlers = [];
// Dates
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
// Validation
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
protected $cleanValidationRules = true;
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = [];
protected $afterInsert = [];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
/**
* Get wiki sections
*
* @return array<WikiSectionEntity>
*/
public function sections() : array
{
return $this->orderBy('order','asc')->findAll();
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Models\Wiki;
use App\Entities\Wiki\WikiSectionRoleEntity;
use CodeIgniter\Model;
class WikiSectionRoleModel extends Model
{
protected $table = 'wiki_section_roles';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = WikiSectionRoleEntity::class;
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = [
"wiki_section_id",
"role"
];
protected bool $allowEmptyInserts = false;
protected bool $updateOnlyChanged = true;
protected array $casts = [];
protected array $castHandlers = [];
// Dates
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
// Validation
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
protected $cleanValidationRules = true;
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = [];
protected $afterInsert = [];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
}

View File

@ -0,0 +1,50 @@
<!-- Modal -->
<div class="modal fade" id="modalSection" tabindex="-1" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title d-none" id="modalTitleNew"><?= lang('Wiki.new_section') ?></h5>
<h5 class="modal-title d-none" id="modalTitleEdit"><?= lang('Wiki.edit_section') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-primary" role="alert">
<?= lang('Wiki.need_reload') ?>
</div>
<form id="formSection">
<div class="form-group">
<div class="row">
<div class="col-12 mb-2">
<label for="section-name" class="form-label"><?= lang('Wiki.name') ?></label>
<input type="text" id="section-name" name="name" placeholder="<?= lang('Wiki.section_placeholder') ?>" class="form-control" required />
</div>
<div class="mb-3">
<label for="section-icon" class="form-label"><?= lang('Wiki.icon') ?></label>
<select id="section-icon" class="select2-icons form-select">
</select>
</div>
<div class="mb-3">
<label for="section-roles" class="form-label"><?= lang('Wiki.roles') ?></label>
<select id="section-roles" class="select2-icons form-select" name="roles[]" multiple>
<?php
foreach(config('AuthGroups')->groups as $key => $value):?>
<option value="<?=$key?>"><?=isset($value['title']) ? $value['title'] : $key?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<?php if (auth()->user()->inGroup('admin')): ?>
<button type="button" class="btn btn-label-secondary" data-bs-dismiss="modal"><?= lang('App.global_come_back') ?></button>
<button type="button" id="submit-new-section" class="btn btn-primary d-none"><?= lang('App.global_save') ?></button>
<button type="button" id="submit-update-section" class="btn btn-primary d-none"><?= lang('App.global_save') ?></button>
<?php endif ?>
</div>
</div>
</div>
</div>

View File

@ -346,6 +346,7 @@ $picture = "/assets/img/default-user.png";
<!-- build:js assets/vendor/js/core.js -->
<script src="<?= site_url('themes/vuexy/vendor/libs/jquery/jquery.js') ?>"></script>
<script src="<?= site_url('themes/vuexy/vendor/libs/popper/popper.js') ?>"></script>
<script src="<?= site_url('themes/vuexy/vendor/libs/jquery/jquery.js') ?>"></script>
<script src="<?= site_url('themes/vuexy/vendor/js/bootstrap.js') ?>"></script>
<script src="<?= site_url('themes/vuexy/vendor/libs/flatpickr/flatpickr.js') ?>"></script>
<script src="<?= site_url('themes/vuexy/vendor/libs/perfect-scrollbar/perfect-scrollbar.js') ?>"></script>

View File

@ -48,10 +48,14 @@
require "menus/mensajes_menu.php";
require "menus/wiki_menu.php";
require "menus/soporte_menu.php";
require "menus/sistema_menu.php";
?>
</ul>

View File

@ -0,0 +1,16 @@
<!-- Messages -->
<li class="menu-item">
<?php if(auth()->user()->inGroup('admin')):?>
<a href="<?= route_to('showWikiPage','intro-admin') ?>" class="menu-link">
<i class="menu-icon tf-icons ti ti-books"></i>
<?= lang("Wiki.help") ?>
</a>
<?php endif;?>
<?php if(auth()->user()->inGroup('cliente-editor')):?>
<a href="<?= route_to('showWikiPage','intro-cliente') ?>" class="menu-link">
<i class="menu-icon tf-icons ti ti-books"></i>
<?= lang("Wiki.help") ?>
</a>
<?php endif;?>
</li>

View File

@ -0,0 +1,54 @@
<?php if (isset($breadcrumb)) { ?>
<div id="bc-card" class="card sticky-breadcrumb border border-secondary mb-3">
<div class="card-header header-elements">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<?php
foreach ($breadcrumb as $item) {
?>
<li class="breadcrumb-item <?= $item['active'] ? 'active' : '' ?>">
<a href="<?= $item['route'] ?>"><?= $item['title'] ?></a>
</li>
<?php
}
?>
</ol>
</nav>
<div id="bc-save-div" class="card-header-elements ms-auto" style="display: none">
<button id="bc-save"
type="button"
class="btn btn-sm btn-primary waves-effect waves-light"
>
<?= lang('Basic.global.Save') ?>
</button>
</div>
</div>
</div>
<?php
} else { ?>
<div class="card sticky-breadcrumb border border-secondary mb-3">
<div class="card-header header-elements">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item active">
<a href="<?= site_url(); ?>">Home</a>
</li>
</ol>
</nav>
</div>
</div>
<?php } ?>
<?= $this->section('globalJsFunctions') ?>
function showBreadCrumbSaveButton(showIt = false){
if(showIt === true){
$('#bc-save-div').show();
$('#bc-card').addClass('bg-warning');
} else if(showIt === false){
$('#bc-save-div').hide();
$('#bc-card').removeClass('bg-warning');
}
}
<?= $this->endSection() ?>

View File

@ -0,0 +1,384 @@
<?php
$session = session();
$settings = $session->get('settings');
$picture = "/assets/img/default-user.png";
?>
<!DOCTYPE html>
<html
lang="<?= $session->get('lang') ?>"
class="h-100 light-style layout-navbar-fixed layout-menu-fixed"
dir="ltr"
data-theme="theme-default"
data-assets-path="<?= site_url('themes/vuexy/') ?>"
data-template="vertical-menu-template-no-customizer">
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
<meta name="locale" content="<?= $session->get("lang") ?>">
<title><?= config('Safekat')->appName ?></title>
<meta name="description" content="" />
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="<?= site_url('themes/vuexy/img/favicon/favicon.ico') ?>" />
<link rel="apple-touch-icon" sizes="57x57" href="<?= site_url('themes/vuexy/img/favicon/apple-icon-57x57.png') ?>">
<link rel="apple-touch-icon" sizes="60x60" href="<?= site_url('themes/vuexy/img/favicon/apple-icon-60x60.png') ?>">
<link rel="apple-touch-icon" sizes="72x72" href="<?= site_url('themes/vuexy/img/favicon/apple-icon-72x72.png') ?>">
<link rel="apple-touch-icon" sizes="76x76" href="<?= site_url('themes/vuexy/img/favicon/apple-icon-76x76.png') ?>">
<link rel="apple-touch-icon" sizes="114x114"
href="<?= site_url('themes/vuexy/img/favicon/apple-icon-114x114.png') ?>">
<link rel="apple-touch-icon" sizes="120x120"
href="<?= site_url('themes/vuexy/img/favicon/apple-icon-120x120.png') ?>">
<link rel="apple-touch-icon" sizes="144x144"
href="<?= site_url('themes/vuexy/img/favicon/apple-icon-144x144.png') ?>">
<link rel="apple-touch-icon" sizes="152x152"
href="<?= site_url('themes/vuexy/img/favicon/apple-icon-152x152.png') ?>">
<link rel="apple-touch-icon" sizes="180x180"
href="<?= site_url('themes/vuexy/img/favicon/apple-icon-180x180.png') ?>">
<link rel="icon" type="image/png" sizes="192x192"
href="<?= site_url('themes/vuexy/img/favicon/android-icon-192x192.png') ?>">
<link rel="icon" type="image/png" sizes="32x32"
href="<?= site_url('themes/vuexy/img/favicon/favicon-32x32.png') ?>">
<link rel="icon" type="image/png" sizes="96x96"
href="<?= site_url('themes/vuexy/img/favicon/favicon-96x96.png') ?>">
<link rel="icon" type="image/png" sizes="16x16"
href="<?= site_url('themes/vuexy/img/favicon/favicon-16x16.png') ?>">
<link rel="manifest" href="<?= site_url('themes/vuexy/img/favicon/manifest.json') ?>">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap"
rel="stylesheet" />
<!-- Icons -->
<link rel="stylesheet" href="<?= site_url('themes/vuexy/vendor/fonts/fontawesome.css') ?>" />
<link rel="stylesheet" href="<?= site_url('themes/vuexy/vendor/fonts/tabler-icons.css') ?>" />
<link rel="stylesheet" href="<?= site_url('themes/vuexy/vendor/fonts/flag-icons.css') ?>" />
<!-- Core CSS -->
<link rel="stylesheet" href="<?= site_url('themes/vuexy/vendor/css/rtl/core.css') ?>" />
<link rel="stylesheet" href="<?= site_url('themes/vuexy/vendor/css/rtl/theme-semi-dark.css') ?>" />
<link rel="stylesheet" href="<?= site_url('themes/vuexy/css/safekat.css') ?>" />
<link rel="stylesheet" href="<?= site_url('themes/vuexy/vendor/libs/flatpickr/flatpickr.css') ?>" />
<!-- Vendors CSS -->
<link rel="stylesheet" href="<?= site_url('themes/vuexy/vendor/libs/perfect-scrollbar/perfect-scrollbar.css') ?>" />
<!-- Page CSS -->
<?= $this->renderSection('css') ?>
<link rel="stylesheet" href="<?= site_url('themes/vuexy/css/safekat.css') ?>" />
</head>
<body>
<!-- Layout wrapper -->
<div class="layout-wrapper layout-content-navbar">
<div class="layout-container">
<aside id="layout-menu" class="layout-menu menu-vertical menu bg-menu-theme">
<div class="app-brand">
<a href="<?= site_url('home') ?>" class="app-brand-link">
<span class="app-brand-logo">
<img src="<?= site_url('themes/vuexy/img/safekat/logos/sk-logo.png') ?>" width="150px">
</span>
</a>
<a href="javascript:void(0);" class="layout-menu-toggle menu-link text-large ms-auto">
<i class="ti menu-toggle-icon d-none d-xl-block ti-sm align-middle"></i>
<i class="ti ti-x d-block d-xl-none ti-sm align-middle"></i>
</a>
</div>
<div class="menu-inner-shadow"></div>
<ul class="menu-inner py-1 list-group list-group-flush" id="menu-inner-list">
<!-- Iterate throught sections -->
<?php foreach ($wiki_sections as $key => $value) : ?>
<?php if (auth()->user()->inGroup(...$value->roles_array()) || auth()->user()->inGroup('admin')): ?>
<li class="menu-item <?= ($value->slug == $slug) ? "active" : "" ?>" data-id="<?= $value->id ?>">
<!-- Check if user can view the section link -->
<span class="d-flex justify-content-between align-items-center">
<a href="<?= site_url("wiki/view/" . $value->slug) ?>" class="menu-link">
<i class="menu-icon tf-icons <?= $value->icon ?>"></i>
<?= lang("Wiki." . $value->slug) ?>
</a>
<?php if (auth()->user()->inGroup('admin')): ?>
<i class="drag-handle cursor-grab icon-base ti ti-arrows-sort align-text-bottom me-2" title="<?= lang('Wiki.alt.sort') ?>"></i>
<?php endif; ?>
</span>
</li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
</aside>
<!-- Layout container -->
<div class="layout-page">
<!-- Navbar -->
<nav
class="layout-navbar container-fluid navbar navbar-expand-xl navbar-detached align-items-center bg-navbar-theme"
id="layout-navbar">
<div class="layout-menu-toggle navbar-nav align-items-xl-center me-3 me-xl-0 d-xl-none">
<a class="nav-item nav-link px-0 me-xl-4" href="javascript:void(0)">
<i class="ti ti-menu-2 ti-sm"></i>
</a>
</div>
<div class="navbar-nav-left d-flex align-items-center" id="navbar-collapse">
<ul class="navbar-nav flex-row justify-content-start align-items-center ms-auto">
<div>
<a class="nav-link" href="<?= route_to("home") ?>">
<i class="ti ti-home rounded-circle me-1 fs-3"></i>
</a>
</div>
</ul>
</div>
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
<ul class="navbar-nav flex-row justify-content-start align-items-center ms-auto">
<!-- Language -->
<li class="nav-item dropdown-language dropdown me-2 me-xl-0">
<a class="nav-link dropdown-toggle hide-arrow" href="javascript:void(0);"
data-bs-toggle="dropdown">
<i class="fi <?= getCurrentLanguageFlag(); ?> fis rounded-circle me-1 fs-3"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="<?= site_url('lang/es'); ?>" data-language="es">
<i class="fi fi-es fis rounded-circle me-1 fs-3"></i>
<span class="align-middle"><?= lang("App.lang_es") ?></span>
</a>
</li>
<li>
<a class="dropdown-item" href="<?= site_url('lang/en'); ?>" data-language="en">
<i class="fi fi-gb fis rounded-circle me-1 fs-3"></i>
<span class="align-middle"><?= lang("App.lang_en") ?></span>
</a>
</li>
</ul>
</li>
<!-- User -->
<li class="nav-item navbar-dropdown dropdown-user dropdown">
<a class="nav-link dropdown-toggle hide-arrow" href="javascript:void(0);"
data-bs-toggle="dropdown">
<div class="avatar">
<img src="<?= $picture ?? '' ?>" alt class="h-auto rounded-circle" />
</div>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="#">
<div class="d-flex">
<div class="flex-shrink-0 me-3">
<div class="avatar avatar">
<img src="<?= $picture ?? '' ?>" alt class="h-auto rounded-circle" />
</div>
</div>
<div class="flex-grow-1">
<span class="fw-semibold d-block"><?= auth()->user()->getFullName(); ?></span>
<small class="text-muted"><?= auth()->user()->getEmail(); ?></small>
</div>
</div>
</a>
</li>
<li>
<div class="dropdown-divider"></div>
</li>
<li>
<a class="dropdown-item" href="<?= site_url('profile'); ?>">
<i class="ti ti-user-check me-2 ti-sm"></i>
<span class="align-middle"><?= lang("App.menu_profile") ?></span>
</a>
</li>
<li>
<div class="dropdown-divider"></div>
</li>
<li>
<a class="dropdown-item" id="btn-log-out" href="<?= site_url("logout") ?>">
<i class="ti ti-logout me-2 ti-sm"></i>
<span class="align-middle"><?= lang("App.menu_logout") ?></span>
</a>
</li>
</ul>
</li>
<!--/ User -->
</ul>
</div>
</nav>
<!-- / Navbar -->
<!-- Content wrapper -->
<div class="content-wrapper">
<!-- Content -->
<div class="container-fluid flex-grow-1 container-p-y">
<?php
// Include breadcrumbs block
include "breadcrumbs.php"
?>
<?=
// Render the content section
$this->renderSection('content')
?>
</div>
<!-- / Content -->
<!-- Footer -->
<footer class="content-footer footer bg-footer-theme">
<div class="container-fluid">
<div class="footer-container d-flex align-items-center justify-content-between py-2 flex-md-row flex-column">
<div>
<a href="#" target="_blank" class="fw-semibold">Safekat</a> © <?= date('Y'); ?>
</div>
</div>
</div>
</footer>
<!-- / Footer -->
<div class="content-backdrop fade"></div>
</div>
<!-- Content wrapper -->
</div>
<!-- / Layout page -->
</div>
<!-- Overlay -->
<div class="layout-overlay layout-menu-toggle"></div>
<!-- Drag Target Area To SlideIn Menu On Small Screens -->
<div class="drag-target"></div>
</div>
<!-- / Layout wrapper -->
<?= $this->renderSection('footerAdditions') ?>
<!-- Core JS -->
<!-- build:js assets/vendor/js/core.js -->
<script src="<?= site_url('themes/vuexy/vendor/libs/jquery/jquery.js') ?>"></script>
<script src="<?= site_url('themes/vuexy/vendor/libs/popper/popper.js') ?>"></script>
<script src="<?= site_url('themes/vuexy/vendor/js/bootstrap.js') ?>"></script>
<script src="<?= site_url('themes/vuexy/vendor/libs/flatpickr/flatpickr.js') ?>"></script>
<script src="<?= site_url('themes/vuexy/vendor/libs/perfect-scrollbar/perfect-scrollbar.js') ?>"></script>
<script src="<?= site_url('themes/vuexy/vendor/libs/hammer/hammer.js') ?>"></script>
<script src="<?= site_url('themes/vuexy/vendor/js/menu.js') ?>"></script>
<script type="module" src="<?= site_url('assets/js/safekat/pages/layout.js') ?>"></script>
<!-- Helpers -->
<script src="<?= site_url('themes/vuexy/vendor/js/helpers.js') ?>"></script>
<script src="<?= site_url('themes/vuexy/js/config.js') ?>"></script>
<!-- endbuild -->
<!-- Vendors JS -->
<?= $this->renderSection('additionalExternalJs') ?>
<script src="<?= site_url('themes/vuexy/js/main.js') ?>"></script>
<?php if (auth()->user()->inGroup('admin')): ?>
<script type="module" src="<?= site_url('assets/js/safekat/pages/wiki/menuSortable.js') ?>"></script>
<?php endif; ?>
<?= sweetAlert() ?>
<?php
if (isset($global_js_variables)) {
echo "<script>\n";
foreach ($global_js_variables as $name => $value):
echo "\t" . "var $name = $value;" . "\n";
endforeach;
echo "</script>\n";
}
?>
<script type="text/javascript">
<?= $this->renderSection('globalJsFunctions') ?>
var theTable;
var <?= csrf_token() ?? 'token' ?>v = '<?= csrf_hash() ?>';
function yeniden(andac = null) {
if (andac == null) {
andac = <?= csrf_token() ?>v;
} else {
<?= csrf_token() ?>v = andac;
}
$('input[name="<?= csrf_token() ?>"]').val(andac);
$('meta[name="<?= config('Security')->tokenName ?>"]').attr('content', andac)
$.ajaxSetup({
headers: {
'<?= config('Security')->headerName ?>': andac,
'X-Requested-With': 'XMLHttpRequest'
},
<?= csrf_token() ?>: andac
});
}
document.addEventListener('DOMContentLoaded', function() {
function adjustSidebar4ContentWrapper() {
if ($('#sidebar').hasClass('d-none') && $(window).width() <= 768) {
$('#contentWrapper').addClass('full-width');
} else {
if (!$('#sidebar').hasClass('inactive')) {
$('#contentWrapper').removeClass('full-width');
}
}
}
adjustSidebar4ContentWrapper();
$('#sidebarCollapse').on('click', function() {
if ($('#sidebar').hasClass('d-none') && $(window).width() <= 768) {
$('#sidebar').removeClass('d-none d-sm-none d-md-block');
$('#contentWrapper').removeClass('full-width');
} else {
$('#sidebar').toggleClass('inactive');
$('#contentWrapper').toggleClass('full-width');
$('.collapse.in').toggleClass('in');
$('a[aria-expanded=true]').attr('aria-expanded', 'false');
}
});
$(window).resize(function() {
adjustSidebar4ContentWrapper();
});
<?= $this->renderSection('additionalInlineJs') ?>
});
</script>
</body>
</html>

View File

@ -0,0 +1,102 @@
<?= $this->include('themes/_commonPartialsBs/select2bs5') ?>
<?= $this->include('themes/_commonPartialsBs/datatables') ?>
<?= $this->include('themes/_commonPartialsBs/_confirm2delete') ?>
<?= $this->include("themes/_commonPartialsBs/sweetalert") ?>
<?= $this->extend('themes/vuexy/wiki/layout') ?>
<?php
use CodeIgniter\I18n\Time;
?>
<?= $this->section('content'); ?>
<div class="row">
<div class="col-md-12">
<div class="card card-info">
<div class="card-header">
<div class="row">
<div class="col-md-10 col-xs-12 col-sm-12 d-flex flex-row flex-wrap justify-content-start align-items-center pb-2 gap-2">
<?php if ($section->content()?->published_data): ?>
<span class="badge badge-center rounded-pill text-bg-success"><i class="ti ti-check"></i></span>
<strong><?= lang('Wiki.published') ?></strong>
<strong><?= $section->content()->published_at ? Time::createFromFormat('Y-m-d H:i:s', $section->content()->published_at)->format('d/m/Y H:i') : "" ?></strong>
<strong class="text-secondary"><?= $section->content()->publish_by() ?></strong>
<?php else: ?>
<span class="badge badge-center rounded-pill text-bg-danger"><i class="ti ti-alert-circle"></i></span>
<strong><?= lang('Wiki.not_published') ?></strong>
<?php endif; ?>
</div>
<?php if (auth()->user()->inGroup('admin')): ?>
<div class="col-md-2 d-flex flex-row flex-wrap justify-content-end align-items-stretch pb-2 gap-2">
<div class="btn-sm-group">
<button type="button" class="btn btn-sm btn-primary btn-icon rounded-pill dropdown-toggle hide-arrow" data-bs-toggle="dropdown">
<i class="icon-base ti ti-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><button type="button" class="dropdown-item btn btn-success btn-xs col-xs-12 " id="new-section"><i class="icon-base ti ti-plus icon-xs me-2"></i><?= lang('Wiki.new_section') ?></button></li>
<li><button type="button" class="dropdown-item btn btn-warning btn-xs col-auto " id="edit-section"><i class="icon-base ti ti-pencil icon-xs me-2"></i><?= lang('Wiki.edit_section') ?></button></li>
<li><button type="button" class="dropdown-item btn btn-danger btn-xs col-auto " id="delete-section"><i class="icon-base ti ti-trash icon-xs me-2"></i><?= lang('Wiki.delete_section') ?></button></li>
<li>
<hr class="dropdown-divider">
</li>
<li><button type="button" class="dropdown-item btn btn-primary btn-xs col-auto " id="save-editor"><i class="icon-base ti ti-device-floppy icon-xs me-2"></i><?= lang('App.global_save') ?></button></li>
<li><button type="button" class="dropdown-item btn btn-danger btn-xs col-auto " id="release-editor"><i class="icon-base ti ti-upload icon-xs me-2"></i><?= lang('Wiki.release') ?></button></li>
<li><button type="button" class="dropdown-item btn btn-secondary btn-xs d-none col-auto " id="preview-editor"><i class="icon-base ti ti-eye icon-xs me-2"></i><?= lang('Wiki.preview') ?></button></li>
<li><button type="button" class="dropdown-item btn btn-warning btn-xs col-auto " id="edit-editor"><i class="icon-base ti ti-pencil icon-xs me-2"></i><?= lang('Wiki.edit') ?></button></li>
</ul>
</div>
<div class="btn-sm-group">
<button type="button" class="btn btn-sm btn-warning btn-icon rounded-pill dropdown-toggle hide-arrow" data-bs-toggle="dropdown" title="<?=lang('Wiki.dropdown_roles')?>">
<i class="icon-base ti ti-users"></i>
</button>
<ul class="dropdown-menu">
<?php foreach ($section->roles() as $key => $roleEntity): ?>
<li><span class="dropdown-item badge bg-label-success"><?=config("AuthGroups")->groups[$roleEntity->role]["title"]?></span></li>
<?php endforeach; ?>
</ul>
</div>
</div>
<?php endif; ?>
</div>
</div><!--//.card-header -->
<div class="card-body">
<div class="row">
<form action="POST" id="form-wiki">
<input type="hidden" name="slug" id="section-slug">
<input type="hidden" name="wiki_page_id" id="wiki-section-id" value="<?= $section->id ?>">
<input type="hidden" name="wiki_page_id" id="wiki-page-id">
<input type="hidden" name="wiki_page_id" id="wiki-content-id">
</form>
<div class="col-md-12">
<div id="editorjs"></div>
</div>
</div>
</div><!--//.card-body -->
<div class="card-footer">
</div><!--//.card-footer -->
</div><!--//.card -->
</div><!--//.col -->
</div><!--//.row -->
<?= view("themes/vuexy/components/modals/modalSection") ?>
<?= $this->endSection() ?>
<?= $this->section('css') ?>
<link rel="stylesheet" href="<?= site_url('themes/vuexy/vendor/libs/sweetalert2/sweetalert2.css') ?>" />
<?= $this->endSection() ?>
<?= $this->section("additionalExternalJs") ?>
<script src="<?= site_url('themes/vuexy/vendor/libs/sweetalert2/sweetalert2.js') ?>"></script>
<?php if (auth()->user()->inGroup('admin')) : ?>
<script type="module" src="<?= site_url('assets/js/safekat/pages/wiki/home.js') ?>"></script>
<?php else : ?>
<script type="module" src="<?= site_url('assets/js/safekat/pages/wiki/viewOnly.js') ?>"></script>
<?php endif; ?>
<?= $this->endSection() ?>

View File

@ -0,0 +1,30 @@
<!-- Menu -->
<aside id="layout-menu" class="layout-menu menu-vertical menu bg-menu-theme">
<div class="app-brand">
<a href="<?= site_url('home') ?>" class="app-brand-link">
<span class="app-brand-logo">
<img src="<?= site_url('themes/vuexy/img/safekat/logos/sk-logo.png') ?>" width="150px">
</span>
</a>
<a href="javascript:void(0);" class="layout-menu-toggle menu-link text-large ms-auto">
<i class="ti menu-toggle-icon d-none d-xl-block ti-sm align-middle"></i>
<i class="ti ti-x d-block d-xl-none ti-sm align-middle"></i>
</a>
</div>
<div class="menu-inner-shadow"></div>
<ul class="menu-inner py-1">
<li class="menu-item">
<?php foreach ($wiki_sections as $key => $value) :?>
<a href="<?= site_url("wiki/presupuesto")?>" class="menu-link menu-toggle" data-id="<?= $slug ?>">
<i class="menu-icon tf-icons ti ti-book"></i>
<?= lang("App.menu_presupuestos") ?>
</a>
<?php endforeach;?>
</li>
</ul>
</aside>
<!-- / Menu -->

View File

@ -0,0 +1,41 @@
import Ajax from "../components/ajax.js";
class TranslationHelper {
constructor() {
this.locale = "es"
this.lang = {}
}
async get_translations(translationFile) {
return new Promise(async (resolve,reject) => {
this.locale = $("meta[name='locale']").attr("content");
this.translationFile = translationFile
const ajax = new Ajax('/translate/getTranslation',
{
locale: this.locale,
translationFile: this.translationFile
},
null,
(response) => {
this.lang = JSON.parse(response)
resolve(this.lang)
},
(error) => {
reject(error)
}
);
ajax.post()
})
}
get_lang(key) {
if (key in this.lang) {
return this.lang[key]
}else{
return key
}
}
}
export default TranslationHelper;

View File

@ -86,3 +86,25 @@ export const initAutonumeric = () => {
// }
// })
}
export const getTablerIconClassName = () => {
const classNames = [];
for (const sheet of document.styleSheets) {
try {
if (sheet.cssRules) {
for (const rule of sheet.cssRules) {
if (rule.selectorText && rule.selectorText.startsWith(".ti-")) {
let className = rule.selectorText.split(" ")[0].replace(".", "");
className = className.split("::")[0]
className = className.split(":")[0]
classNames.push(className);
}
}
}
} catch (e) {
console.warn("Cannot access stylesheet:", sheet.href);
}
}
return classNames.reverse();
}

View File

@ -1,6 +1,5 @@
export const alertConfirmationDelete = (title, type = "primary") => {
return Swal.fire({
title: '¿Está seguro?',
@ -18,7 +17,23 @@ export const alertConfirmationDelete = (title, type = "primary") => {
buttonsStyling: false
})
}
export const alertConfirmAction = (title, type = "primary") => {
return Swal.fire({
title: '¿Está seguro?',
text: title,
icon: 'info',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Sí',
cancelButtonText: 'Cancelar',
customClass: {
confirmButton: 'btn btn-success me-1',
cancelButton: 'btn btn-label-secondary'
},
buttonsStyling: false
})
}
export const alertSuccessMessage = (title, type = "primary") => {
return Swal.fire({
showCancelButton: false,
@ -64,3 +79,51 @@ export const toastPresupuestoSummary = (value, target = 'body') => {
stopKeydownPropagation: false,
})
}
export const alertSuccess = (value, target = 'body') => {
return Swal.mixin({
toast: true,
position: 'bottom-end',
html: `<span class="text-sm-left text-wrap">${value}</span>`,
customClass: {
popup: 'bg-success text-white',
},
icon : 'success',
iconColor: 'white',
target: target,
showConfirmButton: false,
timer: 2000,
timerProgressBar: true,
})
}
export const alertError = (value, target = 'body') => {
return Swal.mixin({
toast: true,
position: 'bottom-end',
html: `<span class="text-sm-left text-wrap">${value}</span>`,
customClass: {
popup: 'bg-danger text-white',
},
icon : 'error',
iconColor: 'white',
target: target,
showConfirmButton: false,
timer: 2000,
timerProgressBar: true,
})
}
export const alertWarning = (value, target = 'body') => {
return Swal.mixin({
toast: true,
position: 'bottom-end',
html: `<span class="text-sm-left text-wrap">${value}</span>`,
customClass: {
popup: 'bg-warning text-white',
},
icon : 'warning',
iconColor: 'white',
target: target,
showConfirmButton: false,
timer: 2000,
timerProgressBar: true,
})
}

View File

@ -0,0 +1,255 @@
import Ajax from '../ajax.js'
import { es } from './lang/es.js';
import '../../../../../themes/vuexy/js/editorjs/toc.js';
import '../../../../../themes/vuexy/js/editorjs/list.js';
import '../../../../../themes/vuexy/js/editorjs/tune.js';
import '../../../../../themes/vuexy/js/editorjs/table.js';
import '../../../../../../themes/vuexy/js/editorjs/image.js';
import '../../../../../../themes/vuexy/js/editorjs/alert.js';
import '../../../../../../themes/vuexy/js/editorjs/header.js';
import '../../../../../../themes/vuexy/js/editorjs/drag-drop.js';
import EditorJS from '../../../../../themes/vuexy/js/editorjs.mjs';
import TranslationHelper from '../../common/TranslationHelper.js';
import { alertConfirmAction, alertError, alertSuccess, alertWarning } from '../alerts/sweetAlert.js';
import WikiSectionForm from '../forms/WikiSectionForm.js'
class WikiEditor extends TranslationHelper{
constructor() {
super();
this.sectionId = $("#wiki-section-id").val();
this.wikiFormSection = new WikiSectionForm()
this.wikiFormSection.setId(this.sectionId)
}
async initEditor()
{
this.wikiFormSection.init()
await this.get_translations("Wiki")
this.editor = new EditorJS({
holder: 'editorjs',
i18n: this.locale == "es" ? es : null,
autofocus: true,
readOnly: true,
tunes: {
alignment: {
class: AlignmentBlockTune,
},
},
tools: {
toc: TOC,
header: {
class: Header,
tunes: ['alignment'],
config: {
placeholder: "",
levels: [1, 2, 3, 4],
defaultLevel: 1
},
},
nestedchecklist: {
class: editorjsNestedChecklist,
config: {
maxLevel: 1,
}
},
alert: {
class: Alert,
inlineToolbar: true,
tunes: ['alignment'],
config: {
alertTypes: ['primary', 'secondary', 'info', 'success', 'warning', 'danger', 'light', 'dark'],
defaultType: 'primary',
messagePlaceholder: 'Introduzca texto',
},
},
image: {
class: ImageTool,
config: {
features: {
border: true,
caption: 'optional',
stretch: false
},
endpoints: {
byFile: `/wiki/file/upload/${this.sectionId}`, // Your backend file uploader endpoint
byUrl: 'fetchUrl', // Your endpoint that provides uploading by Url
}
}
},
table: {
class: Table,
tunes: ['alignment'],
inlineToolbar: true,
config: {
rows: 2,
cols: 3,
maxRows: 5,
maxCols: 5,
},
},
alignment: {
class: AlignmentBlockTune,
config: {
default: "left",
blocks: {
header: 'left',
list: 'left'
}
}
},
},
})
}
async initViewOnly()
{
try {
await this.initEditor()
await this.editor.isReady;
this.handleGetDataPublished();
} catch (reason) {
console.log(`Editor.js initialization failed because of ${reason}`)
}
}
async init() {
try {
await this.initEditor()
await this.editor.isReady;
new DragDrop(this.editor);
/** Do anything you need after editor initialization */
$('#save-editor').on('click', () => {
this.editor.save().then(outputData => {
alertConfirmAction(this.get_lang('save_content')).then(result => {
if (result.isConfirmed) {
this.handleSaveContent(outputData)
}
});
})
})
$('#release-editor').on('click', () => {
this.editor.save().then(outputData => {
alertConfirmAction('Publicar contenido').then(result => {
if (result.isConfirmed) {
this.handlePublishContent(outputData)
}
console.log(result)
});
}).catch((error) => {
alertError(this.get_lang('need_editor_to_save')).fire()
})
})
$('#preview-editor').on('click', () => {
this.editor.readOnly.toggle()
$('#edit-editor').removeClass('d-none')
$('#preview-editor').addClass('d-none')
$('#release-editor').attr('disabled', 'disabled')
$('#save-editor').attr('disabled', 'disabled')
})
$('#edit-editor').on('click', () => {
$('#edit-editor').addClass('d-none')
$('#preview-editor').removeClass('d-none')
$('#release-editor').attr('disabled', null)
$('#save-editor').attr('disabled', null)
this.editor.readOnly.toggle()
})
this.handleGetData();
} catch (reason) {
console.log(`Editor.js initialization failed because of ${reason}`)
}
}
handleGetData() {
const ajax = new Ajax(`/wiki/section/${this.sectionId}`,
null,
null,
this.handleGetDataSuccess.bind(this),
this.handleGetDataError.bind(this))
ajax.get()
}
handleGetDataSuccess(response) {
if (response.data.contents?.editor_data) {
alertSuccess(response.message).fire()
this.renderContent(response.data.contents.editor_data)
} else {
alertWarning('No hay contenido').fire()
}
}
handleGetDataError(error) {
console.log(error)
}
handleGetDataPublished() {
const ajax = new Ajax(`/wiki/section/${this.sectionId}`,
null,
null,
this.handleGetDataPublishedSuccess.bind(this),
this.handleGetDataPublishedError.bind(this))
ajax.get()
}
handleGetDataPublishedSuccess(response) {
if (response.data.contents?.published_data) {
alertSuccess(response.message).fire()
this.renderContent(response.data.contents.published_data)
} else {
alertWarning(this.get_lang('no_content')).fire()
}
}
handleGetDataPublishedError(error) {
alertError(error.error).fire()
}
renderContent(content) {
try {
let parsedContent = JSON.parse(content)
this.editor.render(parsedContent)
this.centerImages()
} catch (error) {
console.log(error)
}
}
handleSaveContent(data) {
const ajax = new Ajax(`/wiki/save/${this.sectionId}`,
data,
null,
this.handleSaveContentSuccess.bind(this),
this.handleSaveContentError.bind(this))
ajax.post()
}
handleSaveContentSuccess(response) {
this.handleGetData()
}
handleSaveContentError(response) { }
handlePublishContent(data) {
const ajax = new Ajax(`/wiki/publish/${this.sectionId}`,
data,
null,
this.handleSaveContentSuccess.bind(this),
this.handleSaveContentError.bind(this))
ajax.post()
}
handleSaveContentSuccess(response) {
this.handleGetData()
}
handleSaveContentError(response) { }
centerImages(){
setInterval(() => {
$(".image-tool img").css('margin','0 auto')
$(".image-tool__caption").css('margin','0 auto').css('border','none').css('text-align','center').css('box-shadow','none')
},500)
}
}
export default WikiEditor

View File

@ -0,0 +1,65 @@
export const dataExample = {
"time": 1739862579806,
"blocks": [
{
"id": "uynEMELQjD",
"type": "header",
"data": {
"text": "Presupuesto",
"level": 1
}
},
{
"id": "5lBp4qELvp",
"type": "header",
"data": {
"text": "Introducción",
"level": 4
}
},
{
"id": "cf8xhN8Zo_",
"type": "paragraph",
"data": {
"text": "\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam lacinia\n diam ut tincidunt ornare. Suspendisse bibendum, velit ut mollis \nplacerat, tellus ante iaculis mi, ut luctus erat ligula sed est. Integer\n aliquam mauris eu diam tristique viverra. Proin id sem nisi. Praesent \ngravida tortor ac aliquam faucibus. Curabitur faucibus, magna vel \nconsectetur luctus, arcu diam iaculis lectus, posuere vehicula nisl \nnulla eget nibh.<b> Praesent tincidunt</b> enim condimentum interdum sodales. \nCurabitur eu diam sed mauris dignissim vestibulum eu quis justo. Nam \nornare odio id tincidunt aliquam. Quisque quis elit quis sem blandit \nimperdiet euismod non quam. Mauris maximus elit eu elementum consequat. \nSed nec cursus nibh. Nullam accumsan enim in tortor semper, in dapibus \nquam sagittis. In volutpat nisl libero, et pharetra dui luctus vitae. \nProin sit amet ornare mi. Sed vulputate ligula at elit posuere, non \nultrices risus ornare.\n"
}
},
{
"id": "2SnovwX73t",
"type": "header",
"data": {
"text": "Configuracion",
"level": 4
}
},
{
"id": "Sz_mcM8_BZ",
"type": "paragraph",
"data": {
"text": "\nFusce varius, erat vitae egestas elementum, ante turpis tincidunt erat, \nsit amet scelerisque massa nisl vel mauris. Cras eu condimentum magna, \nac finibus orci. Donec convallis sapien nulla, ac porta massa elementum \nsed. Nulla elit urna, ornare id quam ac, luctus commodo sem. Nam a \nsemper lectus. Sed eget ex varius, pretium ante ac, ultricies est. Sed \nut est leo. In velit lorem, vestibulum id sodales eu, vulputate et \nlectus. Mauris elementum lectus consequat cursus sollicitudin. Proin in \nsagittis turpis. Pellentesque ut pulvinar mi. Nullam nec purus vel enim \nconvallis rutrum. Pellentesque sem elit, fringilla quis accumsan quis, \nconvallis eu est. Nam malesuada nulla volutpat dui lobortis, vitae \nauctor turpis faucibus.\n"
}
},
{
"id": "Alu9XBLH6v",
"type": "paragraph",
"data": {
"text": "\nMaecenas sit amet iaculis diam. Aliquam sed feugiat neque. Proin et \npellentesque mi, et elementum risus. Sed volutpat, nibh quis finibus \nvenenatis, augue magna mollis est, vel efficitur nulla ligula et urna. \nVestibulum dictum molestie orci, vel pretium sapien fringilla eget. \nPellentesque fringilla facilisis congue. Proin bibendum dui non nisl \nornare rhoncus. Nam sit amet eros est. Integer luctus molestie lacus. \nPraesent elementum condimentum bibendum. Donec lacus nibh, sagittis in \ntempor nec, lacinia lacinia erat. Mauris pulvinar erat non nulla \npharetra dignissim. Quisque commodo dolor a neque lacinia porttitor. \nCras rhoncus ligula nibh.\n"
}
},
{
"id": "DLkw7hyvB1",
"type": "paragraph",
"data": {
"text": "\nPellentesque condimentum ullamcorper faucibus. Quisque vel enim et urna \nvenenatis faucibus at suscipit ipsum. Suspendisse nec nisi sit amet \ntortor suscipit volutpat ac eget mauris. Curabitur arcu arcu, vehicula \nin malesuada vitae, dapibus sit amet massa. Vivamus nec ex porttitor, \nvehicula nunc et, euismod velit. Maecenas dolor tortor, cursus quis \npurus ut, vulputate malesuada nunc. Morbi hendrerit auctor nisi. In \nfaucibus nibh sed quam suscipit lacinia. Cras placerat ornare lorem \nhendrerit blandit. Lorem ipsum dolor sit amet, consectetur adipiscing \nelit. Aliquam porttitor nisl ut arcu pretium, a sodales est iaculis. \nCras commodo sit amet tortor a rutrum. Mauris tristique magna nibh, quis\n bibendum tortor feugiat eget.\n"
}
},
{
"id": "3Ph1J1DR_5",
"type": "paragraph",
"data": {
"text": "\nPraesent iaculis tellus in quam rutrum ultrices. Phasellus ultricies \nlacus in neque volutpat iaculis. Aliquam at egestas tellus, vitae \nsollicitudin sapien. Sed commodo tellus ut ligula elementum iaculis. \nCurabitur sodales consequat dui, et tincidunt nunc vehicula vitae. \nSuspendisse aliquam justo a ante pellentesque ultrices ut in turpis. \nClass aptent taciti sociosqu ad litora torquent per conubia nostra, per \ninceptos himenaeos. Mauris scelerisque mattis tortor, in molestie neque \nbibendum a. Curabitur ut tempor erat. Donec aliquam scelerisque ipsum, \nbibendum placerat ante efficitur at. Sed malesuada, eros ut posuere \nvenenatis, leo est sagittis mi, at pretium augue tortor id ligula. Ut \negestas eget sapien a rhoncus. Ut lectus arcu, consequat posuere ipsum \nin, mollis iaculis augue.\n"
}
}
],
"version": "2.31.0-rc.8"
}

View File

@ -0,0 +1,155 @@
export const es = {
messages: {
/**
* Other below: translation of different UI components of the editor.js core
*/
ui: {
"blockTunes": {
"toggler": {
"Click to tune": "Click para editar",
"or drag to move": "o arrastra para editar",
"Filter" : "Filtrar"
},
"filter" : {
"Filter" : "Filtrar"
}
},
"inlineToolbar": {
"converter": {
"Convert to": "Convertir a",
"Filter" : "Filtrar"
},
},
"toolbar": {
"toolbox": {
"Add": "Añadir",
"Filter" : "Filtrar"
},
}
},
/**
* Section for translation Tool Names: both block and inline tools
*/
toolNames: {
"Text": "Texto",
"Heading": "Encabezado",
"List": "Lista",
"Unordered List": "Lista",
"Ordered List" : "Enumeración",
"Warning": "Advertencia",
"Checklist": "Checklist",
"Quote": "Cita",
"Code": "Codigo",
"Nested Checklist": "Lista anidada",
"Delimiter": "Delimitador",
"Raw HTML": "Raw HTML",
"Table": "Tabla",
"Link": "Enlace",
"Marker": "Subrayar",
"Bold": "Negrita",
"Italic": "Cursiva",
"InlineCode": "Código",
"Image" : "Imagen",
"Alert" : "Alerta",
"Convert to" : "Convertir a",
"TOC" : "Tabla de contenidos"
},
/**
* Section for passing translations to the external tools classes
*/
tools: {
/**
* Each subsection is the i18n dictionary that will be passed to the corresponded plugin
* The name of a plugin should be equal the name you specify in the 'tool' section for that plugin
*/
"warning": { // <-- 'Warning' tool will accept this dictionary section
"Title": "Titulo",
"Message": "Mensaje",
},
"list" : {
"Start with" : "Empezar con",
"Unordered": "Sin orden",
"Ordered" : "Ordenada",
"Counter type" : "Contador",
"Convert to" : "Convertir a",
},
"toc" : {
'Refresh' : "Actualizar",
},
"table" : {
"Add column to left" : "Añadir columna a la izquierda",
"Add column to right" : "Añadir columna a la derecha",
"Delete column" : "Eliminar columna",
"Delete row" : "Eliminar fila",
"Without headings" : "Sin encabezados",
"With headings" : "Con encabezados",
"Stretch" : "Ampliar",
"Add row above" : "Añadir fila arriba",
"Add row below" : "Añadir fila abajo",
},
/**
* Link is the internal Inline Tool
*/
"link": {
"Add a link": "Añadir un enlace"
},
/**
* The "stub" is an internal block tool, used to fit blocks that does not have the corresponded plugin
*/
"stub": {
'The block can not be displayed correctly.': 'El bloque no puede ser mostrado correctamente'
},
"alert" : {
"Primary" : "Color primario",
"Secondary" : "Color secundario",
"Info" : "Info",
"Success" : "Éxito",
"Warning" : "Advertencia",
"Danger" : "Peligro",
"Light" : "Ligero",
"Left" : "Izquierda",
"Right" :"Derecha",
"Dark" : "Oscuro",
"Center" : "Centrar"
},
"image": {
"With border" : "Con borde",
"Stretch image" : "Estirar imagen",
"With background" : "Añadir fondo",
"Select an Image" : "Seleccione una imagen",
"With caption" : "Con título"
}
},
/**
* Section allows to translate Block Tunes
*/
blockTunes: {
/**
* Each subsection is the i18n dictionary that will be passed to the corresponded Block Tune plugin
* The name of a plugin should be equal the name you specify in the 'tunes' section for that plugin
*
* Also, there are few internal block tunes: "delete", "moveUp" and "moveDown"
*/
"delete": {
"Delete": "Eliminar",
"Click to delete" : "Click para eliminar"
},
"moveUp": {
"Move up": "Mover hacia arriba"
},
"moveDown": {
"Move down": "Mover hacia abajo"
}
},
}
}

View File

@ -0,0 +1,168 @@
import Ajax from "../ajax.js";
import { alertConfirmAction, alertConfirmationDelete, alertError, alertSuccess } from "../alerts/sweetAlert.js";
import Modal from "../modal.js"
import { getTablerIconClassName } from "../../common/common.js";
class WikiSectionForm {
constructor() {
this.item = $("#formSection")
this.btnNew = $("#submit-new-section")
this.btnUpdate = $("#submit-update-section")
this.btnDelete = $("#delete-section")
this.name = this.item.find('#section-name')
this.icon = this.item.find('#section-icon')
this.roles = this.item.find('#section-roles').select2({
dropdownParent: this.item.parent()
})
this.newSectionBtn = $("#new-section")
this.editSectionBtn = $("#edit-section")
this.modalSection = $("#modalSection")
this.modal = new Modal(this.modalSection)
this.icons = getTablerIconClassName().map((e, index) => {
return { 'text': e, 'id': `ti ${e}` }
})
}
renderIcon(option) {
var icon = `<i class="ti ${option.text}"></i> ${option.text}`;
return icon;
}
init() {
this.icon.wrap('<div class="position-relative"></div>').select2({
data: this.icons,
dropdownParent: this.icon.parent(),
templateResult: this.renderIcon,
templateSelection: this.renderIcon,
escapeMarkup: function (m) { return m; }
})
this.icon.on('change', () => {
console.log(this.icon.val())
})
this.btnNew.on('click', this.post.bind(this))
this.btnUpdate.on('click', this.update.bind(this))
this.btnDelete.on('click',this.deleteConfirm.bind(this))
this.newSectionBtn.on('click', () => {
this.initPost()
})
this.editSectionBtn.on('click', () => {
this.initUpdate()
})
}
setId(id) {
this.sectionId = id
}
initPost() {
this.showPost()
}
initUpdate() {
this.showUpdate()
}
getFormData() {
return {
name: this.name.val(),
icon: this.icon.val(),
roles: this.roles.val()
}
}
post() {
const ajax = new Ajax('/wiki/section',
this.getFormData(),
null,
this.success.bind(this),
this.error.bind(this)
)
ajax.post()
}
update() {
const ajax = new Ajax('/wiki/update/section',
{
wiki_section_id: this.sectionId,
...this.getFormData()
},
null,
this.success.bind(this),
this.error.bind(this)
)
ajax.post()
}
deleteConfirm() {
alertConfirmAction('Eliminar sección').then(result => {
if (result.isConfirmed) {
this.delete()
}
console.log(result)
});
}
delete() {
const ajax = new Ajax('/wiki/section/' + this.sectionId,
null,
null,
this.success.bind(this),
this.error.bind(this)
)
ajax.delete()
}
success(response) {
alertSuccess(response.message).fire()
this.modal.toggle()
}
error(error) {
alertError(error?.message).fire()
}
showPost() {
this.empty()
this.btnNew.removeClass('d-none')
this.btnUpdate.addClass('d-none')
$("#modalTitleNew").removeClass('d-none')
$("#modalTitleEdit").addClass('d-none')
this.modal.toggle()
}
async showUpdate() {
this.empty()
const sectionData = await this.handleShowSection()
if (sectionData) {
console.log(sectionData)
this.name.val(sectionData.data.name)
this.icon.val(sectionData.data.icon).trigger('change')
this.roles.val(sectionData.data.roles_array).trigger('change')
}
this.btnUpdate.removeClass('d-none')
this.btnNew.addClass('d-none')
$("#modalTitleNew").addClass('d-none')
$("#modalTitleEdit").removeClass('d-none')
this.modal.toggle()
}
handleShowSection() {
return new Promise((resolve, reject) => {
const ajax = new Ajax('/wiki/section/' + this.sectionId,
null,
null,
(response) => {
resolve(response)
},
(error) => {
alertError(error.message).fire()
resolve(false)
}
)
ajax.get()
})
}
empty() {
this.name.val(null)
this.icon.val("").trigger('change')
this.roles.val("").trigger('change')
}
}
export default WikiSectionForm

View File

@ -0,0 +1,37 @@
import "../../../../../themes/vuexy/vendor/libs/sortablejs/sortable.js"
class ListSortable{
constructor(listItem,config = {}) {
this.list = Sortable.create($(listItem)[0], {
animation: 150,
group: 'menuSorting',
handle: '.drag-handle',
direction : 'vertical',
...config,
});
}
getArray(){
const list = this.list.toArray()
return list
}
onChange(callback)
{
this.list.option('onChange',callback)
}
onMove(callback)
{
this.list.option('onMove',callback)
}
onEnd(callback)
{
this.list.option('onEnd',callback)
}
}
export default ListSortable

View File

@ -9,6 +9,7 @@ class Modal{
show(){
this.item.modal('show');
}
}
export default Modal;

View File

@ -0,0 +1,13 @@
import WikiEditor from "../../components/editorjs/WikiEditor.js"
import Ajax from "../../components/ajax.js"
$(async () => {
try {
const editor = new WikiEditor()
await editor.init()
} catch (error) {
}
})

View File

@ -0,0 +1,25 @@
import Ajax from "../../components/ajax.js"
import { alertError, alertSuccess } from "../../components/alerts/sweetAlert.js"
import ListSortable from "../../components/listSortable.js"
$(() => {
const menuSectionList = new ListSortable('#menu-inner-list')
menuSectionList.onEnd(updateWikiSectionOrder)
})
const updateWikiSectionOrder = (evt) =>
{
let list = new ListSortable('#menu-inner-list')
const ajax = new Ajax('/wiki/section/update/order',
{
orders : list.getArray()
},
null,
(response) => alertSuccess(response.message).fire(),
(error) => alertError(error.message).fire(),
)
ajax.post();
}

View File

@ -0,0 +1,13 @@
import WikiEditor from "../../components/editorjs/WikiEditor.js"
$(async() => {
try {
const editor = new WikiEditor()
await editor.initViewOnly()
} catch (error) {
}
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/@editorjs/header@2.8.8/dist/header.umd.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".ce-header{padding:.6em 0 3px;margin:0;line-height:1.25em;outline:none}.ce-header p,.ce-header div{padding:0!important;margin:0!important}")),document.head.appendChild(e)}}catch(n){console.error("vite-plugin-css-injected-by-js",n)}})();
(function(n,s){typeof exports=="object"&&typeof module<"u"?module.exports=s():typeof define=="function"&&define.amd?define(s):(n=typeof globalThis<"u"?globalThis:n||self,n.Header=s())})(this,function(){"use strict";const n="",s='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M19 17V10.2135C19 10.1287 18.9011 10.0824 18.836 10.1367L16 12.5"/></svg>',a='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 11C16 10 19 9.5 19 12C19 13.9771 16.0684 13.9997 16.0012 16.8981C15.9999 16.9533 16.0448 17 16.1 17L19.3 17"/></svg>',h='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 11C16 10.5 16.8323 10 17.6 10C18.3677 10 19.5 10.311 19.5 11.5C19.5 12.5315 18.7474 12.9022 18.548 12.9823C18.5378 12.9864 18.5395 13.0047 18.5503 13.0063C18.8115 13.0456 20 13.3065 20 14.8C20 16 19.5 17 17.8 17C17.8 17 16 17 16 16.3"/></svg>',d='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M18 10L15.2834 14.8511C15.246 14.9178 15.294 15 15.3704 15C16.8489 15 18.7561 15 20.2 15M19 17C19 15.7187 19 14.8813 19 13.6"/></svg>',u='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 15.9C16 15.9 16.3768 17 17.8 17C19.5 17 20 15.6199 20 14.7C20 12.7323 17.6745 12.0486 16.1635 12.9894C16.094 13.0327 16 12.9846 16 12.9027V10.1C16 10.0448 16.0448 10 16.1 10H19.8"/></svg>',g='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M19.5 10C16.5 10.5 16 13.3285 16 15M16 15V15C16 16.1046 16.8954 17 18 17H18.3246C19.3251 17 20.3191 16.3492 20.2522 15.3509C20.0612 12.4958 16 12.6611 16 15Z"/></svg>',c='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M9 7L9 12M9 17V12M9 12L15 12M15 7V12M15 17L15 12"/></svg>';/**
* Header block for the Editor.js.
*
* @author CodeX (team@ifmo.su)
* @copyright CodeX 2018
* @license MIT
* @version 2.0.0
*/class v{constructor({data:e,config:t,api:i,readOnly:r}){this.api=i,this.readOnly=r,this._settings=t,this._data=this.normalizeData(e),this._element=this.getTag()}get _CSS(){return{block:this.api.styles.block,wrapper:"ce-header"}}isHeaderData(e){return e.text!==void 0}normalizeData(e){const t={text:"",level:this.defaultLevel.number};return this.isHeaderData(e)&&(t.text=e.text||"",e.level!==void 0&&!isNaN(parseInt(e.level.toString()))&&(t.level=parseInt(e.level.toString()))),t}render(){return this._element}renderSettings(){return this.levels.map(e=>({icon:e.svg,label:this.api.i18n.t(`Heading ${e.number}`),onActivate:()=>this.setLevel(e.number),closeOnActivate:!0,isActive:this.currentLevel.number===e.number,render:()=>document.createElement("div")}))}setLevel(e){this.data={level:e,text:this.data.text}}merge(e){this._element.insertAdjacentHTML("beforeend",e.text)}validate(e){return e.text.trim()!==""}save(e){return{text:e.innerHTML,level:this.currentLevel.number}}static get conversionConfig(){return{export:"text",import:"text"}}static get sanitize(){return{level:!1,text:{}}}static get isReadOnlySupported(){return!0}get data(){return this._data.text=this._element.innerHTML,this._data.level=this.currentLevel.number,this._data}set data(e){if(this._data=this.normalizeData(e),e.level!==void 0&&this._element.parentNode){const t=this.getTag();t.innerHTML=this._element.innerHTML,this._element.parentNode.replaceChild(t,this._element),this._element=t}e.text!==void 0&&(this._element.innerHTML=this._data.text||"")}getTag(){const e=document.createElement(this.currentLevel.tag);return e.innerHTML=this._data.text||"",e.classList.add(this._CSS.wrapper),e.contentEditable=this.readOnly?"false":"true",e.dataset.placeholder=this.api.i18n.t(this._settings.placeholder||""),e}get currentLevel(){let e=this.levels.find(t=>t.number===this._data.level);return e||(e=this.defaultLevel),e}get defaultLevel(){if(this._settings.defaultLevel){const e=this.levels.find(t=>t.number===this._settings.defaultLevel);if(e)return e;console.warn("(ง'̀-'́)ง Heading Tool: the default level specified was not found in available levels")}return this.levels[1]}get levels(){const e=[{number:1,tag:"H1",svg:s},{number:2,tag:"H2",svg:a},{number:3,tag:"H3",svg:h},{number:4,tag:"H4",svg:d},{number:5,tag:"H5",svg:u},{number:6,tag:"H6",svg:g}];return this._settings.levels?e.filter(t=>this._settings.levels.includes(t.number)):e}onPaste(e){const t=e.detail;if("data"in t){const i=t.data;let r=this.defaultLevel.number;switch(i.tagName){case"H1":r=1;break;case"H2":r=2;break;case"H3":r=3;break;case"H4":r=4;break;case"H5":r=5;break;case"H6":r=6;break}this._settings.levels&&(r=this._settings.levels.reduce((o,l)=>Math.abs(l-r)<Math.abs(o-r)?l:o)),this.data={level:r,text:i.innerHTML}}}static get pasteConfig(){return{tags:["H1","H2","H3","H4","H5","H6"]}}static get toolbox(){return{icon:c,title:"Heading"}}}return v});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/@editorjs/warning@1.4.1/dist/warning.umd.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(`.cdx-warning{position:relative}@media all and (min-width: 736px){.cdx-warning{padding-left:36px}}.cdx-warning [contentEditable=true][data-placeholder]:before{position:absolute;content:attr(data-placeholder);color:#707684;font-weight:400;opacity:0}.cdx-warning [contentEditable=true][data-placeholder]:empty:before{opacity:1}.cdx-warning [contentEditable=true][data-placeholder]:empty:focus:before{opacity:0}.cdx-warning:before{content:"";background-image:url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='5' y='5' width='14' height='14' rx='4' stroke='black' stroke-width='2'/%3E%3Cline x1='12' y1='9' x2='12' y2='12' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3Cpath d='M12 15.02V15.01' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");width:24px;height:24px;background-size:24px 24px;position:absolute;margin-top:8px;left:0}@media all and (max-width: 735px){.cdx-warning:before{display:none}}.cdx-warning__message{min-height:85px}.cdx-warning__title{margin-bottom:6px}`)),document.head.appendChild(e)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}})();
(function(r,n){typeof exports=="object"&&typeof module<"u"?module.exports=n():typeof define=="function"&&define.amd?define(n):(r=typeof globalThis<"u"?globalThis:r||self,r.Warning=n())})(this,function(){"use strict";const r='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><rect width="14" height="14" x="5" y="5" stroke="currentColor" stroke-width="2" rx="4"/><line x1="12" x2="12" y1="9" y2="12" stroke="currentColor" stroke-linecap="round" stroke-width="2"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M12 15.02V15.01"/></svg>',n="";class a{static get isReadOnlySupported(){return!0}static get toolbox(){return{icon:r,title:"Warning"}}static get enableLineBreaks(){return!0}static get DEFAULT_TITLE_PLACEHOLDER(){return"Title"}static get DEFAULT_MESSAGE_PLACEHOLDER(){return"Message"}get CSS(){return{baseClass:this.api.styles.block,wrapper:"cdx-warning",title:"cdx-warning__title",input:this.api.styles.input,message:"cdx-warning__message"}}constructor({data:t,config:e,api:s,readOnly:i}){this.api=s,this.readOnly=i,this.titlePlaceholder=(e==null?void 0:e.titlePlaceholder)||a.DEFAULT_TITLE_PLACEHOLDER,this.messagePlaceholder=(e==null?void 0:e.messagePlaceholder)||a.DEFAULT_MESSAGE_PLACEHOLDER,this.data={title:t.title||"",message:t.message||""}}render(){const t=this._make("div",[this.CSS.baseClass,this.CSS.wrapper]),e=this._make("div",[this.CSS.input,this.CSS.title],{contentEditable:!this.readOnly,innerHTML:this.data.title}),s=this._make("div",[this.CSS.input,this.CSS.message],{contentEditable:!this.readOnly,innerHTML:this.data.message});return e.dataset.placeholder=this.titlePlaceholder,s.dataset.placeholder=this.messagePlaceholder,t.appendChild(e),t.appendChild(s),t}save(t){const e=t.querySelector(`.${this.CSS.title}`),s=t.querySelector(`.${this.CSS.message}`);return Object.assign(this.data,{title:(e==null?void 0:e.innerHTML)??"",message:(s==null?void 0:s.innerHTML)??""})}_make(t,e=null,s={}){const i=document.createElement(t);Array.isArray(e)?i.classList.add(...e):e&&i.classList.add(e);for(const l in s)i[l]=s[l];return i}static get sanitize(){return{title:{},message:{}}}}return a});

File diff suppressed because one or more lines are too long