diff --git a/formwork/src/Panel/Controllers/AbstractController.php b/formwork/src/Panel/Controllers/AbstractController.php index 869a8532b..9484ab66b 100644 --- a/formwork/src/Panel/Controllers/AbstractController.php +++ b/formwork/src/Panel/Controllers/AbstractController.php @@ -5,8 +5,7 @@ use Formwork\Cms\Site; use Formwork\Controllers\AbstractController as BaseAbstractController; use Formwork\Panel\Modals\Modal; -use Formwork\Panel\Modals\ModalCollection; -use Formwork\Panel\Modals\ModalFactory; +use Formwork\Panel\Modals\Modals; use Formwork\Panel\Panel; use Formwork\Parsers\Json; use Formwork\Router\Router; @@ -18,20 +17,16 @@ abstract class AbstractController extends BaseAbstractController { - protected ModalCollection $modals; - public function __construct( private Container $container, protected readonly Router $router, protected readonly CsrfToken $csrfToken, protected readonly Translations $translations, - protected readonly ModalFactory $modalFactory, + protected readonly Modals $modals, protected readonly Site $site, protected readonly Panel $panel, ) { $this->container->call(parent::__construct(...)); - - $this->modals = new ModalCollection(); } /** @@ -65,8 +60,8 @@ protected function hasPermission(string $permission): bool */ protected function modal(string $name): Modal { - $this->modals->add($modal = $this->modalFactory->make($name)); - return $modal; + $this->modals->add($name); + return $this->modals->get($name); } /** diff --git a/formwork/src/Panel/Controllers/DashboardController.php b/formwork/src/Panel/Controllers/DashboardController.php index 735a80f0e..e97a58509 100644 --- a/formwork/src/Panel/Controllers/DashboardController.php +++ b/formwork/src/Panel/Controllers/DashboardController.php @@ -17,10 +17,6 @@ public function index(Statistics $statistics): Response return $this->forward(ErrorsController::class, 'forbidden'); } - $this->modal('newPage'); - - $this->modal('deletePage'); - return new Response($this->view('dashboard.index', [ 'title' => $this->translate('panel.dashboard.dashboard'), 'lastModifiedPages' => $this->view('pages.tree', [ diff --git a/formwork/src/Panel/Controllers/OptionsController.php b/formwork/src/Panel/Controllers/OptionsController.php index b82c68c1f..cccd04c88 100644 --- a/formwork/src/Panel/Controllers/OptionsController.php +++ b/formwork/src/Panel/Controllers/OptionsController.php @@ -66,8 +66,6 @@ public function systemOptions(Schemes $schemes): Response $fields->setValues($this->config->get('system')); - $this->modal('changes'); - return new Response($this->view('options.system', [ 'title' => $this->translate('panel.options.options'), 'tabs' => $this->view('options.tabs', [ @@ -111,8 +109,6 @@ public function siteOptions(Schemes $schemes): Response $fields->setValues($this->site->data()); - $this->modal('changes'); - return new Response($this->view('options.site', [ 'title' => $this->translate('panel.options.options'), 'tabs' => $this->view('options.tabs', [ diff --git a/formwork/src/Panel/Controllers/PagesController.php b/formwork/src/Panel/Controllers/PagesController.php index 43768e76c..9343c1c60 100644 --- a/formwork/src/Panel/Controllers/PagesController.php +++ b/formwork/src/Panel/Controllers/PagesController.php @@ -39,10 +39,6 @@ public function index(): Response return $this->forward(ErrorsController::class, 'forbidden'); } - $this->modal('newPage'); - - $this->modal('deletePage'); - $pages = $this->site->pages(); $indexOffset = $pages->indexOf($this->site->indexPage()); @@ -194,17 +190,7 @@ public function edit(RouteParams $routeParams): Response return $this->redirect($this->generateRoute('panel.pages.edit', ['page' => $page->route()])); } - $this->modal('images'); - - $this->modal('link'); - - $this->modal('changes'); - - $this->modal('deletePage'); - - $this->modal('deleteFile'); - - $this->modal('renameFile'); + $this->modal('images')->setFieldsModel($page); $contentHistory = $page->contentPath() ? new ContentHistory($page->contentPath()) @@ -552,10 +538,6 @@ public function file(RouteParams $routeParams): Response return $this->redirect($this->generateRoute('panel.pages.file', ['page' => $page->route(), 'filename' => $filename])); } - $this->modal('renameFile'); - $this->modal('deleteFile'); - $this->modal('changes'); - return new Response($this->view('pages.file', [ 'title' => $file->name(), 'page' => $page, diff --git a/formwork/src/Panel/Controllers/ToolsController.php b/formwork/src/Panel/Controllers/ToolsController.php index 5bac06fc8..5892d8703 100644 --- a/formwork/src/Panel/Controllers/ToolsController.php +++ b/formwork/src/Panel/Controllers/ToolsController.php @@ -49,8 +49,6 @@ public function backups(): Response 'size' => FileSystem::formatSize(FileSystem::size($path)), ]); - $this->modal('deleteFile'); - return new Response($this->view('tools.backups', [ 'title' => $this->translate('panel.tools.backups'), 'tabs' => $this->view('tools.tabs', [ diff --git a/formwork/src/Panel/Controllers/UsersController.php b/formwork/src/Panel/Controllers/UsersController.php index 0765ba37d..c5cb8c846 100644 --- a/formwork/src/Panel/Controllers/UsersController.php +++ b/formwork/src/Panel/Controllers/UsersController.php @@ -30,10 +30,6 @@ public function index(): Response return $this->forward(ErrorsController::class, 'forbidden'); } - $this->modal('newUser'); - - $this->modal('deleteUser'); - return new Response($this->view('users.index', [ 'title' => $this->translate('panel.users.users'), 'users' => $this->site->users()->sortBy('username'), @@ -198,12 +194,6 @@ public function profile(RouteParams $routeParams): Response $fields = $fields->setValues($user); - $this->modal('changes'); - - $this->modal('deleteUser'); - - $this->modal('deleteUserImage'); - return new Response($this->view('users.profile', [ 'title' => $this->translate('panel.users.userProfile', $user->username()), 'user' => $user, diff --git a/formwork/src/Panel/Modals/Modal.php b/formwork/src/Panel/Modals/Modal.php index 69f61e836..4389cbeef 100644 --- a/formwork/src/Panel/Modals/Modal.php +++ b/formwork/src/Panel/Modals/Modal.php @@ -6,6 +6,7 @@ use Formwork\Data\Traits\DataArrayable; use Formwork\Fields\FieldCollection; use Formwork\Fields\FieldFactory; +use Formwork\Model\Model; use Formwork\Translations\Translation; use Formwork\Utils\Arr; use Formwork\Utils\Str; @@ -20,6 +21,11 @@ class Modal implements Arrayable */ protected string $id; + /** + * Fields model + */ + protected Model $fieldsModel; + /** * Modal buttons */ @@ -96,9 +102,21 @@ public function fields(): FieldCollection // @phpstan-ignore argument.templateType $fieldCollection->setMultiple(Arr::map($this->data['fields'] ?? [], fn($data, $name) => $this->fieldFactory->make($this->id . '.' . $name, $data, $fieldCollection))); + if (isset($this->fieldsModel)) { + $fieldCollection->setModel($this->fieldsModel); + } + return $fieldCollection; } + /** + * Set fields model + */ + public function setFieldsModel(Model $model): void + { + $this->fieldsModel = $model; + } + /** * Get modal buttons */ diff --git a/formwork/src/Panel/Modals/ModalCollection.php b/formwork/src/Panel/Modals/ModalCollection.php index 293d9d1f2..6e1ae6886 100644 --- a/formwork/src/Panel/Modals/ModalCollection.php +++ b/formwork/src/Panel/Modals/ModalCollection.php @@ -6,6 +6,8 @@ class ModalCollection extends AbstractCollection { + protected bool $associative = true; + protected ?string $dataType = Modal::class; protected bool $mutable = true; diff --git a/formwork/src/Panel/Modals/Modals.php b/formwork/src/Panel/Modals/Modals.php new file mode 100644 index 000000000..34690a9ed --- /dev/null +++ b/formwork/src/Panel/Modals/Modals.php @@ -0,0 +1,22 @@ +has($name)) { + $this->set($name, $this->modalFactory->make($name)); + } + } +} diff --git a/formwork/src/Services/Loaders/PanelServiceLoader.php b/formwork/src/Services/Loaders/PanelServiceLoader.php index de8380055..14ee9e460 100644 --- a/formwork/src/Services/Loaders/PanelServiceLoader.php +++ b/formwork/src/Services/Loaders/PanelServiceLoader.php @@ -8,6 +8,7 @@ use Formwork\Log\Registry; use Formwork\Panel\Controllers\ErrorsController; use Formwork\Panel\Modals\ModalFactory; +use Formwork\Panel\Modals\Modals; use Formwork\Panel\Panel; use Formwork\Panel\Security\AccessLimiter; use Formwork\Schemes\Schemes; @@ -41,6 +42,7 @@ public function load(Container $container): Panel $this->request->session()->setDuration($this->config->get('system.panel.sessionTimeout') * 60); $container->define(ModalFactory::class); + $container->define(Modals::class); return $container->build(Panel::class); } diff --git a/panel/src/scss/components/_modals.scss b/panel/src/scss/components/_modals.scss index b01d4de6e..89b7a988f 100644 --- a/panel/src/scss/components/_modals.scss +++ b/panel/src/scss/components/_modals.scss @@ -16,7 +16,7 @@ outline: 0; } -.modal.show { +.modal.open { display: flex; } diff --git a/panel/src/ts/components/form.ts b/panel/src/ts/components/form.ts index f5d37c15a..4e4bb49fd 100644 --- a/panel/src/ts/components/form.ts +++ b/panel/src/ts/components/form.ts @@ -39,12 +39,15 @@ export class Form { element.addEventListener("click", (event) => { if (this.hasChanged()) { event.preventDefault(); - app.modals["changesModal"].show(undefined, (modal) => { + + app.modals["changesModal"].onOpen((modal) => { const continueCommand = $("[data-command=continue]", modal.element); if (continueCommand) { continueCommand.dataset.href = element.href; } }); + + app.modals["changesModal"].open(); } }); }); @@ -60,34 +63,29 @@ export class Form { registerModalExceptions(); function registerModalExceptions() { - const changesModal = document.getElementById("changesModal"); - const deletePageModal = document.getElementById("deletePageModal"); - const deleteUserModal = document.getElementById("deleteUserModal"); + const changesModal = app.modals["changesModal"]; + const deletePageModal = app.modals["deletePageModal"]; + const deleteUserModal = app.modals["deleteUserModal"]; if (changesModal) { - const continueCommand = $("[data-command=continue]", changesModal); - if (continueCommand) { - continueCommand.addEventListener("click", function () { - removeBeforeUnload(); - if (this.dataset.href) { - window.location.href = this.dataset.href; - } - }); - } + changesModal.onCommand("continue", (_, button) => { + removeBeforeUnload(); + if (button?.dataset.href) { + window.location.href = button.dataset.href; + } + }); } if (deletePageModal) { - const deleteCommand = $("[data-command=delete]", deletePageModal); - if (deleteCommand) { - deleteCommand.addEventListener("click", removeBeforeUnload); - } + deletePageModal.onCommand("delete", () => { + removeBeforeUnload(); + }); } if (deleteUserModal) { - const deleteCommand = $("[data-command=delete]", deleteUserModal); - if (deleteCommand) { - deleteCommand.addEventListener("click", removeBeforeUnload); - } + deleteUserModal.onCommand("delete", () => { + removeBeforeUnload(); + }); } } } diff --git a/panel/src/ts/components/inputs/editor/markdown/menu.ts b/panel/src/ts/components/inputs/editor/markdown/menu.ts index 3a5a41da1..80c815253 100644 --- a/panel/src/ts/components/inputs/editor/markdown/menu.ts +++ b/panel/src/ts/components/inputs/editor/markdown/menu.ts @@ -118,6 +118,8 @@ function plugin(items: MenuItem[]) { } export function menuPlugin() { + initModals(); + return plugin([ { name: app.config.EditorInput.labels.paragraph, @@ -227,33 +229,26 @@ export function menuPlugin() { return canInsert(state, schema.nodes.image); } if (view) { - app.modals["imagesModal"].show(undefined, (modal) => { - const commandPickImage = $("[data-command=pick-image]", modal.element) as HTMLElement; + app.modals["imagesModal"].open(); + app.modals["imagesModal"].onCommand("pick-image", (modal) => { const selected = $(".image-picker-thumbnail.selected", modal.element); - if (selected) { - selected.classList.remove("selected"); + + if (selected && view) { + const baseUri = $("textarea", view.dom.parentNode!)!.dataset.baseUri; + const filename = selected.dataset.filename; + view.dispatch( + state.tr.replaceSelectionWith( + schema.nodes.image.createAndFill({ + src: `${baseUri}${filename}`, + alt: "", + })!, + ), + ); + view.focus(); } - const insertImage = () => { - const selected = $(".image-picker-thumbnail.selected", modal.element); - if (selected && view) { - const baseUri = $("textarea", view.dom.parentNode!)!.dataset.baseUri; - const filename = selected.dataset.filename; - view.dispatch( - state.tr.replaceSelectionWith( - schema.nodes.image.createAndFill({ - src: `${baseUri}${filename}`, - alt: "", - })!, - ), - ); - view.focus(); - } - modal.hide(); - commandPickImage.removeEventListener("click", insertImage); - }; - commandPickImage.addEventListener("click", insertImage); + modal.close(); }); } return true; @@ -271,31 +266,24 @@ export function menuPlugin() { toggleMark(schema.marks.link)(state, dispatch); return true; } - app.modals["linkModal"].show(undefined, (modal) => { - const insertLinkCommand = $("[data-command=insert-link]", modal.element) as HTMLElement; - const uri = $('[id="linkModal.uri"]') as HTMLInputElement; - uri.value = "https://"; - uri.setSelectionRange(8, 8); + app.modals["linkModal"].onOpen((modal) => { + const uriInput = $('[id="linkModal.uri"]', modal.element) as HTMLInputElement; + uriInput.value = "https://"; + uriInput.setSelectionRange(8, 8); + }); - const insertLink = () => { - if (view && uri.value) { - toggleMark(schema.marks.link, { href: uri.value })(view.state, view.dispatch); - view.focus(); - } - modal.hide(); - insertLinkCommand.removeEventListener("click", insertLink); - }; - - uri.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - insertLink(); - event.preventDefault(); - } - }); + app.modals["linkModal"].open(); - insertLinkCommand.addEventListener("click", insertLink); + app.modals["linkModal"].onCommand("insert-link", (modal) => { + const uriInput = $('[id="linkModal.uri"]', modal.element) as HTMLInputElement; + if (view && uriInput.value) { + toggleMark(schema.marks.link, { href: uriInput.value })(view.state, view.dispatch); + view.focus(); + } + modal.close(); }); + return true; }, dom: createButton("link", app.config.EditorInput.labels.link), @@ -375,3 +363,13 @@ function isMarkActive(state: EditorState, type: MarkType) { } return state.doc.rangeHasMark(from, to, type); } + +function initModals() { + const linkModal = app.modals["linkModal"]; + $('[id="linkModal.uri"]', linkModal.element)?.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + linkModal.triggerCommand("insert-link"); + event.preventDefault(); + } + }); +} diff --git a/panel/src/ts/components/inputs/image-picker.ts b/panel/src/ts/components/inputs/image-picker.ts index 2053ead5f..cf1406e23 100644 --- a/panel/src/ts/components/inputs/image-picker.ts +++ b/panel/src/ts/components/inputs/image-picker.ts @@ -1,9 +1,7 @@ import { $, $$ } from "../../utils/selectors"; - export class ImagePicker { constructor(element: HTMLSelectElement) { const options = $$("option", element); - const pickCommand = $("[data-command=pick-image]", (element.parentNode as ParentNode).parentNode ?? document); element.hidden = true; element.ariaHidden = "true"; @@ -19,8 +17,8 @@ export class ImagePicker { thumbnail.style.backgroundImage = `url(${option.dataset.thumbnail ?? option.value})`; thumbnail.dataset.uri = option.value; thumbnail.dataset.filename = option.text; - thumbnail.addEventListener("click", handleThumbnailClick); - thumbnail.addEventListener("dblclick", handleThumbnailDblclick); + thumbnail.addEventListener("click", () => pickImage(thumbnail)); + thumbnail.addEventListener("dblclick", () => pickImage(thumbnail)); container.appendChild(thumbnail); } @@ -28,37 +26,11 @@ export class ImagePicker { ($(".image-picker-empty-state") as HTMLElement).style.display = "none"; } - pickCommand?.addEventListener("click", function () { - const selectedThumbnail = $(".image-picker-thumbnail.selected"); - const targetId = this.dataset.target; - if (selectedThumbnail && targetId) { - const target = document.getElementById(targetId) as HTMLSelectElement; - const selectedThumbnailFilename = selectedThumbnail.dataset.filename; - if (target && selectedThumbnailFilename) { - target.value = selectedThumbnailFilename; - target.dispatchEvent(new Event("input", { bubbles: true })); - target.dispatchEvent(new Event("change", { bubbles: true })); - } - } - }); - - function handleThumbnailClick(this: HTMLElement) { + function pickImage(thumbnail: HTMLElement) { $$(".image-picker-thumbnail").forEach((element) => { element.classList.remove("selected"); }); - this.classList.add("selected"); - const targetId = ($("[data-command=pick-image]") as HTMLElement).dataset.target; - if (targetId) { - const target = document.getElementById(targetId) as HTMLSelectElement; - if (target) { - target.value = this.dataset.filename as string; - } - } - } - - function handleThumbnailDblclick(this: HTMLElement) { - this.click(); - $("[data-command=pick-image]")?.click(); + thumbnail.classList.add("selected"); } } } diff --git a/panel/src/ts/components/modal.ts b/panel/src/ts/components/modal.ts index bc507cde7..a867593a7 100644 --- a/panel/src/ts/components/modal.ts +++ b/panel/src/ts/components/modal.ts @@ -1,83 +1,102 @@ import { $, $$ } from "../utils/selectors"; import { Inputs } from "./inputs"; -function getFirstFocusableElement(parent: HTMLElement = document.body): HTMLElement { - return parent.querySelector("button, .button, input:not([type=hidden]), select, textarea") || parent; +interface ModalShowOptions { + action?: string; + triggerElement?: HTMLElement; } +interface ModalHideOptions { + triggerElement?: HTMLElement; +} + +type ModalCallback = (modal: Modal, triggerElement?: HTMLElement) => void; + +type ModalState = "open" | "closed"; + export class Modal { - element: HTMLElement; - inputs: Inputs; + readonly element: HTMLElement; + + readonly inputs: Inputs; + + readonly data: { [key: string]: unknown } = {}; + + private readonly callbacks: { [name: string]: ModalCallback } = {}; + + private state: ModalState = "closed"; constructor(element: HTMLElement) { this.element = element; - document.addEventListener("keyup", (event) => { - if (event.key === "Escape") { - this.hide(); - } - }); + new Inputs(this.element); - window.addEventListener("focus", () => getFirstFocusableElement(this.element).focus()); + this.registerEvents(); + } - this.inputs = new Inputs(this.element); + get isOpen() { + return this.state === "open"; + } - $("[data-dismiss]", element)?.addEventListener("click", () => this.hide()); + get isClosed() { + return this.state === "closed"; + } - let mousedownCaptured = false; - element.addEventListener("mousedown", (event) => { - mousedownCaptured = event.target === element; - }); - element.addEventListener("mouseup", (event) => { - if (mousedownCaptured && event.target === element) { - this.hide(); - } - mousedownCaptured = false; - }); + open(options: ModalShowOptions = {}) { + this.element.role = "dialog"; + this.element.ariaModal = "true"; + this.element.classList.add("open"); - document.addEventListener("click", (event) => { - const target = (event.target as HTMLElement).closest("[data-modal]") as HTMLDivElement; - if (target && target.dataset.modal === element.id) { - const modalAction = target.dataset.modalAction; - if (modalAction) { - this.show(modalAction); - } else { - this.show(); - } + if (options.action) { + const form = $("form", this.element) as HTMLFormElement | null; + if (form) { + form.action = options.action; } - }); - } - - show(action?: string, callback?: (modal: this) => void) { - const modal = this.element; - modal.role = "dialog"; - modal.ariaModal = "true"; - modal.classList.add("show"); - if (action) { - ($("form", modal) as HTMLFormElement).action = action; - } - (document.activeElement as HTMLElement).blur(); // Don't retain focus on any element - if ($("[autofocus]", modal)) { - ($("[autofocus]", modal) as HTMLFormElement).focus(); // Firefox bug - } else { - getFirstFocusableElement(modal).focus(); } - if (typeof callback === "function") { - callback(this); - } - $$(".tooltip").forEach((element) => element.parentNode && element.parentNode.removeChild(element)); + + (document.activeElement as HTMLElement | null)?.blur(); // Don't retain focus on any element + + this.getFirstFocusableElement(this.element)?.focus(); + + $$(".tooltip").forEach((tooltip) => tooltip.parentNode && tooltip.parentNode.removeChild(tooltip)); + this.createBackdrop(); + + this.state = "open"; + + this.dispatchCallback("open", options.triggerElement); } - hide() { + close(options: ModalHideOptions = {}) { const modal = this.element; - modal.classList.remove("show"); + + modal.classList.remove("open"); modal.role = null; modal.ariaModal = null; + this.removeBackdrop(); + + this.state = "closed"; + + this.dispatchCallback("close", options.triggerElement); } - createBackdrop() { + onOpen(callback: ModalCallback) { + this.callbacks["open"] = callback; + } + + onClose(callback: ModalCallback) { + this.callbacks["close"] = callback; + } + + onCommand(command: string, callback: ModalCallback) { + this.callbacks[`command-${command}`] = callback; + } + + triggerCommand(command: string, triggerElement?: HTMLElement) { + this.dispatchCallback(`command-${command}`, triggerElement); + } + + private createBackdrop() { if (!$(".modal-backdrop")) { const backdrop = document.createElement("div"); backdrop.className = "modal-backdrop"; @@ -85,10 +104,60 @@ export class Modal { } } - removeBackdrop() { + private removeBackdrop() { const backdrop = $(".modal-backdrop"); if (backdrop && backdrop.parentNode) { backdrop.parentNode.removeChild(backdrop); } } + + private dispatchCallback(name: string, triggerElement?: HTMLElement) { + const callback = this.callbacks[name]; + if (callback) { + callback(this, triggerElement); + } + } + + private registerEvents() { + document.addEventListener("click", (event) => { + const target = (event.target as HTMLElement).closest(`[data-modal="${this.element.id}"]`) as HTMLDivElement | null; + if (target) { + this.open({ action: target.dataset.modalAction, triggerElement: target }); + } + }); + + $$("[data-command]", this.element).forEach((commandButton) => commandButton.addEventListener("click", () => this.triggerCommand(commandButton.dataset.command as string, commandButton))); + + const dismissButton = $("[data-dismiss]", this.element); + dismissButton?.addEventListener("click", () => this.close({ triggerElement: dismissButton })); + + document.addEventListener("keyup", (event) => { + if (event.key === "Escape") { + this.close(); + } + }); + + let mousedownCaptured = false; + + this.element.addEventListener("mousedown", (event) => { + mousedownCaptured = event.target === this.element; + }); + + this.element.addEventListener("mouseup", (event) => { + if (mousedownCaptured && event.target === this.element) { + this.close(); + } + mousedownCaptured = false; + }); + + window.addEventListener("focus", () => { + if (this.element.classList.contains("open")) { + this.getFirstFocusableElement(this.element).focus(); + } + }); + } + + private getFirstFocusableElement(parent: HTMLElement = document.body): HTMLElement { + return parent.querySelector("[autofocus], button, .button, input:not([type=hidden]), select, textarea") || parent; + } } diff --git a/panel/src/ts/components/views/pages.ts b/panel/src/ts/components/views/pages.ts index ee28dc5e4..9cb6941cd 100644 --- a/panel/src/ts/components/views/pages.ts +++ b/panel/src/ts/components/views/pages.ts @@ -180,13 +180,17 @@ export class Pages { } } - $$("[data-modal=renameFileModal]").forEach((element) => { - element.addEventListener("click", () => { - const input = $('[id="renameFileModal.filename"]') as HTMLInputElement; - input.value = element.dataset.filename as string; - input.setSelectionRange(0, input.value.lastIndexOf(".")); + const renameFileModal = app.modals["renameFileModal"]; + + if (renameFileModal) { + renameFileModal.onOpen((modal, trigger) => { + if (trigger) { + const input = $('[id="renameFileModal.filename"]', modal.element) as HTMLInputElement; + input.value = trigger.dataset.filename as string; + input.setSelectionRange(0, input.value.lastIndexOf(".")); + } }); - }); + } $$("[data-command=replaceFile]").forEach((element) => { element.addEventListener("click", () => { diff --git a/panel/views/dashboard/index.php b/panel/views/dashboard/index.php index 70aec3189..583890e12 100644 --- a/panel/views/dashboard/index.php +++ b/panel/views/dashboard/index.php @@ -1,5 +1,7 @@ layout('panel') ?> +add('newPage') ?> +