diff --git a/.gitignore b/.gitignore index fb34d256..3734c73b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,12 @@ logs .patches codex.patch .env +TODO .codex -.playwright-mcp \ No newline at end of file +.playwright-mcp +package-lock.json +refactor_docs/ +neutral-criteria-task.md +.claude/settings.json +/.gigacode/ +/GIGACODE.md diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 3fa5069d..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "typescript.tsdk": "node_modules\\typescript\\lib", -} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index a27ce48f..600daeeb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,123 @@ -### Agent Instructions - -* **Install Required Modules**: Ensure all necessary dependencies are installed before proceeding. -* **Run the Project**: Launch the development server to confirm that the application starts correctly. -* **Execute the Build Process**: Run the build to verify that everything compiles without errors. -* **Use TSX for Execution**: Follow the TSX-based startup method when running the project. -* **Write Documentation Immediately**: All relevant documentation must be added to the `docs/` directory as you work. -* **Follow Best Practices for Naming and Comments**: All variable names, function names, and comments must follow clean code principles and be written in English. +# Adminizer - Project Context + +## Project Overview + +Adminizer is a framework-agnostic admin panel for Node.js. +It includes a TypeScript backend and a React frontend, and can be integrated into server-side applications with different architectures. + +## Key Characteristics + +- Type: Node.js library/module +- Backend: TypeScript (ESM) +- Frontend: React + TypeScript + Vite +- UI stack: Radix UI, Tailwind CSS v4, shadcn/ui style components +- ORM support: Sequelize (active), TypeORM (experimental) +- License: MIT + +## Current Development Focus + +- Primary ORM: Sequelize +- TypeORM support exists but is experimental; keep Sequelize as the default recommendation +- For model adapter changes, prioritize `src/lib/model/adapter/sequelize.ts` + +## Directory Structure + +```text +adminizer/ +|-- src/ # Main source code +| |-- assets/ # Frontend (React/TSX/CSS) +| |-- controllers/ # Express controllers +| |-- helpers/ # Utilities +| |-- interfaces/ # Types/interfaces +| |-- lib/ # Core modules +| |-- models/ # Internal models +| |-- policies/ # Access policies +| |-- system/ # System wiring +| `-- index.ts # Main entry +|-- fixture/ # Demo app for local development +|-- modules/ # Extendable modules +|-- controls/ # Custom controls +|-- docs/ # Documentation +|-- test/ # Vitest tests +|-- translations/ # i18n resources +`-- dist/ # Build output +``` + +## Build and Run Commands + +### Development + +```bash +npm run dev +npm run dev:no-seed +npm run dev:typeorm +npm run dev:typeorm:no-seed +npm run dev:cors +``` + +### Start + +```bash +npm run start +npm run start:seed +npm run start:typeorm +npm run start:typeorm:seed +``` + +### Build + +```bash +npm run build +npm run build:backend +npm run compile:backend +npm run compile:ui +npm run build:assets +npm run build:module +``` + +### Tests + +```bash +npm run test +npm run test:watch +``` + +## Environment Variables + +See `.env.example`. +Important variables include: + +- `OPENAI_API_KEY` +- `ADMIN_LOGIN` +- `ADMIN_PASS` +- `JWT_SECRET` +- `AP_PASSWORD_SALT` +- `ORM` +- `NO_SEED_DATA` +- `ADMINIZER_ENV` + +## Development Conventions + +- Use TypeScript and keep identifiers in English +- Keep comments concise and in English +- Follow existing project architecture and naming style +- Use axios for frontend HTTP requests (CSRF handling is already integrated) + +## Rules for AI Agents in This Repository + +1. Do not create commits unless the user explicitly asks. +2. Write commit messages in English when commits are requested. +3. Reply in Russian in chat communication. +4. Do not start the development server unless the user explicitly asks. +5. After code changes, run `npm run compile:backend` by default. +6. Update documentation only when the user explicitly asks. +7. Do not translate technical artifacts (paths, keys, code identifiers). +8. rg is not available in the environment, use PowerShell search. + +## Completion Checklist + +Before finishing a coding task: + +1. Ensure changed code is consistent with Sequelize-first direction. +2. Run `npm run compile:backend` (unless user requested a different verification flow). +3. Report what was changed and any checks that were not run. diff --git a/Readme.md b/Readme.md index 0cc6ee12..510ac924 100644 --- a/Readme.md +++ b/Readme.md @@ -8,7 +8,6 @@ The system is specifically designed to be **framework- and ORM-agnostic**, runni Thanks to this approach, Adminizer can be integrated into any server-side application, regardless of its architecture. The project is **open-source**, **free to use**, and focused on building an **open community**. -We would like to express special thanks to **Konstantin Zolotarev**, who laid the foundation with the **sails-adminpanel** project, which we later reworked and generalized. --- @@ -40,6 +39,20 @@ Key features: --- +## Architecture: Handler Registry pattern + +Extension points in Adminizer follow the **Handler Registry** pattern — a combination of *Strategy*, *Template Method* and *Registry*: + +- An **abstract handler** (e.g. `AbstractFeedbackHandler`, `AbstractCustomFilter`) declares the contract — abstract methods the user must implement, plus optional template methods built on top of them. +- A **registry** (e.g. `FeedbackHandler`, `CustomFilterHandler`) stores registered handlers by ID and dispatches calls to the matching strategy at runtime. +- Users plug in their own subclasses as runtime extensions — no rebuilds, no core changes. + +This keeps the core framework-agnostic while letting every integration swap in its own behavior. + +--- + +We would like to express special thanks to **Konstantin Zolotarev**, who laid the foundation with the **sails-adminpanel** project, which we later reworked and generalized. + > All agents must read the **AGENTS.md** file before proceeding. It contains important guidelines and responsibilities required for proper operation. [Go to the documentation](https://github.com/adminization/adminizer/blob/main/docs/index.md) \ No newline at end of file diff --git a/TODO b/TODO deleted file mode 100644 index 072748fb..00000000 --- a/TODO +++ /dev/null @@ -1,2 +0,0 @@ -Для перевода уведомлений нужно создавать instance i18n в локале юзера, для которого отправляются уведомления. -Для перевода строк уведомлений нужно в БД сохранять массив для обработки по анадлогии с (PHP) print_f('user %s has been deleted by %s', guest, admin) \ No newline at end of file diff --git a/adminizer-module.d.ts b/adminizer-module.d.ts index b125e660..a37e1b7f 100644 --- a/adminizer-module.d.ts +++ b/adminizer-module.d.ts @@ -12,7 +12,6 @@ */ import type { FC, ReactNode, ComponentProps, RefObject } from 'react'; -import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; // --------------------------------------------------------------------------- // Sonner toast API (subset exposed via window.sonner) @@ -41,25 +40,52 @@ interface SonnerToast { // adminApi — typed HTTP client (mirrors src/assets/js/lib/admin-api.ts) // --------------------------------------------------------------------------- +type QueryParams = Record; + +interface HttpRequestOptions { + headers?: Record; + params?: QueryParams; + signal?: AbortSignal; + responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text'; +} + +interface HttpResponse { + data: T; + status: number; + headers: Record; +} + /** * Typed HTTP client for backend calls. * * Always sends no-cache headers and appends `_ts` query param to bust caches. - * Throws a human-readable error if the session expired and server returned HTML. + * Use `*Json` methods when the endpoint must return JSON; they throw a + * human-readable error if the session expired and server returned HTML. * * @example * const { data } = await adminApi.getJson<{ items: Item[] }>('/admin/api/items'); * await adminApi.postJson('/admin/api/items', { name: 'New item' }); */ interface AdminApi { - /** GET request, returns typed AxiosResponse */ - getJson(url: string, config?: AxiosRequestConfig): Promise>; - /** POST request with JSON body */ - postJson(url: string, data?: unknown, config?: AxiosRequestConfig): Promise>; - /** PUT request with JSON body */ - putJson(url: string, data?: unknown, config?: AxiosRequestConfig): Promise>; - /** DELETE request */ - deleteJson(url: string, data?: unknown, config?: AxiosRequestConfig): Promise>; + get(url: string, options?: HttpRequestOptions): Promise>; + post(url: string, data?: unknown, options?: HttpRequestOptions): Promise>; + put(url: string, data?: unknown, options?: HttpRequestOptions): Promise>; + patch(url: string, data?: unknown, options?: HttpRequestOptions): Promise>; + delete(url: string, data?: unknown, options?: HttpRequestOptions): Promise>; + getJson(url: string, options?: HttpRequestOptions): Promise>; + postJson(url: string, data?: unknown, options?: HttpRequestOptions): Promise>; + putJson(url: string, data?: unknown, options?: HttpRequestOptions): Promise>; + patchJson(url: string, data?: unknown, options?: HttpRequestOptions): Promise>; + deleteJson(url: string, data?: unknown, options?: HttpRequestOptions): Promise>; +} + +interface AxiosCompat { + get(url: string, config?: HttpRequestOptions): Promise>; + post(url: string, data?: unknown, config?: HttpRequestOptions): Promise>; + put(url: string, data?: unknown, config?: HttpRequestOptions): Promise>; + patch(url: string, data?: unknown, config?: HttpRequestOptions): Promise>; + delete(url: string, config?: HttpRequestOptions & { data?: unknown }): Promise>; + isAxiosError(error: unknown): boolean; } // --------------------------------------------------------------------------- @@ -193,6 +219,17 @@ interface MonacoEditorProps { disabled?: boolean; } +interface HandsonTableProps { + /** Table rows. Use arrays or row objects depending on the Handsontable config. */ + data?: unknown[][] | Record[]; + /** Handsontable GridSettings. Put colHeaders, columns, rowHeaders, licenseKey, etc. here. */ + config: Record; + /** Called with the updated source data after user edits. */ + onChange: (data: unknown) => void; + /** Makes table read-only and visually dimmed */ + disabled?: boolean; +} + // --------------------------------------------------------------------------- // window augmentation // --------------------------------------------------------------------------- @@ -206,8 +243,9 @@ declare global { routePrefix: string; /** - * Typed HTTP client. Use instead of raw `window.axios` for backend calls. - * Adds no-cache headers, cache-buster param, and throws on HTML responses. + * Typed HTTP client. Use instead of legacy `window.axios` for backend calls. + * Adds no-cache headers and cache-buster params. Use `*Json` methods for + * JSON endpoints that should reject HTML responses. * * @example * const { data } = await window.adminApi.getJson<{ rows: Row[] }>(`${window.routePrefix}/api/rows`); @@ -215,9 +253,9 @@ declare global { adminApi: AdminApi; /** - * Raw Axios instance. Prefer `window.adminApi` for JSON API calls. + * Legacy Axios-compatible client. Prefer `window.adminApi`. */ - axios: AxiosInstance; + axios: AxiosCompat; /** * Sonner toast API. @@ -796,9 +834,17 @@ declare global { * Handsontable spreadsheet grid. For heavy editable data tables. * See https://handsontable.com/docs for full config. * @example - * + * */ - HandsonTable: FC<{ data?: unknown[][]; colHeaders?: string[]; rowHeaders?: boolean; licenseKey: string; [k: string]: unknown }>; + HandsonTable: FC; /** * Interactive JSON editor (vanilla-jsoneditor). diff --git a/controls/wysiwyg/ReactQuill.ts b/controls/wysiwyg/ReactQuill.ts deleted file mode 100644 index 2dbec889..00000000 --- a/controls/wysiwyg/ReactQuill.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {AbstractControls, ControlType, Config, Path} from "../../../dist/lib/controls/AbstractControls"; -import {Adminizer} from "../../../dist"; - -export class ReactQuill extends AbstractControls { - readonly name: string = 'react-quill'; - readonly type: ControlType = 'wysiwyg'; - readonly path: Path = { - cssPath: `${this.routPrefix}/assets/modules/react-quill-editor.css`, - jsPath: - { - dev: "/modules/controls/wysiwyg/react-quill-editor.tsx", - production: `${this.routPrefix}/assets/modules/react-quill-editor.es.js` - } - } - readonly config: Config = {}; - - getConfig(): Config { - return this.config; - } - - constructor(adminizer: Adminizer) { - super(adminizer); - } - - getJsPath(): string { - if (process.env.VITE_ENV === 'dev') { - return this.path.jsPath.dev; - } else { - return this.path.jsPath.production - } - } - - getCssPath(): string { - return this.path.cssPath - } - - getName(): string { - return this.name - } -} diff --git a/controls/wysiwyg/react-quill-editor.css b/controls/wysiwyg/react-quill-editor.css deleted file mode 100644 index e867ba75..00000000 --- a/controls/wysiwyg/react-quill-editor.css +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Quill Editor v2.0.2 - * https://quilljs.com - * Copyright (c) 2017-2024, Slab - * Copyright (c) 2014, Jason Chen - * Copyright (c) 2013, salesforce.com - */.ql-container{box-sizing:border-box;font-family:Helvetica,Arial,sans-serif;font-size:13px;height:100%;margin:0;position:relative}.ql-container.ql-disabled .ql-tooltip{visibility:hidden}.ql-container:not(.ql-disabled) li[data-list=checked]>.ql-ui,.ql-container:not(.ql-disabled) li[data-list=unchecked]>.ql-ui{cursor:pointer}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-clipboard p{margin:0;padding:0}.ql-editor{box-sizing:border-box;counter-reset:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;line-height:1.42;height:100%;outline:none;overflow-y:auto;padding:12px 15px;tab-size:4;-moz-tab-size:4;text-align:left;white-space:pre-wrap;word-wrap:break-word}.ql-editor>*{cursor:text}.ql-editor p,.ql-editor ol,.ql-editor pre,.ql-editor blockquote,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{margin:0;padding:0}@supports (counter-set:none){.ql-editor p,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{counter-set:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor p,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{counter-reset:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor table{border-collapse:collapse}.ql-editor td{border:1px solid #000;padding:2px 5px}.ql-editor ol{padding-left:1.5em}.ql-editor li{list-style-type:none;padding-left:1.5em;position:relative}.ql-editor li>.ql-ui:before{display:inline-block;margin-left:-1.5em;margin-right:.3em;text-align:right;white-space:nowrap;width:1.2em}.ql-editor li[data-list=checked]>.ql-ui,.ql-editor li[data-list=unchecked]>.ql-ui{color:#777}.ql-editor li[data-list=bullet]>.ql-ui:before{content:"•"}.ql-editor li[data-list=checked]>.ql-ui:before{content:"☑"}.ql-editor li[data-list=unchecked]>.ql-ui:before{content:"☐"}@supports (counter-set:none){.ql-editor li[data-list]{counter-set:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list]{counter-reset:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered]{counter-increment:list-0}.ql-editor li[data-list=ordered]>.ql-ui:before{content:counter(list-0,decimal) ". "}.ql-editor li[data-list=ordered].ql-indent-1{counter-increment:list-1}.ql-editor li[data-list=ordered].ql-indent-1>.ql-ui:before{content:counter(list-1,lower-alpha) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-1{counter-set:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-1{counter-reset:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-2{counter-increment:list-2}.ql-editor li[data-list=ordered].ql-indent-2>.ql-ui:before{content:counter(list-2,lower-roman) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-2{counter-set:list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-2{counter-reset:list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-3{counter-increment:list-3}.ql-editor li[data-list=ordered].ql-indent-3>.ql-ui:before{content:counter(list-3,decimal) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-3{counter-set:list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-3{counter-reset:list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-4{counter-increment:list-4}.ql-editor li[data-list=ordered].ql-indent-4>.ql-ui:before{content:counter(list-4,lower-alpha) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-4{counter-set:list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-4{counter-reset:list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-5{counter-increment:list-5}.ql-editor li[data-list=ordered].ql-indent-5>.ql-ui:before{content:counter(list-5,lower-roman) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-5{counter-set:list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-5{counter-reset:list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-6{counter-increment:list-6}.ql-editor li[data-list=ordered].ql-indent-6>.ql-ui:before{content:counter(list-6,decimal) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-6{counter-set:list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-6{counter-reset:list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-7{counter-increment:list-7}.ql-editor li[data-list=ordered].ql-indent-7>.ql-ui:before{content:counter(list-7,lower-alpha) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-7{counter-set:list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-7{counter-reset:list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-8{counter-increment:list-8}.ql-editor li[data-list=ordered].ql-indent-8>.ql-ui:before{content:counter(list-8,lower-roman) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-8{counter-set:list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-8{counter-reset:list-9}}.ql-editor li[data-list=ordered].ql-indent-9{counter-increment:list-9}.ql-editor li[data-list=ordered].ql-indent-9>.ql-ui:before{content:counter(list-9,decimal) ". "}.ql-editor .ql-indent-1:not(.ql-direction-rtl){padding-left:3em}.ql-editor li.ql-indent-1:not(.ql-direction-rtl){padding-left:4.5em}.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right{padding-right:3em}.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right{padding-right:4.5em}.ql-editor .ql-indent-2:not(.ql-direction-rtl){padding-left:6em}.ql-editor li.ql-indent-2:not(.ql-direction-rtl){padding-left:7.5em}.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right{padding-right:6em}.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right{padding-right:7.5em}.ql-editor .ql-indent-3:not(.ql-direction-rtl){padding-left:9em}.ql-editor li.ql-indent-3:not(.ql-direction-rtl){padding-left:10.5em}.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right{padding-right:9em}.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right{padding-right:10.5em}.ql-editor .ql-indent-4:not(.ql-direction-rtl){padding-left:12em}.ql-editor li.ql-indent-4:not(.ql-direction-rtl){padding-left:13.5em}.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right{padding-right:12em}.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right{padding-right:13.5em}.ql-editor .ql-indent-5:not(.ql-direction-rtl){padding-left:15em}.ql-editor li.ql-indent-5:not(.ql-direction-rtl){padding-left:16.5em}.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right{padding-right:15em}.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right{padding-right:16.5em}.ql-editor .ql-indent-6:not(.ql-direction-rtl){padding-left:18em}.ql-editor li.ql-indent-6:not(.ql-direction-rtl){padding-left:19.5em}.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right{padding-right:18em}.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right{padding-right:19.5em}.ql-editor .ql-indent-7:not(.ql-direction-rtl){padding-left:21em}.ql-editor li.ql-indent-7:not(.ql-direction-rtl){padding-left:22.5em}.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right{padding-right:21em}.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right{padding-right:22.5em}.ql-editor .ql-indent-8:not(.ql-direction-rtl){padding-left:24em}.ql-editor li.ql-indent-8:not(.ql-direction-rtl){padding-left:25.5em}.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right{padding-right:24em}.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right{padding-right:25.5em}.ql-editor .ql-indent-9:not(.ql-direction-rtl){padding-left:27em}.ql-editor li.ql-indent-9:not(.ql-direction-rtl){padding-left:28.5em}.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right{padding-right:27em}.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right{padding-right:28.5em}.ql-editor li.ql-direction-rtl{padding-right:1.5em}.ql-editor li.ql-direction-rtl>.ql-ui:before{margin-left:.3em;margin-right:-1.5em;text-align:left}.ql-editor table{table-layout:fixed;width:100%}.ql-editor table td{outline:none}.ql-editor .ql-code-block-container{font-family:monospace}.ql-editor .ql-video{display:block;max-width:100%}.ql-editor .ql-video.ql-align-center{margin:0 auto}.ql-editor .ql-video.ql-align-right{margin:0 0 0 auto}.ql-editor .ql-bg-black{background-color:#000}.ql-editor .ql-bg-red{background-color:#e60000}.ql-editor .ql-bg-orange{background-color:#f90}.ql-editor .ql-bg-yellow{background-color:#ff0}.ql-editor .ql-bg-green{background-color:#008a00}.ql-editor .ql-bg-blue{background-color:#06c}.ql-editor .ql-bg-purple{background-color:#93f}.ql-editor .ql-color-white{color:#fff}.ql-editor .ql-color-red{color:#e60000}.ql-editor .ql-color-orange{color:#f90}.ql-editor .ql-color-yellow{color:#ff0}.ql-editor .ql-color-green{color:#008a00}.ql-editor .ql-color-blue{color:#06c}.ql-editor .ql-color-purple{color:#93f}.ql-editor .ql-font-serif{font-family:Georgia,Times New Roman,serif}.ql-editor .ql-font-monospace{font-family:Monaco,Courier New,monospace}.ql-editor .ql-size-small{font-size:.75em}.ql-editor .ql-size-large{font-size:1.5em}.ql-editor .ql-size-huge{font-size:2.5em}.ql-editor .ql-direction-rtl{direction:rtl;text-align:inherit}.ql-editor .ql-align-center{text-align:center}.ql-editor .ql-align-justify{text-align:justify}.ql-editor .ql-align-right{text-align:right}.ql-editor .ql-ui{position:absolute}.ql-editor.ql-blank:before{color:#0009;content:attr(data-placeholder);font-style:italic;left:15px;pointer-events:none;position:absolute;right:15px}.ql-snow.ql-toolbar:after,.ql-snow .ql-toolbar:after{clear:both;content:"";display:table}.ql-snow.ql-toolbar button,.ql-snow .ql-toolbar button{background:none;border:none;cursor:pointer;display:inline-block;float:left;height:24px;padding:3px 5px;width:28px}.ql-snow.ql-toolbar button svg,.ql-snow .ql-toolbar button svg{float:left;height:100%}.ql-snow.ql-toolbar button:active:hover,.ql-snow .ql-toolbar button:active:hover{outline:none}.ql-snow.ql-toolbar input.ql-image[type=file],.ql-snow .ql-toolbar input.ql-image[type=file]{display:none}.ql-snow.ql-toolbar button:hover,.ql-snow .ql-toolbar button:hover,.ql-snow.ql-toolbar button:focus,.ql-snow .ql-toolbar button:focus,.ql-snow.ql-toolbar button.ql-active,.ql-snow .ql-toolbar button.ql-active,.ql-snow.ql-toolbar .ql-picker-label:hover,.ql-snow .ql-toolbar .ql-picker-label:hover,.ql-snow.ql-toolbar .ql-picker-label.ql-active,.ql-snow .ql-toolbar .ql-picker-label.ql-active,.ql-snow.ql-toolbar .ql-picker-item:hover,.ql-snow .ql-toolbar .ql-picker-item:hover,.ql-snow.ql-toolbar .ql-picker-item.ql-selected,.ql-snow .ql-toolbar .ql-picker-item.ql-selected{color:#06c}.ql-snow.ql-toolbar button:hover .ql-fill,.ql-snow .ql-toolbar button:hover .ql-fill,.ql-snow.ql-toolbar button:focus .ql-fill,.ql-snow .ql-toolbar button:focus .ql-fill,.ql-snow.ql-toolbar button.ql-active .ql-fill,.ql-snow .ql-toolbar button.ql-active .ql-fill,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill,.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill,.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill,.ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill{fill:#06c}.ql-snow.ql-toolbar button:hover .ql-stroke,.ql-snow .ql-toolbar button:hover .ql-stroke,.ql-snow.ql-toolbar button:focus .ql-stroke,.ql-snow .ql-toolbar button:focus .ql-stroke,.ql-snow.ql-toolbar button.ql-active .ql-stroke,.ql-snow .ql-toolbar button.ql-active .ql-stroke,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,.ql-snow.ql-toolbar button:hover .ql-stroke-miter,.ql-snow .ql-toolbar button:hover .ql-stroke-miter,.ql-snow.ql-toolbar button:focus .ql-stroke-miter,.ql-snow .ql-toolbar button:focus .ql-stroke-miter,.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter,.ql-snow .ql-toolbar button.ql-active .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter{stroke:#06c}@media (pointer:coarse){.ql-snow.ql-toolbar button:hover:not(.ql-active),.ql-snow .ql-toolbar button:hover:not(.ql-active){color:#444}.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill,.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill{fill:#444}.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke,.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter{stroke:#444}}.ql-snow,.ql-snow *{box-sizing:border-box}.ql-snow .ql-hidden{display:none}.ql-snow .ql-out-bottom,.ql-snow .ql-out-top{visibility:hidden}.ql-snow .ql-tooltip{position:absolute;transform:translateY(10px)}.ql-snow .ql-tooltip a{cursor:pointer;text-decoration:none}.ql-snow .ql-tooltip.ql-flip{transform:translateY(-10px)}.ql-snow .ql-formats{display:inline-block;vertical-align:middle}.ql-snow .ql-formats:after{clear:both;content:"";display:table}.ql-snow .ql-stroke{fill:none;stroke:#444;stroke-linecap:round;stroke-linejoin:round;stroke-width:2}.ql-snow .ql-stroke-miter{fill:none;stroke:#444;stroke-miterlimit:10;stroke-width:2}.ql-snow .ql-fill,.ql-snow .ql-stroke.ql-fill{fill:#444}.ql-snow .ql-empty{fill:none}.ql-snow .ql-even{fill-rule:evenodd}.ql-snow .ql-thin,.ql-snow .ql-stroke.ql-thin{stroke-width:1}.ql-snow .ql-transparent{opacity:.4}.ql-snow .ql-direction svg:last-child{display:none}.ql-snow .ql-direction.ql-active svg:last-child{display:inline}.ql-snow .ql-direction.ql-active svg:first-child{display:none}.ql-snow .ql-editor h1{font-size:2em}.ql-snow .ql-editor h2{font-size:1.5em}.ql-snow .ql-editor h3{font-size:1.17em}.ql-snow .ql-editor h4{font-size:1em}.ql-snow .ql-editor h5{font-size:.83em}.ql-snow .ql-editor h6{font-size:.67em}.ql-snow .ql-editor a{text-decoration:underline}.ql-snow .ql-editor blockquote{border-left:4px solid #ccc;margin-bottom:5px;margin-top:5px;padding-left:16px}.ql-snow .ql-editor code,.ql-snow .ql-editor .ql-code-block-container{background-color:#f0f0f0;border-radius:3px}.ql-snow .ql-editor .ql-code-block-container{margin-bottom:5px;margin-top:5px;padding:5px 10px}.ql-snow .ql-editor code{font-size:85%;padding:2px 4px}.ql-snow .ql-editor .ql-code-block-container{background-color:#23241f;color:#f8f8f2;overflow:visible}.ql-snow .ql-editor img{max-width:100%}.ql-snow .ql-picker{color:#444;display:inline-block;float:left;font-size:14px;font-weight:500;height:24px;position:relative;vertical-align:middle}.ql-snow .ql-picker-label{cursor:pointer;display:inline-block;height:100%;padding-left:8px;padding-right:2px;position:relative;width:100%}.ql-snow .ql-picker-label:before{display:inline-block;line-height:22px}.ql-snow .ql-picker-options{background-color:#fff;display:none;min-width:100%;padding:4px 8px;position:absolute;white-space:nowrap}.ql-snow .ql-picker-options .ql-picker-item{cursor:pointer;display:block;padding-bottom:5px;padding-top:5px}.ql-snow .ql-picker.ql-expanded .ql-picker-label{color:#ccc;z-index:2}.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill{fill:#ccc}.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke{stroke:#ccc}.ql-snow .ql-picker.ql-expanded .ql-picker-options{display:block;margin-top:-1px;top:100%;z-index:1}.ql-snow .ql-color-picker,.ql-snow .ql-icon-picker{width:28px}.ql-snow .ql-color-picker .ql-picker-label,.ql-snow .ql-icon-picker .ql-picker-label{padding:2px 4px}.ql-snow .ql-color-picker .ql-picker-label svg,.ql-snow .ql-icon-picker .ql-picker-label svg{right:4px}.ql-snow .ql-icon-picker .ql-picker-options{padding:4px 0}.ql-snow .ql-icon-picker .ql-picker-item{height:24px;width:24px;padding:2px 4px}.ql-snow .ql-color-picker .ql-picker-options{padding:3px 5px;width:152px}.ql-snow .ql-color-picker .ql-picker-item{border:1px solid transparent;float:left;height:16px;margin:2px;padding:0;width:16px}.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg{position:absolute;margin-top:-9px;right:0;top:50%;width:18px}.ql-snow .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=""]):before{content:attr(data-label)}.ql-snow .ql-picker.ql-header{width:98px}.ql-snow .ql-picker.ql-header .ql-picker-label:before,.ql-snow .ql-picker.ql-header .ql-picker-item:before{content:"Normal"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]:before{content:"Heading 1"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]:before{content:"Heading 2"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]:before{content:"Heading 3"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]:before{content:"Heading 4"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]:before{content:"Heading 5"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]:before{content:"Heading 6"}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]:before{font-size:2em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]:before{font-size:1.5em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]:before{font-size:1.17em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]:before{font-size:1em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]:before{font-size:.83em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]:before{font-size:.67em}.ql-snow .ql-picker.ql-font{width:108px}.ql-snow .ql-picker.ql-font .ql-picker-label:before,.ql-snow .ql-picker.ql-font .ql-picker-item:before{content:"Sans Serif"}.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]:before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]:before{content:"Serif"}.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]:before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]:before{content:"Monospace"}.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]:before{font-family:Georgia,Times New Roman,serif}.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]:before{font-family:Monaco,Courier New,monospace}.ql-snow .ql-picker.ql-size{width:98px}.ql-snow .ql-picker.ql-size .ql-picker-label:before,.ql-snow .ql-picker.ql-size .ql-picker-item:before{content:"Normal"}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]:before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]:before{content:"Small"}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]:before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]:before{content:"Large"}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]:before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]:before{content:"Huge"}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]:before{font-size:10px}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]:before{font-size:18px}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]:before{font-size:32px}.ql-snow .ql-color-picker.ql-background .ql-picker-item{background-color:#fff}.ql-snow .ql-color-picker.ql-color .ql-picker-item{background-color:#000}.ql-code-block-container{position:relative}.ql-code-block-container .ql-ui{right:5px;top:5px}.ql-toolbar.ql-snow{border:1px solid #ccc;box-sizing:border-box;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;padding:8px}.ql-toolbar.ql-snow .ql-formats{margin-right:15px}.ql-toolbar.ql-snow .ql-picker-label{border:1px solid transparent}.ql-toolbar.ql-snow .ql-picker-options{border:1px solid transparent;box-shadow:#0003 0 2px 8px}.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label,.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options{border-color:#ccc}.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected,.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover{border-color:#000}.ql-toolbar.ql-snow+.ql-container.ql-snow{border-top:0}.ql-snow .ql-tooltip{background-color:#fff;border:1px solid #ccc;box-shadow:0 0 5px #ddd;color:#444;padding:5px 12px;white-space:nowrap}.ql-snow .ql-tooltip:before{content:"Visit URL:";line-height:26px;margin-right:8px}.ql-snow .ql-tooltip input[type=text]{display:none;border:1px solid #ccc;font-size:13px;height:26px;margin:0;padding:3px 5px;width:170px}.ql-snow .ql-tooltip a.ql-preview{display:inline-block;max-width:200px;overflow-x:hidden;text-overflow:ellipsis;vertical-align:top}.ql-snow .ql-tooltip a.ql-action:after{border-right:1px solid #ccc;content:"Edit";margin-left:16px;padding-right:8px}.ql-snow .ql-tooltip a.ql-remove:before{content:"Remove";margin-left:8px}.ql-snow .ql-tooltip a{line-height:26px}.ql-snow .ql-tooltip.ql-editing a.ql-preview,.ql-snow .ql-tooltip.ql-editing a.ql-remove{display:none}.ql-snow .ql-tooltip.ql-editing input[type=text]{display:inline-block}.ql-snow .ql-tooltip.ql-editing a.ql-action:after{border-right:0;content:"Save";padding-right:0}.ql-snow .ql-tooltip[data-mode=link]:before{content:"Enter link:"}.ql-snow .ql-tooltip[data-mode=formula]:before{content:"Enter formula:"}.ql-snow .ql-tooltip[data-mode=video]:before{content:"Enter video:"}.ql-snow a{color:#06c}.ql-container.ql-snow{border:1px solid #ccc} diff --git a/copy-modules.js b/copy-modules.js deleted file mode 100644 index 59f844bb..00000000 --- a/copy-modules.js +++ /dev/null @@ -1,45 +0,0 @@ -import shell from 'shelljs'; -import path from 'path'; -import chalk from 'chalk'; - -const CONFIG = { - filesToCopy: [ - 'modules/controls/wysiwyg/react-quill-editor.es.js', - 'modules/controls/wysiwyg/react-quill-editor.css', - 'modules/test/ComponentB.es.js', - 'modules/testCatalog/button-Dk9HuYNn.js', - 'modules/testCatalog/catalogAction.es.js', - 'modules/testCatalog/Group.es.js' - ], - targetDir: path.normalize('dist/assets/modules') -}; - -function copyFiles() { - try { - shell.mkdir('-p', CONFIG.targetDir); - - CONFIG.filesToCopy.forEach(file => { - const sourceFile = path.normalize(file); - const targetFile = path.join(CONFIG.targetDir, path.basename(file)); - - if (shell.test('-f', sourceFile)) { - shell.cp(sourceFile, targetFile); - console.log(chalk.green(`Copied: ${chalk.bold(sourceFile)} → ${targetFile}`)); - } else { - console.log(chalk.yellow(`Source file not found: ${sourceFile}`)); - } - }); - - console.log(chalk.green('Copy process completed!')); - } catch (err) { - console.log(chalk.red(`Copy process failed: ${err.message}`)); - process.exit(1); - } -} - -try { - copyFiles(); -} catch (err) { - console.log(chalk.red(`Script execution failed: ${err.message}`)); - process.exit(1); -} diff --git a/docs/AccessRights/AccessRightsModelFields.md b/docs/AccessRights/AccessRightsModelFields.md index c1dee2aa..2bb2c57c 100644 --- a/docs/AccessRights/AccessRightsModelFields.md +++ b/docs/AccessRights/AccessRightsModelFields.md @@ -1,7 +1,7 @@ ### `DataAccessor` — Data Layer Mediator with Access Control -> Everything that is not prohibited is allowed, Excluding ties to the UserAP model +> Everything that is not prohibited is allowed, excluding ties to the canonical `User` model The `DataAccessor` class is a central utility for handling the interaction between users and the system's data models, while enforcing access control and configuration constraints. @@ -16,7 +16,7 @@ This class: * Filters records according to configured access rules * Dynamically includes associated models and their configurations * Automatically applies or restricts data access based on user roles and group membership -* Helps populate or restrict relation fields (e.g., `UserAP`, `GroupAP`) depending on ownership and configuration +* Helps populate or restrict relation fields (e.g., `User`, `Group`) depending on ownership and configuration * Provides unified methods for reading (`process`, `processMany`), sanitizing (`sanitizeUserRelationAccess`), and updating (`setUserRelationAccess`) data records with permission logic baked in --- @@ -115,4 +115,4 @@ In this example: * The `title` field is visible to everyone. * `internalStatus` is only shown to admins and QA team members. * `createdAt` is hidden entirely. -* Only records where the current user is the `owner` are accessible to non-admins. \ No newline at end of file +* Only records where the current user is the `owner` are accessible to non-admins. diff --git a/docs/AccessRights/access-rights-tokens.md b/docs/AccessRights/access-rights-tokens.md index ae4b8e4c..5534300b 100644 --- a/docs/AccessRights/access-rights-tokens.md +++ b/docs/AccessRights/access-rights-tokens.md @@ -1,33 +1,46 @@ # Access Rights -When sails-adminpanel starts, for every Model it creates 4 access rights tokens: +When Adminizer starts, every configured model gets 4 access rights tokens: - create - read - update - delete -You can also create custom access rights tokens using function `registerToken` of `AccessRightsHelper`. +You can also create custom access rights tokens for app features. > Everything that is unresolved is prohibited -These tokens can be used to give users rights to see information of specific Model, to create new models or edit it. -Also, you can use tokens to create access rights to global and inline actions, or to Model tools. -In controllers you should check access rights through `havePermission` method. +These tokens can be used to give users rights to see information of a specific model, create or edit records, access global and inline actions, or use model tools. -Example: +For an Adminizer app, register the token and protect the controller through `AppSetupContext`: -```javascript -if (sails.config.adminpanel.auth) { - if (!req.session.UserAP) { // check that user is authorized - return res.redirect(`${sails.config.adminpanel.routePrefix}/model/userap/login`); - } else if (!AccessRightsHelper.havePermission(`tokenName`, req.session.UserAP)) { // check permission - return res.sendStatus(403); - } - } +```ts +ctx.accessRight({ + id: "reports-export", + name: "Export reports", + description: "Permission to export reports", + department: "Reports", +}); + +ctx.controller({ + id: "export", + method: "post", + route: "/reports/export", + middleware: async (req, res) => { + // Use req.runtime for app capabilities. + return res.json({ success: true }); + }, + policies: [ + { type: "auth", mode: "api" }, + { type: "permission", token: "reports-export", mode: "api" }, + ], +}); ``` ### Registering Custom Tokens -Custom tokens let you restrict access to additional features beyond the standard CRUD actions. Register them when your application starts: +Custom tokens let you restrict access to additional features beyond the standard CRUD actions. Apps should register them through `ctx.accessRight()` so `AppManager` owns their lifecycle. + +For host-level startup code outside an Adminizer app, tokens can still be registered directly: ```ts adminizer.accessRightsHelper.registerTokens([ @@ -43,9 +56,10 @@ adminizer.accessRightsHelper.registerTokens([ Assign the token to a group so members inherit the permission: ```ts -await GroupAP.updateOne({ name: 'managers' }).set({ - tokens: ['reports-export'] -}) +await adminizer.modelHandler + .internal("access-rights") + .get("Group") + .update({ where: { name: 'managers' } }, { tokens: ['reports-export'] }); ``` @@ -64,8 +78,8 @@ will be found, adminpanel will create admin profile with this credentials. If credentials in config will not be found, adminpanel will create admin with login `admin` and numeric password that will be displayed in console. -```javascript -module.exports.adminpanel = { +```ts +const config = { administrator: { login: 'string', password: 'string' diff --git a/docs/AccessRights/user-owned-records.md b/docs/AccessRights/user-owned-records.md index 03d23b55..25f4fd47 100644 --- a/docs/AccessRights/user-owned-records.md +++ b/docs/AccessRights/user-owned-records.md @@ -1,6 +1,6 @@ ### **User-Owned Records** -In multi-user admin panels, it’s often essential to restrict data access based on ownership — that is, which user a record belongs to. `DataAccessor` supports this logic through model configuration by linking records to the `UserAP` model. This enables: +In multi-user admin panels, it’s often essential to restrict data access based on ownership — that is, which user a record belongs to. `DataAccessor` supports this logic through model configuration by linking records to the canonical `User` system model. This enables: * Automatically setting the `userId` field (or equivalent) on record creation * Restricting access so that users can only read or modify their own records @@ -13,21 +13,21 @@ In the model config, you specify a field that connects the record to a user: ```ts models: { - Report: { - userAccessRelation: "author", // field that links to UserAP - fields: { - content: { type: "text" }, - author: { model: "userap" } - } - } -} -``` + Report: { + userAccessRelation: "author", // field that links to User + fields: { + content: { type: "text" }, + author: { model: "User" } + } + } +} +``` If the user is **not an administrator**, they will only be able to access records where they are listed in the `author` field. This ownership check is enforced automatically through `sanitizeUserRelationAccess`. #### **Create vs Update Behavior** -When `userAccessRelation` points to a `UserAP` association (for example, `author`): +When `userAccessRelation` points to a `User` association (for example, `author`): * On **create**, ownership is assigned automatically via `setUserRelationAccess`: * If a non-admin sends any `author` value manually, it is ignored. @@ -38,8 +38,8 @@ When `userAccessRelation` points to a `UserAP` association (for example, `author #### **Important Notes** -* The `userAccessRelation` field must exist in the ORM model and be an association to `UserAP`. -* The user model (e.g., `UserAP`) must be properly registered and include the required access rights tokens. +* The `userAccessRelation` field must exist in the ORM model and be an association to the canonical `User` system model. If the host ORM target is named differently, configure the adapter `systemModels` mapping. +* The user model must be properly registered and include the required access rights tokens. * Without defining `userAccessRelation`, access is controlled only via global permissions (using `AccessRightsHelper`), not per-record ownership. #### **FK + Alias Support** @@ -47,7 +47,7 @@ When `userAccessRelation` points to a `UserAP` association (for example, `author `userAccessRelation` may reference either: * a direct association field (for example, `owner`), or -* a foreign key field (for example, `ownerId`) when there is an association alias with `via: "ownerId"` that points to `UserAP`. +* a foreign key field (for example, `ownerId`) when there is an association alias with `via: "ownerId"` that points to `User`. This is useful for ORM schemas where relation metadata is stored on an alias field while the configured access field is a plain FK column. diff --git a/docs/AiAssistant.md b/docs/AiAssistant.md index ef8fe38e..3886d5a9 100644 --- a/docs/AiAssistant.md +++ b/docs/AiAssistant.md @@ -1,6 +1,20 @@ # AI Assistant Integration -The AI assistant feature introduces a chat experience that can be toggled from the main header. It is disabled by default and can be enabled through the admin panel configuration. +The AI assistant feature provides the shared chat UI and backend contracts for assistant models. The core package keeps the infrastructure, while concrete assistant models are enabled through an Adminizer app. + +In the fixture, the implementation lives in `fixture/apps/ai-assistant` and can be enabled or disabled through `AppManager`. + +## Core Responsibilities + +The core keeps reusable AI infrastructure: + +* `AiAssistantHandler` stores registered model services and in-memory conversation history per user and model. +* `AbstractAiModelService` defines assistant model metadata and the `generateReply()` contract. +* `AiAssistantController` exposes reusable REST handlers for listing models, reading history, sending prompts, and resetting history. +* The frontend `AiAssistantProvider`, toggle, viewport, and panel read shared config and render the chat UI. +* `AppSetupContext.aiAssistant()` lets an app attach an `AiAssistantHandler` and model services under `AppManager` lifecycle control. + +The core does not auto-bind assistant routes or built-in models. There is no `bindAiAssistant` bootstrap step. Apps register routes, access rights, config patches, and models explicitly. ## Configuration @@ -14,97 +28,80 @@ aiAssistant?: { } ``` -* `enabled` — toggles the feature on or off (defaults to `false`). -* `defaultModel` — preferred model identifier that will be preselected on the client. -* `models` — list of model identifiers to register. Unknown identifiers are ignored with a warning. - -### Enabling/Disabling the Assistant - -When `enabled` is `false` or the `aiAssistant` section is omitted: - -* The `aiAssistantHandler` will not be initialized -* No AI assistant routes will be registered -* The UI toggle for the assistant will not appear -* No AI-related access tokens will be generated - -This ensures that the AI assistant subsystem has zero overhead when disabled. +* `enabled` toggles the shared frontend assistant UI. +* `defaultModel` is the preferred model identifier preselected on the client. +* `models` is the list of model identifiers exposed by the enabled app. -#### Fixture Configuration +The default core config is: -In the fixture, the AI assistant is controlled by the `ENABLE_AI_ASSISTANT` environment variable: +```ts +aiAssistant: { + enabled: false, +} +``` -```bash -# Enable AI assistant in fixture -export ENABLE_AI_ASSISTANT=true +Model defaults are app-owned. The core does not assume a `dummy` model. -# Disable AI assistant in fixture (default) -export ENABLE_AI_ASSISTANT=false -# or simply omit the variable -``` +## Fixture App -This allows testing the application both with and without the AI assistant enabled. +The fixture app is `fixture/apps/ai-assistant/AiAssistantApp.ts`. -The default configuration registers the in-memory `dummy` model that always replies with `Ai-assystant dummy in deveploment`. +It registers: -The fixture configuration enables the assistant with this dummy model so the chat can be exercised locally without any external -dependencies. +* model access tokens through `ctx.accessRight()`; +* the handler and model services through `ctx.aiAssistant()`; +* the `aiAssistant` config layer through `ctx.config()`; +* REST endpoints through `ctx.controller()`. -## Fixture OpenAI Agent +The app is registered in `fixture/index.ts` so `module-manager` can enable or disable it at runtime: -The fixture now also registers an `openai` model that executes structured commands against the database. The agent expects JSON instructions and uses `DataAccessor` under the hood, so every operation is filtered by the requesting user's permissions. +```ts +const aiAssistantApp = new AiAssistantApp({ + defaultModel: adminizer.config.aiAssistant?.defaultModel, + models: adminizer.config.aiAssistant?.models ?? ["openai-data", "dummy"], +}); -Example payload for creating a record: +adminizer.appManager.register(aiAssistantApp); -```json -{ - "action": "create", - "entity": "Example", - "data": { - "title": "Hello from the assistant", - "description": "Generated through the OpenAI agent" - } +if (adminizer.config.aiAssistant?.enabled) { + await adminizer.appManager.enable(aiAssistantApp.name); } ``` -If the user lacks the required access token (for example, `create-example-model`), the agent responds with an authorization error instead of touching the database. The `openai` fixture user (`login: openai`, `password: openai`) belongs to the administrators group, granting full access for experimentation. Regular users can be granted permissions by assigning the `ai-assistant-openai` token to their groups. +When the app is disabled, `AppManager` removes its routes, config layer, access tokens, and assistant handler. -## Backend Overview +## Fixture Configuration -* `AiAssistantHandler` keeps registered model services and in-memory conversation history per user and model. -* Model services extend `AbstractAiModelService`, which automatically registers the corresponding access right (token pattern: `ai-assistant-`). -* The binder (`bindAiAssistant`) attaches the handler to `Adminizer` and registers models declared in the configuration. -* `AiAssistantController` exposes REST endpoints under `/api/ai-assistant` for: - * Listing available models (`GET /models`). - * Fetching conversation history (`GET /history/:modelId`). - * Sending prompts (`POST /query`). - * Resetting history (`DELETE /history/:modelId`). +In `fixture/adminizerConfig.ts`, the assistant is disabled by default: -All endpoints require the user to have the access token generated for the model. - -## Frontend Overview +```ts +aiAssistant: { + enabled: false, + defaultModel: "openai-data", + models: ["openai-data", "dummy"], +} +``` -* `AiAssistantProvider` handles fetching models/history, sending prompts, and exposing chat state via `useAiAssistant`. The provider also keeps the open state and last selected model in memory across Inertia navigations so the dialog stays available while browsing. -* `AiAssistantToggle` renders the sparkles button in the header and toggles the assistant workspace. The button displays a spinner while a request is in flight and disables itself if no models are accessible. -* `AiAssistantViewport` shifts the root application container when the assistant opens so the entire interface slides left and keeps room for the chat. On mobile devices (< 768px), the panel occupies the full screen instead of shifting content. -* `AiAssistantPanel` anchors a full-height chat interface to the right quarter of the viewport as a fixed workspace on desktop, or full-screen on mobile, rendering conversation history and the compose form outside of the main layout. Users can send messages by clicking Send or pressing Ctrl+Enter (Cmd+Enter on Mac). -* Conversation history is stored client-side for rendering while the server keeps the authoritative in-memory copy. +Set `enabled: true` locally to start the fixture with the assistant app enabled. If it remains `false`, the app is still registered and can be enabled through the module manager. -**See [Mobile Layout Documentation](./AiAssistant-Mobile.md) for details on responsive behavior.** +## Routes -## Extending With New Models +The fixture app registers these endpoints under `config.routePrefix`: -To register a new model: +| Method | Route | Purpose | +|---|---|---| +| `GET` | `/api/ai-assistant/models` | List models available to the current user. | +| `GET` | `/api/ai-assistant/history/:modelId` | Fetch conversation history. | +| `POST` | `/api/ai-assistant/query` | Send a prompt to a model. | +| `DELETE` | `/api/ai-assistant/history/:modelId` | Reset conversation history. | -1. Create a class that extends `AbstractAiModelService` and implement `generateReply`. -2. Add a factory entry to `modelFactories` in `bindAiAssistant.ts`. -3. Reference the new model identifier in `aiAssistant.models` within your configuration. -4. Assign the generated access token (`ai-assistant-`) to the user groups that should have access. +Each model has an access token with the pattern `ai-assistant-`. The fixture app declares these tokens through `ctx.accessRight()`. -## Available Models +## Available Fixture Models ### Dummy Model (`dummy`) -A simple development model that always returns `"Ai-assistant dummy in development"`. Useful for testing the UI without external dependencies. +A local development model with no external dependencies. It is useful for checking the UI and lifecycle behavior. ### Simple OpenAI Agent (`openai`) @@ -113,64 +110,53 @@ A JSON-based command executor that expects structured instructions: ```json { "action": "create", - "entity": "Example", + "modelResource": "Example", "data": { "title": "Hello from the agent" } } ``` -This model uses `DataAccessor` for permission checks but requires manual JSON formatting. It's useful for scripted operations but not conversational. +This model uses `DataAccessor` for permission checks, but it expects manually formatted JSON and is mostly useful for scripted experiments. ### OpenAI Data Agent (`openai-data`) -**Recommended for production use.** A conversational AI agent powered by OpenAI's Agents API that: +A conversational fixture model powered by OpenAI's Agents API. It can: -* Answers questions in natural language -* Queries database models automatically using tools -* Respects user permissions through `DataAccessor` -* Maintains conversation context -* Handles complex multi-turn conversations +* answer questions in natural language; +* query Adminizer models through tools; +* create and update records through `DataAccessor`; +* respect user permissions and field-level restrictions; +* keep conversation context. -Example queries: -- "Show me all categories" -- "How many examples are in the database?" -- "What are the recent test records?" +The implementation is in `fixture/apps/ai-assistant/OpenAiDataAgentService.ts`. -## OpenAI Data Agent Setup (fixture) +To enable it locally, set: -The fixture ships with an `openai-data` model that demonstrates how to integrate the SDK with -OpenAI's Agents API while still relying on Adminizer's abstractions: +```bash +export OPENAI_API_KEY="sk-..." +export OPENAI_AGENT_MODEL="gpt-5-nano" +``` -* The agent implementation lives in `fixture/helpers/ai/OpenAiDataAgentService.ts` and extends - `AbstractAiModelService`. -* Database reads are performed through `DataAccessor`, which means the usual access control and field - sanitisation rules are enforced automatically. -* Conversation history is converted into the `@openai/agents` protocol so follow-up questions can - build on previous answers. +`ADMINIZER_OPENAI_KEY` can be used as an alternative API key variable. If no key is available, the fixture app skips `openai-data` and falls back to another configured model. -To enable the agent locally, set the following environment variables before starting the fixture: +## Adding A Model -```bash -export OPENAI_API_KEY="sk-..." # required -export OPENAI_AGENT_MODEL="gpt-4.1-mini" # optional override -``` +To add a model to the fixture app: -`ADMINIZER_OPENAI_KEY` can be used as an alternative variable name for the API key. When a key is -available the fixture automatically registers the model, exposes it in the assistant model list, and -prefers it as the default chat model. If the key is missing the agent stays disabled and a warning is -logged during boot. +1. Create a class in `fixture/apps/ai-assistant` that extends `AbstractAiModelService`. +2. Implement `generateReply(prompt, history, user)`. +3. Add a model definition to `AiAssistantApp.ts`. +4. Register its access token through the app's `ctx.accessRight()` flow. +5. Add the model id to `aiAssistant.models` when it should be enabled. -### Configuration +Model services should not register access rights directly. The app owns tokens and lifecycle resources through `AppManager`. -In your `adminizerConfig.ts`: +## Frontend Behavior -```typescript -aiAssistant: { - enabled: true, - defaultModel: 'openai-data', // Use conversational AI - models: ['openai-data', 'dummy'], -} -``` +* `AiAssistantProvider` fetches models/history, sends prompts, and exposes chat state through `useAiAssistant`. +* `AiAssistantToggle` renders the header button and is hidden when the shared `aiAssistant.enabled` config is false. +* `AiAssistantViewport` shifts the root app container on desktop and switches to a full-screen assistant on mobile. +* `AiAssistantPanel` renders model selection, history, and the compose form. -**Note:** Use `'openai-data'` for natural language conversations, not `'openai'` (which is JSON-only). +Conversation history is rendered client-side while the server keeps the authoritative in-memory copy in `AiAssistantHandler`. diff --git a/docs/Authorization.md b/docs/Authorization.md index e69de29b..54eb1fe8 100644 --- a/docs/Authorization.md +++ b/docs/Authorization.md @@ -0,0 +1,35 @@ +# Authorization + +Adminizer authorization is controlled by the `auth` block in `AdminpanelConfig`. + +```ts +const config = { + routePrefix: "/adminizer", + auth: { + enable: true, + captcha: true, + }, + administrator: { + login: "admin", + password: "change-me", + }, + models: {}, +}; +``` + +When `auth.enable` is `true`, Adminizer registers login, logout, and registration routes under the configured `routePrefix`. + +## Administrator bootstrap + +If there is no administrator account, Adminizer can route users to the initial user setup screen. The fixture provides administrator credentials through `ADMIN_LOGIN` and `ADMIN_PASS`, falling back to local defaults in `fixture/adminizerConfig.ts`. + +## Access gate + +Successful login is not the only check. Users also need the `access-to-adminpanel` token unless they are administrators. Model actions are guarded by generated tokens in the form: + +* `create--model` +* `read--model` +* `update--model` +* `delete--model` + +Custom feature permissions can be registered through `adminizer.accessRightsHelper.registerToken(...)`. diff --git a/docs/BuildingModules.md b/docs/BuildingModules.md index 3ddd2229..4420b608 100644 --- a/docs/BuildingModules.md +++ b/docs/BuildingModules.md @@ -1,373 +1,604 @@ -# Building a Module for Adminizer +# Adminizer App Modules -This guide covers everything you need to create your own React module for an Adminizer-powered project — whether it's a full admin page, a custom field control, or a dashboard widget. +Adminizer modules are backend-first extensions built around `AbstractAdminizerApp` and `AppManager`. A module can register routes, frontend assets, form controls, config patches, access rights, existing ORM models, model access scopes, AI assistants, media managers, catalogs with template components, and event listeners. This is the current extension mechanism for features such as the fixture module pages, AI assistant, media manager, module manager, navigation catalog, and React-Quill control. -**Related docs:** -- [UIComponents.md](UIComponents.md) — full reference for all components available via `window.UIComponents` and `window.JSComponents` -- [adminizer-module.d.ts](../adminizer-module.d.ts) — TypeScript declarations for globals and module contracts +Older standalone React page modules may still exist as frontend patterns, but a full Adminizer module should be installed through `adminizer.appManager`. ---- +## App Boundary -## How the UI layer works +An app must not accept, store, or depend on an `Adminizer` instance. The host application owns Adminizer and uses it only to enable the app: -Adminizer registers React components into `window.UIComponents` at startup via `registerUIComponents()`. Your module consumes them at runtime without bundling its own copy. +```ts +await adminizer.init(adminpanelConfig); +await adminizer.appManager.enable(new MyApp({ + route: "/my-app", +})); +``` -This means: -- consistent look & feel with the core UI; -- no duplicate React / shadcn / Radix in your bundle; -- dark/light theme and spacing handled automatically. +The app constructor accepts only its own configuration and optional app-level dependencies: -See [UIComponents.md](UIComponents.md) for the full list of available components and their props. +```ts +// Correct +new MyApp({ route: "/my-app" }); ---- +// Do not do this +new MyApp(adminizer, { route: "/my-app" }); +``` -## Types of modules +This boundary lets `AppManager` control resource ownership, cleanup, model permissions, and the capabilities exposed to each app. -| Type | Loaded by | Default export receives | -|---|---|---| -| **Page module** | `module.tsx` (full-page Inertia route) | `{ data?: TData }` | -| **Field control** | `DynamicControls` (inside a form field) | `{ initialValue, onChange, name, options }` | +Use the API supplied for each lifecycle phase: ---- +| App code | Access point | +|---|---| +| `setup()` | Register resources through `AppSetupContext`. | +| App controller | Use the app-scoped `req.runtime`. | +| Catalog factory | Use the `runtime` factory argument. | +| Event listener | Use the `runtime` handler argument. | -## 1. Page module +`req.adminizer` is a deprecated compatibility API. It will not be available starting with Adminizer v6 and must not be used in app code. -A page module is a standalone React component rendered as a full admin page. +## Core Types -```tsx -// my-page-module.tsx -import type { PageModuleProps } from 'adminizer/adminizer-module'; - -interface Item { id: number; name: string; status: string } - -export default function MyPageModule({ data }: PageModuleProps<{ items: Item[] }>) { - const { Button, Table, TableHeader, TableBody, TableRow, TableHead, TableCell, - Badge, Toaster } = window.UIComponents; - const { items = [] } = data ?? {}; - const [loading, setLoading] = React.useState(false); - - const handleAction = async (id: number) => { - setLoading(true); - try { - await window.axios.post(`${window.routePrefix}/api/my-resource/${id}/action`); - window.sonner.toast.success('Done'); - } catch { - window.sonner.toast.error('Failed'); - } finally { - setLoading(false); - } - }; +Import module contracts from Adminizer: - return ( - <> - - - - - Name - Status - - - - - {items.map(item => ( - - {item.name} - {item.status} - - - - - ))} - -
- - ); +```ts +import { + AbstractAdminizerApp, + AppSetupContext, + AppRuntime, +} from "adminizer"; +``` + +A module extends `AbstractAdminizerApp`: + +```ts +export class MyApp extends AbstractAdminizerApp { + readonly name = "my-app"; + readonly version = "1.0.0"; + declare readonly config: MyAppConfig; + + constructor(config: Partial = {}) { + super(); + this.config = { + route: "/my-app", + ...config, + }; + } + + setup(ctx: AppSetupContext): void | Promise { + // Register resources here. + } } ``` -**Backend registration (route + Inertia render):** +Enable it after `adminizer.init(config)`: ```ts -// Pass data and JS path to the frontend -res.render('module', { - moduleComponent: `${routePrefix}/modules/my-page/my-page-module.js`, - data: { items: await fetchItems() }, -}); +await adminizer.init(adminpanelConfig); +await adminizer.appManager.enable(new MyApp()); ``` ---- +`AppManager.enable(app)` registers the app if needed, runs `setup(ctx)`, waits for deferred model, model-access, media-manager, and catalog registrations, and stores disposers for later `disable()` or `unregister()`. `setup()` receives only `AppSetupContext`; it does not receive `Adminizer`. -## 2. Field control module +## Resource Registration -A field control replaces the default input for a specific field type (wysiwyg, codeEditor, jsonEditor, etc.). +`setup(ctx)` receives an `AppSetupContext` with these methods: -### 2.1 Frontend component +| Method | Purpose | +|---|---| +| `ctx.asset(asset)` | Expose a JS/CSS/static file and return its URL. In `ADMINIZER_ENV=dev`, `devUrl` is returned directly. In production, Adminizer creates a static controller for `filePath`. | +| `ctx.control(control)` | Register an app-owned form control with its component asset, optional stylesheet asset, type, name, and base config. | +| `ctx.controller(controller)` | Register an Express route under `config.routePrefix`; returns the resolved full path. | +| `ctx.config(patch, id?)` | Merge a config patch into `adminizer.config`. Arrays of objects with `id` are merged by id; other arrays are appended. | +| `ctx.accessRight(token)` | Register an access rights token owned by the app. | +| `ctx.model(model)` | Attach an existing ORM model to the app. The model must be installed before the app is enabled. | +| `ctx.modelAccess(access)` | Allow the app runtime to access selected models through `runtime.models`. | +| `ctx.aiAssistant(resource)` | Register an app-owned AI assistant handler with model factories. | +| `ctx.mediaManager(resource)` | Register an app-owned media manager through a factory that receives `AppRuntime`. | +| `ctx.catalog(catalog)` | Register a catalog factory and its optional React template components. | +| `ctx.listener(event, handler)` | Subscribe to Adminizer events. The handler receives `(payload, runtime)`. | -```tsx -// my-editor.tsx -import type { FieldModuleProps } from 'adminizer/adminizer-module'; +Registration order after `setup()` is intentional: app model bindings first, model access second, AI assistants third, media managers fourth, and catalogs fifth. This lets factories safely use models owned by the same app. -export default function MyEditor({ initialValue, onChange, name, options }: FieldModuleProps) { - const { MonacoEditor } = window.JSComponents; +## App Runtime - return ( - - ); +`AppRuntime` is the public capability object for app code. App controllers receive it through `req.runtime`; media manager factories, catalog factories, and event listeners receive the same app-scoped runtime as an argument. + +```ts +interface AppRuntime { + models: AppModelAccess; + config: { + readonly routePrefix: string; + getModelConfig(modelName: string): ModelConfig | undefined; + }; + notifications: { + send( + notification: Omit + ): Promise; + }; + apps: { + list(): AppRuntimeApp[]; + get(name: string): AppRuntimeApp | undefined; + enable(name: string): Promise; + disable(name: string): Promise; + unregister(name: string): Promise; + }; } ``` -Props received from `DynamicControls`: +Use `runtime.models.get(modelName)` only for models declared through `ctx.modelAccess()`. This keeps internal and app model access explicit. Runtime model access returns a trusted repository and does not apply `DataAccessor` user or field filtering, so user-facing controllers should enforce access through policies and module-level checks. -| Prop | Type | Description | -|---|---|---| -| `initialValue` | `string` | Current field value | -| `onChange` | `(value: string) => void` | Emit new value to the form | -| `name` | `string` | HTML field name attribute | -| `options` | `Record` | Config from `AbstractControls.getConfig()` merged with per-field overrides | +```ts +ctx.modelAccess({ models: ["Navigation", "Category"] }); + +ctx.catalog({ + id: "navigation", + factory: async (runtime) => { + const navigationModel = runtime.models.get("Navigation"); + // Create and return AbstractCatalog. + }, +}); +``` -### 2.2 Backend control class +For controllers, register model access during setup and read the model through `req.runtime`: ```ts -import { AbstractControls, ControlType, Path, Config, Adminizer } from 'adminizer'; - -export class MyEditor extends AbstractControls { - readonly name = 'my-editor'; - readonly type: ControlType = 'codeEditor'; - readonly path: Path = { - jsPath: { - dev: `${this.routPrefix}/modules/my-editor/my-editor.js`, - production: `${this.routPrefix}/modules/my-editor/my-editor.prod.js`, +setup(ctx: AppSetupContext): void { + ctx.modelAccess({ + id: "users", + models: ["User"], + }); + + ctx.controller({ + id: "users", + method: "get", + route: "/my-app/users", + middleware: async (req, res) => { + const users = await req.runtime.models.get("User").find({}); + return res.json({ users }); }, - cssPath: '', - }; - readonly config: Config = { language: 'javascript' }; + policies: [{ type: "auth", mode: "api" }], + }); +} +``` + +`ControllerHandler` assigns the runtime for the app that registered the controller before its middleware executes. Do not capture `Adminizer` in a controller closure and do not use `req.adminizer`. - constructor(adminizer: Adminizer) { super(adminizer); } +## Routes And Policies - getName() { return this.name; } - getConfig() { return this.config; } - getJsPath() { return process.env.VITE_ENV === 'dev' ? this.path.jsPath.dev : this.path.jsPath.production; } - getCssPath() { return this.path.cssPath || undefined; } +`ctx.controller()` registers routes through `ControllerHandler`. The route is automatically prefixed with the configured Adminizer route prefix, which is available to the controller as `req.runtime.config.routePrefix`. + +```ts +const pageUrl = ctx.controller({ + id: "page", + method: "get", + route: "/my-app", + middleware: async (req, res) => { + return req.Inertia.render({ + component: "module", + props: { + moduleComponent, + data: { rows: [] }, + }, + }); + }, + policies: [{ type: "auth", mode: "ui" }], +}); +``` + +Supported policy types: + +| Policy | Meaning | +|---|---| +| `{ type: "auth", mode?: "ui" | "api" }` | Require authenticated user. | +| `{ type: "auth-enabled" }` | Require auth only when auth is enabled. | +| `{ type: "admin", mode?: "ui" | "api" }` | Require admin user. | +| `{ type: "permission", token, mode?: "ui" | "api" }` | Require one access token. | +| `{ type: "any-permission", tokens, mode?: "ui" | "api" }` | Require any token from a list. | + +Use `mode: "ui"` for Inertia pages and `mode: "api"` for JSON endpoints. + +## Frontend Page Module + +Adminizer renders app pages with the shared Inertia page `module`. Your controller passes `moduleComponent`; the page dynamically imports that ES module and renders its default export. + +```tsx +// MyAppPage.tsx +export default function MyAppPage({ data }: { data?: { rows: Array<{ id: number; name: string }> } }) { + const { Button, Card, CardContent } = window.UIComponents; + + return ( + + + {data?.rows.map((row) => ( + + ))} + + + ); } ``` -Available `type` values: `wysiwyg` | `jsonEditor` | `geoJson` | `markdown` | `table` | `codeEditor` +Register the built asset and route: + +```ts +const moduleComponent = ctx.asset({ + id: "component", + filePath: path.resolve(import.meta.dirname, "MyAppPage.es.js"), + devUrl: "/apps/my-app/MyAppPage.tsx", +}); + +ctx.controller({ + id: "page", + method: "get", + route: "/my-app", + middleware: async (req, res) => req.Inertia.render({ + component: "module", + props: { + moduleComponent, + data: { rows: await loadRows() }, + }, + }), + policies: [{ type: "auth", mode: "ui" }], +}); +``` + +Add a sidebar link through a config layer: + +```ts +ctx.config({ + navbar: { + additionalLinks: [{ + id: "my-app", + link: pageUrl, + type: "self", + title: "My App", + icon: "settings", + section: "Platform", + }], + }, +}); +``` -### 2.3 Register and use +## App Models -**Register on `adminizer:loaded`:** +A module declares ownership of an ORM model by name: ```ts -adminizer.emitter.on('adminizer:loaded', () => { - adminizer.controlsHandler.add(new MyEditor(adminizer)); +ctx.model({ + name: "MyAppState", }); + +ctx.modelAccess({ models: ["MyAppState"] }); ``` -**Reference in model field config:** +`AppManager` does not define tables, register native ORM models, or synchronize schemas. The host or module installer must complete those operations before `appManager.enable()`. + +For Sequelize, models can be installed after `adminizer.init()` and immediately before enabling the app: ```ts -fields: { - body: { - title: 'Body', - type: 'codeEditor', - options: { - name: 'my-editor', // matches AbstractControls.name - config: { language: 'sql' } // overrides control defaults - } - } -} +await installMyAppSequelizeModels(sequelize); +await adminizer.appManager.enable(new MyApp()); ``` ---- +Disabling an app removes its Adminizer model wrapper but does not remove the native Sequelize model or its table. + +`adapter` is optional. If omitted, Adminizer uses `config.system.defaultORM` or the only registered ORM adapter. + +### TypeORM + +TypeORM support remains experimental. Core logs a warning whenever an app is enabled while `system.defaultORM` is `typeorm`. A TypeORM entity required by an app must be included in `DataSource.entities` before `dataSource.initialize()`. TypeORM does not support the Sequelize-style dynamic app model installation used by the fixture. + +Apps without their own models can still be enabled normally with TypeORM. An app with a model can also be enabled when its entity was registered before initialization. If the entity is missing, `AppManager` fails during `enable()` with an error explaining this requirement. -## 3. Building with Vite +## AI Assistant Resources -Your module must be built as an ES module with Adminizer's globals as externals — otherwise React and UI components will be duplicated in the bundle. +An app can own the active AI assistant handler and model services: ```ts -// vite.config.module.ts -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import { viteExternalsPlugin } from 'vite-plugin-externals'; +ctx.accessRight({ + id: "ai-assistant-my-model", + name: "My assistant model", + description: "Access to My assistant model", + department: "AI Assistant", +}); -export default defineConfig({ - plugins: [ - react(), - viteExternalsPlugin({ - 'react': 'React', - 'react-dom': 'ReactDOM', - 'axios': 'axios', - 'sonner': 'sonner', - }), +ctx.aiAssistant({ + models: [ + (context) => new MyAiModelService(context), ], - build: { - lib: { - entry: 'src/my-module.tsx', - formats: ['es'], - fileName: 'my-module', - }, - rollupOptions: { - // Keep UIComponents and JSComponents external too - // if you import them as bare specifiers in your module - }, +}); + +ctx.config({ + aiAssistant: { + enabled: true, + defaultModel: "my-model", + models: ["my-model"], }, }); ``` -The output is a small `.js` file that reads globals from the Adminizer page at runtime. +`ctx.aiAssistant()` creates an `AiAssistantHandler` for the app and registers model services returned by its factories. The factory receives an AI context with `runtime`, `routePrefix`, model resource lookup helpers, permission checks, and `createDataAccessor()`. Disabling the app restores the previous handler, removes the app's config layer, and unregisters app-owned access tokens. ---- +Assistant model classes should extend `AbstractAiModelService`, but they should not register access rights themselves. The app owns tokens through `ctx.accessRight()`. Register routes with `ctx.controller()` and the reusable `AiAssistantController` when the app exposes the standard assistant API. The fixture implementation is in `fixture/apps/ai-assistant`. -## 4. TypeScript setup +## Media Manager Resources -Add the Adminizer module declarations to your `tsconfig.json`: +An app can own a media manager implementation: -```json -{ - "compilerOptions": { - "jsx": "react-jsx", - "moduleResolution": "bundler" - }, - "include": [ - "src", - "node_modules/adminizer/adminizer-module.d.ts" - ] -} +```ts +ctx.mediaManager({ + factory: (runtime) => new MyMediaManager(runtime, config), +}); ``` -This gives you: -- autocomplete and hover docs for `window.UIComponents.*`, `window.JSComponents.*`, `window.adminApi`, `window.sonner` -- `PageModuleProps` and `FieldModuleProps` types for your component signatures +The manager is created after the app models and scoped model access are registered. Disabling the app unregisters the manager automatically. The app owns its access right, models, model access, and storage implementation. ---- +In Adminizer v5, the media manager HTTP API, upload adapter, thumbnail endpoint, public-file binding, and file icons remain in core for legacy compatibility. A manager registered through `mediaManagerHandler.add()` and a manager registered through `ctx.mediaManager()` use the same core routes. This transport layer is planned to move to the system runtime in v6. -## 5. Using UI components +The fixture default implementation is in `fixture/apps/media-manager`. Its Sequelize installer runs after `adminizer.init()` and before `appManager.enable()`. It is intentionally not enabled in the TypeORM fixture: TypeORM remains experimental and app entities must be registered in `DataSource.entities` before `initialize()`. -Destructure from `window.UIComponents` at the top of your component. All components follow the same API as documented in [UIComponents.md](UIComponents.md). +`AbstractMediaManager` still accepts `Adminizer` in its constructor and registers its access right through `_bindAccessRight()` for the legacy v5 path. Existing integrations may continue to instantiate a custom manager and call `adminizer.mediaManagerHandler.add(manager)` until v6. App-owned managers should register their access right with `ctx.accessRight()` and pass a no-op legacy host to the base constructor; see the fixture implementation and [MediaManager.md](MediaManager.md). -```tsx -const { - Button, Badge, Card, CardHeader, CardTitle, CardContent, - Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, - Input, Label, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, - Table, TableHeader, TableBody, TableRow, TableHead, TableCell, - Toaster, -} = window.UIComponents; - -const { MultiSelect, MonacoEditor } = window.JSComponents; -const { Pencil, Trash2, Plus } = window.LucideReact; -``` +## Catalog Template Components -**Key rules:** -- Never `import` shadcn, Radix, or Lucide directly — use the globals above. -- Wrap your root with `` once if you use `window.sonner.toast`. -- Wrap with `` once if you use ``. -- Use `window.adminApi` instead of raw `window.axios` for JSON API calls — it adds no-cache headers and throws a readable error if the session expires. +Catalog item types return add/edit templates with a `type` string: ---- +```ts +async getAddTemplate(req: ReqType) { + return { + type: "my-catalog.item-form", + data: { labels: { save: req.i18n.__("Save") } }, + }; +} +``` -## 6. HTTP calls +A module maps that template `type` to a React component in the `templates` array of `ctx.catalog()`: ```ts -// Preferred — typed, no-cache, throws on HTML responses (session expiry) -const { data } = await window.adminApi.getJson<{ rows: Row[] }>( - `${window.routePrefix}/api/my-resource` -); - -await window.adminApi.postJson(`${window.routePrefix}/api/my-resource`, { name: 'New' }); -await window.adminApi.putJson(`${window.routePrefix}/api/my-resource/${id}`, payload); -await window.adminApi.deleteJson(`${window.routePrefix}/api/my-resource/${id}`); -``` +const catalogTemplates = ctx.asset({ + id: "catalog-templates", + filePath: path.resolve(import.meta.dirname, "CatalogTemplates.es.js"), + devUrl: "/apps/my-app/CatalogTemplates.tsx", +}); -Always use `window.routePrefix` to build URLs — it holds the configured admin prefix (e.g. `/admin`). +ctx.catalog({ + id: "my-catalog", + templates: [{ + id: "item-form", + type: "my-catalog.item-form", + component: catalogTemplates, + exportName: "MyCatalogItemTemplate", + }], + factory: (runtime) => new MyCatalog(runtime), +}); +``` ---- +Template components are scoped to the catalog returned by the same resource. `CatalogTemplateComponentHandler` prevents two enabled components from claiming the same template type in that catalog scope. -## 7. Notifications (toasts) +Template props on the frontend: ```ts -window.sonner.toast('Record saved'); -window.sonner.toast.success('Created successfully'); -window.sonner.toast.error('Something went wrong'); -window.sonner.toast.warning('Deprecated field'); -window.sonner.toast.promise( - window.adminApi.postJson('/admin/api/items', payload), - { loading: 'Saving...', success: 'Saved', error: 'Failed' } -); +interface CatalogTemplateComponentProps { + mode: "create" | "update"; + type: string; + template: { type: string; data: any }; + parentId: string | number; + itemType: string | null; + selectedItem?: Record; + messages: Record; + actions: { + close: () => void; + reload: (item?: any) => Promise; + openModelAdd: (model: string) => Promise; + }; +} ``` ---- +A minimal template: -## 8. Navigation links and the Inertia popup problem +```tsx +export function MyCatalogItemTemplate({ template, itemType, parentId, actions }: CatalogTemplateComponentProps) { + const { Button, Input, Label } = window.UIComponents; + const [name, setName] = React.useState(""); + + const save = async () => { + const res = await window.adminApi.post("", { + _method: "createItem", + data: { + type: itemType, + parentId, + name, + }, + }); + actions.close(); + await actions.reload(res.data.data); + }; -Adminizer uses [Inertia.js](https://inertiajs.com/) for SPA navigation. The sidebar's `` component intercepts clicks and performs an XHR fetch to the target URL expecting an Inertia JSON response. + return ( +
+ + setName(e.target.value)} /> + +
+ ); +} +``` -**If the link target is NOT an Inertia endpoint, a popup modal appears and then the page does a full reload.** +## Catalog Module Pattern -This happens because Inertia receives an HTML page instead of the expected JSON response and falls back to a hard redirect, which briefly shows an error popup. +For catalogs, prefer registering through an app instead of calling `adminizer.catalogHandler.add()` directly. The module can register storage models, access rights, sidebar links, catalog templates, and event listeners in one place. -### When does this occur? +```ts +export class MyCatalogApp extends AbstractAdminizerApp { + readonly name = "my-catalog"; + readonly version = "1.0.0"; + declare readonly config: MyCatalogAppConfig; + + constructor(config: MyCatalogAppConfig) { + super(); + this.config = config; + } -- The link uses a **relative path** that resolves to the wrong URL (e.g. `/integrations` instead of `/dashboard/integrations`). -- The target route **does not call `req.Inertia.render()`** — for example it returns plain JSON or redirects. -- The link points to an **external URL** or a route outside the adminizer prefix. + setup(ctx: AppSetupContext): void { + ctx.accessRight({ + id: "catalog-my-catalog", + name: "My Catalog", + description: "Access to My Catalog", + department: "Catalog", + }); + + ctx.model({ + name: "MyCatalogStorage", + }); + + ctx.modelAccess({ models: ["MyCatalogStorage"] }); + + ctx.config({ + navbar: { + additionalLinks: [{ + id: "my-catalog-main", + type: "self", + link: `${this.config.routePrefix}/catalog/my-catalog/main`, + title: "My Catalog", + icon: "folder", + accessRightsToken: "catalog-my-catalog", + }], + }, + }); + + ctx.catalog({ + id: "my-catalog", + factory: async (runtime) => { + const catalog = new MyCatalog(runtime, this.config); + await catalog.ready?.(); + return catalog; + }, + }); + } +} +``` -### How to fix +## Events -**1. Always use full paths with the `routePrefix`:** +`ctx.listener(event, handler)` is useful for synchronizing module data with model changes. Adminizer emits `model:updated` after edit controller updates a record. ```ts -// ❌ Wrong — resolves to /integrations, not an Inertia route -link: '/integrations' +ctx.listener("model:updated", async (event: { modelName: string; record: Record }, runtime) => { + if (event.modelName.toLowerCase() !== "category") { + return; + } -// ✅ Correct — matches the registered Inertia route -link: '/dashboard/integrations' + const storage = runtime.models.get("MyCatalogStorage"); + await storage.update({ where: { label: "main" } }, { touchedAt: Date.now() } as any); +}); ``` -**2. For hash-based navigation (SPA sub-pages), use `` instead of Inertia ``:** +`AppManager` also emits lifecycle events such as `app:registered`, `app:enable:start`, `app:enabled`, `app:disable:start`, `app:disabled`, `app:unregistered`, and resource-level registration events. -Inertia's `` does not understand hash-only URLs. If you navigate to `/dashboard/integrations#edit/123`, Inertia will fetch `/dashboard/integrations#edit/123` as a new page request, ignoring the hash. +## Vite Build For App Components -Use a native `` tag for URLs that contain a `#`: +Build module UI as an ES module and externalize Adminizer globals to avoid bundling another copy of React or UI components. -```tsx -function NavLink({ href, children }: { href: string; children: React.ReactNode }) { - if (href.includes('#')) return {children}; // full reload, keeps hash - return {children}; // Inertia SPA navigation -} +```ts +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; +import { viteExternalsPlugin } from "vite-plugin-externals"; + +export default defineConfig({ + plugins: [ + react(), + viteExternalsPlugin({ + react: "React", + "react-dom": "ReactDOM", + "lucide-react": "LucideReact", + sonner: "sonner", + "@/components/ui/button": "UIComponents", + "@/components/ui/card": "UIComponents", + "@/components/ui/input": "UIComponents", + "@/components/ui/label": "UIComponents", + }), + ], + build: { + outDir: path.resolve(import.meta.dirname, ""), + emptyOutDir: false, + lib: { + entry: path.resolve(import.meta.dirname, "MyAppPage"), + formats: ["es"], + fileName: (format) => `MyAppPage.${format}.js`, + }, + rollupOptions: { + external: [ + "@/components/ui/button", + "@/components/ui/card", + "@/components/ui/input", + "@/components/ui/label", + ], + }, + }, + resolve: { + alias: { + "@": path.resolve(import.meta.dirname, "../../src/assets/js"), + }, + }, +}); ``` -**3. Ensure every custom route renders via Inertia:** +For heavy built-in `window.JSComponents` controls, import Adminizer's lazy proxy modules in development and externalize those imports in production: + +```tsx +import HandsonTable from "@/js-components/handsontable"; +import MonacoEditor from "@/js-components/monaco"; +import VanillaJSONEditor from "@/js-components/jsoneditor"; +``` ```ts -// ✅ Required — Inertia.render() returns the correct JSON envelope -handler: async (req, res) => { - return req.Inertia.render({ component: 'module', props: { ... } }); -} +viteExternalsPlugin({ + "@/js-components/handsontable": ["JSComponents", "HandsonTable"], + "@/js-components/monaco": ["JSComponents", "MonacoEditor"], + "@/js-components/jsoneditor": ["JSComponents", "VanillaJSONEditor"], +}); +``` -// ❌ Wrong — plain res.json() triggers the popup -handler: async (req, res) => { - return res.json({ data: items }); -} +Do not import `@/components/handsontable`, `@/components/monaco-editor`, or `@/components/VanillaJSONEditor` directly from app modules when you want lazy loading. Those component imports bypass the control proxy layer that loads the production assets and related styles on demand. + +Use project-specific build scripts for modules. The fixture uses: + +```bash +npm run build:apps ``` -Note: API routes that return JSON (e.g. `?_method=getItems`) must be handled **before** the Inertia render branch in the same handler, not as separate routes, to avoid Inertia intercepting them. +`build:apps` also runs `build:catalog-modules`, which builds the legacy virtual catalog React templates and actions from `fixture/virtual-catalog`. ---- +## Fixture Examples -## 9. Deployment checklist +The fixture contains several current app-module examples: -- [ ] Bundle contains no duplicated React / shadcn / Radix libs (check with `vite-bundle-visualizer`). -- [ ] All UI elements come from `window.UIComponents` / `window.JSComponents`. -- [ ] `loading`, `error`, and `success` states are handled for every async action. -- [ ] `dev` and `production` paths in `AbstractControls.path` point to the correct built assets. -- [ ] Control is registered inside the `adminizer:loaded` listener. -- [ ] Model field references the control via `options.name`. -- [ ] TypeScript declarations included via `node_modules/adminizer/adminizer-module.d.ts`. +| App | Files | Demonstrates | +|---|---|---| +| `ai-assistant` | `fixture/apps/ai-assistant/*` | App-owned AI assistant handler, model access tokens, standard AI routes, config layer, and runtime enable/disable through `AppManager`. | +| `notification-sender` | `fixture/apps/notification-sender/*` | Page module, asset registration, scoped model access through `req.runtime.models`, user message notifications, UI/API routes, sidebar config patch. | +| `module-manager` | `fixture/apps/module-manager/*` | Access right token, permission-protected page/API routes, app lifecycle control through `req.runtime.apps`. | +| `navigation` | `fixture/apps/navigation/*` | Runtime model, model access, catalog factory, catalog template components, sidebar links, `model:updated` listener. | +| `media-manager` | `fixture/apps/media-manager/*` | Dynamic Sequelize models, app-owned storage implementation, scoped model access, manager registration, lifecycle cleanup. | +| `quill-editor` | `fixture/apps/quill-editor/*` | App-owned WYSIWYG control, lazy component and stylesheet assets, and a dedicated ES module build. | + +## Field Controls + +Custom form controls are app-owned resources registered through `ctx.control()`. Their JavaScript and optional stylesheet are loaded only when the field renderer mounts the control. Disabling the app unregisters the control and removes its asset routes. See [Controls.md](Controls.md) for the complete contract, build configuration, field configuration, and fallback behavior. + +## Checklist + +- App has stable `name` and `version`. +- App constructor accepts app config/dependencies, never an `Adminizer` instance. +- App controllers use `req.runtime`; they do not use the deprecated `req.adminizer`. +- All registered resources have deterministic `id` values. +- UI controllers render through `req.Inertia.render({ component: "module", props: { moduleComponent, ... } })`. +- API controllers use `mode: "api"` policies and return JSON. +- Frontend assets are registered through `ctx.asset()` with both `filePath` and `devUrl` when local development is needed. +- Custom form controls are registered through `ctx.control()` with deterministic component and stylesheet asset IDs. +- App-specific models are declared with `ctx.model()` and allowed with `ctx.modelAccess()` before catalogs use them. +- Catalog templates are registered in `ctx.catalog({ templates: [...] })` instead of hard-coded paths in catalog data. +- Module UI uses `window.UIComponents`, `window.JSComponents`, `window.adminApi`, and global React/Lucide exports instead of bundling duplicates. diff --git a/docs/Catalog.md b/docs/Catalog.md index a5973d06..f7462d60 100644 --- a/docs/Catalog.md +++ b/docs/Catalog.md @@ -1,805 +1,420 @@ # Catalog -## Abstract - -The Adminizer catalog system provides a powerful and flexible way to manage hierarchical (tree-shaped) data structures through a unified interface. Catalogs allow creating editors for any data that have parent-child relationships: navigation menus, product categories, document structures, organizational hierarchies, etc. - -**Why is this needed:** -- **Unified interface**: one UI component for all tree structures -- **Drag & Drop**: dragging elements to change order and nesting -- **Search with context**: searching for elements with automatic display of the parent chain -- **Contextual actions**: custom operations on elements (export, bulk editing, etc.) -- **Element typing**: different types of elements can coexist in one tree (groups, links, model records) -- **Flexible persistence**: saving to the database through any ORM adapter either via parentId, intermediate link model, or the entire catalog tree at once - -**Main features:** -- Creating, editing, deleting elements through modal forms -- Automatic sorting (`sortOrder`) when moving elements -- Multiple catalog instances (e.g., different navigation menus) -- Integration with the Adminizer access rights system -- Support for custom React components for add/edit forms - -## Overview - -The Adminizer catalog system powers tree-shaped resources such as navigation menus, document hierarchies, or any structure that mixes groups with leaf records. Catalogs expose a uniform contract on the backend and deliver pre-normalised data to the frontend so the UI can render, drag, search, and execute contextual actions without knowing the underlying storage. - -Every catalog must be registered in the global [`CatalogHandler`](#cataloghandler). Adminizer exposes the catalog UI at `/admin/catalog/:slug/:id?`, where `slug` selects the catalog and the optional `id` points to a specific storage instance (for example, a navigation section). - -When a catalog exposes exactly one available storage `id`, the frontend treats it as already selected. In that case the UI renders a badge with that `id` instead of the selector dropdown, and if the current route has no `id` yet the client redirects to `/catalog/:slug/:id` automatically. - -## Runtime Architecture - -### AbstractCatalog -`src/lib/catalog/AbstractCatalog.ts:119` defines the abstract base class for any catalog. Its responsibilities: -- Storing catalog metadata (`id`, `name`, `slug`, `icon` and flag `movingGroupsRootOnly`); -- Managing registered element types (`itemTypes`) and global action handlers (`actionHandlers`); -- Delegating CRUD, templates and search queries to the correct element type via `getItemType`; -- Providing helper methods for the controller (`getChilds`, `getActions`, `handleAction`, `getLink`, `getPopUpTemplate`, `createItem`, `updateItem`, `updateModelItems`, `deleteItem`, `getitemTypes`, `search`); -- Registering a catalog-specific access rights token (`catalog-${slug}` or `catalog-${slug}-${id}`) for uniform rights checking. - -`AbstractCatalog` expects concrete implementations to provide `name`, `slug`, `icon` and a list of element type instances via the constructor. Optional catalog-level actions can be added via `addActionHandler`. When `selectedItemTypes` is empty, the handler is considered global; otherwise, it is attached to each matching element type. - -### BaseItem, AbstractGroup, AbstractItem -`BaseItem` models the type of an individual catalog element. Concrete implementations provide: -- Static metadata (`type`, `name`, `icon`, `allowedRoot`, flag `isGroup`); -- Connectors to the Adminizer environment (`adminizer`, optional reference to `storageServices` for shared persistence); -- Data access methods (`find`, `create`, `update`, `deleteItem`, `updateModelItems`, `getChilds`, `search`); -- UI template factories (`getAddTemplate`, `getEditTemplate`), returning either a path to a React component, or a navigation template, or a model-based selector. - -`BaseItem` enriches each returned record with `type` and `icon` fields via the `_enrich` method so that the frontend can render unified nodes. Helper methods `_find` and `_getChilds` apply this enrichment automatically. `AbstractGroup` and `AbstractItem` provide sensible defaults for grouping (`type: "group"`, `icon: "folder"`, `isGroup: true`) and leaf elements (`isGroup: false`). - -### ActionHandler -Action handlers describe operations that can be invoked from the context menu or toolbar. The abstract contract in `src/lib/catalog/AbstractCatalog.ts:203` provides: -- `type`: either `basic`, `external`, or `link`; -- `displayContext` / `displayTool`: placement hints for UI; -- `selectedItemTypes`: restricts handlers to specific element `type` values; empty array means the handler is global; -- `id`, `name`, `icon`: UI descriptors; -- `getPopUpTemplate`, `getLink`: template providers for `external` and `link` actions; -- `handler`: backend implementation that receives permitted `Item` entities plus optional payload. - -Handlers are discovered via `AbstractCatalog.getActions`. `FrontendCatalog.getActions` filters them for use in context or toolbar before returning data to the client. - -## Request Lifecycle -`catalogController` (`src/controllers/catalog/Catalog.ts`) handles HTTP traffic: -- Checks access rights using the rights token registered by the catalog; -- Resolves the catalog by slug and optional storage `id` (also used to get permitted IDs via `getIdList`); -- For `GET` requests renders the Inertia page `catalog`; -- For `POST`, `PUT` and `DELETE` requests creates a `FrontendCatalog` instance and dispatches operations based on `_method`. - -The controller currently supports the following `_method` values: -- `getCatalog`: returns the tree, available element types, catalog metadata and toolbar actions; -- `getChilds`, `createItem`, `updateItem`, `deleteItem`: CRUD endpoints working with specific nodes; -- `getAddTemplate`, `getEditTemplate`: provide UI payloads for modal forms or custom components; -- `search`: returns tagged search results along with their ancestors and siblings; -- `getActions`, `handleAction`, `getLink`, `getPopUpTemplate`: execute or describe action handlers; -- `updateTree`: saves order and parent changes on drag-and-drop, recalculating `sortOrder` and `parentId` of each node. - -## Frontend Adapter -`FrontendCatalog` (`src/controllers/catalog/FrontendCatalogAdapter.ts`) wraps a catalog instance and normalizes responses for the React tree view. Main responsibilities include: -- Converting backend elements to `NodeModel` instances (`FrontendCatalogUtils.arrayToNode`, `treeToNode`); -- Replacing `null` parent IDs with `0` for UI and restoring them on write (`normalizeForFrontend`, `refinement`); -- Resolving localized catalog UI strings via `getLocales` so the client can display multilingual labels; -- Recursively deleting nodes with `deleteItem` to ensure that deleting a group also cleans up its descendants before backend cleanup. - -The helper class `FrontendCatalogUtils` contains stateless converters that React components use directly. When adding new element data points, update these utilities so the frontend continues to receive complete node descriptors. - -## Creating a Custom Catalog -1. **Develop a storage strategy for elements.** Decide how catalog data will be stored. Follow the `StorageService` example or implement persistence API directly in each element type. -2. **Implement element types.** Extend `AbstractGroup` for folders and `AbstractItem` for leaf records. Provide required metadata, CRUD methods and templates. Use `_enrich` via provided helpers if returning raw entities. -3. **Assemble the catalog.** Create a subclass of `AbstractCatalog`, instantiate element types and call `super(adminizer, itemTypes)`. Optionally configure action handlers and override `getIdList` when the catalog supports multiple storage instances. -4. **Register the catalog.** During Adminizer boot add the catalog to the global handler: `adminizer.catalogHandler.add(new MyCatalog(adminizer));` as shown in `src/lib/catalog/CatalogHandler.ts`. -5. **Provide storage identifiers.** If the catalog uses multiple roots, add them to `idList` so the controller can validate incoming `/catalog/:slug/:id` requests. - -## CatalogHandler -`src/lib/catalog/CatalogHandler.ts` stores registered catalogs in memory. It provides three methods: -- `add(catalog: AbstractCatalog)`: stores the catalog instance and returns it; -- `getAll()`: lists all registered catalogs (useful for diagnostics or dashboards); -- `getCatalog(slug: string)`: retrieves the catalog by slug that the controller gets from the request. - -Call `CatalogHandler.add` during Adminizer initialization so the catalog becomes available at `/admin/catalog/:slug`. - -## Example: Simple Product Catalog - -An example of a simple product catalog where groups (categories) and products are linked via `parentId`. It is assumed that `Group` and `Product` models are already defined in Sequelize with fields `id`, `name`, `parentId`, `sortOrder` and `description` for products. - -### Catalog Implementation - -```typescript -// lib/catalog/ProductCatalog.ts -import { AbstractCatalog, AbstractGroup, AbstractItem, Item } from './AbstractCatalog'; -import { Adminizer } from '../Adminizer'; -import Group from '../../models/Group'; -import Product from '../../models/Product'; - -interface CatalogItem extends Item { - description?: string; +Adminizer catalogs provide a shared tree editor for hierarchical data: navigation menus, product categories, page trees, document structures, and other resources with parent-child relations. The backend owns storage and item behavior; the frontend receives normalized nodes and template metadata. + +Catalogs can still be added directly to `CatalogHandler`, but the preferred current pattern is to register catalogs from an `AbstractAdminizerApp` with `ctx.catalog()`. That lets the module register its models, access rights, sidebar links, template components, and event listeners together. + +## Runtime Shape + +A catalog is an `AbstractCatalog` subclass with: + +| Member | Purpose | +|---|---| +| `name` | Human-readable catalog name. | +| `slug` | Route identifier used by `/catalog/:slug/:id?`. Must be unique. | +| `icon` | Material icon name for UI metadata. | +| `id` | Current storage instance id. Set from route params by `FrontendCatalog.setId()`. | +| `itemTypes` | Registered element type instances. Exactly one group item type is allowed. | +| `actionHandlers` | Global catalog actions. Item-specific actions live on item types. | +| `movingGroupsRootOnly` | UI hint that restricts group movement to root level. | + +The item shape is defined by `Item`: + +```ts +interface Item { + id: string | number; + name: string; + parentId: string | number | null; + childs?: Item[]; + sortOrder: number; + icon: string; + type: string; + marked?: boolean; } +``` -// Product Group -class ProductGroup extends AbstractGroup { - readonly type = 'group'; - readonly name = 'Product Group'; - readonly icon = 'folder'; - readonly allowedRoot = true; - - constructor(protected adminizer: Adminizer) { - super(); - } +Use `null` for root-level `parentId`. The frontend adapter converts root `parentId` to `0` for `@minoru/react-dnd-treeview` and converts it back to `null` before writes. - async find(itemId: string | number): Promise { - const group = await Group.findByPk(itemId); - if (!group) throw new Error('Group not found'); - - return { - id: group.id, - name: group.name, - parentId: group.parentId, - sortOrder: group.sortOrder, - type: this.type, - icon: this.icon, - }; - } +## AbstractCatalog Responsibilities - async create(data: any): Promise { - const group = await Group.create({ - name: data.name, - parentId: data.parentId || null, - sortOrder: data.sortOrder || 0, - }); +`AbstractCatalog` delegates all item-specific work to `BaseItem` instances and provides controller-facing helpers: - return { - id: group.id, - name: group.name, - parentId: group.parentId, - sortOrder: group.sortOrder, - type: this.type, - icon: this.icon, - }; - } +| Method | Purpose | +|---|---| +| `getChilds(parentId, byItemType?, req?)` | Load children across all item types or one item type, sorted by `sortOrder`. | +| `find(item, req?)` | Resolve one item through its `type`. | +| `createItem(data, req?)` | Create an item through its `type`. Throws if the type is unknown or the handler returns no item. | +| `updateItem(id, type, data, req?)` | Update one catalog node. Throws on unknown type or empty result. | +| `updateModelItems(modelId, type, data, req?)` | Update catalog nodes linked to a source model record. | +| `deleteItem(type, id, req?)` | Delete one item through its item type. | +| `getAddTemplate(item, req)` | Return add-form template metadata for the selected item type. | +| `getEditTemplate(item, id, req, modelId?)` | Return edit-form template metadata. | +| `getActions(items?)` | Return global actions or actions for one selected item type. | +| `handleAction(actionId, items?, data?, req?)` | Execute a catalog action. | +| `getIdList()` | Return valid storage instance ids, for example navigation sections. Default is `[]`. | +| `search(s, hasExtras?, req?)` | Search all item types and build enough parent context for the tree UI. | - async update(itemId: string | number, data: CatalogItem): Promise { - const group = await Group.findByPk(itemId); - if (!group) throw new Error('Group not found'); - - await group.update({ - name: data.name, - parentId: data.parentId, - sortOrder: data.sortOrder, - }); +`getItemType(type)` is still available, but internal write operations now require a valid item type. This avoids silent `undefined` results and makes template/type mismatches visible as explicit errors. - return { - id: group.id, - name: group.name, - parentId: group.parentId, - sortOrder: group.sortOrder, - type: this.type, - icon: this.icon, - }; - } +## Item Types - async deleteItem(itemId: string | number): Promise { - await Group.destroy({ where: { id: itemId } }); - } +A catalog item type extends either `AbstractGroup` or `AbstractItem`. - async getChilds(parentId: string | number | null): Promise { - const groups = await Group.findAll({ - where: { parentId }, - order: [['sortOrder', 'ASC']], - }); +```ts +class MyGroup extends AbstractGroup { + readonly type = "group"; + readonly name = "Group"; + readonly allowedRoot = true; + readonly adminizer = legacyAdminizerHost; + readonly actionHandlers = []; + + async find(itemId: string | number, catalogId: string, req?: ReqType): Promise {} + async create(data: MyCatalogItem, catalogId: string, req?: ReqType): Promise {} + async update(itemId: string | number, data: MyCatalogItem, catalogId: string, req?: ReqType): Promise {} + async updateModelItems(modelId: string | number, data: any, catalogId: string, req?: ReqType): Promise {} + async deleteItem(itemId: string | number, catalogId: string, req?: ReqType): Promise {} + async getChilds(parentId: string | number | null, catalogId: string, req?: ReqType): Promise {} + async search(s: string, catalogId: string, req?: ReqType): Promise {} + async getAddTemplate(req: ReqType): Promise {} + async getEditTemplate(id: string | number, catalogId: string, req: ReqType, modelId?: string | number): Promise {} +} +``` - return groups.map(group => ({ - id: group.id, - name: group.name, - parentId: group.parentId, - sortOrder: group.sortOrder, - type: this.type, - icon: this.icon, - })); - } +`BaseItem._getChilds()` and `BaseItem._find()` call `_enrich()` automatically. `_enrich()` writes `type` and `icon` to returned items, so raw records do not need to persist those service fields. - async search(s: string): Promise { - const groups = await Group.findAll({ - where: { - name: { [Op.iLike]: `%${s}%` }, - }, - }); +## Templates - return groups.map(group => ({ - id: group.id, - name: group.name, - parentId: group.parentId, - sortOrder: group.sortOrder, - type: this.type, - icon: this.icon, - })); - } +`getAddTemplate()` and `getEditTemplate()` return a `CatalogTemplate`: - async getAddTemplate(req: any): Promise { - return { - type: 'model', - data: { - model: 'Group', - items: [], - labels: { - title: req.i18n.__('Add Group'), - save: req.i18n.__('Save'), - }, - }, - }; - } - - async getEditTemplate(id: string | number, catalogId: string, req: any): Promise { - const item = await this.find(id); - return { - type: 'model', - data: { - item, - model: 'Group', - labels: { - title: req.i18n.__('Edit Group'), - save: req.i18n.__('Save'), - }, - }, - }; - } +```ts +interface CatalogTemplate { + type: string; + data: TData; } +``` -// Product -class ProductItem extends AbstractItem { - readonly type = 'product'; - readonly name = 'Product'; - readonly icon = 'shopping_cart'; - readonly allowedRoot = false; - - constructor(protected adminizer: Adminizer) { - super(); - } - - async find(itemId: string | number): Promise { - const product = await Product.findByPk(itemId); - if (!product) throw new Error('Product not found'); - - return { - id: product.id, - name: product.name, - parentId: product.parentId, - sortOrder: product.sortOrder, - type: this.type, - icon: this.icon, - description: product.description, - }; - } +There are three practical template modes. - async create(data: any): Promise { - const product = await Product.create({ - name: data.name, - parentId: data.parentId, - sortOrder: data.sortOrder || 0, - description: data.description || '', - }); +### Built-in model form - return { - id: product.id, - name: product.name, - parentId: product.parentId, - sortOrder: product.sortOrder, - type: this.type, - icon: this.icon, - description: product.description, - }; - } +Return `type: "model"` when the catalog should open the standard Adminizer add/edit form for a model: - async update(itemId: string | number, data: CatalogItem): Promise { - const product = await Product.findByPk(itemId); - if (!product) throw new Error('Product not found'); - - await product.update({ - name: data.name, - parentId: data.parentId, - sortOrder: data.sortOrder, - description: data.description, - }); - - return { - id: product.id, - name: product.name, - parentId: product.parentId, - sortOrder: product.sortOrder, - type: this.type, - icon: this.icon, - description: product.description, - }; - } +```ts +return { + type: "model", + data: { + model: "Product", + }, +}; +``` - async deleteItem(itemId: string | number): Promise { - await Product.destroy({ where: { id: itemId } }); - } +### Legacy dynamic component path - async getChilds(parentId: string | number | null): Promise { - const products = await Product.findAll({ - where: { parentId }, - order: [['sortOrder', 'ASC']], - }); +Older catalogs can return a custom component path in `data.path`. The catalog UI imports it directly. This remains supported for compatibility, but new modules should prefer registered catalog template components. - return products.map(product => ({ - id: product.id, - name: product.name, - parentId: product.parentId, - sortOrder: product.sortOrder, - type: this.type, - icon: this.icon, - description: product.description, - })); - } +```ts +return { + type: "component", + data: { + path: myCatalogFormUrl, + }, +}; +``` - async search(s: string): Promise { - const products = await Product.findAll({ - where: { - [Op.or]: [ - { name: { [Op.iLike]: `%${s}%` } }, - { description: { [Op.iLike]: `%${s}%` } }, - ], - }, - }); +Use `adminizer.assetHandler.register(...)` or `ctx.asset(...)` to produce `myCatalogFormUrl`; do not rely on manually copied files under the static assets directory. In development, the registered asset can point at a Vite-served `.tsx` URL such as `/fixture/virtual-catalog/group.tsx`; in production, `AssetHandler` serves the built `.es.js` file through an app asset route. - return products.map(product => ({ - id: product.id, - name: product.name, - parentId: product.parentId, - sortOrder: product.sortOrder, - type: this.type, - icon: this.icon, - description: product.description, - })); - } +### Registered catalog template component - async getAddTemplate(req: any): Promise { - return { - type: 'model', - data: { - model: 'Product', - labels: { - title: req.i18n.__('Add Product'), - save: req.i18n.__('Save'), - }, - }, - }; - } +Current module-based catalogs should return a stable template type and register the React component in the `templates` array of `ctx.catalog()`: - async getEditTemplate(id: string | number, catalogId: string, req: any): Promise { - const item = await this.find(id); - return { - type: 'model', - data: { - item, - model: 'Product', - labels: { - title: req.i18n.__('Edit Product'), - save: req.i18n.__('Save'), - }, +```ts +// Item type +async getAddTemplate(req: ReqType) { + return { + type: "products.product-form", + data: { + labels: { + title: req.i18n.__("Product"), + save: req.i18n.__("Save"), }, - }; - } -} - -// Product Catalog -export class ProductCatalog extends AbstractCatalog { - readonly name = 'Product Catalog'; - readonly slug = 'products'; - readonly icon = 'inventory'; - - constructor(adminizer: Adminizer) { - const itemTypes = [ - new ProductGroup(adminizer), - new ProductItem(adminizer), - ]; - super(adminizer, itemTypes); - } + }, + }; } ``` -### Catalog Registration - -```typescript -// In the Adminizer initialization file -import { ProductCatalog } from './lib/catalog/ProductCatalog'; - -const adminizer = new Adminizer(/* config */); - -// Register the catalog -adminizer.catalogHandler.add(new ProductCatalog(adminizer)); +```ts +// App setup +ctx.catalog({ + id: "products", + templates: [{ + id: "product-form", + type: "products.product-form", + component: productTemplatesAsset, + exportName: "ProductFormTemplate", + }], + factory: (runtime) => new ProductsCatalog(runtime), +}); ``` -Now the catalog will be available at `/admin/catalog/products` and will allow managing the hierarchy of groups and products through the drag & drop interface. +When `getCatalog` is called, the controller resolves the registered template components for the catalog slug. The frontend matches `template.type.toLowerCase()` against those records, dynamically imports `component`, and renders `exportName` or the default export. -## Обзор +Template props are documented in [BuildingModules.md](BuildingModules.md#catalog-template-components). -The Adminizer catalog system powers tree-shaped resources such as navigation menus, document hierarchies, or any structure that mixes groups with leaf records. Catalogs expose a uniform contract on the backend and deliver pre-normalised data to the frontend so the UI can render, drag, search, and execute contextual actions without knowing the underlying storage. +## Controller Lifecycle -Every catalog must be registered in the global [`CatalogHandler`](#cataloghandler). Adminizer exposes the catalog UI at `/admin/catalog/:slug/:id?`, where `slug` selects the catalog and the optional `id` points to a specific storage instance (for example, a navigation section). +`catalogController` handles `/catalog/:slug/:id?`: -## Runtime Architecture +1. Resolve catalog by `slug` from `CatalogHandler`. +2. Validate optional `id` against `catalog.getIdList()` when the list is non-empty. +3. Render Inertia page `catalog` for `GET`. +4. For `POST`, `PUT`, and `DELETE`, create `FrontendCatalog`, set catalog id, and dispatch by `_method`. -### AbstractCatalog -`src/lib/catalog/AbstractCatalog.ts:119` определяет абстрактный базовый класс для любого каталога. Его обязанности: -- Хранение метаданных каталога (`id`, `name`, `slug`, `icon` и флаг `movingGroupsRootOnly`); -- Управление зарегистрированными типами элементов (`itemTypes`) и глобальными обработчиками действий (`actionHandlers`); -- Делегирование CRUD, шаблонов и поисковых запросов правильному типу элемента через `getItemType`; -- Предоставление вспомогательных методов для контроллера (`getChilds`, `getActions`, `handleAction`, `getLink`, `getPopUpTemplate`, `createItem`, `updateItem`, `updateModelItems`, `deleteItem`, `getitemTypes`, `search`); -- Регистрация специфичного для каталога токена прав доступа (`catalog-${slug}` или `catalog-${slug}-${id}`) для единообразной проверки прав. +Supported `_method` values: -`AbstractCatalog` ожидает, что конкретные реализации предоставят `name`, `slug`, `icon` и список экземпляров типов элементов через конструктор. Опциональные действия уровня каталога можно добавить через `addActionHandler`. Когда `selectedItemTypes` пуст, обработчик считается глобальным; в противном случае он прикрепляется к каждому совпадающему типу элемента. +| Method | HTTP | Purpose | +|---|---|---| +| `getCatalog` | `POST` | Return root nodes, item types, catalog metadata, toolbar actions, and registered template components. | +| `getAddTemplate` | `POST` | Return add template for selected item type. | +| `getEditTemplate` | `POST` | Return edit template for selected node. | +| `createItem` | `POST` | Create item and normalize returned `parentId` for frontend. | +| `getChilds` | `POST` | Lazy-load children for a node. | +| `search` | `POST` | Return tree nodes for matching items with parent context. | +| `getActions` | `POST` | Return context/tool actions for selected nodes. | +| `updateTree` | `PUT` | Persist drag-and-drop parent/order changes. | +| `updateItem` | `PUT` | Update item or linked model item. | +| `handleAction` | `PUT` | Execute action handler. | +| `getLink` | `PUT` | Resolve link action URL. | +| `getPopUpTemplate` | `PUT` | Resolve external action component. | +| `deleteItem` | `DELETE` | Recursively delete selected item and descendants. | -### BaseItem, AbstractGroup, AbstractItem -`BaseItem` моделирует тип отдельного элемента каталога. Конкретные реализации предоставляют: -- Статические метаданные (`type`, `name`, `icon`, `allowedRoot`, флаг `isGroup`); -- Коннекторы к окружению Adminizer (`adminizer`, опциональная ссылка на `storageServices` для общей персистентности); -- Методы доступа к данным (`find`, `create`, `update`, `deleteItem`, `updateModelItems`, `getChilds`, `search`); -- Фабрики UI-шаблонов (`getAddTemplate`, `getEditTemplate`), возвращающие либо путь к React-компоненту, либо шаблон навигации, либо селектор на основе модели. +## Frontend Adapter + +`FrontendCatalog` and `FrontendCatalogUtils` convert backend items to tree nodes: -`BaseItem` обогащает каждую возвращаемую запись полями `type` и `icon` через метод `_enrich`, чтобы фронтенд мог рендерить унифицированные узлы. Вспомогательные методы `_find` и `_getChilds` применяют это обогащение автоматически. `AbstractGroup` и `AbstractItem` предоставляют разумные значения по умолчанию для группировки (`type: "group"`, `icon: "folder"`, `isGroup: true`) и листовых элементов (`isGroup: false`). +```ts +interface NodeModel { + text: string; + droppable: boolean; + id: string; + parent: number; + data?: TDataType; + children?: NodeModel[]; +} +``` -### ActionHandler -Обработчики действий описывают операции, которые могут быть вызваны из контекстного меню или панели инструментов. Абстрактный контракт в `src/lib/catalog/AbstractCatalog.ts:203` предоставляет: -- `type`: либо `basic`, `external`, либо `link`; -- `displayContext` / `displayTool`: подсказки размещения для UI; -- `selectedItemTypes`: ограничивает обработчики определенными значениями `type` элемента; пустой массив означает, что обработчик глобальный; -- `id`, `name`, `icon`: дескрипторы UI; -- `getPopUpTemplate`, `getLink`: провайдеры шаблонов для действий `external` и `link`; -- `handler`: бэкенд-реализация, которая получает разрешенные сущности `Item` плюс опциональный payload. +Important behavior: -Обработчики обнаруживаются через `AbstractCatalog.getActions`. `FrontendCatalog.getActions` фильтрует их для использования в контексте или панели инструментов перед возвратом данных клиенту. +- `getCatalog()` loads root items with `catalog.getChilds(null)`. +- `getChilds()` unwraps `node.data`, treats `0` and missing id as root, and returns child nodes. +- `updateTree()` updates every child under the dropped parent with recalculated `sortOrder` and `parentId`. +- `deleteItem()` recursively deletes children before deleting the selected node. +- `normalizeForFrontend()` converts `parentId: null` to `parentId: 0` and now throws if a catalog handler returns an empty item. +## CatalogHandler -## Жизненный цикл запроса -`catalogController` (`src/controllers/catalog/Catalog.ts`) обрабатывает HTTP-трафик: -- Проверяет права доступа используя токен прав доступа, зарегистрированный каталогом; -- Разрешает каталог по slug и опциональному `id` хранилища (также используется для получения разрешенных ID через `getIdList`); -- Для `GET` запросов рендерит Inertia-страницу `catalog`; -- Для `POST`, `PUT` и `DELETE` запросов создает экземпляр `FrontendCatalog` и диспетчеризует операции на основе `_method`. +`CatalogHandler` stores two catalog groups: -Контроллер в настоящее время поддерживает следующие значения `_method`: -- `getCatalog`: возвращает дерево, доступные типы элементов, метаданные каталога и действия панели инструментов; -- `getChilds`, `createItem`, `updateItem`, `deleteItem`: CRUD-эндпоинты, работающие с конкретными узлами; -- `getAddTemplate`, `getEditTemplate`: предоставляют UI-полезные нагрузки для модальных форм или кастомных компонентов; -- `search`: возвращает помеченные результаты поиска вместе с их предками и соседями; -- `getActions`, `handleAction`, `getLink`, `getPopUpTemplate`: выполняют или описывают обработчики действий; -- `updateTree`: сохраняет изменения порядка и родителей при drag-and-drop, пересчитывая `sortOrder` и `parentId` каждого узла. +| Source | API | Use case | +|---|---|---| +| Core/manual catalogs | `catalogHandler.add(catalog)` | Legacy direct registration. | +| App catalogs | `catalogHandler.register(appName, catalog)` | Preferred module-owned registration through `ctx.catalog()`. | -## Frontend Adapter (Адаптер фронтенда) -`FrontendCatalog` (`src/controllers/catalog/FrontendCatalogAdapter.ts`) оборачивает экземпляр каталога и нормализует ответы для React-представления дерева. Основные обязанности включают: -- Конвертирование backend-элементов в экземпляры `NodeModel` (`FrontendCatalogUtils.arrayToNode`, `treeToNode`); -- Замену `null` parent IDs на `0` для UI и восстановление их при записи (`normalizeForFrontend`, `refinement`); -- Разрешение локализованных UI-строк каталогов через `getLocales`, чтобы клиент мог отображать мультиязычные метки; -- Рекурсивное удаление узлов с помощью `deleteItem` для гарантии, что удаление группы также очищает её потомков перед очисткой на бэкенде. +`register()` rejects duplicate app/catalog ids and duplicate catalog slugs. `unregister()`, `disable()`, and `enable()` are used by `AppManager` disposers. -Вспомогательный класс `FrontendCatalogUtils` содержит stateless-конвертеры, которые React-компоненты используют напрямую. При добавлении новых точек данных элементов, обновляйте эти утилиты, чтобы фронтенд продолжал получать полные дескрипторы узлов. +## App-Based Catalog Example -## Создание кастомного каталога -1. **Разработайте стратегию хранения элементов.** Решите, как будут сохраняться данные каталога. Следуйте примеру `StorageService` или реализуйте API персистентности прямо в каждом типе элемента. -2. **Реализуйте типы элементов.** Расширьте `AbstractGroup` для папок и `AbstractItem` для листовых записей. Предоставьте требуемые метаданные, CRUD-методы и шаблоны. Используйте `_enrich` через предоставленные хелперы, если возвращаете сырые сущности. -3. **Соберите каталог.** Создайте подкласс `AbstractCatalog`, создайте экземпляры типов элементов и вызовите `super(adminizer, itemTypes)`. Опционально настройте обработчики действий и переопределите `getIdList`, когда каталог поддерживает множественные экземпляры хранилища. -4. **Зарегистрируйте каталог.** Во время загрузки Adminizer добавьте каталог в глобальный обработчик: `adminizer.catalogHandler.add(new MyCatalog(adminizer));` как показано в `src/lib/catalog/CatalogHandler.ts`. -5. **Предоставьте идентификаторы хранилища.** Если каталог использует множественные корни, добавьте их в `idList`, чтобы контроллер мог валидировать входящие запросы `/catalog/:slug/:id`. +```ts +import { + AbstractAdminizerApp, + AbstractCatalog, + AbstractGroup, + AbstractItem, + AppRuntime, + AppSetupContext, + Item, +} from "adminizer"; -## CatalogHandler -`src/lib/catalog/CatalogHandler.ts` хранит зарегистрированные каталоги в памяти. Он предоставляет три метода: -- `add(catalog: AbstractCatalog)`: сохраняет экземпляр каталога и возвращает его; -- `getAll()`: перечисляет все зарегистрированные каталоги (полезно для диагностики или дашбордов); -- `getCatalog(slug: string)`: получает каталог по slug, который контроллер получает из запроса. +interface TreeItem extends Item { + url?: string; +} -Вызывайте `CatalogHandler.add` во время инициализации Adminizer, чтобы каталог стал доступен по адресу `/admin/catalog/:slug`. +class MemoryStorage { + private items = new Map(); -## Пример: Простой продукт каталог + async set(item: TreeItem): Promise { + this.items.set(item.id, item); + return item; + } -Пример простого каталога продуктов, где группы (категории) и продукты связаны через `parentId`. Предполагается, что модели `Group` и `Product` уже определены в Sequelize с полями `id`, `name`, `parentId`, `sortOrder` и `description` для продуктов. + async find(id: string | number): Promise { + const item = this.items.get(id); + if (!item) throw new Error(`Item ${id} was not found`); + return item; + } -### Реализация каталога + async children(parentId: string | number | null, type?: string): Promise { + return Array.from(this.items.values()) + .filter((item) => item.parentId === parentId && (!type || item.type === type)) + .sort((a, b) => a.sortOrder - b.sortOrder); + } -```typescript -// lib/catalog/ProductCatalog.ts -import { AbstractCatalog, AbstractGroup, AbstractItem, Item } from './AbstractCatalog'; -import { Adminizer } from '../Adminizer'; -import Group from '../../models/Group'; -import Product from '../../models/Product'; + async delete(id: string | number): Promise { + this.items.delete(id); + } -interface CatalogItem extends Item { - description?: string; + async search(query: string, type: string): Promise { + return Array.from(this.items.values()).filter((item) => + item.type === type && item.name.toLowerCase().includes(query.toLowerCase()) + ); + } } -// Product group -class ProductGroup extends AbstractGroup { - readonly type = 'group'; - readonly name = 'Product Group'; - readonly icon = 'folder'; +class GroupItem extends AbstractGroup { + readonly name = "Group"; readonly allowedRoot = true; + readonly adminizer = {} as any; + readonly actionHandlers = []; - constructor(protected adminizer: Adminizer) { + constructor(private storage: MemoryStorage) { super(); } - async find(itemId: string | number): Promise { - const group = await Group.findByPk(itemId); - if (!group) throw new Error('Group not found'); - - return { - id: group.id, - name: group.name, - parentId: group.parentId, - sortOrder: group.sortOrder, - type: this.type, - icon: this.icon, - }; + async find(id: string | number): Promise { + return this.storage.find(id); } - async create(data: any): Promise { - const group = await Group.create({ - name: data.name, - parentId: data.parentId || null, - sortOrder: data.sortOrder || 0, - }); - - return { - id: group.id, - name: group.name, - parentId: group.parentId, - sortOrder: group.sortOrder, - type: this.type, + async create(data: TreeItem): Promise { + return this.storage.set({ + ...data, + id: crypto.randomUUID(), + parentId: data.parentId ?? null, + sortOrder: data.sortOrder ?? 0, icon: this.icon, - }; - } - - async update(itemId: string | number, data: CatalogItem): Promise { - const group = await Group.findByPk(itemId); - if (!group) throw new Error('Group not found'); - - await group.update({ - name: data.name, - parentId: data.parentId, - sortOrder: data.sortOrder, - }); - - return { - id: group.id, - name: group.name, - parentId: group.parentId, - sortOrder: group.sortOrder, type: this.type, - icon: this.icon, - }; + }); } - async deleteItem(itemId: string | number): Promise { - await Group.destroy({ where: { id: itemId } }); + async update(id: string | number, data: TreeItem): Promise { + return this.storage.set({ ...data, id }); } - async getChilds(parentId: string | number | null): Promise { - const groups = await Group.findAll({ - where: { parentId }, - order: [['sortOrder', 'ASC']], - }); + async updateModelItems(modelId: string | number, data: TreeItem): Promise { + return this.update(modelId, data); + } - return groups.map(group => ({ - id: group.id, - name: group.name, - parentId: group.parentId, - sortOrder: group.sortOrder, - type: this.type, - icon: this.icon, - })); + async deleteItem(id: string | number): Promise { + await this.storage.delete(id); } - async search(s: string): Promise { - const groups = await Group.findAll({ - where: { - name: { [Op.iLike]: `%${s}%` }, - }, - }); + async getChilds(parentId: string | number | null): Promise { + return this.storage.children(parentId, this.type); + } - return groups.map(group => ({ - id: group.id, - name: group.name, - parentId: group.parentId, - sortOrder: group.sortOrder, - type: this.type, - icon: this.icon, - })); + async search(s: string): Promise { + return this.storage.search(s, this.type); } - async getAddTemplate(req: any): Promise { + async getAddTemplate(req: ReqType) { return { - type: 'model', - data: { - model: 'Group', - items: [], - labels: { - title: req.i18n.__('Add Group'), - save: req.i18n.__('Save'), - }, - }, + type: "demo.group-template", + data: { labels: { title: req.i18n.__("Title"), save: req.i18n.__("Save") } }, }; } - async getEditTemplate(id: string | number, catalogId: string, req: any): Promise { - const item = await this.find(id); + async getEditTemplate(id: string | number, catalogId: string, req: ReqType) { return { - type: 'model', - data: { - item, - model: 'Group', - labels: { - title: req.i18n.__('Edit Group'), - save: req.i18n.__('Save'), - }, - }, + type: "demo.group-template", + data: { item: await this.find(id), labels: { title: req.i18n.__("Title"), save: req.i18n.__("Save") } }, }; } } -// Product -class ProductItem extends AbstractItem { - readonly type = 'product'; - readonly name = 'Product'; - readonly icon = 'shopping_cart'; - readonly allowedRoot = false; +class LinkItem extends AbstractItem { + readonly type = "link"; + readonly name = "Link"; + readonly icon = "insert_link"; + readonly allowedRoot = true; + readonly adminizer = {} as any; + readonly actionHandlers = []; - constructor(protected adminizer: Adminizer) { + constructor(private storage: MemoryStorage) { super(); } - async find(itemId: string | number): Promise { - const product = await Product.findByPk(itemId); - if (!product) throw new Error('Product not found'); - - return { - id: product.id, - name: product.name, - parentId: product.parentId, - sortOrder: product.sortOrder, - type: this.type, - icon: this.icon, - description: product.description, - }; - } - - async create(data: any): Promise { - const product = await Product.create({ - name: data.name, - parentId: data.parentId, - sortOrder: data.sortOrder || 0, - description: data.description || '', - }); - - return { - id: product.id, - name: product.name, - parentId: product.parentId, - sortOrder: product.sortOrder, - type: this.type, - icon: this.icon, - description: product.description, - }; - } + async find(id: string | number): Promise { return this.storage.find(id); } + async create(data: TreeItem): Promise { return this.storage.set({ ...data, id: crypto.randomUUID(), parentId: data.parentId ?? null, sortOrder: data.sortOrder ?? 0, icon: this.icon, type: this.type }); } + async update(id: string | number, data: TreeItem): Promise { return this.storage.set({ ...data, id }); } + async updateModelItems(modelId: string | number, data: TreeItem): Promise { return this.update(modelId, data); } + async deleteItem(id: string | number): Promise { await this.storage.delete(id); } + async getChilds(parentId: string | number | null): Promise { return this.storage.children(parentId, this.type); } + async search(s: string): Promise { return this.storage.search(s, this.type); } + async getAddTemplate() { return { type: "demo.link-template", data: {} }; } + async getEditTemplate(id: string | number) { return { type: "demo.link-template", data: { item: await this.find(id) } }; } +} - async update(itemId: string | number, data: CatalogItem): Promise { - const product = await Product.findByPk(itemId); - if (!product) throw new Error('Product not found'); - - await product.update({ - name: data.name, - parentId: data.parentId, - sortOrder: data.sortOrder, - description: data.description, - }); +class DemoCatalog extends AbstractCatalog { + readonly name = "Demo Catalog"; + readonly slug = "demo"; + readonly icon = "account_tree"; + readonly actionHandlers = []; - return { - id: product.id, - name: product.name, - parentId: product.parentId, - sortOrder: product.sortOrder, - type: this.type, - icon: this.icon, - description: product.description, - }; + constructor(_runtime: AppRuntime) { + const storage = new MemoryStorage(); + super({ accessRightsHelper: { registerToken: () => undefined } } as any, [ + new GroupItem(storage), + new LinkItem(storage), + ]); } - async deleteItem(itemId: string | number): Promise { - await Product.destroy({ where: { id: itemId } }); + async getIdList(): Promise { + return ["main"]; } +} - async getChilds(parentId: string | number | null): Promise { - const products = await Product.findAll({ - where: { parentId }, - order: [['sortOrder', 'ASC']], - }); +export class DemoCatalogApp extends AbstractAdminizerApp<{ routePrefix: string }> { + readonly name = "demo-catalog"; + readonly version = "1.0.0"; + declare readonly config: { routePrefix: string }; - return products.map(product => ({ - id: product.id, - name: product.name, - parentId: product.parentId, - sortOrder: product.sortOrder, - type: this.type, - icon: this.icon, - description: product.description, - })); - } - - async search(s: string): Promise { - const products = await Product.findAll({ - where: { - [Op.or]: [ - { name: { [Op.iLike]: `%${s}%` } }, - { description: { [Op.iLike]: `%${s}%` } }, - ], + constructor(config: { routePrefix: string }) { + super(); + this.config = config; + } + + setup(ctx: AppSetupContext): void { + ctx.config({ + navbar: { + additionalLinks: [{ + id: "demo-catalog", + type: "self", + link: `${this.config.routePrefix}/catalog/demo/main`, + title: "Demo Catalog", + icon: "account_tree", + }], }, }); - return products.map(product => ({ - id: product.id, - name: product.name, - parentId: product.parentId, - sortOrder: product.sortOrder, - type: this.type, - icon: this.icon, - description: product.description, - })); - } - - async getAddTemplate(req: any): Promise { - return { - type: 'model', - data: { - model: 'Product', - labels: { - title: req.i18n.__('Add Product'), - save: req.i18n.__('Save'), - }, - }, - }; - } - - async getEditTemplate(id: string | number, catalogId: string, req: any): Promise { - const item = await this.find(id); - return { - type: 'model', - data: { - item, - model: 'Product', - labels: { - title: req.i18n.__('Edit Product'), - save: req.i18n.__('Save'), - }, - }, - }; - } -} - -// Product catalog -export class ProductCatalog extends AbstractCatalog { - readonly name = 'Product Catalog'; - readonly slug = 'products'; - readonly icon = 'inventory'; - - constructor(adminizer: Adminizer) { - const itemTypes = [ - new ProductGroup(adminizer), - new ProductItem(adminizer), - ]; - super(adminizer, itemTypes); + ctx.catalog({ + id: "demo", + factory: (runtime) => new DemoCatalog(runtime), + }); } } ``` -### Регистрация каталога +Enable after Adminizer initialization: -```typescript -// In the Adminizer bootstrap file -import { ProductCatalog } from './lib/catalog/ProductCatalog'; +```ts +await adminizer.init(adminpanelConfig); +await adminizer.appManager.enable(new DemoCatalogApp({ routePrefix: adminpanelConfig.routePrefix })); +``` -const adminizer = new Adminizer(/* config */); +## Navigation As A Catalog Module -// Catalog registration -adminizer.catalogHandler.add(new ProductCatalog(adminizer)); -``` +Navigation is no longer part of Adminizer core config. The fixture implements it as an app module in `fixture/apps/navigation`: + +- `NavigationApp.ts` registers access rights, assets, template components, a runtime storage model, model access, navbar links, the catalog factory, and a `model:updated` listener. +- `NavigationCatalog.ts` implements `AbstractCatalog`, item types, and in-memory section storages backed by the runtime model. +- `NavigationCatalogTemplates.tsx` implements add/edit forms registered through `CatalogTemplateComponentHandler`. -Теперь каталог будет доступен по адресу `/admin/catalog/products` и позволит управлять иерархией групп и продуктов через drag & drop интерфейс. +Use this fixture as the current reference for multi-section catalog modules. diff --git a/docs/Commands.md b/docs/Commands.md index c9722bb9..47986b8f 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -1,48 +1,81 @@ - -## Adminizer Commands - +## Adminizer Commands + ## Development + - **`npm run dev`** - Starts the application in development mode using Sequelize with file watching. -- **`npm run dev:waterline`** - Starts the application in development mode using Waterline with file watching. - -- **`npm run watch:backend`** - Watches for changes in backend files and recompiles TypeScript continuously. - -## Build Commands -- **`npm run build:assets`** - Builds frontend assets using Vite. - -- **`npm run build:backend`** - Combines backend copy and compilation steps. - -- **`npm run build`** - Full build process: copies backend, compiles backend, and builds assets. - -## Module-specific Builds (this is just for tests, as an example, you should create your own commands to build modules) -- **`npm run build:module`** - Builds test modules using a custom Vite config. - -- **`npm run build:react-quill`** - Builds the React-Quill WYSIWYG module using a custom Vite config. - -- **`npm run copy:modules`** - Copies modules using `copy-modules.js` script. - -- **`npm run build:assets:modules`** - Builds assets and copies modules. - -## Demo & Seeding -- **`npm run start:seed`** - Starts the application with seed data enabled (using `SEED_DATA=true`). - -- **`npm run demo:build`** - Prepares a demo build: copies backend, compiles backend, builds assets, and copies modules. - -- **`npm run demo`** - Alias for `start:seed` - runs the demo with seeded data. - -## Preview mode -- **`npm start`** - Starts the application using TSX with the Waterline fixture configuration. \ No newline at end of file + Starts the Sequelize fixture in development mode with file watching and seed data. + +- **`npm run dev:no-seed`** + Starts the Sequelize fixture in development mode without seed data. + +- **`npm run dev:typeorm`** + Starts the experimental TypeORM fixture in development mode with file watching and seed data. + +- **`npm run dev:typeorm:no-seed`** + Starts the experimental TypeORM fixture in development mode without seed data. + +- **`npm run dev:cors`** + Starts the Sequelize fixture with CORS settings for frontend integration testing. + +- **`npm run dev:no-seed-clean`** + Starts the Sequelize fixture without seed data and removes `.tmp` before startup. + +- **`npm run tsc:watch`** + Watches backend TypeScript and recompiles continuously. + +Development mode is detected with `ADMINIZER_ENV=dev`. The bundled `dev` scripts set it automatically. In this mode app assets registered with `ctx.asset({ devUrl })` use `devUrl` instead of the production file. + +## Start Commands + +- **`npm run start`** + Starts the Sequelize fixture without seed data. + +- **`npm run start:seed`** + Starts the Sequelize fixture with seed data. + +- **`npm run start:typeorm`** + Starts the experimental TypeORM fixture without seed data. + +- **`npm run start:typeorm:seed`** + Starts the experimental TypeORM fixture with seed data. + +## Build Commands + +- **`npm run build`** + Full package build: copy backend, compile backend, compile UI, and build frontend assets. + +- **`npm run build:backend`** + Copy backend files and compile backend TypeScript. + +- **`npm run compile:backend`** + Compile backend TypeScript with `src/tsconfig.json` and append `.js` extensions in generated imports. + +- **`npm run compile:ui`** + Compile the shared UI TypeScript package. + +- **`npm run build:assets`** + Build the main Adminizer frontend assets and all built-in control ES modules with Vite. + +- **`npm run build:controls`** + Build only the built-in control ES modules and their styles. + +- **`npm run copy:backend`** + Copy backend resources before compilation, including `translations`, `files`, and `fileicons` used by the core media manager routes. + +## App And Module Builds + +- **`npm run build:apps`** + Builds all fixture app frontend modules and fixture virtual catalog components: notification-sender, module-manager, navigation catalog templates, React-Quill, and virtual catalog React templates/actions. + +- **`npm run build:catalog-modules`** + Builds fixture virtual catalog React components from `fixture/virtual-catalog`. + +Project modules should usually define their own Vite build script. See [BuildingModules.md](BuildingModules.md) for current `AbstractAdminizerApp` and `AppManager` usage. + +## Tests + +- **`npm run test`** + Runs Vitest once with `--watch=false`. + +- **`npm run test:watch`** + Runs Vitest in watch/verbose mode. diff --git a/docs/Configuration/CustomComponents.md b/docs/Configuration/CustomComponents.md index f57f1ba8..b9d822ee 100644 --- a/docs/Configuration/CustomComponents.md +++ b/docs/Configuration/CustomComponents.md @@ -4,21 +4,25 @@ Adminizer can be extended with custom controls and dashboard widgets. ## Controls -Controls are reusable form inputs. Create a class extending `AbstractControls` and register it via `ControlsHandler`: +Controls are reusable form inputs loaded only when their field is rendered. Custom controls are registered as app-owned resources through `ctx.control()`: ```ts -class ReactQuill extends AbstractControls { - readonly name = 'react-quill'; - readonly type = 'wysiwyg'; - readonly path = { - jsPath: { dev: '/modules/react-quill.tsx', production: '/assets/react-quill.es.js' }, - cssPath: '/assets/react-quill.css' - }; +setup(ctx: AppSetupContext): void { + ctx.control({ + type: "wysiwyg", + name: "react-quill", + component: { + id: "editor", + filePath: path.resolve(import.meta.dirname, "react-quill-editor.es.js"), + devUrl: "/apps/react-quill/react-quill-editor.tsx", + }, + stylesheet: { + id: "editor-css", + filePath: path.resolve(import.meta.dirname, "react-quill-editor.css"), + devUrl: "/apps/react-quill/react-quill-editor.css", + }, + }); } - -adminizer.emitter.on('adminizer:loaded', () => { - adminizer.controlsHandler.add(new ReactQuill(adminizer)); -}); ``` After registration the control can be referenced in field options: @@ -31,6 +35,8 @@ editor: { } ``` +Enable the owning app after `adminizer.init()`. See [Controls](../Controls.md) for the complete app, Vite build, lazy-loading, and fallback behavior. + ## Widgets Widgets are dashboard blocks. See [Widgets](Widgets.md) for a detailed example of creating and bundling custom widgets. diff --git a/docs/Configuration/Fields.md b/docs/Configuration/Fields.md index bd27d825..c51e35ca 100644 --- a/docs/Configuration/Fields.md +++ b/docs/Configuration/Fields.md @@ -19,4 +19,19 @@ Field definitions can be placed globally under `models.fields` or inside an acti ### Types -Commonly used field types include `string`, `password`, `date`, `datetime`, `integer`, `boolean`, `text`, `select` and more. When a `text` field has the `editor` option, a WYSIWYG control will be used. +Commonly used field types include `string`, `password`, `date`, `datetime`, `integer`, `boolean`, `text`, `select`, `wysiwyg`, `markdown`, `table`, `jsonEditor`, `codeEditor`, and `geoJson`. + +Control fields can select a registered implementation by name: + +```ts +editor: { + title: "Editor", + type: "wysiwyg", + options: { + name: "react-quill", + config: {}, + }, +} +``` + +Control JavaScript and CSS are loaded when the field is rendered. If the named control is unavailable, Adminizer logs a server warning and uses the built-in default for that control type. See [Controls](../Controls.md). diff --git a/docs/Configuration/General.md b/docs/Configuration/General.md index 7711c6ab..a0d4b6bf 100644 --- a/docs/Configuration/General.md +++ b/docs/Configuration/General.md @@ -8,7 +8,7 @@ import { AdminizerConfig } from "adminizer"; const config: AdminizerConfig = { routePrefix: "/admin", favicon: "/brand/admin-favicon.ico", - auth: true, + auth: { enable: true }, dashboard: true, models: {}, }; @@ -20,15 +20,22 @@ const config: AdminizerConfig = { |--------|-------------| | `routePrefix` | Base URL for the panel. Defaults to `/admin`. | | `favicon` | Custom favicon URL. Relative values are resolved from `routePrefix`; default is `/files/favicon.png`. | +| `auth` | Authentication settings, for example `{ enable: true, captcha: true }`. | | `linkAssets` | Symlink static assets instead of copying them. | | `identifierField` | Default primary key for models (usually `id`). | -| `showORMtime` | Show `createdAt`/`updatedAt` fields in forms. | +| `showORMtime` | Show `createdAt`/`updatedAt` fields on create/edit pages. | | `models` | Object with model definitions. | | `dashboard` | Enable dashboard widgets. | | `showVersion` | Display Adminizer version in the sidebar. | +| `system.defaultORM` | Adapter that owns the required system models. Sequelize is recommended; TypeORM is experimental. | +| `system.internalModelAccess` | Extend allowlisted internal model access scopes for trusted system modules. | Additional options like `welcome`, `translation` and `administrator` credentials can also be provided. +## Migrations + +Starting with Adminizer 5, built-in migrations are not supported. Projects should manage database schema changes with their own ORM or migration tool. For Sequelize projects, register Adminizer system models and synchronize or migrate them through the host application's database workflow. See [System Models](Models.md). + **`favicon` examples** ```ts @@ -44,3 +51,29 @@ favicon: "files/my-custom-favicon.png" // -> /admin/files/my-custom-favicon.png // 4) full external URL: favicon: "https://cdn.example.com/admin/favicon.svg" ``` + +## Internal Model Access + +Adminizer system modules can use `modelHandler.internal(scope)` to access selected internal models without `DataAccessor` user filtering. Built-in scopes cover Adminizer's own modules; projects can add custom scopes through `system.internalModelAccess`. + +```ts +const config: AdminizerConfig = { + routePrefix: "/admin", + system: { + internalModelAccess: { + "my-module": ["User", "Filter"] + } + } +}; +``` + +Usage: + +```ts +const filters = await adminizer.modelHandler + .internal("my-module") + .get("Filter") + .find({ where: { modelName: "Example" } }); +``` + +Use this only for trusted system-level code. Normal user-facing operations should continue to use model methods with `DataAccessor`. diff --git a/docs/Configuration/InlineEditing.md b/docs/Configuration/InlineEditing.md new file mode 100644 index 00000000..fd74ec34 --- /dev/null +++ b/docs/Configuration/InlineEditing.md @@ -0,0 +1,197 @@ +# Inline Editing in List View + +Adminizer supports inline editing of fields directly in the list view. This allows administrators to quickly update values without navigating to the edit form. + +## Overview + +When inline editing is enabled for a field, users can double-click on a cell value in the table to edit it directly. Changes are saved via an API call and the table updates instantly without a full page reload. + +## Enabling Inline Editing + +To enable inline editing for a field, set the `inlineEditable` flag in the field configuration: + +```ts +models: { + Example: { + title: 'Example', + model: 'example', + fields: { + title: { + title: 'Title', + type: 'string', + inlineEditable: true + }, + isActive: { + title: 'Active', + type: 'boolean', + inlineEditable: true + }, + price: { + title: 'Price', + type: 'number', + inlineEditable: true, + inlineValidation: { + min: 0, + max: 99999 + } + } + } + } +} +``` + +## Supported Field Types + +Inline editing is only available for the following field types: + +| Type | Control | +|------|---------| +| `string` | Text input | +| `integer` | Number input | +| `number` | Number input | +| `float` | Number input | +| `email` | Email input | +| `range` | Number input with min/max | +| `boolean` | Checkbox | + +If `inlineEditable: true` is set on an unsupported field type (e.g., `text`, `date`, `json`), the flag is **ignored** and the field remains read-only. + +## Conditions for Editing + +A field is editable in the list view only when **all** of the following conditions are met: + +| Condition | Description | +|-----------|-------------| +| `inlineEditable: true` | The flag must be explicitly set | +| No `displayModifier` | Fields with `displayModifier` are always read-only (priority) | +| Supported field type | The field type must be in the allowed list above | + +## Validation + +You can define validation rules for inline editing using the `inlineValidation` option: + +```ts +fieldName: { + title: 'Field Name', + type: 'string', + inlineEditable: true, + inlineValidation: { + minLength: 3, + maxLength: 100, + pattern: '^[a-zA-Z]+$', + validate: (value) => value.trim().length > 0 || 'Value cannot be empty' + } +} +``` + +### Validation Options + +| Option | Description | Applicable Types | +|--------|-------------|------------------| +| `minLength` | Minimum string length | `string`, `email` | +| `maxLength` | Maximum string length | `string`, `email` | +| `min` | Minimum value | `integer`, `number`, `float`, `range` | +| `max` | Maximum value | `integer`, `number`, `float`, `range` | +| `pattern` | Regex pattern for validation | `string`, `email` | +| `validate` | Custom validation function | All types | + +## User Interaction + +### Text and Number Fields + +1. **Double-click** on the cell value to start editing +2. An input field appears with the current value selected +3. Press **Enter** or click outside to save +4. Press **Escape** to cancel + +### Boolean Fields + +1. **Double-click** on the cell to start editing +2. A checkbox appears +3. Toggle the checkbox to change the value +4. Click outside the checkbox to save +5. Press **Escape** to cancel + +## API Endpoint + +Inline editing uses the following API endpoint: + +``` +PATCH /adminizer/model/:name/inline/:id +``` + +**Request body:** +```json +{ + "field": "fieldName", + "value": "newValue" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "id": "record-id", + "field": "fieldName", + "value": "newValue" + } +} +``` + +## Example Configuration + +```ts +import { AdminpanelConfig } from "adminizer"; + +const config: AdminpanelConfig = { + routePrefix: "/admin", + models: { + Product: { + title: 'Products', + model: 'product', + fields: { + name: { + title: 'Name', + type: 'string', + inlineEditable: true, + inlineValidation: { + minLength: 2, + maxLength: 200 + } + }, + price: { + title: 'Price', + type: 'number', + inlineEditable: true, + inlineValidation: { + min: 0, + max: 999999 + } + }, + active: { + title: 'Active', + type: 'boolean', + inlineEditable: true + }, + email: { + title: 'Contact Email', + type: 'email', + inlineEditable: true, + inlineValidation: { + pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$' + } + }, + // This field has displayModifier — inlineEditable will be ignored + displayName: { + title: 'Display Name', + type: 'string', + inlineEditable: true, // ← Ignored due to displayModifier + displayModifier: (v) => v?.toUpperCase() + } + } + } + } +}; +``` diff --git a/docs/Configuration/Localization.md b/docs/Configuration/Localization.md index df7eda43..f5c58b1d 100644 --- a/docs/Configuration/Localization.md +++ b/docs/Configuration/Localization.md @@ -2,20 +2,22 @@ Adminizer supports interface translations using the `translation` block in the configuration. -```js -module.exports.adminpanel = { +```ts +const config = { translation: { locales: ['en', 'de'], - path: 'config/locales', + directory: 'config/locales', defaultLocale: 'en' } }; ``` * `locales` – list of available locales -* `path` – relative path to translation files +* `directory` – relative path to translation files * `defaultLocale` – locale used when none is specified +`path` is still accepted for older configs, but is deprecated. Use `directory` for new projects. + In Inertia mode, interface strings are translated on the server through `req.i18n.__(...)` before props are sent to React components. This includes shared UI props such as sidebar navbar item titles and section labels. ## Shared UI keys @@ -27,7 +29,9 @@ If you localize Adminizer UI through translation dictionaries, make sure the loc * `Visible` * `First`, `Last`, `Previous`, `Next` -These keys are passed via shared `uiMessages` in Inertia mode and are used by menu/user controls and pagination UI. +These keys are passed via shared `i18n.common` in Inertia mode and are used by menu/user controls and pagination UI. + +For page-specific dictionaries, Adminizer uses `i18nPage` props, while reusable/common labels stay in `i18n.common`. ## Locale parity diff --git a/docs/Configuration/Models.md b/docs/Configuration/Models.md index ad751921..34610aee 100644 --- a/docs/Configuration/Models.md +++ b/docs/Configuration/Models.md @@ -1,19 +1,106 @@ # System Models -Adminizer ships with several built‑in models used for authentication, media management and navigation. These definitions reside in `src/models` and can be automatically registered for **Waterline** and **Sequelize** ORMs by calling `registerSystemModels()` on the corresponding adapter. +Adminizer does not create or synchronize ORM models. The host application owns ORM initialization and must register all Adminizer system models before calling `adminizer.init()`. -The provided models are: +Adminizer core uses canonical system model names without project-specific suffixes: -- `UserAP` -- `GroupAP` -- `MediaManagerAP` -- `MediaManagerAssociationsAP` -- `MediaManagerMetaAP` -- `NavigationAP` +- `User` +- `Group` +- `Filter` +- `FilterColumn` +- `HistoryActions` +- `Notification` +- `UserNotification` -They can be created and queried like any other models once registered. +During startup Adminizer obtains these models from the adapter selected by `config.system.defaultORM` and validates their fields, primary keys, and required associations. Extra fields are allowed. Startup fails immediately when a model is missing or does not satisfy the system contract. -When using Sequelize the adapter generates explicit foreign keys using the -`Id` pattern. This prevents naming collisions between attributes and -associations when models reference themselves or each other. +## Host Model Names +The ORM model name does not have to match the Adminizer canonical name. If a project already has host models named `UserAP`, `GroupAP`, or even custom names such as `Cats`, pass the mapping to the adapter: + +```ts +const adapter = new SequelizeAdapter(sequelize, { + systemModels: { + User: "UserAP", + Group: "GroupAP", + Filter: "FilterAP", + FilterColumn: "FilterColumnAP", + HistoryActions: "HistoryActionsAP", + Notification: "NotificationAP", + UserNotification: "UserNotificationAP", + }, +}); +``` + +Adminizer will use `User`, `Group`, and the other canonical names internally, while validating and querying the mapped host ORM models. The same binding also works as a runtime alias, so app code with access to that model can resolve either the canonical name or the mapped host name. + +If your host ORM models already use canonical names, no mapping is required: + +```ts +const adapter = new SequelizeAdapter(sequelize); +``` + +## Sequelize + +Sequelize is the primary and recommended ORM. Register system models in the host Sequelize instance, define their associations, synchronize or migrate the database, and then construct Adminizer with the adapter: + +```ts +registerSequelizeSystemModels(sequelize); +await sequelize.sync(); + +const adminizer = new Adminizer([ + new SequelizeAdapter(sequelize, { + systemModels: { + User: "UserAP", + Group: "GroupAP", + Filter: "FilterAP", + FilterColumn: "FilterColumnAP", + HistoryActions: "HistoryActionsAP", + Notification: "NotificationAP", + UserNotification: "UserNotificationAP", + }, + }), +]); + +await adminizer.init(config); +``` + +The fixture helper `registerSequelizeSystemModels` still registers host models with the `AP` suffix. That is fixture-specific naming, not a core requirement. + +App-owned Sequelize models may be installed later, immediately before `appManager.enable(app)`. + +## App-Owned Models + +Models registered by apps are not part of the core system contract. `AppManager` only attaches an existing ORM model through `ctx.model()`; it does not define tables or synchronize schemas. + +The fixture media manager is an example. Its `MediaManagerAP`, `MediaManagerMetaAP`, and `MediaManagerAssociationsAP` models are installed by `fixture/apps/media-manager/MediaManagerModels.ts` after `adminizer.init()` and before the media manager app is enabled. Adminizer does not require or create these models unless that app is used. + +## TypeORM + +TypeORM support is experimental. System entities must be included in `DataSource.entities` before initialization: + +```ts +const dataSource = new DataSource({ + // ... + entities: [ + ...typeOrmSystemModels, + ...projectEntities, + ], +}); + +await dataSource.initialize(); + +const adapter = new TypeOrmAdapter(dataSource, { + systemModels: { + User: "UserAP", + Group: "GroupAP", + Filter: "FilterAP", + FilterColumn: "FilterColumnAP", + HistoryActions: "HistoryActionsAP", + Notification: "NotificationAP", + UserNotification: "UserNotificationAP", + }, +}); +``` + +App entities must also be included before `dataSource.initialize()`. Dynamic installation of app models into an initialized TypeORM `DataSource` is not supported. Apps without models continue to work, and apps with models work only when their entities were registered before initialization. diff --git a/docs/Configuration/Widgets.md b/docs/Configuration/Widgets.md index 493bd1f4..cd72ba4e 100644 --- a/docs/Configuration/Widgets.md +++ b/docs/Configuration/Widgets.md @@ -1,6 +1,6 @@ This is an example of customizing a custom widget (CustomOne) in a React application that is built using Vite. -### **1. Custom Widget Definition (`fixture/test-widgets/Custom.ts`)** +### **1. Custom Widget Definition (`fixture/widgets/Custom.ts`)** - This is a widget class (`CustomOne`) that extends a base widget class (`CustomBase`). - It defines metadata for the widget, such as: - `id`, `name`, `description`, and `department` (e.g., `"test"`) @@ -10,7 +10,7 @@ This is an example of customizing a custom widget (CustomOne) in a React applica - It specifies JavaScript paths for **development** (direct `.tsx` file) and **production** (a compiled `.es.js` file). - The widget is initialized with a `routePrefix` (likely a base URL for asset loading). -### **2. React Component (`modules/test/ComponentB.tsx`)** +### **2. React Component** - This is the actual UI implementation of the widget. - It displays: - A **message** (passed as a prop) @@ -18,20 +18,20 @@ This is an example of customizing a custom widget (CustomOne) in a React applica - The **current counter value** (managed via React’s `useState`) - The button uses basic styling (similar to a styled button component). -### **3. Vite Build Configuration (`modules/test/vite.config.module.ts`)** +### **3. Vite Build Configuration** - Configures the build process for the React component. - **Key settings:** - Builds the component as an **ES module** (`lib` mode). - - Outputs the file as `ComponentB.es.js`. + - Outputs the file as `CustomWidget.es.js`. - Treats `react` and `react-dom` as **external dependencies** (they won’t be bundled; expected to be loaded separately). - Uses path aliases (e.g., `@` points to a shared JS directory). - Supports both **development** (direct `.tsx` usage) and **production** (optimized `.es.js` file). ### **How It All Works Together** 1. **`CustomOne`** defines the widget’s metadata and where its JavaScript lives. -2. **`ComponentB`** provides the interactive UI (a counter button with a message). -3. **Vite** compiles the React component into a standalone ES module (`ComponentB.es.js`). +2. **`CustomWidget`** provides the interactive UI (a counter button with a message). +3. **Vite** compiles the React component into a standalone ES module (`CustomWidget.es.js`). - In **development**, it loads the `.tsx` file directly. - - In **production**, it loads the optimized `.es.js` file from a CDN-like path (`/assets/modules/`). + - In **production**, it should load the optimized `.es.js` file from a registered asset URL. Prefer `AssetHandler`/app asset routes for new code instead of manually copied static asset paths. This setup allows the widget to be reusable, dynamically loaded, and integrated into a larger application. diff --git a/docs/Controls.md b/docs/Controls.md index aa95ac3a..efca8843 100644 --- a/docs/Controls.md +++ b/docs/Controls.md @@ -1,328 +1,269 @@ [Back](index.md) + # Controls + ## Overview -AbstractControls is an abstract base class that defines the common interface for control components in the Adminizer system. It provides the foundational structure that all specific control types must implement. -## Type Definitions -### `ControlType` -```typescript -type ControlType = 'wysiwyg' | 'jsonEditor' | 'geoJson' | 'markdown' | 'table' | 'codeEditor' -``` -Defines the available types of controls that can be implemented. -### `Path` -```typescript -interface Path { - jsPath: { - dev: string - production: string - }, - cssPath: string -} -``` -Defines the path structure for JavaScript and CSS assets, with separate paths for development and production environments. -### `Config` -```typescript -type Config = Record -``` -Represents a configuration object that can contain various types of values. -## AbstractControls Class -### Properties -`name` -```typescript -public abstract readonly name: string -``` -The unique name identifier for the control (abstract, must be implemented). +Controls are dynamically loaded form components used for WYSIWYG, Markdown, table, JSON, code, and GeoJSON fields. -`type` -```typescript -public abstract readonly type: ControlType -``` -The type of the control (abstract, must be implemented). +Adminizer has two control sources: -`path` -```typescript -public abstract readonly path: Path -``` -The asset paths for the control (abstract, must be implemented). +- built-in controls registered by the core during initialization; +- app-owned controls registered through `AppSetupContext.control()`. -`config` -```typescript -public abstract readonly config: Config -``` -The configuration object for the control (abstract, must be implemented). +Control JavaScript and CSS are not loaded with the main Adminizer bundle. The field renderer imports them when a control field is rendered on an add or edit page. Visiting the dashboard does not load control bundles. -`routPrefix` -```typescript -public readonly routPrefix: string -``` -The route prefix inherited from the Adminizer instance. -### Constructor -```typescript -protected constructor(adminizer: Adminizer) -``` -Initializes the control with a reference to the Adminizer instance and sets the route prefix. - -**Parameters:** -* `adminizer`: The Adminizer instance that owns this control -### Methods -`getConfig()` -```typescript -public abstract getConfig(): Config | undefined -``` -Retrieves the control's configuration (abstract, must be implemented). +## Control Contract -**Returns**: The configuration object or undefined -`getJsPath()` -```typescript -public abstract getJsPath(): string | undefined -``` -Gets the appropriate JavaScript path based on environment (abstract, must be implemented). +The public control types are exported from `adminizer`: -**Returns**: The JavaScript path string or undefined -`getCssPath()` -```typescript -public abstract getCssPath(): string | undefined -``` -Gets the CSS path (abstract, must be implemented). +```ts +export type ControlType = + | "wysiwyg" + | "jsonEditor" + | "geoJson" + | "markdown" + | "table" + | "codeEditor"; -**Returns**: The CSS path string or undefined -`getName()` -```typescript -public abstract getName(): string -``` -Gets the control name (abstract, must be implemented). - -**Returns**: The control name string -## Usage -To create a custom control, extend this abstract class and implement all abstract members: -```typescript -class MyCustomControl extends AbstractControls { - public readonly name = "my-control"; - public readonly type: ControlType = 'wysiwyg'; - public readonly path = { - jsPath: { dev: '/dev/path.js', production: '/prod/path.js' }, - cssPath: '/styles.css' - }; - public readonly config = { /* ... */ }; +export type Config = + Record; - constructor(adminizer: Adminizer) { - super(adminizer); - } +export interface Control { + readonly name: string; + readonly type: ControlType; - // Implement all abstract methods... + getConfig(): Config | undefined; + getJsPath(): string | undefined; + getCssPath(): string | undefined; + getName(): string; } ``` -# ControlsHandler -## Overview -The `ControlsHandler` class provides a centralized management system for control components in the Adminizer framework. It organizes controls by type and name, allowing for easy retrieval, addition, and removal of controls. -## Class Structure -### Properties -`controls` -```typescript -private controls = new Map>() -``` -Internal storage structure that maintains controls in a nested Map: -* Outer Map: Key is `ControlType` -* Inner Map: Key is control name, value is the `AbstractControls` instance -### Methods -`add(control: AbstractControls): void` - -Adds a new control to the handler. - -**Parameters:** -* `control`: The control instance to add (must extend `AbstractControls`) -* -**Behavior**: - -* Creates a new type group if the control type doesn't exist -* Throws an error if a control with the same name already exists -* Adds the control to the appropriate type group - -**Example:** -```typescript -handler.add(new MyControl(adminizer)); -``` -`get(type: T, name: string): AbstractControls | undefined` -Retrieves a specific control by its type and name. +There is no `AbstractControls` base class. Built-in controls implement `Control` directly and receive only `routePrefix` in their constructors. -**Parameters:** +`Path` is an optional helper type used by the built-in implementations: -* `type`: The type of control to retrieve -* `name`: The name of the specific control +```ts +export interface Path { + jsPath: { + dev: string; + production: string; + }; + cssPath: string; +} +``` -**Returns**: The control instance or undefined if not found +## Built-In Controls -**Example:** -```typescript -const editor = handler.get('wysiwyg', 'main-editor'); -``` -`getByType(type: T): AbstractControls[]` +Core registers these defaults: -Retrieves all controls of a specific type. +| Type | Default name | +|---|---| +| `wysiwyg` | `ckeditor` | +| `markdown` | `toast-ui` | +| `table` | `handsontable` | +| `jsonEditor` | `jsoneditor` | +| `codeEditor` | `monaco` | +| `geoJson` | `leaflet` | -**Parameters:** -* `type`: The type of controls to retrieve +Each built-in control is compiled to a separate ES module under `/assets/controls`. Production styles are also emitted separately. In development, Vite serves the TSX entry directly. -**Returns:** An array of control instances (empty array if none found) +Some built-in controls are also exposed through `window.JSComponents`. Those exports are lazy proxies: importing the Adminizer UI globals does not immediately download the control implementation. -**Example:** -```typescript -const allEditors = handler.getByType('wysiwyg'); -``` -`remove(type: ControlType, name: string): boolean` +## App-Owned Controls -Removes a control from the handler. +Custom controls should be implemented as Adminizer apps. The app registers the component and optional stylesheet with `ctx.control()`: -**Parameters:** -* `type`: The type of control to remove -* `name`: The name of the specific control +```ts +import path from "path"; +import { + AbstractAdminizerApp, + type AppSetupContext, + type Config, +} from "adminizer"; -**Returns:** `true` if the control was found and removed, `false` otherwise +interface MyEditorAppConfig { + config: Config; + componentFile: string; + componentDevUrl: string; + stylesheetFile: string; + stylesheetDevUrl: string; +} -**Example:** -```typescript -const wasRemoved = handler.remove('wysiwyg', 'old-editor'); +export class MyEditorApp extends AbstractAdminizerApp { + readonly name = "my-editor"; + readonly version = "1.0.0"; + declare readonly config: MyEditorAppConfig; + + constructor(config: Partial = {}) { + super(); + this.config = { + config: {}, + componentFile: path.resolve(import.meta.dirname, "my-editor.es.js"), + componentDevUrl: "/apps/my-editor/my-editor.tsx", + stylesheetFile: path.resolve(import.meta.dirname, "my-editor.css"), + stylesheetDevUrl: "/apps/my-editor/my-editor.css", + ...config, + }; + } + + setup(ctx: AppSetupContext): void { + ctx.control({ + type: "wysiwyg", + name: "my-editor", + config: this.config.config, + component: { + id: "editor", + filePath: this.config.componentFile, + devUrl: this.config.componentDevUrl, + }, + stylesheet: { + id: "editor-css", + filePath: this.config.stylesheetFile, + devUrl: this.config.stylesheetDevUrl, + }, + }); + } +} ``` -`getAll(): Record` -Retrieves all controls grouped by type. +`component` is required. `stylesheet` is optional. Both use the same `AppAsset` contract as `ctx.asset()`: + +```ts +interface AppAsset { + id: string; + filePath: string; + route?: string; + devUrl?: string; +} +``` -**Returns:** An object where keys are control types and values are arrays of controls +In development, `devUrl` is returned directly. In production, Adminizer creates app-owned asset routes under `routePrefix`. -**Example:** -```typescript -const allControls = handler.getAll(); -// Returns: { wysiwyg: [...], markdown: [...], ... } +Enable the app after Adminizer initialization: + +```ts +await adminizer.init(adminpanelConfig); +await adminizer.appManager.enable(new MyEditorApp()); ``` -`collectAndGenerateStyleLinks(): string[]` -Collects all CSS paths from registered controls. -Returns: An array of CSS paths +Disabling or unregistering the app removes its control registration and asset routes: -**Example:** -```typescript -const stylesheets = handler.collectAndGenerateStyleLinks(); +```ts +await adminizer.appManager.disable("my-editor"); ``` -### Usage Example -```typescript -// Initialize handler -const handler = new ControlsHandler(); -// Add controls -handler.add(new WysiwygControl(adminizer)); -handler.add(new MarkdownControl(adminizer)); +## Frontend Component -// Get controls -const mainEditor = handler.get('wysiwyg', 'main-editor'); -const allMarkdown = handler.getByType('markdown'); +A control module must export its React component as the default export: -// Get all stylesheets -const styles = handler.collectAndGenerateStyleLinks(); -``` -## Redefining basic controls -Create a new `class`, for example: -```typescript -export class ReactQuill extends AbstractControls { - readonly name: string = 'react-quill'; - readonly type: ControlType = 'wysiwyg'; - readonly path: Path = { - cssPath: `${this.routPrefix}/assets/modules/react-quill-editor.css`, - jsPath: - { - dev: "/modules/controls/wysiwyg/react-quill-editor.tsx", - production: `${this.routPrefix}/assets/modules/react-quill-editor.es.js` - } - } - readonly config: Config = {}; - - getConfig(): Config { - return this.config; - } - - constructor(adminizer: Adminizer) { - super(adminizer); - } - - getJsPath(): string { - if (process.env.VITE_ENV === 'dev') { - return this.path.jsPath.dev; - } else { - return this.path.jsPath.production - } - } - - getCssPath(): string { - return this.path.cssPath - } - - getName(): string { - return this.name - } +```tsx +interface ControlProps { + options?: Record; + initialValue: unknown; + onChange: (value: unknown) => void; + name: string; + disabled?: boolean; } -``` -Then add it via `ControlsHandler`, like this: -```typescript -// add custom control wysiwyg -adminizer.emitter.on('adminizer:loaded', () => { - adminizer.controlsHandler.add(new ReactQuill(adminizer)) -}) -``` -After that, you can use in the configuration: -```js - editor: { - title: 'Editor', - type: 'wysiwyg', - options: { - name: 'react-quill', - } +export default function MyEditor({ + initialValue, + onChange, + disabled, +}: ControlProps) { + // Render the editor and call onChange with its current value. } ``` -See the implementation example here `fixture` & here `modules/controls/wysiwyg` -To build the module, you will need custom `vite.config` -```typescript -import {defineConfig} from 'vite'; -import react from '@vitejs/plugin-react'; -import path from 'path'; +Adminizer loads the stylesheet and component module in parallel. Loaded stylesheets are cached and are not appended more than once. + +## Vite Build + +Build the frontend component as an ES module. Externalize React so the control uses the Adminizer runtime copy: + +```ts +import {defineConfig} from "vite"; +import react from "@vitejs/plugin-react"; import {viteExternalsPlugin} from "vite-plugin-externals"; +import path from "path"; export default defineConfig({ - define: { - 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), - }, - plugins: [ - react(), - viteExternalsPlugin({ - react: 'React', - 'react-dom': 'ReactDOM', - }), - ], - build: { - outDir: path.resolve(import.meta.dirname, ''), - emptyOutDir: false, - lib: { - entry: path.resolve(import.meta.dirname, 'react-quill-editor.tsx'), - name: 'ComponentB', - formats: ['es'], - fileName: (format) => `react-quill-editor.${format}.js`, - }, - rollupOptions: { - output: { - assetFileNames: ({ names }) => { - if (names && names[0].endsWith('.css')) { - return 'react-quill-editor.css'; - } - return '[name]-[hash][extname]'; - }, - }, - }, - }, - resolve: { - alias: { - '@': path.resolve(import.meta.dirname, '../../../src/assets/js'), - }, + define: { + "process.env.NODE_ENV": JSON.stringify( + process.env.NODE_ENV || "production" + ), + }, + plugins: [ + react(), + viteExternalsPlugin({ + react: "React", + "react-dom": "ReactDOM", + }), + ], + build: { + outDir: import.meta.dirname, + emptyOutDir: false, + lib: { + entry: path.resolve(import.meta.dirname, "my-editor.tsx"), + name: "MyEditor", + formats: ["es"], + fileName: (format) => `my-editor.${format}.js`, + cssFileName: "my-editor", }, + }, }); ``` + +The fixture React-Quill example is in `fixture/apps/quill-editor`. All fixture app frontend modules, including Quill, are built together: + +```bash +npm run build:apps +``` + +## Field Configuration + +Reference the registered control by name: + +```ts +editor: { + title: "Editor", + type: "wysiwyg", + options: { + name: "my-editor", + config: { + theme: "snow", + }, + }, +} +``` + +The control's base config is merged with the field config. Field values take precedence. + +## Missing Or Disabled Controls + +If a configured control is not registered, for example because its app is disabled, Adminizer: + +1. logs one server warning per model field and missing control; +2. falls back to the built-in default for that control type; +3. does not pass the missing control's field config to the fallback control. + +The third rule prevents options intended for one editor from being applied to an incompatible editor. + +If the built-in default is also missing, Adminizer throws a configuration error. + +## ControlsHandler + +`ControlsHandler` stores controls by type and name. Its API is: + +```ts +add(control: Control): void; +get(type: ControlType, name: string): Control | undefined; +getByType(type: ControlType): Control[]; +remove(type: ControlType, name: string): boolean; +getAll(): Record; +``` + +Adding a duplicate name within the same type throws an error. + +Application modules should normally use `ctx.control()` instead of calling `adminizer.controlsHandler.add()` directly. App registration provides ownership, asset serving, rollback when enable fails, and cleanup during disable or unregister. diff --git a/docs/Dashboard.md b/docs/Dashboard.md index bb6d1570..a41e5132 100644 --- a/docs/Dashboard.md +++ b/docs/Dashboard.md @@ -48,18 +48,17 @@ module.exports.adminpanel = { * link [LinkBase] - provide link logic in widget * custom [CustomBase] - mount JS/html widget into dashboard widget -Widgets have magic on loading sails project such as model or service. This function work on -`sails.config.adminpanel.autoloadWidgetsPath: string`. A widget class is created from an abstract base and passed to `WidgetHandler.add`. +Widgets are registered on the Adminizer instance through `adminizer.widgetHandler.add(widget)`. The dashboard config can also define `autoloadWidgetsPath` for project-specific widget loading. > ⚠️ Any new widget type must be added to `WidgetType` inside `/lib/widgets/widgetHandller.ts`. Within this handler you also need to implement the logic for the new type in the `getAll` method. -> ⚠️ You can access widget handler by call method of admin panel `sails.hooks.adminpanel.getWidgetHandler()` +> ⚠️ You can access the widget handler as `adminizer.widgetHandler`. - > ⚠️ You can add a widget by calling `sails.hooks.adminpanel.addDashboardWidget(widget: WidgetType)`. + > ⚠️ You can add a widget by calling `adminizer.widgetHandler.add(widget)`. WidgetBase instance has this props: -- `ID`: string, required +- `id`: string, required - `name`: string, - `description`: string, - `icon`: string | null, default = null @@ -118,11 +117,11 @@ public abstract readonly widgetType: Next Handling Custom Widgets. Add logic to `getAll` method: else if (widget instanceof CustomBase) { - if (AccessRightsHelper.havePermission(`widget-${widget.ID}`, user)) { + if (adminizer.accessRightsHelper.hasPermission(`widget-${widget.id}`, user)) { widgets.push({ - id: `${widget.ID}_${id_key}`, + id: `${widget.id}_${id_key}`, type: widget.widgetType, - api: `${config.routePrefix}/widgets-custom/${widget.ID}`, + api: `${config.routePrefix}/widgets-custom/${widget.id}`, description: widget.description, icon: widget.icon, name: widget.name, @@ -134,7 +133,7 @@ Next Handling Custom Widgets. Add logic to `getAll` method: Then we`ll create Instance of this widget type class CustomOne extends CustomBase { - readonly ID: string = 'site_custom'; + readonly id: string = 'site_custom'; readonly department: string = 'test'; readonly description: string = 'Widget Custom One'; readonly icon: string = 'dog'; @@ -146,13 +145,13 @@ class CustomOne extends CustomBase { And export it as function which create our instances -const setCustomsWidgets = () => { - WidgetHandler.add(new CustomOne()) +const setCustomsWidgets = (adminizer) => { + adminizer.widgetHandler.add(new CustomOne()) } `module.exports = { setCustomsWidgets }` -Then call `setCustomsWidgets` inside `bootstrap.js` +Then call `setCustomsWidgets(adminizer)` after Adminizer is initialized. ### customizable and programmable promotion diff --git a/docs/Export.md b/docs/Export.md new file mode 100644 index 00000000..c481f35e --- /dev/null +++ b/docs/Export.md @@ -0,0 +1,168 @@ +# Export Data + +Adminizer supports exporting filtered data from model list views in three formats: **JSON**, **CSV**, and **XLSX**. + +## Overview + +The export feature allows users to download all records (respecting the currently active filter) from any model list page. Export is available without pagination limits. + +## Supported Formats + +| Format | Description | Use Case | +|--------|-------------|----------| +| **JSON** | Raw JSON array of objects | API integration, data backup, programmatic processing | +| **CSV** | Comma-separated values with UTF-8 BOM | Opening in Excel, Google Sheets, data analysis | +| **XLSX** | Native Excel spreadsheet with auto-sized columns | Business reports, presentations | + +## Backend API + +### Endpoint + +``` +POST /adminizer/model/:modelName/export +``` + +### Request Body + +```json +{ + "format": "json|csv|xlsx", + "filterId": "optional-filter-id" +} +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `format` | string | No | Export format: `json`, `csv`, or `xlsx`. Defaults to `json`. | +| `filterId` | string | No | ID of a saved filter to apply. If omitted, exports all records. Use `temporary` for session-based temporary filters. | +| `selectedFields` | string[] | No | Array of field names to include in export. If omitted, uses filter's column config or all fields. | + +### Response + +- **Success:** File download with appropriate `Content-Type` and `Content-Disposition` headers +- **400:** Invalid format or missing required data +- **401:** Unauthorized (if auth is enabled) +- **403:** Forbidden (insufficient permissions) +- **404:** Model not found +- **500:** Export failed (e.g., xlsx library not installed) + +### Response Headers + +``` +Content-Type: application/json (for JSON) +Content-Type: text/csv; charset=utf-8 (for CSV) +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet (for XLSX) +Content-Disposition: attachment; filename="modelname_export_1234567890.json" +``` + +## Frontend Usage + +The export button appears automatically in the table toolbar when the page loads. + +### How to Export + +1. Navigate to any model list page +2. (Optional) Apply a saved filter to narrow down results +3. Click the **Экспорт** (Export) button in the toolbar +4. Select the desired format: JSON, CSV, or XLSX +5. The file downloads automatically + +### Filter Integration + +- If a **saved filter** is active, only filtered records are exported +- If a **temporary filter** is applied, it is used for the export +- If **no filter** is active, all records are exported + +## Implementation Details + +### Controller: `src/controllers/exportData.ts` + +The export controller: +1. Validates the user has `read-{modelName}-model` permission +2. Loads the saved filter (if `filterId` is provided) +3. Converts datetime conditions from HTML5 format to UTC +4. Applies custom column configuration from filter +5. Converts filter conditions through `QueryBuilder` into internal `QueryCriteria` +6. Executes a query with no pagination (large limit) +7. Selects only real database columns at SQL level; relation/display fields are loaded and formatted after records are fetched +8. Formats data based on field types (relationships, booleans, dates, JSON) +9. Generates the file and sends it as a download + +### Route Registration: `src/system/Router.ts` + +```typescript +import _exportData from "../controllers/exportData"; + +adminizer.app.post(`${baseRoute}/export`, adminizer.policyManager.bindPolicies(policies, _exportData)); +``` + +### Frontend: `src/assets/js/components/list-table/table-toolbar.tsx` + +The export dropdown: +- Uses Adminizer HTTP client (`adminApi`) for export requests and creates a `Blob` for download +- Reads `filterId` from URL to preserve filter state +- Extracts filename from `Content-Disposition` header +- Shows toast notifications for success/error + +## Dependencies + +| Package | Purpose | Required | +|---------|---------|----------| +| `xlsx` | XLSX file generation | Yes (for XLSX export) | + +Install xlsx: +```bash +npm install xlsx +``` + +## CSV Encoding + +CSV files include a **UTF-8 BOM** (`\uFEFF`) prefix to ensure proper character encoding when opened in Microsoft Excel. + +## Data Formatting + +The export applies human-readable transformations: +- **Boolean:** `true` → `Yes`, `false` → `No` +- **Relationships:** Display field (title/name/id) is used +- **Has-Many:** Comma-separated list of display values +- **JSON:** Pretty-printed with indentation (2 spaces) +- **Dates:** ISO 8601 format (`YYYY-MM-DDTHH:mm:ss.sssZ`) +- **Arrays:** Comma-separated values + +## Security + +- Export respects the same **access rights** as the list view +- Only users with `read-{modelName}-model` permission can export +- Filter access control is enforced (private filters only accessible by owner) +- Datetime conditions are validated and converted to prevent injection + +## Extending + +To add a new export format: + +1. Add the format to the `ExportFormat` type in `exportData.ts` +2. Create a new `send{Format}Export` function +3. Add a case to the switch statement in the main controller +4. Add a menu item in `table-toolbar.tsx` + +Example for PDF export: + +```typescript +// In exportData.ts +async function sendPdfExport(res: ResType, data: Record[], modelName: string) { + // Generate PDF... + return res.send(buffer); +} + +// In the switch +case 'pdf': + return sendPdfExport(res, exportData, entity.name); +``` + +```tsx +// In table-toolbar.tsx + handleExport('pdf')}> + PDF + PDF документ + +``` diff --git a/docs/FeedExport.md b/docs/FeedExport.md new file mode 100644 index 00000000..f90f05e5 --- /dev/null +++ b/docs/FeedExport.md @@ -0,0 +1,198 @@ +# Feed Export API + +Public API for exporting filtered data in JSON and XML (Atom) formats. It allows external systems to fetch filtered data by URL without an Adminizer UI session. + +## Overview + +Filters with enabled API access generate a unique `apiKey`, which can be used to fetch data in the following formats: + +- **JSON** for programmatic integrations +- **XML (Atom)** for RSS-like feeds, marketplaces, news aggregators, and similar consumers + +## Enabling API Access For A Filter + +1. Open the record list for the target model. +2. Click **Filters** and select an existing filter to edit. +3. Find the **API access (feed)** section at the bottom of the dialog. +4. Enable the toggle. +5. Click **Show** to view the feed links. +6. Copy the JSON or XML URL. + +### Key Generation + +- The first time API access is enabled, Adminizer automatically generates an `apiKey` (UUID v4). +- The **Regenerate** button creates a new key, and the previous link stops working. +- The key is stored in the database in the `Filter` model. + +## Endpoints + +### JSON Export + +```http +GET /adminizer/api/feed/{apiKey}.json +``` + +**Response:** + +```json +{ + "feed": { + "title": "Filter title", + "description": "Filter description", + "modelName": "Example", + "generatedAt": "2026-04-06T12:00:00.000Z", + "totalItems": 42, + "apiKey": "550e8400-e29b-41d4-a716-446655440000", + "items": [ + { + "id": 1, + "name": "Record 1", + "status": "active", + "createdAt": "2026-04-01T10:00:00.000Z" + } + ] + } +} +``` + +### XML (Atom) Export + +```http +GET /adminizer/api/feed/{apiKey}.xml +``` + +**Response:** + +```xml + + + /adminizer/api/feed/550e8400-e29b-41d4-a716-446655440000 + Filter title + Filter description + 2026-04-06T12:00:00.000Z + + Adminizer + + 1 + Record 1 + 2026-04-01T10:00:00.000Z + Record 1 + + + id1 + nameRecord 1 + + ]]> + + {"id":1,"name":"Record 1"} + + +``` + +## Usage Examples + +### curl + +```bash +# JSON +curl https://example.com/adminizer/api/feed/YOUR_API_KEY.json + +# XML +curl https://example.com/adminizer/api/feed/YOUR_API_KEY.xml +``` + +### JavaScript / Fetch + +```javascript +const response = await fetch('/adminizer/api/feed/YOUR_API_KEY.json'); +const data = await response.json(); +console.log(data.feed.items); +``` + +### Python + +```python +import requests + +url = "https://example.com/adminizer/api/feed/YOUR_API_KEY.json" +response = requests.get(url) +data = response.json() + +for item in data['feed']['items']: + print(item['name']) +``` + +### RSS Aggregator + +Add `https://example.com/adminizer/api/feed/YOUR_API_KEY.xml` to your RSS reader, such as Feedly or Inoreader. + +## Security + +- Access requires two keys: the filter `apiKey` and the user `userKey` (`User.apiKey`). +- The `apiKey` must belong to a private filter with API access enabled. +- The `userKey` is validated before the feed is generated. +- Global authorization must be enabled (`auth.enable`). +- If the filter key is compromised, regenerate the `apiKey` in the filter UI. +- If the user key is compromised, regenerate the `userKey`. + +## Errors + +| Code | Description | +|------|-------------| +| `400` | Invalid format. Only `json` and `xml` are supported. | +| `401` | `userKey` was not provided. | +| `403` | Authorization is disabled or `userKey` is invalid. | +| `404` | Filter was not found or API access is disabled. | +| `500` | Internal server error. | + +**Error example:** + +```json +{ + "error": "Filter not found or API access is disabled", + "message": "Check your apiKey or ensure API access is enabled for this filter" +} +``` + +## Architecture + +### Files + +| File | Description | +|------|-------------| +| `src/services/FeedService.ts` | Feed generation service (JSON + XML). | +| `src/controllers/feed.ts` | HTTP controller. | +| `src/system/Router.ts` | Route registration. | +| `src/controllers/filter-fields/savedFilters.ts` | `apiKey` generation on save. | +| `src/assets/js/components/list-table/filter-panel-save-dialog.tsx` | API management UI. | + +### Data Format + +Data is formatted the same way as in regular exports (`exportData.ts`): + +- Relations are rendered by title field. +- Dates use ISO 8601. +- Booleans are rendered as `"Yes"` or `"No"`. +- Arrays are joined with commas. +- JSON values are serialized to strings. + +Filters are executed through the same flow as list export: filter conditions are converted by `QueryBuilder` into internal `QueryCriteria`, and the ORM adapter translates the criteria into its query format. + +## Customization + +### Change The JSON Structure + +Edit the `generateJsonFeed()` method in `FeedService.ts`: + +```typescript +async generateJsonFeed(filter: Filter): Promise { + const { records, fields, modelName } = await this.fetchFilterData(filter); + // Custom logic + return { /* custom structure */ }; +} +``` + +### Change The XML Format + +Edit the `buildAtomXml()` method in `FeedService.ts`. diff --git a/docs/Filters.md b/docs/Filters.md new file mode 100644 index 00000000..fec5fb26 --- /dev/null +++ b/docs/Filters.md @@ -0,0 +1,327 @@ +# Filters in Adminizer + +This document describes the current filter implementation in the codebase (backend + frontend). + +## Overview + +The filter subsystem allows users to: + +- build and apply temporary filters; +- save personal/shared filters with metadata; +- store per-filter column order; +- expose private filters as JSON/XML feeds (with API keys); +- use relation and custom conditions through Adminizer's internal query language. + +Main files: + +- `src/models/Filter.ts` +- `src/models/FilterColumn.ts` +- `src/lib/filters/FilterService.ts` +- `src/lib/query-builder/QueryBuilder.ts` +- `src/interfaces/queryCriteria.ts` +- `src/controllers/filter-fields/*` +- `src/assets/js/components/list-table/filter-panel.tsx` + +## Data Models + +### `Filter` + +| Field | Type | Notes | +|---|---|---| +| `id` | `string` | UUID | +| `name` | `string` | Required | +| `description` | `string?` | Optional | +| `modelName` | `string` | Required | +| `conditions` | `FilterCondition[]` | JSON array | +| `sortField` | `string?` | Optional | +| `sortDirection` | `'ASC' \| 'DESC'?` | Optional | +| `visibility` | `'private' \| 'public' \| 'groups' \| 'system'` | Access mode | +| `ownerId` | `number` | Owner user id | +| `groupIds` | `number[]?` | Used when visibility is `groups` | +| `apiEnabled` | `boolean` | Feed enabled flag | +| `apiKey` | `string?` | Filter API key for feeds | +| `icon` | `string?` | UI icon | +| `color` | `string?` | UI color | +| `version` | `number` | Filter format version (`1`) | +| `createdAt` / `updatedAt` | `Date` | Auto timestamps | + +### `FilterColumn` + +`FilterColumn` stores one row per selected column: + +| Field | Type | Notes | +|---|---|---| +| `id` | `number` | Auto-increment | +| `filter` | `string \| Filter` | Relation to `Filter` | +| `fieldName` | `string` | Model field id | +| `order` | `number` | Display order | + +### `FilterCondition` + +```ts +interface FilterCondition { + id: string; + field?: string; + operator?: FilterOperator; + value?: any; + + logic?: 'AND' | 'OR' | 'NOT'; + children?: FilterCondition[]; + + relation?: string; + relationField?: string; + + customHandler?: string; + customHandlerName?: string; + customHandlerParams?: any; + + rawSQL?: string; // legacy shape, not executed by QueryCriteria + rawSQLParams?: any[]; +} +``` + +Usage map (current code): + +| Field | Used in code | Notes | +|---|---|---| +| `id` | yes | Condition identity in UI/backend validation | +| `field` | yes | Standard field filtering path | +| `operator` | yes | Required for query mapping and validation | +| `value` | yes | Operator payload | +| `logic` | yes | Group logic (`AND`/`OR`/`NOT`) in backend/query builder | +| `children` | yes | Nested group conditions in backend/query builder | +| `relation` | yes | Relation filter path, converted to adapter-neutral criteria | +| `relationField` | yes | Related field in relation filter | +| `customHandler` | yes | Custom handler ID for custom filter execution | +| `customHandlerName` | yes | UI display label (optional, fallbacks exist) | +| `customHandlerParams` | yes | Optional params passed to custom handler execution | +| `rawSQL` | legacy | Not executed by `QueryCriteria`; use a custom handler that returns `criteria` | +| `rawSQLParams` | legacy | Kept for compatibility with older condition payloads | + +`FilterCondition` is a UI/saved-filter format. Runtime execution converts it into `QueryCriteria`; ORM adapters do not receive `FilterCondition` directly. + +## Operators + +`FilterOperator` currently includes: + +- `eq`, `neq` +- `gt`, `gte`, `lt`, `lte` +- `like`, `ilike`, `startsWith`, `endsWith` +- `in`, `notIn` +- `between` +- `today`, `month`, `year`, `monthBetween`, `yearBetween` +- `isNull`, `isNotNull` +- `regex` +- `custom` + +Type-specific availability is enforced in `ConditionValidator` and `QueryBuilder`. + +## Query Execution + +Filters are not passed to ORM adapters directly. The execution path is: + +```text +FilterCondition[] + list/search params + -> QueryBuilder + -> QueryCriteria + -> ORM adapter +``` + +`QueryCriteria` is documented in [Internal Queries](InternalQueries.md). It contains only adapter-neutral fields such as `where`, `select`, `populate`, `sort`, `limit`, and `skip`. UI concepts such as `filters`, `globalSearch`, and visible table columns stay outside the internal query language. + +### Important UI note + +The current filter panel supports a flat condition list (combined with `AND`). +Nested `AND/OR/NOT` groups are supported by backend condition format and query builder, but the standard UI does not provide a group editor. + +### Relation filter note + +- Relation conditions are converted to adapter-neutral relation criteria. +- In the current UI and validator flow for relation conditions, allowed operators are `eq` and `neq`. + +## Visibility and Permissions + +Visibility types: + +- `private` - owner + admin +- `groups` - users whose groups intersect with `groupIds` + admin +- `public` - all users with access to the model +- `system` - visible to all users with model access; excluded from saved filter list by default + +Edit/delete rules: + +- admins can edit/delete any filter; +- non-admin users can edit/delete only their own filters. + +Creation rules in `POST /filter`: + +- `public` is allowed only for admins; +- `groups` is allowed for admins or users with `manage-group-filter-visibility` permission token; +- otherwise visibility is forced to `private`. + +When editing someone else's filter, visibility is preserved. + +## Backend API + +Routes are registered under `/model/:entityName/...` patterns. + +### Filter metadata and fields + +- `GET /adminizer/model/:model/filter-fields` + - Returns available fields for filter UI. + - Applies model filter config: `models..filters` (or legacy `modelFilters.`). + - Applies `excludeFromFilters`. + - Always excludes `id`, `ownerId`, `updatedAt`. + - Relation fields are returned only for Sequelize. + - Custom filter metadata is returned for Sequelize fields with `customFilter.handlerId`. + +### Saved filters + +- `GET /adminizer/model/:model/saved-filters` + - Returns accessible filters with: + - owner info (`ownerInfo`) + - computed result count (`resultCount`) + +- `POST /adminizer/model/:model/filter` + - Creates or updates a saved filter. + - Update mode uses `filterId` in body. + - Optional: `columns`, `visibility`, `groupIds`, `sortField`, `sortDirection`, `icon`, `color`. + - For private owner filters, supports `apiEnabled` and `regenerateApiKey`. + - If same-name filter already exists for the same owner and `filterId` is not provided, returns `409` with `requiresConfirmation: true`. + +- `DELETE /adminizer/model/:model/filter/:id` + - Returns `204` for successful deletion. + - Also returns `204` for "not found or access denied" to avoid ownership enumeration. + +### Temporary filters + +- `POST /adminizer/model/:model/filter/apply` + - Stores temporary filter in server session as `req.session.temporaryFilters[modelName]`. + - Returns `filterId: "temporary"`. + +- `GET /adminizer/model/:model/filter/temporary` + - Reads temporary filter from server session. + +Frontend additionally stores temporary filter meta in `sessionStorage` for local UI state; backend source of truth is server session. + +### Columns + +- `GET /adminizer/model/:model/columns?filterId=` + - Returns `availableColumns` and current `filterColumns`. + +- `POST /adminizer/model/:model/filter/:filterId/columns` + - Replaces column rows for the filter. + +### Groups + +- `GET /adminizer/groups` + - Returns group list for group visibility selector. + - Protected by `manage-group-filter-visibility` permission token. + +### Cross-model user filter list + +- `GET /adminizer/api/all-user-filters` + - Returns all filters visible to the current user across models. + - Supports query params: `search`, `modelName`, `offset`, `limit`. + +## Public Feed API + +Feed routes: + +- `GET /adminizer/api/feed/:apiKey` +- `GET /adminizer/api/feed/:apiKey.:format` (`json` or `xml`) + +Requirements: + +- filter must be `visibility: 'private'`; +- filter must have `apiEnabled: true`; +- caller must provide `userKey` query parameter; +- auth must be enabled globally (`config.auth.enable`); +- `userKey` must match an existing user API key (`User.apiKey`). + +Helper endpoints for user API keys: + +- `GET /adminizer/api/user-key` +- `POST /adminizer/api/user-key/regenerate` + +## Custom Filter Handlers + +Custom handler runtime: + +- registry class: `CustomFilterHandler` +- handler base class: `AbstractCustomFilter` +- handler id format: `ModelName.fieldName` + +Register handlers programmatically: + +```ts +adminizer.customFilterHandler.add(new MyFilterHandler(), { force: true }); +``` + +Attach handler to model field: + +```ts +fields: { + myJson: { + type: 'jsoneditor', + customFilter: { + handlerId: 'Example.myJson' + } + } +} +``` + +Notes: + +- custom filter metadata is exposed by `/filter-fields` only for Sequelize fields; +- handler `inputConfig` supports up to 3 inputs; +- in UI, custom condition is serialized with `operator: 'custom'` and `customHandler`. + +## Validation and Security Limits + +Execution-level limits (`FILTER_SECURITY_LIMITS` in `QueryBuilder`): + +| Limit | Value | +|---|---| +| Max nesting depth | 10 | +| Max conditions per group | 100 | +| Max total conditions | 500 | +| Max `IN` values | 1000 | +| Max string length | 10000 | + +`ConditionValidator` provides extra condition validation helpers (field/type/operator/value checks and legacy raw SQL pattern checks), but it is a utility class and is not the only enforcement layer. Query execution still validates key constraints in `QueryBuilder`. + +## Model Configuration + +Global switch: + +```ts +filters: { + enabled: true +} +``` + +Per-model filter configuration: + +```ts +models: { + Example: { + model: 'Example', + title: 'Example', + filters: { + enabled: true, + excludeFromFilters: ['createdAt', 'updatedAt'] + } + } +} +``` + +Legacy fallback is still supported: + +```ts +modelFilters: { + Example: { + excludeFromFilters: ['createdAt'] + } +} +``` diff --git a/docs/Forms.md b/docs/Forms.md index 58060afe..1954af72 100644 --- a/docs/Forms.md +++ b/docs/Forms.md @@ -1,60 +1,5 @@ # Forms -You can add forms when you have no need to create a Model. For example, for -single block on page that will not be repeated. Or like a link with model settings. +Legacy configuration-driven Forms are no longer used starting with Adminizer 5. -You can add forms directly to adminpanel configuration or put them in files with -`.json` extension which should be named as form slug. Put your forms in your -directory and write down path to it in `path` field. - -```javascript -module.exports.adminpanel = { - forms: { - path: 'forms', - data: { - formSlug: { - field1: { - title: "Field1", - type: "string", - value: "Some string", - required: true, - tooltip: 'tooltip for field1', - description: "some description" - } - } - } - }, -} -``` - -- path - relative path to your forms from project directory -- data - object with forms which are named by unique slug - -You can use forms by adding adminpanel special links like `tools`, `additionaLinks` -in navbar, `global` or `inline` actions. To do that read [Links](Links.md) - -## How to create a form - -Forms is `.json` files that contains fields with `BaseFieldConfig` options. -`title` and `type` are required. - -```json -{ - "label": { - "title": "Label", - "type": "string", - "value": "From example from file", - "required": true, - "tooltip": "tooltip for label", - "description": "some description" - }, - "teaser": { - "title": "Тизер", - "type": "text", - "required": true, - "tooltip": "tooltip for teaser", - "description": "some description", - "value": "" - } -} -``` +Use model create/edit pages and model configuration instead. diff --git a/docs/GlobalUI.md b/docs/GlobalUI.md index 21b0b84f..3504fb4f 100644 --- a/docs/GlobalUI.md +++ b/docs/GlobalUI.md @@ -37,3 +37,31 @@ export default function Example() { Make sure the Adminizer bundle is loaded before your module so that `registerUIComponents()` has populated `window.UIComponents`. The `registerUIComponents()` function automatically creates `window.UIComponents` if it doesn't already exist. This avoids runtime errors like `Uncaught TypeError: Cannot convert undefined or null to object` when the container is missing. + +## Built-In Control Globals + +`window.JSComponents` also exposes selected built-in controls such as `VanillaJSONEditor`, `HandsonTable`, and `MonacoEditor`. + +These exports are lazy proxies. Registering UI globals does not place the control implementations in the main Adminizer bundle and does not download them on dashboard entry. The corresponding ES module and production stylesheet are loaded when the proxy is first rendered. + +When app modules need the heavy built-in controls in local development, import Adminizer's lazy proxy modules instead of importing `@/components/...` directly: + +```tsx +import HandsonTable from "@/js-components/handsontable"; +import MonacoEditor from "@/js-components/monaco"; +import VanillaJSONEditor from "@/js-components/jsoneditor"; +``` + +Externalize the same imports in the module Vite config: + +```ts +viteExternalsPlugin({ + "@/js-components/handsontable": ["JSComponents", "HandsonTable"], + "@/js-components/monaco": ["JSComponents", "MonacoEditor"], + "@/js-components/jsoneditor": ["JSComponents", "VanillaJSONEditor"], +}); +``` + +This keeps development and production behavior aligned: the module loads only a small proxy first, while the control implementation and styles are loaded on first render. + +App-owned custom controls should use `ctx.control()` instead of adding new entries to `window.JSComponents`. See [Controls](Controls.md). diff --git a/docs/HistoryActions.md b/docs/HistoryActions.md index e64e8bf0..1f7ea74a 100644 --- a/docs/HistoryActions.md +++ b/docs/HistoryActions.md @@ -30,7 +30,7 @@ An abstract base class defining the contract for all history adapters. It includ ## Default Adapter: `DefaultHistoryAdapter` -This built-in adapter uses the `HistoryActionsAP` model to persist history records in the database. +This built-in adapter uses the canonical `HistoryActions` system model to persist history records in the database. ### How It Works @@ -59,7 +59,7 @@ This built-in adapter uses the `HistoryActionsAP` model to persist history recor 4. **Security** - Respects RBAC (Role-Based Access Control). - - Internal models (like `UserAP`, `MediaManagerAP`, etc.) are excluded by default. + - Adminizer infrastructure models are excluded by default. The app-owned media manager models remain excluded by name for v5 compatibility, but they are not core system models. - Configurable extra exclusions via `config.history.excludeModels`. --- @@ -94,7 +94,7 @@ You can replace or extend the default behavior by implementing your own adapter. ```ts import { AbstractHistoryAdapter } from '../lib/history-actions/AbstractHistoryAdapter'; -import { HistoryActionsAP, UserAP } from '../models'; +import { HistoryActions, User } from '../models'; export class MyCustomHistoryAdapter extends AbstractHistoryAdapter { public id = 'myadapter'; // must be unique @@ -153,4 +153,4 @@ These are auto-registered with the Adminizer access rights system. ## Notes The system is opt-out: all models are tracked unless listed in excludeModels. -Future improvements may include diff-only storage and compression. \ No newline at end of file +Future improvements may include diff-only storage and compression. diff --git a/docs/InertiaAdapter.md b/docs/InertiaAdapter.md index 83c13166..a5c6e72a 100644 --- a/docs/InertiaAdapter.md +++ b/docs/InertiaAdapter.md @@ -144,16 +144,16 @@ adminizer.app.use( ); req.Inertia.setViewData({ - lang: req.session.UserAP?.locale || 'en', + lang: req.session.User?.locale || 'en', }) const menuHelper = new InertiaMenuHelper(adminizer) req.Inertia.shareProps({ auth: { - user: req.session.UserAP + user: req.session.User }, - menu: req.session.UserAP ? menuHelper.getMenuItems(req) : null, + menu: req.session.User ? menuHelper.getMenuItems(req) : null, brand: menuHelper.getBrandTitle(), logout: menuHelper.getLogoutUrl(), //... @@ -161,8 +161,9 @@ req.Inertia.shareProps({ ``` `menuHelper.getMenuItems(req)` returns sidebar items already localized on the server (titles, sections, and nested action titles are translated via `req.i18n.__(...)` before sending Inertia props). -You can also share a common dictionary (for example `uiMessages`) via `shareProps` and consume it in React components for reusable interface labels. -For dashboard widgets, shared `uiMessages` can include keys like `Show`, `Hide`, `On`, `Off`, and `No widgets found`. +You can also share a common dictionary via `i18n.common` in `shareProps` and consume it in React components for reusable interface labels. +For dashboard widgets, common keys can include labels like `Show`, `Hide`, `On`, `Off`, and `No widgets found`. +Page-specific dictionaries should be passed via `i18nPage`. **See the implementation here** `src/system/bindInertia.ts` ### Usage in controllers diff --git a/docs/Install.md b/docs/Install.md index 432d1b4c..0bd898f2 100644 --- a/docs/Install.md +++ b/docs/Install.md @@ -1,170 +1,191 @@ -# How to Start Adminizer - -To launch **Adminizer**, you need to follow a few simple steps: - ---- - -## 1. Create an Adminizer instance - -You must create an instance of the `Adminizer` class. -When creating it, you should pass an array of adapters — adapters are responsible for connecting Adminizer to your data sources (e.g., SequelizeAdapter or WaterlineAdapter). - ---- - -## 2. Configure the adapters - -Each adapter acts as a bridge between Adminizer and your models. -You need to prepare and configure at least one adapter: -- For **Sequelize**, create a database connection and register your models. -- For **Waterline**, create the ORM, register models, and initialize it with configuration. -- Or _use any other ORM_ by implementing a custom adapter that extends AbstractAdapter. -Adminizer is designed to be flexible and can be integrated with any ORM or data source. ---- - -## 3. Initialize Adminizer - -After creating the Adminizer instance, call the `init` method and pass the project configuration, which includes: -- the route prefix (e.g., `/adminizer`), -- the project name, -- whether authentication is required, -- and optionally database connection settings. - ---- - -## 4. Create an HTTP server - -To access Adminizer through the browser, you must create an HTTP server (e.g., using Node.js `http` module). -You should forward all requests starting with the route prefix (like `/adminizer`) to Adminizer’s application handler. -Other requests can be processed separately (e.g., by returning a basic welcome page). - ---- - -## 5. Start the server - -Once everything is set up, start the server and listen on a port (typically 3000). -After that, the Adminizer panel will be available at a URL like `http://localhost:3000/adminizer`. - ---- - -# What Happens After Startup - -- When a user navigates to `/adminizer`, the request is routed to Adminizer. -- Adminizer renders the administrative panel UI. -- If models were registered via adapters, Adminizer automatically displays them for: - - viewing records, - - creating new entries, - - editing and deleting records. -- If authentication is enabled, users must log in before accessing the panel. - ---- - -# Minimum Requirements to Run Adminizer - -- An Adminizer instance (`new Adminizer([...])`). -- At least one adapter (can be empty or connected to a database). -- A call to the `init` method with basic configuration. -- A server that routes requests to Adminizer. - ---- - -# Troubleshooting Adminizer Startup - -If you encounter difficulties launching **Adminizer**, -you can use the ready-made **fixture** (template project) available in the repository folder [`fixture`](https://github.com/adminization/adminizer/tree/main/fixture). - -This project already includes: -- a fully configured Adminizer setup, -- working Waterline models, -- example seed data, -- and a minimal configuration required to start. - -It serves as a practical starting point to help you quickly understand how Adminizer, models, adapters, and configuration fit together. - -### How to use the armature: - -```bash -git clone https://github.com/adminization/adminizer.git -cd adminizer -npm install --force -npm run demo:build && npm run demo -``` - -After running these commands, you will have a fully working Adminizer instance ready for testing and development at `http://localhost:3000/adminizer`. - ---- - -Example: - -```typescript -import { Adminizer } from "@adminization/adminizer"; -import { SequelizeAdapter } from "@adminization/adminizer/v4/model/adapter/sequelize"; -import { Sequelize, DataTypes } from "sequelize"; -import http from "http"; - -async function start() { - // 1. Initialize Sequelize - const sequelize = new Sequelize('sqlite::memory:', { logging: false }); - - // 2. Define a model - const Example = sequelize.define('example', { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - title: { - type: DataTypes.STRING, - allowNull: false, - }, - description: { - type: DataTypes.TEXT, - }, - }, { - tableName: 'examples', - timestamps: false, - }); - - // 3. Synchronize the database (this will create the table) - await sequelize.sync({ force: true }); - - // 4. Create an adapter - const sequelizeAdapter = new SequelizeAdapter({ - sequelize: sequelize, - models: { example: Example }, - }); - - // 5. Create Adminizer instance - const adminizer = new Adminizer([sequelizeAdapter]); - - await adminizer.init({ - routePrefix: "/adminizer", - projectName: "Adminizer with Sequelize", - dbConnection: null, - auth: false, - buildAssets: true, - }); - - // 6. HTTP server - const mainApp = http.createServer((req, res) => { - if (req.url.startsWith("/adminizer")) { - adminizer.app(req, res, (err) => { - if (err) { - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end('Internal Server Error'); - } - }); - } else { - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end('

Welcome

Go to Adminizer

'); - } - }); - - mainApp.listen(3000, () => { - console.log('Server started: http://localhost:3000/adminizer'); - }); -} - -start(); - -``` - +# How to Start Adminizer + +To launch **Adminizer**, you need to follow a few simple steps: + +--- + +## 1. Create an Adminizer instance + +You must create an instance of the `Adminizer` class. +When creating it, you should pass an array of adapters — adapters are responsible for connecting Adminizer to your data sources. Sequelize is the primary supported adapter. TypeORM support exists, but is experimental. + +--- + +## 2. Configure the adapters + +Each adapter acts as a bridge between Adminizer and your models. +You need to prepare and configure at least one adapter: +- For **Sequelize**, create a database connection and register your models. +- For **TypeORM**, use the experimental `TypeOrmAdapter` only after testing it against your project models. +- Or _use any other ORM_ by implementing a custom adapter that extends AbstractAdapter. +Adminizer is designed to be flexible and can be integrated with any ORM or data source. + +The host application must register the required Adminizer system models before calling `adminizer.init()`. Adminizer validates those models at startup but does not create or synchronize them. See [System Models](Configuration/Models.md). + +Adminizer core refers to system models by canonical names such as `User` and `Group`. If your ORM models use different names, pass the mapping through the adapter `systemModels` option. + +--- + +## 3. Initialize Adminizer + +After creating the Adminizer instance, call the `init` method and pass the project configuration, which includes: +- the route prefix (e.g., `/adminizer`), +- the project name, +- whether authentication is required, +- and optionally database connection settings. + +--- + +## 4. Create an HTTP server + +To access Adminizer through the browser, you must create an HTTP server (e.g., using Node.js `http` module). +You should forward all requests starting with the route prefix (like `/adminizer`) to Adminizer’s application handler. +Other requests can be processed separately (e.g., by returning a basic welcome page). + +--- + +## 5. Start the server + +Once everything is set up, start the server and listen on a port (typically 3000). +After that, the Adminizer panel will be available at a URL like `http://localhost:3000/adminizer`. + +--- + +# What Happens After Startup + +- When a user navigates to `/adminizer`, the request is routed to Adminizer. +- Adminizer renders the administrative panel UI. +- If models were registered via adapters, Adminizer automatically displays them for: + - viewing records, + - creating new entries, + - editing and deleting records. +- If authentication is enabled, users must log in before accessing the panel. + +--- + +# Minimum Requirements to Run Adminizer + +- An Adminizer instance (`new Adminizer([...])`). +- At least one adapter containing the required Adminizer system models. +- A call to the `init` method with basic configuration. +- A server that routes requests to Adminizer. + +--- + +# Troubleshooting Adminizer Startup + +If you encounter difficulties launching **Adminizer**, +you can use the ready-made **fixture** (template project) available in the repository folder [`fixture`](https://github.com/adminization/adminizer/tree/main/fixture). + +This project already includes: +- a fully configured Adminizer setup, +- working Sequelize models, +- experimental TypeORM models, +- example seed data, +- and a minimal configuration required to start. + +It serves as a practical starting point to help you quickly understand how Adminizer, models, adapters, and configuration fit together. + +### How to use the armature: + +```bash +git clone https://github.com/adminization/adminizer.git +cd adminizer +npm install --force +npm run build +npm run start:seed +``` + +After running these commands, you will have a fully working Adminizer instance ready for testing and development at `http://localhost:3000/adminizer`. + +--- + +Example: + +```typescript +import { Adminizer, SequelizeAdapter } from "adminizer"; +import { Sequelize, DataTypes } from "sequelize"; +import express from "express"; +import { registerSequelizeSystemModels } from "./models/adminizer-system-models"; + +async function start() { + // 1. Initialize Sequelize + const sequelize = new Sequelize('sqlite::memory:', { logging: false }); + registerSequelizeSystemModels(sequelize); + + // 2. Define a model + const Example = sequelize.define('example', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.TEXT, + }, + }, { + tableName: 'examples', + timestamps: false, + }); + + // 3. Synchronize the database (this will create the table) + await sequelize.sync({ force: true }); + + // 4. Create an adapter and map Adminizer system names to host ORM model names + const sequelizeAdapter = new SequelizeAdapter(sequelize, { + systemModels: { + User: "UserAP", + Group: "GroupAP", + Filter: "FilterAP", + FilterColumn: "FilterColumnAP", + HistoryActions: "HistoryActionsAP", + Notification: "NotificationAP", + UserNotification: "UserNotificationAP", + }, + }); + + // 5. Create Adminizer instance + const adminizer = new Adminizer([sequelizeAdapter]); + + await adminizer.init({ + routePrefix: "/adminizer", + auth: { enable: false }, + models: { + Example: { + title: "Examples", + model: "example", + fields: { + id: { title: "ID", type: "integer" }, + title: { title: "Title", type: "string" }, + description: { title: "Description", type: "text" }, + }, + list: true, + add: true, + edit: true, + view: true, + remove: true, + }, + }, + }); + + // 6. HTTP server + const mainApp = express(); + mainApp.use(adminizer.getMiddleware()); + mainApp.get("/", (_req, res) => { + res.type("html").send('

Welcome

Go to Adminizer

'); + }); + + mainApp.listen(3000, () => { + console.log('Server started: http://localhost:3000/adminizer'); + }); +} + +start(); + +``` + +The media manager is not a built-in system model set. Its storage models and implementation must be provided by an app and enabled after `adminizer.init()`. See [Media Manager](MediaManager.md) and the reference implementation in `fixture/apps/media-manager`. diff --git a/docs/InternalQueries.md b/docs/InternalQueries.md new file mode 100644 index 00000000..bb1340e0 --- /dev/null +++ b/docs/InternalQueries.md @@ -0,0 +1,196 @@ +# Internal Queries + +Adminizer uses an internal query language named `QueryCriteria` between higher-level services and ORM adapters. The goal is to keep Adminizer code independent from a specific ORM query syntax. UI filters, global search, and custom filter state are converted before execution; they are not part of `QueryCriteria`. + +## QueryCriteria + +```ts +interface QueryCriteria { + where?: CriteriaWhere; + select?: string[] | Record; + populate?: Record; + sort?: string | Record; + limit?: number; + skip?: number; +} +``` + +Basic example: + +```ts +await model.find({ + where: { + parent: null, + mimeType: { contains: "image" }, + group: "default" + }, + sort: "createdAt DESC", + limit: 20, + skip: 0, + select: ["id", "filename", "url"] +}); +``` + +## Operators + +Field values can be primitives, arrays, or operator objects. + +```ts +await model.find({ + where: { + status: "published", + title: { contains: "release" }, + createdAt: { between: [fromDate, toDate] }, + id: { in: [1, 2, 3] }, + deletedAt: { isNull: true } + } +}); +``` + +Supported operators: + +| Operator | Meaning | +|---|---| +| `eq` | Equals | +| `ne` | Not equals | +| `gt`, `gte`, `lt`, `lte` | Numeric/date comparisons | +| `contains` | String contains | +| `startsWith`, `endsWith` | String prefix/suffix | +| `in`, `notIn` | Value is in/not in array | +| `between` | Inclusive range | +| `isNull`, `isNotNull` | Null checks | +| `regex` | Adapter-supported regular expression | +| `jsonContains` | Adapter-supported JSON containment | + +Arrays are treated as `in`: + +```ts +await model.find({ where: { id: [1, 2, 3] } }); +``` + +## Boolean Groups + +`and`, `or`, and `not` are adapter-neutral boolean groups. + +```ts +await model.find({ + where: { + and: [ + { status: "published" }, + { + or: [ + { title: { contains: "Adminizer" } }, + { description: { contains: "Adminizer" } } + ] + }, + { not: { archived: true } } + ] + } +}); +``` + +## Populate + +`populate` describes relations to load. A relation can be `true` or another `QueryCriteria`. +When a relation criteria contains `select`, the selected fields are evaluated against the related model, not the parent model. + +```ts +await mediaModel.find({ + where: { parent: null }, + populate: { + variants: { + sort: "createdAt DESC", + select: ["id", "url", "mimeType"] + }, + meta: { + where: { isPublic: true } + } + } +}); +``` + +Nested `populate` is allowed: + +```ts +await notificationModel.find({ + populate: { + userNotifications: { + populate: { + userId: true + } + } + } +}); +``` + +## QueryBuilder Boundary + +`QueryBuilder` accepts UI-oriented parameters such as `filters`, `globalSearch`, and `fields`, then converts them into `QueryCriteria`. + +This separation is intentional: + +- `FilterCondition[]` belongs to the filter UI and saved filter models. +- `globalSearch` belongs to list/table UX. +- `fields` is a display/export selection. +- ORM adapters receive only `QueryCriteria`. + +Raw SQL and in-memory custom filter results are not part of `QueryCriteria`. Custom filters that participate in database queries should return adapter-neutral `criteria`. + +## Internal Model Access + +System modules should access internal models through `ModelHandler`: + +```ts +const user = await adminizer.modelHandler + .internal("widgets") + .get("User") + .findOne({ where: { login: "admin" } }); +``` + +This path is intended for trusted Adminizer subsystems that must bypass `DataAccessor` user filtering. It replaces direct calls to protected adapter methods such as: + +```ts +// Do not use in application/module code. +adminizer.modelHandler.model.get("User")["_findOne"](...) +``` + +## Allowlist Scopes + +Each internal access scope has an allowlist of models. Built-in scopes include: + +| Scope | Purpose | +|---|---| +| `auth` | Login, registration, init user | +| `users` | Built-in user/group controllers | +| `filters` | Saved filters and filter columns | +| `media-manager` | Legacy v5 access to app-owned media manager models | +| `history` | History actions | +| `notifications` | Notification models | +| `navigation` | Navigation storage | +| `widgets` | Dashboard widgets | +| `feed` | Feed export | + +Projects can extend the allowlist with `system.internalModelAccess`: + +```ts +const config = { + system: { + internalModelAccess: { + "my-module": ["User", "Filter"] + } + } +}; +``` + +Then use the custom scope: + +```ts +const filters = await adminizer.modelHandler + .internal("my-module") + .get("Filter") + .find({ where: { modelName: "Example" } }); +``` + +Use `DataAccessor` for normal user-facing model operations. Use `internal(...)` only for system-level Adminizer modules or trusted integrations. + +The `media-manager` scope is retained for compatibility. It does not make media manager models part of the core system contract and does not create them. New media manager apps should declare model access through `ctx.modelAccess()` and use `runtime.models`. diff --git a/docs/MediaManager.md b/docs/MediaManager.md new file mode 100644 index 00000000..96a0394e --- /dev/null +++ b/docs/MediaManager.md @@ -0,0 +1,145 @@ +# Media Manager + +The media manager has two layers in Adminizer v5: + +- Core owns the legacy HTTP transport, upload adapter, thumbnail endpoint, public-file binding, and file icons. +- An app owns the storage models and the `AbstractMediaManager` implementation. + +Adminizer no longer creates media manager ORM models. The fixture implementation in `fixture/apps/media-manager` is the Sequelize reference. + +## App Registration + +A media manager app declares its access right, existing models, scoped model access, and manager factory: + +```ts +setup(ctx: AppSetupContext): void { + ctx.accessRight({ + id: "mediaManager-default", + name: "default", + description: "Access to edit the default media manager", + department: "media-manager", + }); + + ctx.model({ name: "MediaManagerAP", adapter: "sequelize" }); + ctx.model({ name: "MediaManagerMetaAP", adapter: "sequelize" }); + ctx.model({ name: "MediaManagerAssociationsAP", adapter: "sequelize" }); + + ctx.modelAccess({ + models: [ + "MediaManagerAP", + "MediaManagerMetaAP", + "MediaManagerAssociationsAP", + ], + }); + + ctx.mediaManager({ + factory: (runtime) => new ProjectMediaManager(runtime, config), + }); +} +``` + +`AppManager` creates the manager after model bindings and model access are ready. Disabling the app unregisters the manager, but it does not remove native ORM models or database tables. + +## Sequelize Setup + +Sequelize app models can be installed after Adminizer startup and immediately before the app is enabled: + +```ts +await adminizer.init(adminpanelConfig); + +await installMediaManagerSequelizeModels(sequelize, true); +await adminizer.appManager.enable( + new MediaManagerApp(adminpanelConfig.mediamanager!) +); +``` + +The installer and app above are fixture reference code, not automatically registered package models. A host project should provide an equivalent installer, migrations, and app implementation. + +The fixture models are: + +- `MediaManagerAP` +- `MediaManagerMetaAP` +- `MediaManagerAssociationsAP` + +They are app-owned and are not part of the required system model contract described in [System Models](Configuration/Models.md). + +## TypeORM + +TypeORM support remains experimental. The core system models can be used with TypeORM, but app entities cannot be added dynamically after `DataSource.initialize()`. + +To implement a TypeORM media manager app: + +1. Define the media manager entities in the host project. +2. Add them to `DataSource.entities` before `initialize()`. +3. Initialize the data source and Adminizer. +4. Enable the app that attaches those existing entities. + +The fixture does not currently enable its media manager app for TypeORM. Core logs a warning when an app is enabled while `system.defaultORM` is `typeorm`. + +## Core Transport In Version 5 + +The following compatibility endpoints and resources remain in core: + +- `${routePrefix}/media-manager-uploader/:id` +- `${routePrefix}/media-manager-uploader/:id/upload` +- `${routePrefix}/media-manager-uploader/:id/upload-variant` +- `${routePrefix}/get-thumbs` +- `${routePrefix}/fileicons` +- `/public` when `bind.public` is enabled and `mediamanager.fileStoragePath` is configured + +The controllers currently resolve managers through `req.adminizer.mediaManagerHandler`. Moving this transport to the system runtime is deferred to v6. + +`npm run copy:backend` copies `src/fileicons` to `dist/fileicons`, so production startup from `dist` uses the built resource path. + +## Legacy Registration + +Custom media managers that are not apps remain supported during v5: + +```ts +class CustomMediaManager extends AbstractMediaManager { + constructor(adminizer: Adminizer) { + super(adminizer); + } + + // Implement the abstract media manager methods. +} + +const manager = new CustomMediaManager(adminizer); +adminizer.mediaManagerHandler.add(manager); +``` + +The `AbstractMediaManager` constructor calls the legacy `_bindAccessRight()` path. Both the constructor dependency and direct `mediaManagerHandler.add()` registration are deprecated and planned for removal in v6. + +An app-owned manager should register its token with `ctx.accessRight()`. Until the base constructor changes in v6, it can pass a no-op legacy host to `super(...)`; `fixture/apps/media-manager/DefaultMediaManager.ts` shows this compatibility pattern. + +## Configuration + +Core transport still reads `AdminpanelConfig.mediamanager`: + +```ts +mediamanager: { + fileStoragePath: ".tmp/public", + allowMIME: ["image/*", "application/*", "text/*", "video/*"], + maxByteSize: 2 * 1024 * 1024, + imageSizes: { + sm: { width: 350, height: 350 }, + lg: { width: 750, height: 750 }, + }, +}, +bind: { + public: true, +}, +``` + +Fields using the media manager select a registered manager by id: + +```ts +media: { + type: "mediamanager", + options: { + id: "default", + group: "images", + accept: ["image/jpeg", "image/png"], + }, +}, +``` diff --git a/docs/Navigation.md b/docs/Navigation.md index 425cb770..e8c5bbd8 100644 --- a/docs/Navigation.md +++ b/docs/Navigation.md @@ -1,333 +1,415 @@ -# Navigation Catalog +# Navigation Module -## Abstract +Navigation is implemented as an Adminizer app module in the fixture: `fixture/apps/navigation`. Use that implementation as the reference for building project-specific navigation editors. -The Navigation Catalog is a built-in Adminizer system for managing hierarchical site navigation menus — header, footer, or any other sections. It is built on top of the [Catalog](Catalog.md) infrastructure and adds specialized item types: records from any configured model (linked via a URL template), free-form groups with custom metadata fields, and plain external links. +The module is built on the catalog system and registers its own runtime model, catalog, template components, access token, sidebar links, and synchronization listener. -**Why is this needed:** -- **Visual tree editor**: drag-and-drop ordering and nesting in the admin panel UI -- **Multiple sections**: manage header, footer, and any other menus independently in one place -- **Mixed content**: model records, groups, and external links coexist in the same tree -- **URL templating**: item URLs are generated automatically from model record fields -- **Persistent storage**: the entire tree is stored as JSON in the database, one row per section -- **Ready-made API**: read the navigation tree from your frontend application via a single query +## Fixture Files -**Main features:** -- Add records from any configured model as navigation items -- Add folder-like groups with custom extra fields -- Add manual external links -- Drag-and-drop reordering and re-nesting -- Per-item `visible` and `targetBlank` flags -- Full-text search across all items -- No extra code required — enabled entirely through `adminizerConfig.ts` +| File | Purpose | +|---|---| +| `fixture/apps/navigation/NavigationApp.ts` | App entry point. Registers all module resources through `AppSetupContext`. | +| `fixture/apps/navigation/navigationConfig.ts` | Example module config used by the fixture. | +| `fixture/apps/navigation/NavigationTypes.ts` | Config interfaces. | +| `fixture/apps/navigation/NavigationModel.ts` | Runtime model name and schema for persisted trees. | +| `fixture/apps/navigation/NavigationCatalog.ts` | Catalog, storage, item types, and CRUD logic. | +| `fixture/apps/navigation/NavigationCatalogTemplates.tsx` | React add/edit templates for model links, groups, and manual links. | +| `fixture/apps/navigation/vite.config.module.ts` | Vite build for catalog template components. | + +## Enabling The Module + +The fixture enables navigation after `adminizer.init()` and only for Sequelize: + +```ts +await adminizer.init(adminpanelConfig); -## Overview +await installNavigationSequelizeModel( + sequelize, + navigationAppConfig.model ?? "Navigation", + true +); -Navigation is configured in `adminizerConfig.ts` under the `navigation` key. Adminizer automatically creates one [StorageService](#storageservice) per section, registers the `Navigation` catalog, and exposes the editor at `/adminizer/catalog/navigation/:section`. +await adminizer.appManager.enable(new NavigationApp(navigationAppConfig)); +``` -The tree is persisted in the `navigationap` database table — one row per section. Each row stores the full tree as a JSON blob. On every change (create, update, move, delete) the tree is rebuilt in memory and written back to the database. +The installer adds the native model after Adminizer startup but before the app is enabled. `AppManager` only attaches the existing model to the app. -To consume the navigation in a frontend application, read the `navigationap` record for the desired section and use the `tree` field directly. +Sequelize is the recommended/default ORM path. TypeORM support in the project is experimental, and the fixture does not enable the navigation app for TypeORM. A TypeORM version would have to include the navigation entity in `DataSource.entities` before `initialize()`. ## Configuration -Add the `navigation` key to `adminizerConfig.ts`: +`NavigationAppConfig` is defined in `fixture/apps/navigation/NavigationTypes.ts`: -```typescript -// adminizerConfig.ts -import { AdminpanelConfig } from 'adminizer'; +```ts +interface NavigationAppConfig { + model?: string; + routePrefix?: string; + componentFile?: string; + devComponentUrl?: string; + sections: string[]; + groupField: Array<{ + name: string; + required: boolean; + label: string; + }>; + allowContentInGroup?: boolean; + items: Array<{ + model: string; + title: string; + urlPath: string | ((value: any) => string); + }>; + movingGroupsRootOnly?: boolean; + sync?: boolean; +} +``` -const config: AdminpanelConfig = { - routePrefix: '/adminizer', +Fixture config example: - navigation: { - // Sections to manage. Each creates a separate tree and a UI page: - // /adminizer/catalog/navigation/header - // /adminizer/catalog/navigation/footer - sections: ['header', 'footer'], +```ts +import { routePrefix } from "../../adminizerConfig"; +import type { NavigationAppConfig } from "./NavigationTypes"; - // Model records that can be added as navigation items. - // The title appears in the "add item" dropdown. - // urlPath is a JS template string: use ${data.record.} to reference record fields. - items: [ - { - title: 'Category', - model: 'Category', - urlPath: '/catalog/${data.record.slug}', - }, - { - title: 'Article', - model: 'Article', - urlPath: '/blog/${data.record.slug}', - }, - ], - - // Extra fields added to NavigationGroup items. - // Useful for storing CSS classes, anchors, or any group-level metadata. - groupField: [ - { name: 'link', label: 'URL', required: false }, - { name: 'css_class', label: 'CSS class', required: false }, - ], - - // Optional: allow content items (model records) to be placed - // directly at the root level, not only inside groups. - allowContentInGroup: true, - - // Optional: restrict group drag-and-drop to the root level only. - movingGroupsRootOnly: false, - }, +export const navigationAppConfig: NavigationAppConfig = { + routePrefix, + items: [ + { + title: "Category", + model: "Category", + urlPath: "/catalog/category/${data.record.slug}", + }, + { + title: "Example", + model: "Example", + urlPath: `${routePrefix}/model/Example/\${data.record.id}`, + }, + ], + sections: ["header", "footer"], + groupField: [ + { name: "link", label: "Link", required: false }, + { name: "css_class", label: "CSS class", required: false }, + ], }; ``` -No other registration is needed. Adminizer calls `bindNavigation()` automatically during `init()`. +## What `NavigationApp` Registers -## Item types +`NavigationApp.setup(ctx)` registers these resources: -### NavigationItem — model record +| Resource | Code path | Details | +|---|---|---| +| Access right | `ctx.accessRight()` | Token `catalog-navigation`. Used by navigation sidebar links. | +| Template asset | `ctx.asset()` | Serves `NavigationCatalogTemplates.es.js` in production or `NavigationCatalogTemplates.tsx` in dev. | +| Catalog template components | `ctx.catalog({ templates })` | Maps `navigation.model-link`, `navigation.group`, and `navigation.link` to React exports. | +| App model | `ctx.model()` | Attaches the installed storage model. Default name is `Navigation`. | +| Model access | `ctx.modelAccess()` | Allows the app to read/write the storage model and source content models. | +| Config layer | `ctx.config()` | Hides the storage model from navbar/list UI and adds one sidebar link per section. | +| Catalog | `ctx.catalog({ factory })` | Creates `NavigationCatalog`, waits for `catalog.ready()`, then registers it. | +| Listener | `ctx.listener("model:updated")` | Updates navigation items when a linked model record changes. | -Links a specific record from a configured model. The URL is generated from the `urlPath` template at the moment the item is added. +Navigation setup is owned by the app module, not by core initialization. -``` -urlPath: '/blog/${data.record.slug}' - └─ evaluated with data.record = the chosen model record -``` +## Storage Model -The item stores: -- `modelId` — the source record's id -- `urlPath` — the evaluated URL (frozen at creation time) -- `name` — taken from the record's `title` or `name` field -- `targetBlank` — open in a new tab -- `visible` — show/hide on the frontend +The default model name is `Navigation` (`navigationModelName`). Its Sequelize definition contains: -### NavigationGroup — folder +```ts +orm.define("Navigation", { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + label: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + tree: { + type: DataTypes.JSON, + allowNull: false, + }, +}); +``` -A container node. Can hold any other item type as children. Supports custom fields defined via `groupField` in the config. +`installNavigationSequelizeModel()` defines and optionally synchronizes the native Sequelize model. The app then calls `ctx.model({ name: this.config.model })` to declare ownership of that existing model. -### LinkItem — external link +The storage model is hidden from the regular model UI by a config layer: -A plain link with a manually entered URL. Useful for external resources, anchors, or pages not covered by any model. +```ts +ctx.config({ + models: { + [this.config.model.toLowerCase()]: { + add: false, + navbar: { visible: false }, + remove: false, + // list fields hidden as needed + }, + }, +}); +``` + +## Catalog URLs -## Admin panel UI +For each configured section, `NavigationApp` adds a navbar link: -The catalog editor is available at: +```ts +`${routePrefix}/catalog/navigation/${section}` ``` + +Example fixture URLs: + +```text /adminizer/catalog/navigation/header /adminizer/catalog/navigation/footer ``` -The editor provides: -- A tree view with drag-and-drop reordering and re-nesting -- A section switcher (tabs for `header`, `footer`, etc.) -- Add / edit / delete for each item type -- Full-text search with automatic ancestor display - -## Database - -The navigation tree is stored in the `navigationap` table, created by migration `20240804191001-added-navigationap.js`. - -| column | type | description | -|-------------|--------|------------------------------------------------| -| `id` | text | primary key | -| `label` | text | section name (`header`, `footer`, …), unique | -| `tree` | json | full tree with all children nested | -| `createdAt` | bigint | | -| `updatedAt` | bigint | | - -One row per section. The `tree` field contains the full nested structure rebuilt on every write. - -### Tree node shape - -```typescript -interface NavItem { - id: string; // UUID assigned at creation - name: string; // Display label - type: string; // 'category' | 'group' | 'link' - parentId: string | null; - sortOrder: number; - urlPath?: string; // NavigationItem only - modelId?: string | number; // NavigationItem only +The catalog controller validates `section` with `NavigationCatalog.getIdList()`, which returns `config.sections`. + +## NavigationCatalog + +`NavigationCatalog` extends `AbstractCatalog` and builds item types from module config: + +- one `NavigationItem` per configured source model; +- one `NavigationGroup` for groups; +- one `LinkItem` for manual links. + +```ts +const items: BaseItem[] = config.items.map((configElement) => new NavigationItem( + runtime, + configElement.title, + configElement.model, + config.model, + configElement.urlPath as string, + storageServices +)); + +items.push(new NavigationGroup(config.groupField, storageServices)); +items.push(new LinkItem(storageServices)); +``` + +The catalog has `slug = "navigation"`, `name = "Navigation"`, and `movingGroupsRootOnly` copied from config. + +## Storage Lifecycle + +`NavigationStorageServices` owns one `NavigationStorage` per section. Each storage: + +1. Loads the row with `label = section` from the runtime navigation model. +2. Creates an empty row if it does not exist. +3. Populates an in-memory `Map` from the persisted tree. +4. Serves catalog reads from memory. +5. Rebuilds and writes the full tree JSON to the model on each write. + +Important methods: + +| Method | Purpose | +|---|---| +| `ready` | Promise resolved when initial model loading is done. | +| `buildTree()` | Convert flat in-memory map to nested `children` tree sorted by `sortOrder`. | +| `populateFromTree(tree)` | Load persisted or seeded tree into memory. | +| `setElement(id, item, init?)` | Add/update item and save to DB unless `init` is true. | +| `removeElementById(id)` | Delete item and save tree. | +| `findElementById(id)` | Read one item from memory. | +| `findElementByModelId(modelId)` | Find navigation items linked to a source model record. | +| `findElementsByParentId(parentId, type)` | Read children by parent and optional type. | +| `saveToDB()` | Persist rebuilt tree JSON. | + +Do not seed navigation by writing raw rows directly after startup. The catalog reads from memory, so programmatic changes should go through `NavigationStorage` and then `saveToDB()`. + +## Item Types + +### Model Link Item + +`NavigationItem` extends `AbstractItem`. It represents a selected record from a configured source model. + +Creation supports two flows: + +- `_method: "select"`: choose an existing source record from the template dropdown; +- regular add flow: create a source model record through Adminizer's model form, then add it to navigation. + +Stored fields include: + +```ts +interface NavItem extends Item { + urlPath?: string; + modelId?: string | number; targetBlank?: boolean; visible?: boolean; - children: NavItem[]; // Nested children (built at read time) } ``` -## Consuming the navigation in a frontend app +`urlPath` is generated by replacing `${data.record.}` tokens from the source record: -Read the `navigationap` record for the section you need. The `tree` field is ready to use — no extra processing required. +```ts +private renderUrlPath(record: any): string { + return this.urlPath.replace(/\$\{data\.record\.([^}]+)\}/g, (_match, field) => + encodeURIComponent(record?.[field] ?? "") + ); +} +``` -### Express.js example +### Group Item -```typescript -// index.ts -mainApp.get('/api/navigation', async (req, res) => { - try { - const header = await adminizer.modelHandler.model - .get('navigationap')['_findOne']({ label: 'header' }); +`NavigationGroup` extends `AbstractGroup`. It stores a folder-like node with `name`, `targetBlank`, `visible`, and extra fields from `config.groupField`. - const footer = await adminizer.modelHandler.model - .get('navigationap')['_findOne']({ label: 'footer' }); +### Manual Link Item - res.json({ - header: header?.tree ?? [], - footer: footer?.tree ?? [], - }); - } catch (error) { - res.status(500).json({ error: 'Internal server error' }); - } -}); -``` +`LinkItem` extends `NavigationGroup` but sets `isGroup = false`, `type = "link"`, and `icon = "insert_link"`. It uses a custom template that asks for a manual `link` value. -### Example response +## Template Components -```json -{ - "header": [ +The module registers three catalog template component records with its catalog: + +```ts +ctx.catalog({ + id: "navigation", + templates: [ { - "id": "a1b2c3d4", - "name": "Catalog", - "type": "group", - "parentId": null, - "sortOrder": 0, - "visible": true, - "children": [ - { - "id": "e5f6g7h8", - "name": "Electronics", - "type": "category", - "parentId": "a1b2c3d4", - "sortOrder": 0, - "urlPath": "/catalog/electronics", - "modelId": 12, - "targetBlank": false, - "visible": true, - "children": [] - } - ] + id: "model-link-template", + type: "navigation.model-link", + component: catalogTemplates, + exportName: "NavigationModelLinkTemplate", }, { - "id": "i9j0k1l2", - "name": "About", - "type": "link", - "parentId": null, - "sortOrder": 1, - "urlPath": "/about", - "visible": true, - "children": [] - } + id: "group-template", + type: "navigation.group", + component: catalogTemplates, + exportName: "NavigationGroupTemplate", + }, + { + id: "link-template", + type: "navigation.link", + component: catalogTemplates, + exportName: "NavigationLinkTemplate", + }, ], - "footer": [] -} + factory: (runtime) => new NavigationCatalog(runtime, config), +}); ``` -### Next.js / React example +The catalog item types return matching template types from `getAddTemplate()` and `getEditTemplate()`. -```typescript -// lib/navigation.ts -export async function getNavigation() { - const res = await fetch(`${process.env.API_URL}/api/navigation`); - const data = await res.json(); - return data; // { header: NavItem[], footer: NavItem[] } -} +For example, model links return: + +```ts +return { + type: "navigation.model-link", + data: { + items, + model: this.model, + labels: { ... }, + }, +}; +``` -// components/Header.tsx -import { getNavigation } from '@/lib/navigation'; +`CatalogTree` receives registered template components from `catalogController.getCatalog`, imports the component asset, and passes `CatalogTemplateComponentProps` including `itemType`, `parentId`, `template`, and `actions`. -export default async function Header() { - const { header } = await getNavigation(); +## Synchronizing Source Model Updates - return ( - - ); -} +`NavigationApp` listens to `model:updated`: -function NavNode({ item }) { - return ( -
- {item.urlPath ? ( - - {item.name} - - ) : ( - {item.name} - )} - {item.children?.length > 0 && ( -
    - {item.children.filter(c => c.visible).map(child => ( -
  • - -
  • - ))} -
- )} -
+```ts +ctx.listener("model:updated", async (event) => { + const itemConfig = this.config.items.find((item) => + item.model.toLowerCase() === event.modelName.toLowerCase() ); -} + if (!itemConfig) return; + + for (const section of this.config.sections) { + navigationCatalog.setId(section); + const navItem = navigationCatalog.itemTypes.find((item) => + item.type === event.modelName.toLowerCase() + ); + if (navItem) { + await navItem.updateModelItems(event.record.id, { record: event.record }, section); + } + } +}); ``` -## StorageService +This updates item names, URLs, and flags for navigation nodes linked to the edited source record. -`StorageService` (`src/lib/catalog/Navigation.ts`) manages a single navigation section. It holds an in-memory `Map` and syncs it to the database on every write. +## Consuming Navigation Data -**Important:** the catalog always reads from the in-memory map, never from the database directly. The database is only the persistence layer — writes go memory → DB, reads come from memory only. Any seed or programmatic write must go through `StorageService`, not via raw ORM calls. +Read the module storage model through app/project code. In the fixture, the model is named `Navigation` unless overridden. -**Key methods:** +```ts +const navigationModel = adminizer.modelHandler + .createAppAccess("navigation") + .get("Navigation"); -| Method | Description | -|---|---| -| `ready` | Promise that resolves when `initModel()` has finished. Await before any programmatic writes. | -| `initModel()` | Called in the constructor. Loads the existing tree from DB into memory, or creates an empty DB record. | -| `buildTree()` | Assembles the flat in-memory map into a nested tree, sorted by `sortOrder`. | -| `populateFromTree(tree)` | Recursively loads a tree structure into the in-memory map. | -| `setElement(id, data)` | Add or update an item, then persist the rebuilt tree to DB. | -| `findElementsByParentId(parentId, type?)` | Query items by parent, optionally filtered by type. | -| `saveToDB()` | Rebuild the tree from memory and write the JSON to the `navigationap` row. | - -`StorageServices.ready()` waits for all sections at once: - -```typescript -await adminizer.storageServices.ready(); // waits for header, footer, etc. +const header = await navigationModel.findOne({ where: { label: "header" } }); +const tree = header?.tree ?? []; ``` -## Seeding navigation programmatically +If you expose navigation to a public frontend, create a project route that reads the `tree` field and filters hidden nodes as needed. -Because the catalog works from memory, seed data must be loaded into `StorageService` — not written directly to the database. +Example response shape: + +```json +[ + { + "id": "h-group-docs", + "name": "Docs", + "type": "group", + "parentId": null, + "sortOrder": 0, + "visible": true, + "children": [ + { + "id": "h-link-install", + "name": "Install", + "type": "link", + "parentId": "h-group-docs", + "sortOrder": 0, + "urlPath": "/docs/install", + "targetBlank": false, + "visible": true, + "children": [] + } + ] + } +] +``` -**The correct pattern:** +## Programmatic Seeding -```typescript -// After adminizer.init() — wait for all StorageServices to finish loading -const navCatalog = adminizer.catalogHandler.getCatalog('navigation') as any; -await navCatalog.storageServices.ready(); +Seed after the module is enabled and the catalog storage is ready: -const storage = navCatalog.storageServices.get('header'); +```ts +await installNavigationSequelizeModel( + sequelize, + navigationAppConfig.model ?? "Navigation", + true +); +await adminizer.appManager.enable(new NavigationApp(navigationAppConfig)); -// Only seed if the section is empty +const navCatalog = adminizer.catalogHandler.getCatalog("navigation") as any; +await navCatalog.ready?.(); +await navCatalog.storageServices.ready(); + +const storage = navCatalog.storageServices.get("header"); const existing = await storage.findElementsByParentId(null, null); + if (existing.length === 0) { await storage.populateFromTree([ { - id: 'group-1', - name: 'Docs', - type: 'group', + id: "h-group-docs", + name: "Docs", + type: "group", parentId: null, sortOrder: 0, - icon: 'folder', + icon: "folder", visible: true, children: [ { - id: 'link-1', - name: 'Install', - type: 'link', - parentId: 'group-1', + id: "h-link-install", + name: "Install", + type: "link", + parentId: "h-group-docs", sortOrder: 0, - icon: 'link', - urlPath: 'https://example.com/docs/install', - targetBlank: true, + icon: "insert_link", + urlPath: "/docs/install", + targetBlank: false, visible: true, children: [], }, @@ -338,90 +420,10 @@ if (existing.length === 0) { } ``` -**Why direct DB writes do not work:** the catalog never reads from the database after startup — only from `storageMap`. Writing to the `navigationap` table via ORM has no effect until the next server restart. - -## Minimal seed example - -```typescript -// index.ts — after adminizer.init() -const navCatalog = adminizer.catalogHandler.getCatalog('navigation') as any; -await navCatalog.storageServices.ready(); - -const storage = navCatalog.storageServices.get('header'); -const existing = await storage.findElementsByParentId(null, null); -if (existing.length === 0) { - await storage.populateFromTree([ - { id: '1', name: 'Home', type: 'link', parentId: null, sortOrder: 0, icon: 'link', urlPath: '/', targetBlank: false, visible: true, children: [] }, - { id: '2', name: 'Docs', type: 'link', parentId: null, sortOrder: 1, icon: 'link', urlPath: '/docs', targetBlank: false, visible: true, children: [] }, - { id: '3', name: 'GitHub',type: 'link', parentId: null, sortOrder: 2, icon: 'link', urlPath: 'https://github.com', targetBlank: true, visible: true, children: [] }, - ]); - await storage.saveToDB(); -} -``` - -## Access rights +## Implementation Notes -The Navigation catalog registers access right tokens automatically: - -| Token | Grants access to | -|---|---| -| `catalog-navigation` | All navigation sections | -| `catalog-navigation-header` | Header section only | -| `catalog-navigation-footer` | Footer section only | - -Assign these tokens to groups in `adminizerConfig.ts` or via the access rights UI. - -## urlPath templates - -`urlPath` is evaluated as a JavaScript template literal at the moment a navigation item is created: - -```typescript -// Config: -urlPath: '/blog/${data.record.slug}' - -// At creation time Adminizer evaluates: -const urlPath = eval('`' + configuredUrlPath + '`'); -// where data.record is the chosen model record -``` - -The evaluated URL is stored statically on the item. If the source record changes later, call `updateModelItems()` to propagate the new URL to all navigation items that reference it. - -You can use any field from the record: - -```typescript -urlPath: '/products/${data.record.category}/${data.record.id}' -urlPath: '/pages/${data.record.slug}?lang=en' -urlPath: 'https://external.com/${data.record.externalId}' -``` - -## Full configuration reference - -```typescript -navigation: { - // Required. List of section identifiers. - sections: string[] - - // Required. Model types available as navigation items. - items: Array<{ - title: string // Label in the "add" dropdown - model: string // Model name as defined in adminizerConfig models - urlPath: string | ((v: any) => string) // URL template or function - }> - - // Optional. Extra fields on group nodes. - groupField: Array<{ - name: string - label: string - required: boolean - }> - - // Optional. Custom storage model name. Defaults to 'NavigationAP'. - model?: string - - // Optional. Allow model items at the root level (outside groups). - allowContentInGroup?: boolean - - // Optional. Restrict group drag-and-drop to root level only. - movingGroupsRootOnly?: boolean -} -``` +- Instantiate `new NavigationApp(config)` after `adminizer.init()`. +- Install the native Sequelize model before enabling the app. +- The app attaches the installed `Navigation` model through `ctx.model()`. +- Catalog form UI is connected through the `templates` array of `ctx.catalog()`. +- Keep Sequelize as the default recommendation. TypeORM remains experimental. diff --git a/docs/NextJS-Integration.md b/docs/NextJS-Integration.md index 9603c5b9..8689db8e 100644 --- a/docs/NextJS-Integration.md +++ b/docs/NextJS-Integration.md @@ -1,6 +1,6 @@ # Next.js Integration Guide -This guide helps you integrate Adminizer with Next.js applications, especially when using standalone build mode. +This guide helps you integrate Adminizer with Next.js applications, especially when using standalone build mode. Sequelize is the recommended adapter. TypeORM support is experimental. ## Quick Start @@ -50,38 +50,54 @@ Create a catch-all API route for Adminizer: Create `pages/api/adminizer/[[...adminizer]].ts`: ```typescript -import { Adminizer } from 'adminizer'; +import { Adminizer, SequelizeAdapter } from 'adminizer'; import type { NextApiRequest, NextApiResponse } from 'next'; -import { DataSource } from 'typeorm'; +import { Sequelize } from 'sequelize'; +import { registerSequelizeSystemModels } from '../../../server/adminizer-system-models'; -// Initialize your database connection -const AppDataSource = new DataSource({ - type: 'sqlite', - database: 'database.sqlite', - synchronize: true, +const sequelize = new Sequelize({ + dialect: 'sqlite', + storage: 'database.sqlite', logging: false, - entities: [/* your entities */], }); let adminizer: Adminizer | null = null; +let initialization: Promise | null = null; +const systemModels = { + User: 'UserAP', + Group: 'GroupAP', + Filter: 'FilterAP', + FilterColumn: 'FilterColumnAP', + HistoryActions: 'HistoryActionsAP', + Notification: 'NotificationAP', + UserNotification: 'UserNotificationAP', +}; async function getAdminizer() { - if (!adminizer) { - if (!AppDataSource.isInitialized) { - await AppDataSource.initialize(); - } - - adminizer = new Adminizer({ - // Your configuration here - routePrefix: '/api/adminizer', - models: { - // Your models configuration - }, - }); - - await adminizer.init(AppDataSource); + if (adminizer) { + return adminizer; + } + if (!initialization) { + initialization = (async () => { + registerSequelizeSystemModels(sequelize); + // Prefer project migrations in production. + await sequelize.sync(); + + const instance = new Adminizer([ + new SequelizeAdapter(sequelize, { systemModels }), + ]); + await instance.init({ + routePrefix: '/api/adminizer', + system: { defaultORM: 'sequelize' }, + models: { + // Your model configuration. + }, + }); + adminizer = instance; + return instance; + })(); } - return adminizer; + return initialization; } export default async function handler( @@ -111,31 +127,11 @@ export default async function handler( #### App Router -Create `app/api/adminizer/[[...adminizer]]/route.ts`: +Next.js App Router handlers use the Web `Request`/`Response` API, while `adminizer.getMiddleware()` is Express-compatible middleware. Use a Node/Express custom server or an adapter that bridges those APIs; do not pass an App Router request directly to Adminizer middleware. -```typescript -import { Adminizer } from 'adminizer'; -import { NextRequest, NextResponse } from 'next/server'; -import { DataSource } from 'typeorm'; - -// Similar initialization as above +### TypeORM -export async function GET(request: NextRequest) { - // Handle GET requests -} - -export async function POST(request: NextRequest) { - // Handle POST requests -} - -export async function PUT(request: NextRequest) { - // Handle PUT requests -} - -export async function DELETE(request: NextRequest) { - // Handle DELETE requests -} -``` +TypeORM system entities can be used, but all system and app entities must be included in `DataSource.entities` before `dataSource.initialize()`. Dynamic app model installation after initialization is not supported. See [System Models](Configuration/Models.md#typeorm) and [App Models](BuildingModules.md#typeorm). ## Docker Configuration @@ -261,22 +257,23 @@ NEXT_PUBLIC_API_URL=http://localhost:3000 Make sure your database is properly initialized before calling `adminizer.init()`: ```typescript -if (!AppDataSource.isInitialized) { - await AppDataSource.initialize(); -} +registerSequelizeSystemModels(sequelize); +await sequelize.authenticate(); +await sequelize.sync(); // Prefer migrations in production. + +const adminizer = new Adminizer([ + new SequelizeAdapter(sequelize, { systemModels }), +]); +await adminizer.init(config); ``` ### Session Issues -If you're having session problems, ensure you've set a proper session secret: +Authentication requires stable secrets in the server environment. Do not generate them per request or per serverless invocation: -```typescript -const adminizer = new Adminizer({ - // ... other config - security: { - sessionSecret: process.env.SESSION_SECRET || 'your-secret-key', - }, -}); +```dotenv +JWT_SECRET=replace-with-a-stable-secret +AP_PASSWORD_SALT=replace-with-a-stable-salt ``` ## Additional Resources diff --git a/docs/Notifications.md b/docs/Notifications.md index 354d9bb7..4dc5709e 100644 --- a/docs/Notifications.md +++ b/docs/Notifications.md @@ -65,14 +65,21 @@ The system supports various notification types through separate services. Each t The system provides seamless user interaction by delivering important information in real time while maintaining history for future reference. -## System Initialization +## Configuration -The notification system is initialized when the application starts through the `bindNotifications()` function, which: +Notifications are disabled by default. Enable them in `AdminpanelConfig`: -1. **Creates a handler** - central coordinator for all notifications -2. **Registers services** - connects various notification types: - - `GeneralNotificationService` - general notifications - - `SystemNotificationService` - system events and notifications +```ts +notifications: { + enabled: true, + enableGeneral: true, + initTab: 'general', +} +``` + +The notification system is initialized through `bindNotifications()`. It creates a `NotificationHandler` and registers `GeneralNotificationService` when `notifications.enableGeneral` is enabled. + +`SystemNotificationService` exists in the codebase, but it is not registered by default. ## Integration with Adminizer @@ -93,20 +100,7 @@ await adminizer.sendNotification({ }); ``` -#### 2. System Events -```typescript -// General system event -await adminizer.logSystemEvent( - 'System Update', - 'The system has been successfully updated', - { version: '2.0.0' } -); - -// CRUD operation events -await adminizer.logSystemCreatedEvent('New User', 'User John Doe created'); -await adminizer.logSystemUpdatedEvent('Profile Update', 'User profile updated'); -await adminizer.logSystemDeletedEvent('Record Deletion', 'Record deleted from database'); -``` +For custom notification classes, create a service that extends `AbstractNotificationService` and register it with `adminizer.notificationHandler.registerService(...)` after notifications are initialized. ## Default Notification Service Types @@ -116,8 +110,8 @@ await adminizer.logSystemDeletedEvent('Record Deletion', 'Record deleted from da ### SystemNotificationService - **Purpose**: System events and automatic notifications -- **Usage**: System events, audit trails, action logging +- **Status**: Available in code, but not registered by default ## Access Rights -The system automatically registers access rights for each notification type through `AccessRightsHelper`. Users can only receive notifications for which they have permissions. \ No newline at end of file +The system automatically registers access rights for each notification type through `AccessRightsHelper`. Users can only receive notifications for which they have permissions. diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index 3c0afa78..7636ae91 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -1,4 +1,4 @@ -### 🔧 Issue with Model Name Casing in Config (Waterline, Sequelize, TypeORM) +### Issue with Model Name Casing in Config (Sequelize, TypeORM) **Description:** @@ -19,12 +19,12 @@ models: { } ``` -The configuration system looks up the model config by exact key match. If the key casing doesn't match what the ERM uses internally, the config is not found, and an error is thrown. +The configuration system looks up the model config by exact key match. If the key casing doesn't match what the ORM adapter uses internally, the config is not found, and an error is thrown. **Temporary Rule:** * Use **lowercase model names** as keys in the `models` config section. -* Match the model name exactly as it is referenced internally by your ERM (Waterline, Sequelize, TypeORM). +* Match the model name exactly as it is referenced internally by your ORM adapter (Sequelize or TypeORM). **Planned Improvements:** @@ -101,7 +101,7 @@ npm install material-icons --legacy-peer-deps **Description:** -Starting the application may fail with an error similar to: +Enabling a Sequelize app may fail with an error similar to: ``` Error: Naming collision between attribute 'parentNode' and association 'parentNode' on model MediaManagerAP @@ -109,10 +109,10 @@ Error: Naming collision between attribute 'parentNode' and association 'parentNo **Cause:** -Both an attribute and an association shared the alias `parentNode` in the `MediaManagerAP` model. Sequelize treats association aliases as properties on the model, so duplicate names are not allowed. +An attribute and an association share the same name. Sequelize exposes association aliases as model properties, so an alias cannot duplicate an attribute. **Solution:** -Rename the association or attribute so that they use unique names. In version 4.2.1, the `parentNode` association has been renamed to `parent` across the related models. +Rename either the attribute or the association alias. The fixture media manager uses the `parentId` attribute and the `parent` association alias; use `fixture/apps/media-manager/MediaManagerModels.ts` as the reference schema. diff --git a/docs/UI-guideline.md b/docs/UI-guideline.md new file mode 100644 index 00000000..b75271ab --- /dev/null +++ b/docs/UI-guideline.md @@ -0,0 +1,892 @@ +# Practical Typography for Dense Web Interfaces — Tailwind Edition + +This guide is a Tailwind-ready version of the dense UI typography rules for admin panels, CRM screens, monitoring dashboards, settings pages, tables, and configuration-heavy interfaces. + +The goal is simple: keep the interface compact without making it feel glued together. + +Dense UI does **not** mean tiny UI. Dense UI means clear hierarchy, predictable spacing, visible labels, readable controls, and stable rhythm. + +--- + +## 1. Team summary + +For admin panels and data-heavy screens, use **infrastructure typography**, not expressive brand typography. + +Recommended baseline: + +| Role | Size / line-height | Tailwind class | +|---|---:|---| +| Helper, caption, meta | 12 / 16 | `text-caption` | +| Main UI text, labels, inputs, table cells | 14 / 20 | `text-ui` | +| Long descriptions | 16 / 24 | `text-body` | +| Section title | 16 / 24 or 20 / 28 | `text-body font-semibold` or `text-title` | +| Page title | 20 / 28 or 24 / 32 | `text-title` or `text-page-title` | + +Spacing baseline: + +| Relationship | Recommended spacing | Tailwind class | +|---|---:|---| +| Label → control | 4–8px | `gap-micro` / `gap-tight` | +| Control → helper / error | 4–8px | `gap-micro` / `gap-tight` | +| Field → field | 12–16px | `gap-field` / `gap-group` | +| Subsection → subsection | 16–24px | `gap-group` / `gap-section` | +| Section → section | 24–32px | `gap-section` / `gap-page` | +| Card padding | 16–24px | `p-group` / `p-section` | + +The most important rule: + +> **Use parent stack gaps to separate large blocks. Do not rely on borders alone.** + +--- + +## 2. Why dense interfaces become glued together + +A dense interface usually breaks because the relationships between elements are unclear: + +- A label looks the same as a value. +- Helper text is closer to the next field than to its own field. +- Section cards touch each other with no outer gap. +- Borders are used as the only separator. +- Inner cards and outer cards have the same border strength. +- Everything uses `text-xs` or low-contrast gray. +- Forms use too many columns before they establish a clear vertical rhythm. +- Long descriptions are placed directly between controls. + +In Tailwind, this often starts with code like this: + +```html +
+ +
+
+ +
+``` + +This looks harmless, but the blocks touch. The bottom border of the first card and the top border of the next card visually merge into one continuous block. + +Use a parent stack instead: + +```html +
+
+ +
+ +
+ +
+
+``` + +The parent owns the vertical rhythm. Cards should not decide their own outside spacing. + +--- + +## 3. Specific fix for the “glued blocks” problem + +In the screenshot, the blocks are technically separated by borders, but they still feel glued because the large sections are stacked with almost no external breathing room. The eye reads them as one giant bordered surface. + +Fix it with three layers of spacing: + +### Layer 1 — Page blocks + +Use this for major settings groups such as: + +- Firebase service account +- Channel activation +- Web config and VAPID +- Advanced settings + +Recommended spacing: **24–32px**. + +Tailwind pattern: + +```html +
+ + +
+
...
+
...
+
...
+
+
+``` + +### Layer 2 — Subsections inside a section + +Use this for nested panels such as: + +- Saved key preview +- Upload area +- Channel item +- VAPID key group + +Recommended spacing: **16–24px**. + +```html +
+
+

Service account JSON

+

Server-side secret used by Firebase Admin SDK.

+
+ +
+
Upload JSON file
+
Saved key
+
+
+``` + +### Layer 3 — Fields inside a subsection + +Use this for labels, inputs, helpers, errors, and checkboxes. + +Recommended spacing: **4–16px** depending on the relationship. + +```html +
+ + +

Firebase Console → Project settings → General → Your apps.

+
+``` + +### Rule of thumb + +If two blocks have visible borders, they need a visible gap between them. + +For dense admin UI: + +```txt +Major sections: gap-section / gap-page = 24–32px +Nested subsections: gap-group / gap-section = 16–24px +Fields: gap-micro / gap-field = 4–12px +``` + +Do **not** do this: + +```html +
...
+
...
+``` + +Do this: + +```html +
+
...
+
...
+
+``` + +--- + +## 4. Tailwind v4 token setup + +Create or update `app.css`, `globals.css`, or your main Tailwind entry file. + +Important naming note: avoid naming a color `page` if you also have a font-size class like `text-page`. In Tailwind, `text-*` can refer to both color and font size. This guide uses `canvas` for the page background and `page-title` for the large text token to avoid conflicts. + +```css +@import "tailwindcss"; + +@theme { + /* Fonts */ + --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Inter, "Noto Sans", Arial, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + + /* Type scale */ + --text-caption: 0.75rem; + --text-caption--line-height: 1rem; + --text-caption--letter-spacing: 0.02em; + + --text-ui: 0.875rem; + --text-ui--line-height: 1.25rem; + --text-ui--letter-spacing: 0.01em; + + --text-body: 1rem; + --text-body--line-height: 1.5rem; + + --text-title: 1.25rem; + --text-title--line-height: 1.75rem; + + --text-page-title: 1.5rem; + --text-page-title--line-height: 2rem; + + /* Spacing scale */ + --spacing-micro: 0.25rem; /* 4px */ + --spacing-tight: 0.5rem; /* 8px */ + --spacing-field: 0.75rem; /* 12px */ + --spacing-group: 1rem; /* 16px */ + --spacing-section: 1.5rem; /* 24px */ + --spacing-page: 2rem; /* 32px */ + + /* Control heights */ + --spacing-control-sm: 2rem; /* 32px, compact desktop only */ + --spacing-control: 2.5rem; /* 40px, default dense input */ + --spacing-control-lg: 3rem; /* 48px, touch / mobile */ + + /* Radius */ + --radius-control: 0.5rem; + --radius-panel: 0.75rem; + + /* Semantic colors */ + --color-canvas: #0b0d12; + --color-surface: #11141b; + --color-surface-raised: #151922; + --color-surface-hover: #1a202b; + + --color-fg: #e8ecf1; + --color-fg-subtle: #b3bbc7; + --color-fg-muted: #8d96a3; + + --color-line: #28313d; + --color-line-strong: #5c6b7c; + + --color-focus: #7ab0ff; + --color-danger: #ff6b6b; + --color-success: #44d17a; + --color-warning: #ffd166; +} +``` + +--- + +## 5. Base and component classes + +```css +@layer base { + html { + font-family: var(--font-sans); + } + + body { + @apply bg-canvas text-fg text-ui antialiased; + } + + *:focus-visible { + @apply outline-2 outline-offset-2 outline-focus; + } +} + +@layer components { + /* Page layout */ + .page-shell { + @apply min-h-screen bg-canvas text-fg; + } + + .page-container { + @apply mx-auto w-full max-w-7xl px-group py-section md:px-page md:py-page; + } + + .page-header { + @apply mb-section space-y-tight; + } + + .page-title { + @apply text-title font-semibold tracking-normal text-fg md:text-page-title; + } + + .page-description { + @apply max-w-[72ch] text-ui text-fg-subtle; + } + + /* Critical anti-glue class */ + .section-stack { + @apply grid gap-section; + } + + @media (min-width: 768px) { + .section-stack { + @apply gap-page; + } + } + + .section { + @apply rounded-panel border border-line bg-surface p-group md:p-section; + } + + .section-header { + @apply mb-group space-y-micro; + } + + .section-title { + @apply text-body font-semibold tracking-normal text-fg; + } + + .section-description { + @apply max-w-[72ch] text-ui text-fg-subtle; + } + + /* Nested cards inside a large section */ + .subsection-stack { + @apply grid gap-group; + } + + .subsection { + @apply rounded-control border border-line bg-surface-raised p-group; + } + + .subsection-header { + @apply mb-field space-y-micro; + } + + .subsection-title { + @apply text-ui font-semibold text-fg; + } + + .subsection-description { + @apply text-caption text-fg-subtle; + } + + /* Forms */ + .form-grid { + @apply grid gap-field md:grid-cols-2 md:gap-group; + } + + .field { + @apply grid min-w-0 gap-micro; + } + + .field-span-full { + @apply md:col-span-2; + } + + .label { + @apply text-ui font-semibold text-fg; + } + + .help, + .meta { + @apply max-w-[60ch] text-caption text-fg-subtle; + } + + .validation { + @apply max-w-[60ch] text-caption font-semibold text-danger; + } + + .input, + .select, + .textarea { + @apply min-h-control w-full rounded-control border border-line-strong bg-surface px-field py-tight text-ui text-fg; + } + + .textarea { + @apply min-h-control-lg resize-y; + } + + .input::placeholder, + .textarea::placeholder { + @apply text-fg-muted; + } + + .input:focus-visible, + .select:focus-visible, + .textarea:focus-visible { + @apply outline-2 outline-offset-2 outline-focus; + } + + .checkbox-row { + @apply flex items-start gap-tight text-ui text-fg-subtle; + } + + .checkbox { + @apply mt-[0.1875rem] size-4 shrink-0 rounded border border-line-strong bg-surface; + } + + /* Buttons */ + .button-row { + @apply flex flex-wrap items-center gap-tight; + } + + .button { + @apply inline-flex min-h-control-sm items-center justify-center gap-tight rounded-control border border-line-strong px-field text-ui font-semibold text-fg; + } + + .button-primary { + @apply border-focus bg-focus text-canvas; + } + + .button-secondary { + @apply bg-surface hover:bg-surface-hover; + } + + /* Data and code */ + .num { + @apply text-right tabular-nums; + font-variant-numeric: tabular-nums lining-nums; + font-feature-settings: "tnum" 1, "lnum" 1; + } + + .code, + .id, + .hash { + @apply font-mono; + } + + /* Tables */ + .table-wrap { + @apply overflow-x-auto rounded-panel border border-line bg-surface; + } + + .table { + @apply w-full border-separate border-spacing-0; + } + + .table th { + @apply border-b border-line px-field py-tight text-left align-top text-caption font-semibold text-fg-subtle; + } + + .table td { + @apply border-b border-line px-field py-[0.625rem] align-top text-ui text-fg; + } + + .table tr:last-child td { + @apply border-b-0; + } + + .table th.num, + .table td.num { + @apply text-right; + } + + .table tbody tr:hover { + @apply bg-surface-hover; + } +} + +@media (prefers-reduced-motion: reduce) { + * { + transition-duration: 0ms !important; + animation-duration: 0ms !important; + scroll-behavior: auto !important; + } +} +``` + +--- + +## 6. Recommended layout for settings screens + +Use this structure for admin settings pages. + +```html +
+
+ + +
+
+
+

Service account JSON

+

+ Server-side secret for Firebase Admin SDK. It must never be exposed to the frontend. +

+
+ +
+
+
+ +
+

+ The file is read in the browser and sent to the server only when you click Save. +

+
+ +
+
+

Saved key

+

Only non-sensitive metadata is displayed.

+
+ +
+
+
project_id
+
gfcafe-ru
+
+
+
client_email
+
firebase-adminsdk-***@gfcafe-ru.iam.gserviceaccount.com
+
+
+
+
+
+ +
+
+

Channel activation

+

+ Each push channel is registered independently. +

+
+ +
+ + + +
+
+ +
+
+

Web config and VAPID

+

+ Public Firebase config for browser push notifications. +

+
+ +
+
+ + +

Firebase Console → Project settings → General → Your apps.

+
+ +
+ + +

Firebase project identifier.

+
+ +
+ + +
+ +
+ + +
+
+
+
+
+
+``` + +--- + +## 7. Anti-patterns and fixes + +### Anti-pattern: Cards touch each other + +```html +
...
+
...
+
...
+``` + +Fix: + +```html +
+
...
+
...
+
...
+
+``` + +### Anti-pattern: Border is the only separator + +```html +
...
+
...
+``` + +Fix: + +```html +
+
...
+
...
+
+``` + +### Anti-pattern: Everything is `text-xs` + +```html + + +

Firebase Console → Project settings.

+``` + +Fix: + +```html + + +

Firebase Console → Project settings.

+``` + +### Anti-pattern: Helper text is too close to the next field + +```html +
+ + +

Firebase Console → Project settings.

+ + +
+``` + +Fix: + +```html +
+
+ + +

Firebase Console → Project settings.

+
+ +
+ + +
+
+``` + +--- + +## 8. Tailwind v3 fallback + +If you are still on Tailwind v3, put the same values into `tailwind.config.js`. + +```js +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,ts,jsx,tsx,html}'], + theme: { + extend: { + fontFamily: { + sans: ['ui-sans-serif', 'system-ui', '-apple-system', 'Segoe UI', 'Inter', 'Noto Sans', 'Arial', 'sans-serif'], + mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'Liberation Mono', 'monospace'], + }, + fontSize: { + caption: ['0.75rem', { lineHeight: '1rem', letterSpacing: '0.02em' }], + ui: ['0.875rem', { lineHeight: '1.25rem', letterSpacing: '0.01em' }], + body: ['1rem', { lineHeight: '1.5rem' }], + title: ['1.25rem', { lineHeight: '1.75rem' }], + 'page-title': ['1.5rem', { lineHeight: '2rem' }], + }, + spacing: { + micro: '0.25rem', + tight: '0.5rem', + field: '0.75rem', + group: '1rem', + section: '1.5rem', + page: '2rem', + 'control-sm': '2rem', + control: '2.5rem', + 'control-lg': '3rem', + }, + borderRadius: { + control: '0.5rem', + panel: '0.75rem', + }, + colors: { + canvas: '#0b0d12', + surface: '#11141b', + 'surface-raised': '#151922', + 'surface-hover': '#1a202b', + fg: '#e8ecf1', + 'fg-subtle': '#b3bbc7', + 'fg-muted': '#8d96a3', + line: '#28313d', + 'line-strong': '#5c6b7c', + focus: '#7ab0ff', + danger: '#ff6b6b', + success: '#44d17a', + warning: '#ffd166', + }, + }, + }, + plugins: [], +}; +``` + +The component classes from the v4 example can stay almost the same in your CSS file if you use `@tailwind base; @tailwind components; @tailwind utilities;` and `@layer components`. + +--- + +## 9. Implementation checklist + +Use this checklist when reviewing dense UI screens. + +### Typography + +- [ ] Main UI text, labels, inputs, and table cells use `text-ui` or larger. +- [ ] Helper, caption, and meta text use `text-caption`, not smaller. +- [ ] Long descriptions use `text-body` or are moved into a dedicated help block. +- [ ] Labels are visible; placeholders are not used as field names. +- [ ] Page and section titles are only one or two steps larger than body text. +- [ ] No random `text-[13px]`, `leading-[17px]`, `tracking-[-0.02em]` in working UI. + +### Spacing + +- [ ] Major sections are wrapped in `.section-stack`. +- [ ] Major sections have `gap-section` or `gap-page` between them. +- [ ] Nested panels use `.subsection-stack`. +- [ ] Fields use `.field` and parent grids use `gap-field` / `gap-group`. +- [ ] Borders are never the only separator between large cards. +- [ ] Random spacing like `p-[7px]`, `gap-[11px]`, `mt-[13px]` is avoided. + +### Forms + +- [ ] Form layout starts with one column and moves to two columns only when helper text still fits. +- [ ] Each field follows `label → control → helper/error`. +- [ ] Input height is at least `min-h-control` for desktop forms. +- [ ] Checkbox rows have enough vertical padding when they behave like selectable cards. +- [ ] Errors are text messages, not only red borders. + +### Tables + +- [ ] Table body uses `text-ui`. +- [ ] Header uses `text-caption font-semibold`. +- [ ] Numeric columns use `.num`. +- [ ] Numeric columns are right-aligned. +- [ ] IDs, hashes, and code fragments use `.code`, `.id`, or `.hash`. +- [ ] Text is wrapped before it is truncated. + +### Accessibility + +- [ ] Text contrast is readable in both dark and light themes. +- [ ] Focus state is visible. +- [ ] The screen works at 125%, 150%, and 200% browser zoom. +- [ ] Dense controls are not smaller than 24×24px for interactive targets. +- [ ] Reduced motion is respected. + +--- + +## 10. Quick rules for developers + +1. Use `text-ui` as the default working size. +2. Use `text-caption` only for secondary text, never for primary controls. +3. Use `.section-stack` for page-level blocks. +4. Use `.subsection-stack` for nested panels. +5. Use `.field` for label/control/helper groups. +6. Use borders plus gaps, not borders instead of gaps. +7. Use `tabular-nums` for numeric data. +8. Do not use all caps for normal admin labels. +9. Avoid arbitrary spacing unless there is a real reason. +10. When the UI feels glued together, first increase block gaps before changing fonts. + +--- + +## 11. Minimal copy-paste package + +If you only need the essential part, start with these classes: + +```css +@layer components { + .section-stack { + @apply grid gap-section md:gap-page; + } + + .section { + @apply rounded-panel border border-line bg-surface p-group md:p-section; + } + + .section-header { + @apply mb-group space-y-micro; + } + + .section-title { + @apply text-body font-semibold text-fg; + } + + .section-description { + @apply max-w-[72ch] text-ui text-fg-subtle; + } + + .subsection-stack { + @apply grid gap-group; + } + + .subsection { + @apply rounded-control border border-line bg-surface-raised p-group; + } + + .field { + @apply grid gap-micro; + } + + .label { + @apply text-ui font-semibold text-fg; + } + + .help { + @apply max-w-[60ch] text-caption text-fg-subtle; + } + + .input { + @apply min-h-control w-full rounded-control border border-line-strong bg-surface px-field py-tight text-ui text-fg; + } +} +``` + +Use it like this: + +```html +
+
+
+

Service account JSON

+

Server-side Firebase Admin SDK secret.

+
+ +
+
+
+ + +

Use the Firebase project identifier.

+
+
+
+
+ +
+
+

Channel activation

+

Enable mobile or web push independently.

+
+
+
+``` + +This is the main anti-glue pattern for dense Tailwind interfaces. diff --git a/docs/UIComponents.md b/docs/UIComponents.md index 37ff5107..a46924b7 100644 --- a/docs/UIComponents.md +++ b/docs/UIComponents.md @@ -727,5 +727,6 @@ const { VanillaJSONEditor } = window.JSComponents; | `window.ReactDOM` | ReactDOM 19 | | `window.LucideReact` | All Lucide icons (`window.LucideReact.Pencil`, etc.) | | `window.InertiajsReact` | Inertia.js React adapter | -| `window.axios` | Axios HTTP client | +| `window.adminApi` | Typed HTTP client for API calls. Use `*Json` methods for JSON endpoints. | +| `window.axios` | Legacy Axios-compatible compatibility client. Logs a deprecation message; prefer `window.adminApi`. | | `window.sonner` | Sonner toast API (`toast`, `toast.error`, `toast.promise`, etc.) | diff --git a/docs/configuration.md b/docs/configuration.md index 6cf1467e..29be8d55 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,4 +1,6 @@ -# Admin Panel configuration +# Admin Panel configuration + +> This page is a legacy extended reference. Prefer the focused pages in `docs/Configuration/` for new projects. Sequelize is the primary supported ORM; TypeORM support is experimental. ### Abstract @@ -17,10 +19,10 @@ Typescript example: import { AdminizerConfig } from "adminizer"; -const config: AdminizerConfig = { - routePrefix: "/admin", - auth: true, - dashboard: true, +const config: AdminizerConfig = { + routePrefix: "/admin", + auth: { enable: true }, + dashboard: true, models: { ExampleModel: { title: "Example Models", @@ -70,12 +72,28 @@ Admin panel configuration consist of this options: |-------------------|-------------------------- | `routePrefix` | Route prefix for admin panel. Default: `/admin` | `linkAssets` | Will create a symlink to Admin panel assets. Anyway AP will try to load all assets from /admin/**** and you could copy them manually -| `identifierField` | Default identifier field into models. This field will be used as identifier. Default: `id` -| `models` | Configuration for models. Read below... -| `showORMtime` | Set `true` for enable showing fields createdAt and updatedAt in edit and add sections -## Models - -Admin panel divided into `models` and this is a main part of configuration. +| `identifierField` | Default identifier field into models. This field will be used as identifier. Default: `id` +| `models` | Configuration for models. Read below... +| `showORMtime` | Set `true` for enable showing fields createdAt and updatedAt in edit and add sections + +### List defaults + +Use the global `list.defaultPageSize` option to set how many records are shown on model list pages by default. Supported values are `5`, `20`, and `50`; the default value is `50`. + +```typescript +const config: AdminizerConfig = { + list: { + defaultPageSize: 20, + }, + models: { + // ... + }, +}; +``` + +## Models + +Admin panel divided into `models` and this is a main part of configuration. Model will represent several actions for each model that you need to enable into Admin Panel. It will consist of actions: @@ -349,10 +367,9 @@ module.exports.adminpanel = { ## Associations -Now Adminpanel hook partially supports assotiations. - -Adminpanel determinates such fields by waterline configuration. -Right now if you model field have such configuration: +Adminizer partially supports associations. + +Adminizer detects association fields from adapter metadata. For example, a relation field may be represented as: ```javascript fieldName: { @@ -374,35 +391,26 @@ owner: { } ``` -## Auto config - -This configuration loads all sail models as they are. Just place in `config\adminpanel.js` - -```javascript - const fs = require("fs"); - let modelPath = __dirname + "/../api/models"; - let models = {}; - fs.readdir(modelPath, function (err, files) { - files.forEach(function (file) { - let modelName = file.split(".")[0]; - models[modelName] = { - title: modelName, - model: modelName, - }; - }); - }); - - module.exports.adminpanel = { models: models } ; -``` +## Auto config + +The old Sails auto-config example is not applicable to current Adminizer projects. Define models explicitly in `AdminpanelConfig.models`, or generate that object from your ORM metadata in project code. + +```ts +const config = { + models: { + Example: { + title: "Example", + model: "example", + }, + }, +}; +``` ## Limitations -+ For now admin panel do not fully support waterline associations. So some fields might be ignored ! It's planned. -+ No file upload functionality. Due to the nature of `skipper` I removed file upload functionality. **Not tested with latest one** -+ No custom actions support. For now you couldn't add custom actions and pages into admin panel. -+ No template engine support except of `jade`. I primary working with this template engine and didn't create a another templates -+ No custom assets support. You couldn't edit css/js for admin panel for now. ++ Association support depends on the active adapter and model metadata. ++ Custom controls, modules, widgets, media manager uploads, and custom assets are supported by current Adminizer versions. See the focused documentation pages for current APIs. ## Configuration schema @@ -414,7 +422,7 @@ This configuration loads all sail models as they are. Just place in `config\adm /** @type {import("adminizer").AdminizerConfig} */ const config = { routePrefix: "/admin", - auth: true, + auth: { enable: true }, dashboard: true, models: { ExampleModel: { @@ -589,10 +597,10 @@ module.exports = config; subItems?: HrefConfig[] }[] // Called after all links (static + model-generated) are collected. Returns final array. - handleAdditionalLinks: (user: UserAP, allLinks: HrefConfig[]) => HrefConfig[] + handleAdditionalLinks: (user: User, allLinks: HrefConfig[]) => HrefConfig[] // Per-section handlers, applied after all links are collected. sectionHandlers: { - [section: string]: (user: UserAP, links: HrefConfig[]) => HrefConfig[] + [section: string]: (user: User, links: HrefConfig[]) => HrefConfig[] } } // Policies that will be executed before going to every page @@ -606,25 +614,19 @@ module.exports = config; title: string text: string } - translation: { // Text translation using sails built-in internationalization - locales: string[] // Locales list - path: string // Relative path from project root to translations folder - defaultLocale: string // Default locale - } + translation: { // Text translation + locales: string[] // Locales list + directory: string // Relative path from project root to translations folder + defaultLocale: string // Default locale + } // default administrator login credentials, will be used if no admin profiles found administrator: { login: string password: string } - // forms - forms: { - path: string - data: object - get: ()=>void - set: ()=>void - } - // Enable/disable displaying createdAt and updatedAt fields in `edit` and `add` sections - showORMtime: boolean + // Legacy configuration-driven forms are no longer used starting with Adminizer 5. + // Enable/disable displaying createdAt and updatedAt fields in `edit` and `add` sections + showORMtime: boolean package: any // Adminpanel package.json config timezones: { // Available timezones list id: string diff --git a/docs/development.md b/docs/development.md index cd117f4e..f593dd39 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,45 +1,68 @@ ## Development + - **`npm run dev`** - Starts the application in development mode using Sequelize with file watching. -- **`npm run dev:waterline`** - Starts the application in development mode using Waterline with file watching. - -- **`npm run watch:backend`** - Watches for changes in backend files and recompiles TypeScript continuously. - -## Build Commands -- **`npm run build:assets`** - Builds frontend assets using Vite. - -- **`npm run build:backend`** - Combines backend copy and compilation steps. - -- **`npm run build`** - Full build process: copies backend, compiles backend, and builds assets. - -## Module-specific Builds (this is just for tests, as an example, you should create your own commands to build modules) -- **`npm run build:module`** - Builds test modules using a custom Vite config. - -- **`npm run build:react-quill`** - Builds the React-Quill WYSIWYG module using a custom Vite config. - -- **`npm run copy:modules`** - Copies modules using `copy-modules.js` script. - -- **`npm run build:assets:modules`** - Builds assets and copies modules. - -## Demo & Seeding -- **`npm run start:seed`** - Starts the application with seed data enabled (using `SEED_DATA=true`). - -- **`npm run demo:build`** - Prepares a demo build: copies backend, compiles backend, builds assets, and copies modules. - -- **`npm run demo`** - Alias for `start:seed` - runs the demo with seeded data. - -## Preview mode -- **`npm start`** - Starts the application using TSX with the Waterline fixture configuration. + Starts the Sequelize fixture in development mode with file watching and seed data. + +- **`npm run dev:no-seed`** + Starts the Sequelize fixture in development mode without seed data. + +- **`npm run dev:typeorm`** + Starts the experimental TypeORM fixture in development mode with file watching and seed data. + +- **`npm run dev:typeorm:no-seed`** + Starts the experimental TypeORM fixture in development mode without seed data. + +- **`npm run dev:cors`** + Starts the Sequelize fixture with CORS settings for frontend integration testing. + +- **`npm run dev:no-seed-clean`** + Starts the Sequelize fixture without seed data and removes `.tmp` before startup. + +- **`npm run tsc:watch`** + Watches backend TypeScript and recompiles continuously. + +Adminizer uses `ADMINIZER_ENV=dev` to switch on development behavior such as Vite middleware and app asset `devUrl` paths. + +## Build Commands + +- **`npm run build`** + Full package build: copy backend, compile backend, compile UI, and build frontend assets. + +- **`npm run build:backend`** + Copy backend files and compile backend TypeScript. + +- **`npm run compile:backend`** + Compile backend TypeScript with `src/tsconfig.json` and append `.js` extensions in generated imports. + +- **`npm run compile:ui`** + Compile the shared UI TypeScript package. + +- **`npm run build:assets`** + Build the main Adminizer frontend assets and all built-in control ES modules with Vite. + +- **`npm run build:controls`** + Build only the built-in control ES modules and their styles. + +## App And Module Builds + +- **`npm run build:apps`** + Builds all fixture app frontend modules and fixture virtual catalog components: notification-sender, module-manager, navigation catalog templates, React-Quill, and virtual catalog React templates/actions. + +- **`npm run build:catalog-modules`** + Builds fixture virtual catalog React components from `fixture/virtual-catalog`. + +Project modules should usually define their own Vite build script. See [BuildingModules.md](BuildingModules.md) for current `AbstractAdminizerApp` and `AppManager` usage. + +## Start And Seeding + +- **`npm run start`** + Starts the Sequelize fixture without seed data. + +- **`npm run start:seed`** + Starts the Sequelize fixture with seed data. + +- **`npm run start:typeorm`** + Starts the experimental TypeORM fixture without seed data. + +- **`npm run start:typeorm:seed`** + Starts the experimental TypeORM fixture with seed data. diff --git a/docs/index.md b/docs/index.md index 7816e569..caf07cd1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -# 📘 Adminizer Documentation +# Adminizer Documentation ## 1. Getting Started @@ -9,29 +9,48 @@ * [General Settings](Configuration/General.md) * [Model Definitions](Configuration/Models.md) * [Field Options](Configuration/Fields.md) +* [Inline Editing](Configuration/InlineEditing.md) * [Custom Components](Configuration/CustomComponents.md) +* [Localization](Configuration/Localization.md) -## Other +## 3. Extension Points -* [Localization](Configuration/Localization.md) +* [App Modules](BuildingModules.md) +* [Catalogs](Catalog.md) +* [Navigation Module](Navigation.md) +* [Media Manager](MediaManager.md) +* [Admin Controls](Controls.md) +* [Widgets](Configuration/Widgets.md) +* [Global UI Components](GlobalUI.md) +* [UI Component List](UIComponents.md) * [Icons](Icons.md) + +## 4. Admin Panel Features + +* [Filters](Filters.md) +* [Export](Export.md) +* [Feed Export](FeedExport.md) * [Notifications](Notifications.md) -* [History-Actions](HistoryActions.md) -## 3. Frontend Integration +* [History Actions](HistoryActions.md) +* [AI Assistant](AiAssistant.md) +* [Feedback](Feedback.md) + +## 5. Frontend Integration * [Inertia Adapter & Flash](InertiaAdapter.md) * [Form Error Management](FormError.md) * [Appearance](Appearance.md) -## 4. Admin Panel Features +## 6. Access Control -* [Admin Controls](Controls.md) -* [Navigation Catalog](Navigation.md) +* [Authorization](Authorization.md) +* [Access Rights Tokens](AccessRights/access-rights-tokens.md) +* [Field-Level Restrictions](AccessRights/AccessRightsModelFields.md) +* [User-Owned Records](AccessRights/user-owned-records.md) -## 5. Access Control +## Other -* [Model-Level Permissions](AccessControl/ModelPermissions.md) -* [Field-Level Restrictions](AccessControl/FieldRestrictions.md) -* [Role & Group Management](AccessControl/RoleGroups.md) -* [Global UI Components](GlobalUI.md) -* [UI Component List](UIComponents.md) +* [Commands](Commands.md) +* [Development](development.md) +* [Internal Queries](InternalQueries.md) +* [Troubleshooting](Troubleshooting.md) diff --git a/docs/login-additional-link.md b/docs/login-additional-link.md index 0147c6b4..29d36d5f 100644 --- a/docs/login-additional-link.md +++ b/docs/login-additional-link.md @@ -23,7 +23,7 @@ auth: { captcha: true, // Shown as a plain link at the bottom of the login card addishinalLoginPage: { - link: '/model/userap/register', // becomes /model/userap/register + link: '/model/User/register', // becomes /model/User/register textKey: 'Additional login page' // optional; defaults to this value } } diff --git a/fixture/adminizerConfig.ts b/fixture/adminizerConfig.ts index b6ca318c..87192bcb 100644 --- a/fixture/adminizerConfig.ts +++ b/fixture/adminizerConfig.ts @@ -1,16 +1,20 @@ import { AdminpanelConfig } from "../dist"; -const routePrefix = "/adminizer"; +export const routePrefix = "/adminizer"; const models: AdminpanelConfig["models"] = { Test: { title: 'Test model', model: 'test', displayName: 'title', - userAccessRelation: 'owner', + // userAccessRelation: 'owner', fields: { - createdAt: false, - updatedAt: false, + createdAt: { + visible: false + }, + updatedAt: { + visible: false + }, title: { title: 'Title', type: 'string', @@ -20,9 +24,9 @@ const models: AdminpanelConfig["models"] = { title: 'Images', type: 'mediamanager', options: { - id: "default", // 'default' is default instance (src/lib/mediamanager/DefaultMediaManager) + id: "default", group: 'banner', - accept: ['image/jpeg, image/png'] + accept: ['image/jpeg', 'image/png'] } }, schema: {} @@ -46,37 +50,45 @@ const models: AdminpanelConfig["models"] = { title: 'All controls', model: 'example', displayName: 'description', - userAccessRelation: 'owner', - tools: [ - { - id: '1', - link: `https://google.com`, - type: 'blank', - title: 'Some new action', - icon: 'reorder', + filters: { + enabled: true, + excludeFromFilters: ['createdAt', 'id', 'ownerId', 'testRelation', 'testRelationExample'], + }, + // userAccessRelation: 'owner', + // tools: [ + // { + // id: '1', + // link: `https://google.com`, + // type: 'blank', + // title: 'Some new action', + // icon: 'reorder', + // }, + // ], + fields: { + createdAt: { + title: 'created At', + type: 'string', }, - { - id: '2', - link: `${routePrefix}/form/global`, - type: 'self', - title: 'Form example', - icon: 'payment', + updatedAt: { + visible: false }, - { - id: '3', - link: 'https://google.com', - type: 'blank', - title: 'Form example from file Form example from file', - icon: 'touch_app', - } - ], - fields: { - createdAt: false, - updatedAt: false, title: { title: 'Title', type: 'string', - required: true + required: true, + // inlineEditable: false + }, + tui: { + type: 'tuieditor', + options: { + name: 'toast-ui', + config: { + hideModeSwitch: true, + // previewStyle: 'vertical', + initialEditType: 'wysiwyg', + height: '800px' + }, + } }, description: { title: 'Textarea', @@ -86,7 +98,8 @@ const models: AdminpanelConfig["models"] = { }, sort: { type: 'boolean', - title: 'Boolean' + title: 'Boolean', + // inlineEditable: true }, disabled_text: { title: 'Disabled', @@ -100,7 +113,8 @@ const models: AdminpanelConfig["models"] = { options: { min: 10, max: 80 - } + }, + // inlineEditable: true }, select: { title: 'Select', @@ -135,6 +149,7 @@ const models: AdminpanelConfig["models"] = { number: { title: 'Number', type: 'number', + // inlineEditable: true }, color: { title: 'color', @@ -145,16 +160,10 @@ const models: AdminpanelConfig["models"] = { type: 'week', }, json: { - type: 'jsoneditor' - }, - tui: { - type: 'tuieditor', - options: { - name: 'toast-ui', - config: { - hideModeSwitch: true, - previewStyle: 'vertical', - }, + type: 'jsoneditor', + customFilter: { + handlerId: 'Example.json', + label: 'Custom filtering' } }, code: { @@ -173,6 +182,9 @@ const models: AdminpanelConfig["models"] = { datatable: { title: 'Price', type: 'table', + customFilter: { + handlerId: 'Example.datatable' + }, options: { config: { dataSchema: { name: null, footage: null, price: null }, @@ -190,44 +202,41 @@ const models: AdminpanelConfig["models"] = { isIn: ['Sone', 'Stwo', 'Sthree', 'Sfour', 'Sfive'], type: 'select-many' }, - checkboxes: { - title: 'Checkboxes', - isIn: ['one', 'two', 'three'] - }, editor: { title: 'Editor', type: 'wysiwyg', options: { - // name: 'react-quill', - name: 'ckeditor', - config: { - items: [ - // 'sourceEditing', // This is for test, see full list of items in src/lib/controls/wysiwyg/CKeditor.ts - // 'showBlocks', - // '|', - 'heading', - '|', - 'bold', - 'italic', - 'underline', - '|', - // 'horizontalLine', - 'link', - 'insertImage', - 'insertTable', - 'blockQuote', - '|', - 'alignment', - '|', - 'bulletedList', - 'numberedList', - 'outdent', - 'indent', - ] - } + name: 'react-quill', + config: {}, + // name: 'ckeditor', + // config: { + // items: [ + // // 'sourceEditing', // This is for test, see full list of items in src/lib/controls/wysiwyg/CKeditor.ts + // // 'showBlocks', + // // '|', + // 'heading', + // '|', + // 'bold', + // 'italic', + // 'underline', + // '|', + // // 'horizontalLine', + // 'link', + // 'insertImage', + // 'insertTable', + // 'blockQuote', + // '|', + // 'alignment', + // '|', + // 'bulletedList', + // 'numberedList', + // 'outdent', + // 'indent', + // ] + // } } }, - testRelation: { + testRelationExample: { title: 'Test one association', displayModifier: function (data) { return data?.title; @@ -236,30 +245,42 @@ const models: AdminpanelConfig["models"] = { }, tests: { title: 'One to many association', - displayModifier: function (data) { - return data?.title; + displayModifier: function (data: any) { + if (Array.isArray(data)) { + return data + .map((item: any) => item?.title) + .filter(Boolean) + .join(', '); + } + return data?.title || ''; + }, + disabled: false + }, + owner: { + title: 'Owner', + displayModifier: function (data: any) { + return data?.login || ''; }, disabled: false }, }, list: { fields: { - json: false, - tui: false, - geojson: false, - week: false, - color: false, - range: false, - date: false, - month: false, - selectMany: false, - select: false, - dateTime: false, - testRelation: false, - tests: false, - price: false, - code: false, - datatable: false + // json: false, + // tui: false, + // geojson: false, + // week: false, + // color: false, + // range: false, + // date: false, + // month: false, + // selectMany: false, + // select: false, + // testRelation: false, + // tests: false, + // price: false, + // code: false, + // datatable: false }, actions: { global: [ @@ -287,12 +308,6 @@ const models: AdminpanelConfig["models"] = { type: 'blank', title: 'Google', icon: 'insert_link' - }, { - id: "5", - link: `${routePrefix}/form/global`, - type: 'self', - title: 'Form', - icon: 'insert_link' }, ], inline: [ @@ -400,12 +415,14 @@ const models: AdminpanelConfig["models"] = { createdAt: { title: 'Created at', }, - updatedAt: false, + updatedAt: { + visible: false + }, mediamanager_one: { title: 'Images 1', type: 'mediamanager', options: { - id: "default", // 'default' is default instance (src/lib/mediamanager/DefaultMediaManager) + id: "default", group: 'banner', accept: ['image/svg+xml'] } @@ -414,7 +431,7 @@ const models: AdminpanelConfig["models"] = { title: 'Images 2', type: 'mediamanager', options: { - id: 'default', // 'default' is default instance (src/lib/mediamanager/DefaultMediaManager) + id: 'default', group: 'avatars', initTab: 'table-application', accept: ['image/jpeg'] @@ -451,13 +468,29 @@ const models: AdminpanelConfig["models"] = { visible: false, }, fields: { - createdAt: false, - updatedAt: false, + createdAt: { + visible: false + }, + updatedAt: { + visible: false + } } - }, + } } const config: AdminpanelConfig = { + list: { + defaultPageSize: 5 + }, + filters: { + enabled: true + }, + system: { + defaultORM: process.env.ORM ?? "sequelize", + internalModelAccess: { + "test-catalog": ["TestCatalog", 'Category'] + } + }, mediamanager: { fileStoragePath: '.tmp/public', allowMIME: ['image/*', 'application/*', 'text/*', 'video/*'], @@ -510,208 +543,19 @@ const config: AdminpanelConfig = { defaultWidgets: [ 'action_one', 'info_one', + 'app_info_widget', + 'app_switcher_widget', + 'app_action_widget', + 'app_link_widget', + 'app_custom_counter', 'siteLinks', 'site_custom', 'site_switcher' ], autoloadWidgetsPath: 'fixture/widgets' }, - navigation: { - items: [ - { - title: 'Category', - model: "Category", - urlPath: '/catalog/category/${data.record.slug}' - }, - { - title: 'Example', - model: "Example", - urlPath: `${routePrefix}/model/Example/\${data.record.id}` - } - ], - // Links in the admin panel leading to different navigation data (for example: header, footer) should end the same way as you specify in the array - // /admin/catalog/navigation/footer - // /admin/catalog/navigation/header - sections: ['header', 'footer'], - groupField: [ - { - name: 'link', - label: 'Ссылка', - required: false - }, - { - name: "test_field", - label: 'Test', - required: false - }, - { - name: "test_feild2", - label: 'Test2', - required: false - } - ], - }, - forms: { - data: { - global: { - field1: { - title: 'Field1', - type: 'string', - value: 'Some string', - required: true, - }, - mediamanager: { - title: 'Images', - type: 'mediamanager', - options: { - id: "default", - group: 'form_global_images', - initTab: "table-application", - accept: ["application/pdf", "application/msword"] - }, - value: null - }, - field2: { - title: 'Field2', - type: 'text', - value: 'Some text', - required: true, - tooltip: 'tooltip for field2', - }, - json: { - title: 'Json', - type: 'jsoneditor' - }, - text: { - title: 'Editor', - type: 'wysiwyg', - }, - tabs_video: { - "title": "Табы видео", - "type": "table", - "tooltip": "Нажмите на таблице правой клавишей мыши, выберите \"Вставить строку ниже\", в добавленную строку вставьте код полученный на сервисе ВКонтакте или YouTube через кнопку \"Поделиться\"", - "options": { - "config": { - "dataSchema": { - "iframe": null - }, - "colHeaders": [ - "Iframe" - ], - "rowHeaders": true, - "columns": [ - { - "data": "iframe" - } - ], - "height": "auto", - "width": "100%", - "manualColumnResize": true, - "contextMenu": true, - "licenseKey": "non-commercial-and-evaluation" - } - }, - "value": null - }, - tabs_tests: { - "title": "Табы онлайн-тесты", - "tooltip": "Нажмите на таблице правой клавишей мыши, выберите \"Вставить строку ниже\", в соответствующих столбцах вставьте ссылку на тест и его название", - "type": "table", - "options": { - "config": { - "dataSchema": { - "link": null, - "text": null - }, - "colHeaders": [ - "Ссылка", - "Текст" - ], - "rowHeaders": true, - "columns": [ - { - "data": "link" - }, - { - "data": "text" - } - ], - "height": "auto", - "width": "100%", - "manualColumnResize": true, - "contextMenu": true, - "licenseKey": "non-commercial-and-evaluation" - } - }, - "value": null - }, - tabs_links: { - "title": "Табы полезные ссылки", - "type": "table", - "tooltip": "Нажмите на таблице правой клавишей мыши, выберите \"Вставить строку ниже\", в соответствующих столбцах вставьте ссылку на сайт и его название", - "options": { - "config": { - "dataSchema": { - "link": null, - "text": null - }, - "colHeaders": [ - "Ссылка", - "Текст" - ], - "rowHeaders": true, - "columns": [ - { - "data": "link" - }, - { - "data": "text" - } - ], - "height": "auto", - "width": "100%", - "manualColumnResize": true, - "contextMenu": true, - "licenseKey": "non-commercial-and-evaluation" - } - }, - "value": null - } - } - } - }, navbar: { additionalLinks: [ - { - id: '1', - link: `${routePrefix}/form/global`, - title: 'Global Settings', - type: 'self', - icon: 'build', - accessRightsToken: 'read-global-form' - }, - { - id: '2', - link: `${routePrefix}/module-test`, - type: 'self', - title: 'Test Module', - icon: '360', - accessRightsToken: 'read-global-form' - }, - { - id: '3', - type: "self", - link: `${routePrefix}/catalog/navigation/header`, - title: 'Nav Header', - icon: 'menu' - }, - { - id: '4', - type: "self", - link: `${routePrefix}/catalog/navigation/footer`, - title: 'Nav Footer', - icon: 'menu' - }, { id: '5', type: "self", @@ -743,13 +587,6 @@ const config: AdminpanelConfig = { type: 'blank', icon: 'share' }, - { - id: "2", - title: 'Sub 3 Sub 3 Sub 3 Sub 3', - link: `${routePrefix}/form/global`, - type: 'self', - icon: 'insert_link' - }, { id: "3", title: 'Sub 4', @@ -806,19 +643,13 @@ const config: AdminpanelConfig = { }, translation: { locales: ['en', 'ru', 'de', 'ua'], - //path: 'fixture/locales', // relative path to translations directory + directory: 'fixture/locales', // relative path to translations directory + // missingTranslationDirectory: 'fixture/locales_missing', defaultLocale: 'en' }, models: models, //@ts-ignore generator: {}, - globalSettings: { // Global project settings - enableMigrations: true - }, - migrations: { - path: 'mg_path', // path to migrations - //config: string | object // db-migrate config - }, showVersion: { link: 'https://docs.adminizer.org', hint: 'Adminizer documentation', diff --git a/fixture/apps/ai-assistant/AiAssistantApp.ts b/fixture/apps/ai-assistant/AiAssistantApp.ts new file mode 100644 index 00000000..0763648c --- /dev/null +++ b/fixture/apps/ai-assistant/AiAssistantApp.ts @@ -0,0 +1,151 @@ +import { + AbstractAdminizerApp, + Adminizer, + AppSetupContext, +} from "../../../dist"; +import type {AppAiAssistantContext} from "../../../dist"; +import {AiAssistantController} from "../../../dist/controllers/ai/AiAssistantController"; +import {AbstractAiModelService} from "../../../dist/lib/ai-assistant/AbstractAiModelService"; +import {DummyAiModelService} from "./DummyAiModelService"; +import {OpenAiDataAgentService} from "./OpenAiDataAgentService"; +import {OpenAiModelService} from "./OpenAiModelService"; + +interface AiAssistantAppConfig { + route: string; + defaultModel?: string; + models: string[]; +} + +interface AiAssistantModelDefinition { + id: string; + name: string; + description: string; + enabled?: () => boolean; + factory: (context: AppAiAssistantContext) => AbstractAiModelService; +} + +const modelDefinitions: Record = { + dummy: { + id: "dummy", + name: "Dummy assistant", + description: "Access to dummy AI assistant model", + factory: () => new DummyAiModelService(), + }, + openai: { + id: "openai", + name: "OpenAI fixture agent", + description: "Access to OpenAI fixture agent", + factory: (context) => new OpenAiModelService(context), + }, + "openai-data": { + id: "openai-data", + name: "OpenAI data explorer", + description: "Access to OpenAI data explorer", + enabled: () => Boolean(process.env.OPENAI_API_KEY ?? process.env.ADMINIZER_OPENAI_KEY), + factory: (context) => new OpenAiDataAgentService(context), + }, +}; + +export class AiAssistantApp extends AbstractAdminizerApp { + readonly name = "ai-assistant"; + readonly version = "1.0.0"; + declare readonly config: AiAssistantAppConfig; + + constructor(config: Partial = {}) { + super(); + this.config = { + route: "/api/ai-assistant", + defaultModel: "openai-data", + models: ["openai-data", "dummy"], + ...config, + }; + } + + setup(ctx: AppSetupContext): void { + const enabledDefinitions = this.getEnabledModelDefinitions(); + const modelIds = enabledDefinitions.map((model) => model.id); + const defaultModel = this.resolveDefaultModel(modelIds); + + for (const model of enabledDefinitions) { + ctx.accessRight({ + id: `ai-assistant-${model.id}`, + name: model.name, + description: model.description, + department: "AI Assistant", + }); + } + + ctx.aiAssistant({ + models: enabledDefinitions.map((model) => model.factory), + }); + + ctx.config({ + aiAssistant: { + enabled: true, + models: modelIds, + ...(defaultModel ? {defaultModel} : {}), + }, + }); + + ctx.controller({ + id: "models", + method: "get", + route: `${this.config.route}/models`, + middleware: AiAssistantController.getModels, + policies: [{type: "auth", mode: "api"}], + }); + + ctx.controller({ + id: "history", + method: "get", + route: `${this.config.route}/history/:modelId`, + middleware: AiAssistantController.getHistory, + policies: [{type: "auth", mode: "api"}], + }); + + ctx.controller({ + id: "query", + method: "post", + route: `${this.config.route}/query`, + middleware: AiAssistantController.sendMessage, + policies: [{type: "auth", mode: "api"}], + }); + + ctx.controller({ + id: "reset-history", + method: "delete", + route: `${this.config.route}/history/:modelId`, + middleware: AiAssistantController.resetHistory, + policies: [{type: "auth", mode: "api"}], + }); + } + + private getEnabledModelDefinitions(): AiAssistantModelDefinition[] { + const enabled: AiAssistantModelDefinition[] = []; + + for (const modelId of this.config.models) { + const definition = modelDefinitions[modelId]; + if (!definition) { + Adminizer.log.warn(`[AiAssistantApp] Unknown AI assistant model "${modelId}". Skipping.`); + continue; + } + + if (definition.enabled && !definition.enabled()) { + Adminizer.log.warn(`[AiAssistantApp] AI assistant model "${modelId}" is disabled by environment. Skipping.`); + continue; + } + + enabled.push(definition); + } + + return enabled.length ? enabled : [modelDefinitions.dummy]; + } + + private resolveDefaultModel(modelIds: string[]): string | undefined { + if (this.config.defaultModel && modelIds.includes(this.config.defaultModel)) { + return this.config.defaultModel; + } + + return modelIds[0]; + } +} diff --git a/fixture/apps/ai-assistant/DummyAiModelService.ts b/fixture/apps/ai-assistant/DummyAiModelService.ts new file mode 100644 index 00000000..9f2468a7 --- /dev/null +++ b/fixture/apps/ai-assistant/DummyAiModelService.ts @@ -0,0 +1,21 @@ +import {AbstractAiModelService} from "../../../dist/lib/ai-assistant/AbstractAiModelService"; +import type {AiAssistantMessage} from "../../../dist/interfaces/types"; +import type {User} from "../../../dist/models/User"; + +export class DummyAiModelService extends AbstractAiModelService { + constructor() { + super({ + id: "dummy", + name: "Dummy assistant", + description: "Returns a simple echo response for local testing.", + }); + } + + async generateReply( + _prompt: string, + _history: AiAssistantMessage[], + user: User, + ): Promise { + return `Hello ${user.login}, this is a dummy AI response.`; + } +} diff --git a/fixture/helpers/ai/OpenAiDataAgentService.ts b/fixture/apps/ai-assistant/OpenAiDataAgentService.ts similarity index 86% rename from fixture/helpers/ai/OpenAiDataAgentService.ts rename to fixture/apps/ai-assistant/OpenAiDataAgentService.ts index 34564d6e..7d8da826 100644 --- a/fixture/helpers/ai/OpenAiDataAgentService.ts +++ b/fixture/apps/ai-assistant/OpenAiDataAgentService.ts @@ -8,22 +8,23 @@ import { RunContext, } from '@openai/agents'; import {AbstractAiModelService} from '../../../dist/lib/ai-assistant/AbstractAiModelService'; -import {AiAssistantMessage, Entity} from '../../../dist/interfaces/types'; -import {ModelConfig} from '../../../dist/interfaces/adminpanelConfig'; +import type {AiAssistantMessage, ModelResource} from '../../../dist/interfaces/types'; +import type {ModelConfig} from '../../../dist/interfaces/adminpanelConfig'; import {Adminizer} from '../../../dist/lib/Adminizer'; -import {DataAccessor} from '../../../dist/lib/DataAccessor'; -import {UserAP} from '../../../dist/models/UserAP'; +import type {AppAiAssistantContext} from '../../../dist/lib/app-manager/AdminizerApp'; +import type {DataAccessor} from '../../../dist/lib/DataAccessor'; +import type {User} from '../../../dist/models/User'; interface AgentContext { - user: UserAP; + user: User; } export class OpenAiDataAgentService extends AbstractAiModelService { private readonly apiKey?: string; private readonly model: string; - constructor(adminizer: Adminizer) { - super(adminizer, { + constructor(private readonly context: AppAiAssistantContext) { + super({ id: 'openai-data', name: 'OpenAI data explorer', description: 'Answers questions with live data retrieved via Adminizer models.', @@ -46,7 +47,7 @@ export class OpenAiDataAgentService extends AbstractAiModelService { public async generateReply( prompt: string, history: AiAssistantMessage[], - user: UserAP, + user: User, ): Promise { if (!this.isEnabled()) { return 'The OpenAI data agent is not configured. Please set the OPENAI_API_KEY environment variable.'; @@ -80,7 +81,7 @@ export class OpenAiDataAgentService extends AbstractAiModelService { } } - private createAgent(user: UserAP): Agent { + private createAgent(user: User): Agent { const accessibleModels = this.listReadableModels(user); const modelSummary = accessibleModels.length > 0 ? accessibleModels.map(({name, config}) => `• ${name} (model key: ${config.model})`).join('\n') @@ -137,7 +138,7 @@ export class OpenAiDataAgentService extends AbstractAiModelService { throw new Error(`Model "${input.model}" is not registered in Adminizer.`); } - const accessor = new DataAccessor(this.adminizer, activeUser, entity, 'list'); + const accessor = this.context.createDataAccessor(entity, activeUser, 'list'); let criteria = {}; // Handle filter parameter - can be empty string (default), undefined, or actual filter @@ -163,7 +164,7 @@ export class OpenAiDataAgentService extends AbstractAiModelService { // Handle fields parameter - can be empty array (default), undefined, or actual fields const fieldsValue = input.fields; const projected = fieldsValue && Array.isArray(fieldsValue) && fieldsValue.length > 0 - ? limited.map((record) => this.pickFields(record, fieldsValue)) + ? limited.map((record: Record) => this.pickFields(record, fieldsValue)) : limited; if (fieldsValue && fieldsValue.length > 0) { @@ -220,7 +221,7 @@ export class OpenAiDataAgentService extends AbstractAiModelService { throw new Error(`Model "${input.model}" is not registered in Adminizer.`); } - const accessor = new DataAccessor(this.adminizer, activeUser, entity, 'add'); + const accessor = this.context.createDataAccessor(entity, activeUser, 'add'); console.log('🔐 [Tool] Preparing create payload with permission checks...'); const sanitizedPayload = await this.prepareCreatePayload(input.payload, accessor); console.log('✅ [Tool] Sanitized payload:', JSON.stringify(sanitizedPayload, null, 2)); @@ -283,7 +284,7 @@ export class OpenAiDataAgentService extends AbstractAiModelService { throw new Error(`Model "${input.model}" is not registered in Adminizer.`); } - const accessor = new DataAccessor(this.adminizer, activeUser, entity, 'edit'); + const accessor = this.context.createDataAccessor(entity, activeUser, 'edit'); console.log('🔐 [Tool] Preparing update payload with permission checks...'); const sanitizedPayload = await this.prepareUpdatePayload(input.payload, accessor); console.log('✅ [Tool] Sanitized payload:', JSON.stringify(sanitizedPayload, null, 2)); @@ -461,75 +462,33 @@ export class OpenAiDataAgentService extends AbstractAiModelService { }); } - private listReadableModels(user: UserAP): Array<{name: string; config: ModelConfig}> { + private listReadableModels(user: User): Array<{name: string; config: ModelConfig}> { const readable: Array<{name: string; config: ModelConfig}> = []; - for (const [entityName, rawConfig] of Object.entries(this.adminizer.config.models ?? {})) { - const config = this.ensureModelConfig(entityName, rawConfig); - const modelInstance = config.model - ? this.adminizer.modelHandler.model.get(config.model.toLowerCase()) - : undefined; - - if (!modelInstance) { + for (const resource of this.context.getModelResources()) { + if (!resource.model) { continue; } - const token = `read-${modelInstance.modelname}-model`; - if (this.adminizer.accessRightsHelper.hasPermission(token, user)) { - readable.push({name: entityName, config}); + const token = `read-${resource.model.modelname}-model`; + if (this.context.hasPermission(token, user)) { + readable.push({name: resource.name, config: resource.config}); } } return readable; } - private resolveEntity(modelName: string): Entity { - const normalized = modelName.trim().toLowerCase(); - const models = this.adminizer.config.models ?? {}; - - for (const [entityName, rawConfig] of Object.entries(models)) { - const config = this.ensureModelConfig(entityName, rawConfig); - const candidateNames = new Set([ - entityName.toLowerCase(), - config.model?.toLowerCase(), - ]); - - if (!candidateNames.has(normalized)) { - continue; - } - - const modelInstance = this.adminizer.modelHandler.model.get(config.model?.toLowerCase()); - if (!modelInstance) { - throw new Error(`Model adapter is not initialized for "${config.model}".`); - } - - return { - name: entityName, - config, - model: modelInstance, - type: 'model', - uri: `${this.adminizer.config.routePrefix}/model/${entityName}`, - }; + private resolveEntity(modelName: string): ModelResource { + const resource = this.context.resolveModelResource(modelName.trim()); + if (!resource) { + throw new Error(`Unknown model "${modelName}".`); } - throw new Error(`Unknown model "${modelName}".`); - } - - private ensureModelConfig(entityName: string, config: ModelConfig | boolean): ModelConfig { - if (typeof config === 'boolean') { - const modelId = entityName.toLowerCase(); - return { - model: modelId, - title: entityName, - icon: 'description', - list: true, - add: true, - edit: true, - remove: true, - view: true, - } as ModelConfig; + if (!resource.model) { + throw new Error(`Model adapter is not initialized for "${resource.config.model}".`); } - return config; + return resource; } } diff --git a/fixture/apps/ai-assistant/OpenAiModelService.ts b/fixture/apps/ai-assistant/OpenAiModelService.ts new file mode 100644 index 00000000..2c7688a4 --- /dev/null +++ b/fixture/apps/ai-assistant/OpenAiModelService.ts @@ -0,0 +1,175 @@ +import {Adminizer} from "../../../dist"; +import {AbstractAiModelService} from "../../../dist/lib/ai-assistant/AbstractAiModelService"; +import type {AppAiAssistantContext} from "../../../dist/lib/app-manager/AdminizerApp"; +import type {ActionType} from "../../../dist/interfaces/adminpanelConfig"; +import type {AiAssistantMessage, ModelResource} from "../../../dist/interfaces/types"; +import type {DataAccessor} from "../../../dist/lib/DataAccessor"; +import type {User} from "../../../dist/models/User"; + +type AgentAction = "create" | "list" | "update" | "delete"; + +interface AgentInstruction { + action: AgentAction; + modelResource: string; + payload?: Record; + criteria?: Record; +} + +type DataAccessorFactory = (modelResource: ModelResource, user: User, action: ActionType) => DataAccessor; + +const ACTION_TOKENS: Record = { + add: "create", + edit: "update", + list: "read", + view: "read", + remove: "delete", +}; + +export class OpenAiModelService extends AbstractAiModelService { + private readonly createAccessor: DataAccessorFactory; + + constructor( + private readonly context: AppAiAssistantContext, + accessorFactory?: DataAccessorFactory, + ) { + super({ + id: "openai", + name: "OpenAI fixture agent", + description: "Executes structured commands with DataAccessor using the current user permissions.", + }); + this.createAccessor = accessorFactory ?? context.createDataAccessor; + } + + public async generateReply(prompt: string, _history: AiAssistantMessage[], user: User): Promise { + const instruction = this.parseInstruction(prompt); + + if (!instruction) { + return this.usageMessage(); + } + + switch (instruction.action) { + case "create": + return this.handleCreate(instruction, user); + default: + return `Unsupported action "${instruction.action}". The OpenAI agent currently supports only the "create" action.`; + } + } + + private async handleCreate(instruction: AgentInstruction, user: User): Promise { + const modelResource = this.context.resolveModelResource(instruction.modelResource); + if (!modelResource?.model) { + return `Model "${instruction.modelResource}" is not available in this project.`; + } + + if (!this.userHasPermission(modelResource, user, "add")) { + return `User "${user.login}" does not have permission to create ${modelResource.name} records.`; + } + + if (!instruction.payload || typeof instruction.payload !== "object") { + return 'The "create" action requires a "data" object with field values.'; + } + + try { + const accessor = this.createAccessor(modelResource, user, "add"); + const created = await modelResource.model.create(instruction.payload as any, accessor); + const preview = JSON.stringify(created, null, 2); + const recordId = this.extractPrimaryKey(created, modelResource); + const idMessage = recordId !== undefined ? ` (id: ${recordId})` : ""; + return `Record created in ${modelResource.name}${idMessage}:\n${preview}`; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + Adminizer.log.error("OpenAI agent failed to create record", error); + return `Failed to create ${modelResource.name} record: ${message}`; + } + } + + private parseInstruction(prompt: string): AgentInstruction | null { + try { + const parsed = JSON.parse(prompt); + if (!parsed || typeof parsed !== "object") { + return null; + } + + const rawAction = this.extractString(parsed, ["action", "type"]); + if (!rawAction) { + return null; + } + + const action = rawAction.toLowerCase(); + if (!["create", "list", "update", "delete"].includes(action)) { + return null; + } + + const modelResource = this.extractString(parsed, ["modelResource", "model"]); + if (!modelResource) { + return null; + } + + const payload = this.extractObject(parsed, ["data", "payload", "record"]); + const criteria = this.extractObject(parsed, ["criteria", "where", "filter"]); + + return { + action: action as AgentAction, + modelResource, + payload, + criteria, + }; + } catch { + return null; + } + } + + private usageMessage(): string { + return [ + "Provide JSON instructions so the OpenAI agent can execute them with your permissions.", + "Example:", + "```json", + "{", + ' "action": "create",', + ' "modelResource": "Example",', + ' "data": { "title": "Hello from the agent" }', + "}", + "```", + "The agent uses DataAccessor, so any field-level restrictions from your account are respected.", + ].join("\n"); + } + + private userHasPermission(modelResource: ModelResource, user: User, action: ActionType): boolean { + const token = this.getPermissionToken(modelResource, action); + return this.context.hasPermission(token, user); + } + + private getPermissionToken(modelResource: ModelResource, action: ActionType): string { + const verb = ACTION_TOKENS[action]; + const modelName = modelResource.model?.modelname ?? modelResource.config?.model ?? modelResource.name; + return `${verb}-${modelName}-model`.toLowerCase(); + } + + private extractPrimaryKey(record: Partial>, modelResource: ModelResource): unknown { + if (!record) { + return undefined; + } + const primaryKey = (modelResource.model as any)?.primaryKey ?? "id"; + return (record as Record)[primaryKey]; + } + + private extractString(source: any, keys: string[]): string | undefined { + for (const key of keys) { + const value = source?.[key]; + if (typeof value === "string" && value.trim().length > 0) { + return value.trim(); + } + } + return undefined; + } + + private extractObject(source: any, keys: string[]): Record | undefined { + for (const key of keys) { + const value = source?.[key]; + if (value && typeof value === "object" && !Array.isArray(value)) { + return value as Record; + } + } + return undefined; + } +} diff --git a/fixture/apps/handsontable-test/HandsontableTest.es.js b/fixture/apps/handsontable-test/HandsontableTest.es.js new file mode 100644 index 00000000..29093f20 --- /dev/null +++ b/fixture/apps/handsontable-test/HandsontableTest.es.js @@ -0,0 +1,65 @@ +var d = { exports: {} }, s = {}; +var l; +function m() { + if (l) return s; + l = 1; + var r = /* @__PURE__ */ Symbol.for("react.transitional.element"), o = /* @__PURE__ */ Symbol.for("react.fragment"); + function a(x, e, t) { + var u = null; + if (t !== void 0 && (u = "" + t), e.key !== void 0 && (u = "" + e.key), "key" in e) { + t = {}; + for (var i in e) + i !== "key" && (t[i] = e[i]); + } else t = e; + return e = t.ref, { + $$typeof: r, + type: x, + key: u, + ref: e !== void 0 ? e : null, + props: t + }; + } + return s.Fragment = o, s.jsx = a, s.jsxs = a, s; +} +var c; +function p() { + return c || (c = 1, d.exports = m()), d.exports; +} +var n = p(); +const R = window.React.useMemo, v = window.React.useState, f = window.JSComponents.HandsonTable, h = [ + { feature: "window.JSComponents", status: "loaded", value: 1 }, + { feature: "HandsonTable", status: "rendered", value: 2 }, + { feature: "production asset", status: "checked", value: 3 } +]; +function w() { + const [r, o] = v(h), a = R(() => ({ + colHeaders: ["Feature", "Status", "Value"], + columns: [ + { data: "feature", type: "text" }, + { data: "status", type: "text" }, + { data: "value", type: "numeric" } + ], + rowHeaders: !0, + stretchH: "all", + height: 320, + width: "100%", + licenseKey: "non-commercial-and-evaluation" + }), []); + return /* @__PURE__ */ n.jsxs("section", { className: "space-y-4", children: [ + /* @__PURE__ */ n.jsxs("div", { children: [ + /* @__PURE__ */ n.jsx("h1", { className: "text-xl font-semibold", children: "Handsontable Test" }), + /* @__PURE__ */ n.jsx("p", { className: "text-sm text-muted-foreground", children: "Source: @/js-components/handsontable" }) + ] }), + /* @__PURE__ */ n.jsx( + f, + { + data: r, + config: a, + onChange: o + } + ) + ] }); +} +export { + w as default +}; diff --git a/fixture/apps/handsontable-test/HandsontableTest.tsx b/fixture/apps/handsontable-test/HandsontableTest.tsx new file mode 100644 index 00000000..92d51912 --- /dev/null +++ b/fixture/apps/handsontable-test/HandsontableTest.tsx @@ -0,0 +1,48 @@ +import {useMemo, useState} from "react"; +import HandsonTable from "@/js-components/handsontable"; + +type TableRow = { + feature: string; + status: string; + value: number; +}; + +const initialRows: TableRow[] = [ + {feature: "window.JSComponents", status: "loaded", value: 1}, + {feature: "HandsonTable", status: "rendered", value: 2}, + {feature: "production asset", status: "checked", value: 3}, +]; + +export default function HandsontableTest() { + const [rows, setRows] = useState(initialRows); + const options = useMemo(() => ({ + colHeaders: ["Feature", "Status", "Value"], + columns: [ + {data: "feature", type: "text"}, + {data: "status", type: "text"}, + {data: "value", type: "numeric"}, + ], + rowHeaders: true, + stretchH: "all", + height: 320, + width: "100%", + licenseKey: "non-commercial-and-evaluation", + }), []); + + return ( +
+
+

Handsontable Test

+

+ Source: @/js-components/handsontable +

+
+ + +
+ ); +} diff --git a/fixture/apps/handsontable-test/HandsontableTestApp.ts b/fixture/apps/handsontable-test/HandsontableTestApp.ts new file mode 100644 index 00000000..a0a5a8cc --- /dev/null +++ b/fixture/apps/handsontable-test/HandsontableTestApp.ts @@ -0,0 +1,73 @@ +import path from "path"; +import {AbstractAdminizerApp, AppSetupContext} from "../../../dist"; + +interface HandsontableTestAppConfig { + route: string; + sidebarId: string; + title: string; + icon: string; + section: string; + componentFile: string; + devComponentUrl: string; +} + +export class HandsontableTestApp extends AbstractAdminizerApp { + readonly name = "handsontable-test"; + readonly version = "1.0.0"; + declare readonly config: HandsontableTestAppConfig; + + constructor(config: Partial = {}) { + super(); + this.config = { + route: "/handsontable-test", + sidebarId: "handsontable-test", + title: "Handsontable Test", + icon: "table_chart", + section: "Platform", + componentFile: path.resolve(import.meta.dirname, "HandsontableTest.es.js"), + devComponentUrl: "/fixture/apps/handsontable-test/HandsontableTest.tsx", + ...config, + }; + } + + setup(ctx: AppSetupContext): void { + const moduleComponent = ctx.asset({ + id: "component", + filePath: this.config.componentFile, + devUrl: this.config.devComponentUrl, + }); + + const pageUrl = ctx.controller({ + id: "page", + method: "get", + route: this.config.route, + middleware: this.renderModule(moduleComponent), + policies: [{type: "auth", mode: "ui"}], + }); + + ctx.config({ + navbar: { + additionalLinks: [{ + id: this.config.sidebarId, + link: pageUrl, + type: "self", + title: this.config.title, + icon: this.config.icon as any, + section: this.config.section, + }], + }, + }); + } + + private renderModule(moduleComponent: string): MiddlewareType { + return async (req, res) => { + return req.Inertia.render({ + component: "module", + props: { + moduleComponent, + data: {}, + }, + }); + }; + } +} diff --git a/fixture/apps/handsontable-test/vite.config.module.ts b/fixture/apps/handsontable-test/vite.config.module.ts new file mode 100644 index 00000000..d0b6c282 --- /dev/null +++ b/fixture/apps/handsontable-test/vite.config.module.ts @@ -0,0 +1,38 @@ +import {defineConfig} from "vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; +import {viteExternalsPlugin} from "vite-plugin-externals"; + +export default defineConfig({ + define: { + "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "production"), + }, + plugins: [ + react(), + viteExternalsPlugin({ + react: "React", + "react-dom": "ReactDOM", + "@/js-components/handsontable": ["JSComponents", "HandsonTable"], + }), + ], + build: { + outDir: path.resolve(import.meta.dirname, ""), + emptyOutDir: false, + lib: { + entry: path.resolve(import.meta.dirname, "HandsontableTest"), + name: "HandsontableTest", + formats: ["es"], + fileName: (format) => `HandsontableTest.${format}.js`, + }, + rollupOptions: { + external: [ + "@/js-components/handsontable", + ], + }, + }, + resolve: { + alias: { + "@": path.resolve(import.meta.dirname, "../../../src/assets/js"), + }, + }, +}); diff --git a/fixture/apps/media-manager/DefaultMediaManager.ts b/fixture/apps/media-manager/DefaultMediaManager.ts new file mode 100644 index 00000000..5e838f37 --- /dev/null +++ b/fixture/apps/media-manager/DefaultMediaManager.ts @@ -0,0 +1,152 @@ +import { + AbstractMediaManager, + Adminizer, + AppRuntime, + File, + MediaManagerItem, + MediaManagerWidgetClientItem, + MediaManagerWidgetData, + QueryCriteria, + SortCriteria, +} from "../../../dist"; +import {ApplicationItem, ImageItem, TextItem, VideoItem} from "./Items"; +import {mediaManagerModelNames} from "./MediaManagerModels"; +import type {MediaManagerAssociationRecord} from "./MediaManagerTypes"; +import {populateVariants} from "../../../dist/lib/media-manager/helpers/MediaManagerHelper"; + +export class DefaultMediaManager extends AbstractMediaManager { + readonly itemTypes: File[] = []; + readonly id: string; + declare readonly urlPathPrefix: string; + declare readonly fileStoragePath: string; + + constructor( + private readonly runtime: AppRuntime, + id: string, + urlPathPrefix: string, + fileStoragePath: string, + imageSizes: Record + ) { + super(createLegacyMediaManagerHost()); + this.id = id; + this.urlPathPrefix = urlPathPrefix; + this.fileStoragePath = fileStoragePath; + this.itemTypes.push(new ImageItem(runtime, urlPathPrefix, fileStoragePath, imageSizes)); + this.itemTypes.push(new TextItem(runtime, urlPathPrefix, fileStoragePath)); + this.itemTypes.push(new ApplicationItem(runtime, urlPathPrefix, fileStoragePath)); + this.itemTypes.push(new VideoItem(runtime, urlPathPrefix, fileStoragePath)); + } + + async getAll(limit: number, skip: number, sort: SortCriteria, group?: string) { + const media = this.runtime.models.get(mediaManagerModelNames.media); + const where: QueryCriteria["where"] = {parent: null, group}; + const data = await media.find({ + where, + limit, + skip, + sort, + populate: {variants: {sort}, meta: true}, + }); + for (const item of data) { + item.variants = await populateVariants( + this.runtime, + item.variants ?? [], + mediaManagerModelNames.media + ); + } + const next = await media.find({where, limit, skip: skip + limit, sort}); + return {data, next: next.length > 0}; + } + + async searchAll(search: string, group?: string): Promise { + const data = await this.runtime.models + .get(mediaManagerModelNames.media) + .find({ + where: {filename: {contains: search}, parent: null, group}, + sort: "createdAt DESC", + limit: 1000, + populate: {variants: {sort: "createdAt DESC"}, meta: true}, + }); + for (const item of data) { + item.variants = await populateVariants( + this.runtime, + item.variants ?? [], + mediaManagerModelNames.media + ); + } + return data; + } + + async setRelations( + data: MediaManagerWidgetData[], + model: string, + modelId: string | number, + widgetName: string + ): Promise { + if (modelId == null) { + throw new Error("modelId must be a string or number"); + } + + const associations = this.runtime.models.get( + mediaManagerModelNames.associations + ); + const where = { + modelId: String(modelId), + model: model.toLowerCase(), + widgetName, + }; + for (const association of await associations.find({where})) { + await associations.destroy({where: {id: association.id}}); + } + for (const [index, item] of data.entries()) { + await associations.create({ + mediaManagerId: this.id, + model: model.toLowerCase(), + modelId: String(modelId), + file: item.id, + widgetName, + sortOrder: index + 1, + }); + } + } + + async getRelations( + model: string, + widgetName: string, + modelId: string | number + ): Promise { + if (modelId == null) { + throw new Error("modelId must be a string or number"); + } + + const files = await this.runtime.models.get( + mediaManagerModelNames.associations + ).find({ + where: { + model: model.toLowerCase(), + widgetName, + modelId: String(modelId), + }, + sort: "sortOrder ASC", + populate: {file: true}, + }); + + return files + .filter((association) => association.file) + .map((association) => ({ + id: association.file.id, + mimeType: association.file.mimeType, + filename: association.file.filename, + url: association.file.url, + variants: [] as MediaManagerItem[], + })); + } +} + +function createLegacyMediaManagerHost(): Adminizer { + return { + accessRightsHelper: { + registerToken: (): void => undefined, + }, + } as unknown as Adminizer; +} diff --git a/fixture/apps/media-manager/Items.ts b/fixture/apps/media-manager/Items.ts new file mode 100644 index 00000000..74b63217 --- /dev/null +++ b/fixture/apps/media-manager/Items.ts @@ -0,0 +1,337 @@ +import { + AppRuntime, + File, + MediaFileType, + MediaManagerItem, + QueryCriteria, + SortCriteria, + UploaderFile, +} from "../../../dist"; +import sizeOf from "image-size"; +import sharp from "sharp"; +import * as fs from "fs"; +import path from "path"; +import {mediaManagerModelNames} from "./MediaManagerModels"; +import {populateVariants} from "../../../dist/lib/media-manager/helpers/MediaManagerHelper"; +import type { + MediaManagerAssociationRecord, + MediaManagerMetaRecord, + PublicMediaManagerMeta, +} from "./MediaManagerTypes"; + +type ImageSizes = Record; + +export class ImageItem extends File { + readonly type: MediaFileType = "image"; + + constructor( + protected readonly runtime: AppRuntime, + urlPathPrefix: string, + fileStoragePath: string, + private readonly imageSizes: ImageSizes = {} + ) { + super(urlPathPrefix, fileStoragePath); + } + + protected media() { + return this.runtime.models.get(mediaManagerModelNames.media); + } + + protected meta() { + return this.runtime.models.get(mediaManagerModelNames.meta); + } + + protected associations() { + return this.runtime.models.get( + mediaManagerModelNames.associations + ); + } + + async getItems(limit: number, skip: number, sort: SortCriteria, group?: string) { + const where: QueryCriteria["where"] = { + parent: null, + mimeType: {contains: this.type}, + group, + }; + const data = await this.media().find({ + where, + limit, + skip, + sort, + populate: {variants: {sort}, meta: true}, + }); + for (const item of data) { + item.variants = await populateVariants( + this.runtime, + item.variants ?? [], + mediaManagerModelNames.media + ); + } + const next = await this.media().find({where, limit, skip: skip + limit, sort}); + return {data, next: next.length > 0}; + } + + async search(search: string, group?: string): Promise { + const data = await this.media().find({ + where: { + filename: {contains: search}, + mimeType: {contains: this.type}, + parent: null, + group, + }, + sort: "createdAt DESC", + populate: {variants: {sort: "createdAt DESC"}, meta: true}, + }); + for (const item of data) { + item.variants = await populateVariants( + this.runtime, + item.variants ?? [], + mediaManagerModelNames.media + ); + } + return data; + } + + async upload( + file: UploaderFile, + filename: string, + originalName: string, + group?: string + ): Promise { + const parent = await this.media().create({ + parent: null, + mimeType: file.mimetype, + size: file.size, + path: this.storagePath(filename), + group, + tag: "origin", + filename: originalName, + url: this.publicUrl(filename), + }); + await this.createMeta(parent.id); + await this.addImageSizeMeta(file.path, parent.id); + if (Object.keys(this.imageSizes).length && file.mimetype !== "image/svg+xml") { + await this.createVariants(file, parent, filename, group); + } + return [await this.getFile(parent.id)]; + } + + async getVariants(id: string): Promise { + const item = await this.media().findOne({ + where: {id}, + populate: {variants: {sort: "createdAt DESC"}}, + }); + return populateVariants( + this.runtime, + item?.variants ?? [], + mediaManagerModelNames.media + ); + } + + async getOrigin(id: string): Promise { + return (await this.media().findOne({where: {id}})).path; + } + + async getFile(id: number | string): Promise { + const item = await this.media().findOne({ + where: {id: String(id)}, + populate: {variants: {sort: "createdAt DESC"}, meta: true}, + }); + if (item) { + item.variants = await populateVariants( + this.runtime, + item.variants ?? [], + mediaManagerModelNames.media + ); + } + return item; + } + + async getMeta(id: string): Promise { + const item = await this.media().findOne({ + where: {id}, + populate: {meta: {where: {isPublic: true}}}, + }); + return (item?.meta ?? []).flatMap((meta) => + typeof meta.key === "string" && typeof meta.value === "string" + ? [{key: meta.key, value: meta.value}] + : [] + ); + } + + async setMeta(id: string, data: Record): Promise { + for (const [key, value] of Object.entries(data)) { + await this.meta().update({where: {parentId: id, key}}, {value}); + } + } + + async uploadVariant( + parent: MediaManagerItem, + file: UploaderFile, + filename: string, + group?: string, + localeId?: string + ): Promise { + const dimensions = sizeOf(fs.readFileSync(file.path)); + const item = await this.media().create({ + parent: parent.id, + mimeType: file.mimetype, + size: file.size, + path: file.path, + group, + tag: localeId ? `loc:${localeId}` : `size:${dimensions.width}x${dimensions.height}`, + filename: parent.filename, + url: this.publicUrl(filename), + }); + await this.addImageSizeMeta(file.path, item.id); + return this.media().findOne({where: {id: item.id}}); + } + + async delete(id: string): Promise { + if ((await this.associations().find({where: {fileId: id}})).length) { + return false; + } + + const criteria: QueryCriteria = {where: {id}}; + const record = await this.media().findOne({ + ...criteria, + populate: {variants: true, meta: true}, + }); + if (!record) { + return true; + } + + for (const meta of record.meta ?? []) { + await this.meta().destroy({where: {id: meta.id}}); + } + for (const variant of record.variants ?? []) { + await this.media().destroy({where: {id: variant.id}}); + await deleteFile(variant.path); + } + await this.media().destroy(criteria); + await deleteFile(record.path); + return true; + } + + protected async createMeta(id: string): Promise { + for (const key of ["author", "description", "title"]) { + await this.meta().create({key, value: "", parent: id, isPublic: true}); + } + } + + private async addImageSizeMeta(filePath: string, id: string): Promise { + await this.meta().create({ + key: "imageSizes", + value: sizeOf(fs.readFileSync(filePath)), + parent: id, + isPublic: false, + }); + } + + private async createVariants( + file: UploaderFile, + parent: MediaManagerItem, + filename: string, + group?: string + ): Promise { + const dimensions = sizeOf(fs.readFileSync(file.path)); + for (const [sizeName, target] of Object.entries(this.imageSizes)) { + if (dimensions.width < target.width || dimensions.height < target.height) { + continue; + } + + const variantName = addFileSuffix(filename, sizeName); + const output = this.storagePath(variantName); + await fs.promises.mkdir(path.dirname(output), {recursive: true}); + const resized = await sharp(file.path) + .resize({width: target.width, height: target.height}) + .toFile(output); + const variant = await this.media().create({ + parent: parent.id, + mimeType: parent.mimeType, + size: resized.size, + filename: parent.filename, + group, + path: output, + tag: `size:${sizeName}`, + url: this.publicUrl(variantName), + }); + await this.addImageSizeMeta(output, variant.id); + } + } + + protected storagePath(filename: string): string { + return path.join(this.fileStoragePath, this.urlPathPrefix, filename); + } + + protected publicUrl(filename: string): string { + return `/${this.urlPathPrefix}/${filename}`; + } +} + +export class TextItem extends ImageItem { + readonly type: MediaFileType = "text"; + + async upload( + file: UploaderFile, + filename: string, + originalName: string, + group?: string + ): Promise { + const item = await this.media().create({ + parent: null, + mimeType: file.mimetype, + size: file.size, + path: this.storagePath(filename), + group, + filename: originalName, + tag: "origin", + url: this.publicUrl(filename), + }); + await this.createMeta(item.id); + return [await this.getFile(item.id)]; + } + + async uploadVariant( + parent: MediaManagerItem, + file: UploaderFile, + filename: string, + group?: string, + localeId?: string + ): Promise { + const variants = (parent.variants ?? []).filter((item) => !/^loc:/.test(item.tag)); + const item = await this.media().create({ + parent: parent.id, + mimeType: file.mimetype, + size: file.size, + path: file.path, + group, + tag: localeId ? `loc:${localeId}` : `ver:${variants.length + 1}`, + filename: parent.filename, + url: this.publicUrl(filename), + }); + return this.media().findOne({where: {id: item.id}}); + } +} + +export class ApplicationItem extends TextItem { + readonly type: MediaFileType = "application"; +} + +export class VideoItem extends TextItem { + readonly type: MediaFileType = "video"; +} + +function addFileSuffix(filename: string, suffix: string): string { + return filename.replace(/\.[^.]+$/, `_${suffix}$&`); +} + +async function deleteFile(filePath: string): Promise { + try { + await fs.promises.unlink(filePath); + } catch (error: unknown) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + } +} diff --git a/fixture/apps/media-manager/MediaManagerApp.ts b/fixture/apps/media-manager/MediaManagerApp.ts new file mode 100644 index 00000000..9d37a03b --- /dev/null +++ b/fixture/apps/media-manager/MediaManagerApp.ts @@ -0,0 +1,56 @@ +import { + AbstractAdminizerApp, + AppSetupContext, + MediaManagerConfig, +} from "../../../dist"; +import {DefaultMediaManager} from "./DefaultMediaManager"; +import {mediaManagerModelNames} from "./MediaManagerModels"; + +export interface DefaultMediaManagerAppConfig extends MediaManagerConfig { + id?: string; + urlPathPrefix?: string; +} + +export class MediaManagerApp extends AbstractAdminizerApp { + readonly name = "media-manager"; + readonly version = "1.0.0"; + declare readonly config: DefaultMediaManagerAppConfig; + + constructor(config: DefaultMediaManagerAppConfig) { + super(); + this.config = { + ...config, + id: config.id ?? "default", + urlPathPrefix: config.urlPathPrefix ?? "media-manager", + }; + } + + setup(ctx: AppSetupContext): void { + const permissionToken = `mediaManager-${this.config.id}`; + + ctx.accessRight({ + id: permissionToken, + name: this.config.id, + description: `Access to edit media-manager for ${this.config.id}`, + department: "media-manager", + }); + + for (const modelName of Object.values(mediaManagerModelNames)) { + ctx.model({name: modelName, adapter: "sequelize"}); + } + ctx.modelAccess({ + id: "storage", + models: Object.values(mediaManagerModelNames), + }); + ctx.mediaManager({ + factory: (runtime) => + new DefaultMediaManager( + runtime, + this.config.id, + this.config.urlPathPrefix, + this.config.fileStoragePath, + this.config.imageSizes ?? {} + ), + }); + } +} diff --git a/fixture/apps/media-manager/MediaManagerModels.ts b/fixture/apps/media-manager/MediaManagerModels.ts new file mode 100644 index 00000000..08402fad --- /dev/null +++ b/fixture/apps/media-manager/MediaManagerModels.ts @@ -0,0 +1,79 @@ +import { + DataTypes, + Model, + ModelAttributes, + ModelStatic, + Sequelize, +} from "sequelize"; + +export const mediaManagerModelNames = { + media: "MediaManagerAP", + meta: "MediaManagerMetaAP", + associations: "MediaManagerAssociationsAP", +} as const; + +export async function installMediaManagerSequelizeModels( + orm: Sequelize, + sync: boolean = false +): Promise { + const Media = getOrDefine(orm, mediaManagerModelNames.media, { + id: {type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true}, + parentId: {type: DataTypes.INTEGER, allowNull: true}, + mimeType: {type: DataTypes.STRING, allowNull: false}, + path: {type: DataTypes.STRING, allowNull: false}, + size: {type: DataTypes.INTEGER, allowNull: false}, + group: DataTypes.STRING, + tag: DataTypes.STRING, + url: {type: DataTypes.STRING, allowNull: false}, + filename: {type: DataTypes.STRING, allowNull: false}, + }); + const Meta = getOrDefine(orm, mediaManagerModelNames.meta, { + id: {type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true}, + key: {type: DataTypes.STRING, allowNull: false}, + value: DataTypes.JSON, + isPublic: {type: DataTypes.BOOLEAN, defaultValue: true}, + parentId: {type: DataTypes.INTEGER, allowNull: false}, + }); + const Associations = getOrDefine(orm, mediaManagerModelNames.associations, { + id: {type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true}, + mediaManagerId: {type: DataTypes.STRING, allowNull: false}, + model: {type: DataTypes.STRING, allowNull: false}, + modelId: {type: DataTypes.STRING, allowNull: false}, + widgetName: {type: DataTypes.STRING, allowNull: false}, + sortOrder: {type: DataTypes.INTEGER, allowNull: false}, + fileId: {type: DataTypes.INTEGER, allowNull: false}, + }); + + if (!Media.associations.parent) { + Media.belongsTo(Media, {as: "parent", foreignKey: "parentId"}); + Media.hasMany(Media, {as: "variants", foreignKey: "parentId"}); + } + if (!Media.associations.meta) { + Media.hasMany(Meta, {as: "meta", foreignKey: "parentId"}); + Meta.belongsTo(Media, {as: "parent", foreignKey: "parentId"}); + } + if (!Media.associations.modelAssociation) { + Media.hasMany(Associations, {as: "modelAssociation", foreignKey: "fileId"}); + Associations.belongsTo(Media, {as: "file", foreignKey: "fileId"}); + } + + if (sync) { + await Media.sync(); + await Meta.sync(); + await Associations.sync(); + } +} + +function getOrDefine( + orm: Sequelize, + name: string, + attributes: ModelAttributes> +): ModelStatic { + const existing = Object.values(orm.models).find( + (model) => model.name.toLowerCase() === name.toLowerCase() + ); + return existing ?? orm.define(name, attributes, { + tableName: name.toLowerCase(), + timestamps: true, + }); +} diff --git a/fixture/apps/media-manager/MediaManagerTypes.ts b/fixture/apps/media-manager/MediaManagerTypes.ts new file mode 100644 index 00000000..6750f72b --- /dev/null +++ b/fixture/apps/media-manager/MediaManagerTypes.ts @@ -0,0 +1,26 @@ +import type {MediaManagerItem} from "../../../dist"; + +export interface MediaManagerMetaRecord { + id?: string; + key: string; + value: unknown; + isPublic: boolean; + parentId?: string; + parent?: MediaManagerItem; +} + +export interface MediaManagerAssociationRecord { + id?: string; + mediaManagerId: string; + model: string; + modelId: string; + widgetName: string; + sortOrder: number; + fileId?: string; + file?: MediaManagerItem; +} + +export interface PublicMediaManagerMeta { + key: string; + value: string; +} diff --git a/fixture/apps/module-manager/ModuleManager.es.js b/fixture/apps/module-manager/ModuleManager.es.js new file mode 100644 index 00000000..167b21df --- /dev/null +++ b/fixture/apps/module-manager/ModuleManager.es.js @@ -0,0 +1,104 @@ +var p = { exports: {} }, a = {}; +var h; +function R() { + if (h) return a; + h = 1; + var c = /* @__PURE__ */ Symbol.for("react.transitional.element"), u = /* @__PURE__ */ Symbol.for("react.fragment"); + function o(x, t, e) { + var r = null; + if (e !== void 0 && (r = "" + e), t.key !== void 0 && (r = "" + t.key), "key" in t) { + e = {}; + for (var s in t) + s !== "key" && (e[s] = t[s]); + } else e = t; + return t = e.ref, { + $$typeof: c, + type: x, + key: r, + ref: t !== void 0 ? t : null, + props: e + }; + } + return a.Fragment = u, a.jsx = o, a.jsxs = o, a; +} +var j; +function v() { + return j || (j = 1, p.exports = R()), p.exports; +} +var n = v(); +const f = window.React.useState, U = window.UIComponents.Button, I = window.UIComponents.Table, $ = window.UIComponents.TableBody, l = window.UIComponents.TableCell, i = window.UIComponents.TableHead, g = window.UIComponents.TableHeader, T = window.UIComponents.TableRow, y = window.UIComponents.Card, E = window.UIComponents.CardContent, k = window.UIComponents.CardHeader, M = window.UIComponents.CardTitle, d = window.sonner.toast, b = window.axios, _ = window.UIComponents.Toaster, m = window.InertiajsReact.router, H = window.JSComponents.DeleteModal; +function J({ data: c }) { + const [u, o] = f(c.modules), x = async (e, r) => { + const s = r === "enabled" ? "disable" : "enable"; + try { + await b.post(`${window.routePrefix}/module-manager/${s}`, { name: e }), d.success(`Модуль ${e} успешно ${s === "enable" ? "включён" : "выключен"}.`), o( + (C) => C.map( + (w) => w.name === e ? { ...w, state: s === "enable" ? "enabled" : "disabled" } : w + ) + ), m.reload(); + } catch { + d.error(`Ошибка при ${s === "enable" ? "включении" : "выключении"} модуля ${e}.`); + } + }, t = async (e) => { + console.log("handleUnregister called for:", e); + try { + await b.post(`${window.routePrefix}/module-manager/unregister`, { name: e }), d.success(`Модуль ${e} успешно удалён.`), o((r) => r.filter((s) => s.name !== e)), m.reload(); + } catch { + d.error(`Ошибка при удалении модуля ${e}.`); + } + }; + return /* @__PURE__ */ n.jsxs(n.Fragment, { children: [ + /* @__PURE__ */ n.jsx(_, { position: "top-center", richColors: !0, closeButton: !0 }), + /* @__PURE__ */ n.jsxs(y, { className: "py-4", children: [ + /* @__PURE__ */ n.jsx(k, { children: /* @__PURE__ */ n.jsx(M, { children: "Менеджер модулей" }) }), + /* @__PURE__ */ n.jsx(E, { children: /* @__PURE__ */ n.jsxs(I, { children: [ + /* @__PURE__ */ n.jsx(g, { children: /* @__PURE__ */ n.jsxs(T, { children: [ + /* @__PURE__ */ n.jsx(i, { children: "Имя" }), + /* @__PURE__ */ n.jsx(i, { children: "Версия" }), + /* @__PURE__ */ n.jsx(i, { children: "Статус" }), + /* @__PURE__ */ n.jsx(i, { children: "Действия" }) + ] }) }), + /* @__PURE__ */ n.jsx($, { children: u.map((e) => /* @__PURE__ */ n.jsxs(T, { children: [ + /* @__PURE__ */ n.jsx(l, { children: e.name }), + /* @__PURE__ */ n.jsx(l, { children: e.version }), + /* @__PURE__ */ n.jsx(l, { children: /* @__PURE__ */ n.jsx( + "span", + { + className: `px-2 py-1 rounded text-xs font-medium ${e.state === "enabled" ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}`, + children: e.state + } + ) }), + /* @__PURE__ */ n.jsxs(l, { className: "flex gap-x-4", children: [ + /* @__PURE__ */ n.jsx( + U, + { + onClick: () => x(e.name, e.state), + variant: e.state === "enabled" ? "destructive" : "default", + size: "sm", + children: e.state === "enabled" ? "Выключить" : "Включить" + } + ), + /* @__PURE__ */ n.jsx( + H, + { + btnTitle: "Удалить", + variant: "destructive", + btnCLass: "", + handleDelete: () => t(e.name), + isLink: !1, + delModal: { + yes: "Да", + no: "Нет", + text: `Вы уверены, что хотите удалить модуль ${e.name}? Это действие нельзя отменить.` + } + } + ) + ] }) + ] }, e.name)) }) + ] }) }) + ] }) + ] }); +} +export { + J as default +}; diff --git a/fixture/apps/module-manager/ModuleManager.tsx b/fixture/apps/module-manager/ModuleManager.tsx new file mode 100644 index 00000000..9c45623d --- /dev/null +++ b/fixture/apps/module-manager/ModuleManager.tsx @@ -0,0 +1,112 @@ +import {useState} from "react"; +import {Button} from "@/components/ui/button"; +import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"; +import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"; +import {toast} from "sonner"; +import axios from "@/lib/axios-compat"; +import {Toaster} from "@/components/ui/sonner"; +import {router} from "@inertiajs/react"; +import DeleteModal from "@/components/modals/del-modal"; + +interface Module { + name: string; + version: string; + state: string; +} + +interface ModuleManagerProps { + data: { + modules: Module[]; + }; +} + +export default function ModuleManager({data}: ModuleManagerProps) { + const [modules, setModules] = useState(data.modules); + + const handleToggle = async (name: string, currentState: string) => { + const action = currentState === 'enabled' ? 'disable' : 'enable'; + try { + await axios.post(`${window.routePrefix}/module-manager/${action}`, {name}); + toast.success(`Модуль ${name} успешно ${action === 'enable' ? 'включён' : 'выключен'}.`); + setModules(prevModules => + prevModules.map(mod => + mod.name === name + ? {...mod, state: action === 'enable' ? 'enabled' : 'disabled'} + : mod + ) + ); + router.reload() + } catch (e) { + toast.error(`Ошибка при ${action === 'enable' ? 'включении' : 'выключении'} модуля ${name}.`); + } + + }; + + const handleUnregister = async (name: string) => { + console.log('handleUnregister called for:', name); + try { + await axios.post(`${window.routePrefix}/module-manager/unregister`, {name}); + toast.success(`Модуль ${name} успешно удалён.`); + setModules(prevModules => prevModules.filter(mod => mod.name !== name)); + router.reload(); + } catch (e) { + toast.error(`Ошибка при удалении модуля ${name}.`); + } + }; + + return ( + <> + + + + Менеджер модулей + + + + + + Имя + Версия + Статус + Действия + + + + {modules.map((mod) => ( + + {mod.name} + {mod.version} + + {mod.state} + + + + handleUnregister(mod.name)} + isLink={false} + delModal={{ + yes: 'Да', + no: 'Нет', + text: `Вы уверены, что хотите удалить модуль ${mod.name}? Это действие нельзя отменить.` + }} + /> + + + ))} + +
+
+
+ + ); +} diff --git a/fixture/apps/module-manager/ModuleManagerApp.ts b/fixture/apps/module-manager/ModuleManagerApp.ts new file mode 100644 index 00000000..42fa63a1 --- /dev/null +++ b/fixture/apps/module-manager/ModuleManagerApp.ts @@ -0,0 +1,183 @@ +import path from "path"; +import {AbstractAdminizerApp, AppSetupContext} from "../../../dist"; + +const MODULE_MANAGER_ACCESS_TOKEN = "module-manager"; + +interface ModuleManagerAppConfig { + route: string; + sidebarId: string; + title: string; + icon: string; + section: string; + componentFile: string; + devComponentUrl: string; +} + +export class ModuleManagerApp extends AbstractAdminizerApp { + readonly name = "module-manager"; + readonly version = "1.0.0"; + declare readonly config: ModuleManagerAppConfig; + + constructor(config: Partial = {}) { + super(); + this.config = { + route: "/module-manager", + sidebarId: "module-manager", + title: "Module Manager", + icon: "settings", + section: "Platform", + componentFile: path.resolve(import.meta.dirname, "ModuleManager.es.js"), + devComponentUrl: "/fixture/apps/module-manager/ModuleManager.tsx", + ...config, + }; + } + + setup(ctx: AppSetupContext): void { + ctx.accessRight({ + id: MODULE_MANAGER_ACCESS_TOKEN, + name: "Module manager", + description: "Access to module manager", + department: "Modules", + }); + + const moduleComponent = ctx.asset({ + id: "component", + filePath: this.config.componentFile, + devUrl: this.config.devComponentUrl, + }); + + const pageUrl = ctx.controller({ + id: "page", + method: "get", + route: this.config.route, + middleware: this.renderModule(moduleComponent), + policies: [{type: "permission", token: MODULE_MANAGER_ACCESS_TOKEN, mode: "ui"}], + }); + + ctx.controller({ + id: "enable", + method: "post", + route: `${this.config.route}/enable`, + middleware: this.enableModule, + policies: [{type: "permission", token: MODULE_MANAGER_ACCESS_TOKEN, mode: "api"}], + }); + + ctx.controller({ + id: "disable", + method: "post", + route: `${this.config.route}/disable`, + middleware: this.disableModule, + policies: [{type: "permission", token: MODULE_MANAGER_ACCESS_TOKEN, mode: "api"}], + }); + + ctx.controller({ + id: "unregister", + method: "post", + route: `${this.config.route}/unregister`, + middleware: this.unregisterModule, + policies: [{type: "permission", token: MODULE_MANAGER_ACCESS_TOKEN, mode: "api"}], + }); + + ctx.config({ + navbar: { + additionalLinks: [ + { + id: this.config.sidebarId, + link: pageUrl, + type: "self", + title: this.config.title, + icon: this.config.icon as any, + section: this.config.section, + accessRightsToken: MODULE_MANAGER_ACCESS_TOKEN, + }, + ], + }, + }); + } + + private renderModule(moduleComponent: string): MiddlewareType { + return async (req, res) => { + const modules = req.runtime.apps.list() + .filter(({name}) => name !== "module-manager") + .map(({name, version, state}) => ({ + name, + version, + state, + })); + + return req.Inertia.render({ + component: "module", + props: { + moduleComponent, + data: { + modules + }, + }, + }); + }; + } + + private enableModule: MiddlewareType = async (req, res) => { + const {name} = req.body; + if (!name) { + return res.status(400).json({error: "Name is required"}); + } + + if (name === "module-manager") { + return res.status(400).json({error: "The 'module-manager' app cannot be enabled through this interface"}); + } + + try { + await req.runtime.apps.enable(name); + return res.json({ + success: true, + }); + } catch (error) { + req.flash.setFlashMessage('error', `Failed to enable app ${name}.`); + return res.status(500).json({error: error.message}); + } + }; + + private disableModule: MiddlewareType = async (req, res) => { + const {name} = req.body; + if (!name) { + return res.status(400).json({error: "Name is required"}); + } + + if (name === "module-manager") { + return res.status(400).json({error: "The 'module-manager' app cannot be disabled"}); + } + + try { + await req.runtime.apps.disable(name); + return res.json({ + success: true + }) + } catch (error) { + req.flash.setFlashMessage('error', `Failed to disable app ${name}.`); + return res.status(500).json({error: error.message}); + } + }; + + private unregisterModule: MiddlewareType = async (req, res) => { + const {name} = req.body; + if (!name) { + return res.status(400).json({error: "Name is required"}); + } + + if (name === "module-manager") { + return res.status(400).json({error: "The 'module-manager' app cannot be unregistered"}); + } + + try { + await req.runtime.apps.unregister(name); + + return res.json({ + success: true + }) + } catch (error) { + req.flash.setFlashMessage('error', `Failed to unregister app ${name}.`); + return res.status(500).json({error: error.message}); + } + }; +} diff --git a/fixture/apps/module-manager/vite.config.module.ts b/fixture/apps/module-manager/vite.config.module.ts new file mode 100644 index 00000000..ea327059 --- /dev/null +++ b/fixture/apps/module-manager/vite.config.module.ts @@ -0,0 +1,104 @@ +import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import {viteExternalsPlugin} from "vite-plugin-externals"; + +export default defineConfig({ + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), + }, + plugins: [ + react(), + viteExternalsPlugin({ + react: 'React', + axios: 'axios', + sonner: 'sonner', + 'lucide-react': 'LucideReact', + 'react-dom': 'ReactDOM', + '@/components/ui/avatar': 'UIComponents', + '@/components/ui/badge': 'UIComponents', + '@/components/ui/breadcrumb': 'UIComponents', + '@/components/ui/button': 'UIComponents', + '@/components/ui/calendar': 'UIComponents', + '@/components/ui/card': 'UIComponents', + '@/components/ui/checkbox': 'UIComponents', + '@/components/ui/collapsible': 'UIComponents', + '@/components/ui/command': 'UIComponents', + '@/components/ui/context-menu': 'UIComponents', + '@/components/ui/dialog': 'UIComponents', + '@/components/ui/dialog-stack': 'UIComponents', + '@/components/ui/dropdown-menu': 'UIComponents', + '@/components/ui/input': 'UIComponents', + '@/components/ui/label': 'UIComponents', + '@/components/ui/menubar': 'UIComponents', + '@/components/ui/pagination': 'UIComponents', + '@/components/ui/popover': 'UIComponents', + '@/components/ui/select': 'UIComponents', + '@/components/ui/separator': 'UIComponents', + '@/components/ui/sheet': 'UIComponents', + '@/components/ui/sidebar': 'UIComponents', + '@/components/ui/skeleton': 'UIComponents', + '@/components/ui/slider': 'UIComponents', + '@/components/ui/sonner': 'UIComponents', + '@/components/ui/switch': 'UIComponents', + '@/components/ui/table': 'UIComponents', + '@/components/ui/textarea': 'UIComponents', + '@/components/ui/tooltip': 'UIComponents', + '@/components/modals/del-modal': ['JSComponents', 'DeleteModal'], + '@/lib/axios-compat': 'axios', + '@inertiajs/react': 'InertiajsReact' + }), + ], + build: { + outDir: path.resolve(import.meta.dirname, ''), + emptyOutDir: false, + lib: { + entry: path.resolve(import.meta.dirname, 'ModuleManager'), + name: 'ModuleManager', + formats: ['es'], + fileName: (format) => `ModuleManager.${format}.js`, + }, + rollupOptions: { + external: [ + 'tailwindcss', + '@/components/ui/avatar', + '@/components/ui/badge', + '@/components/ui/breadcrumb', + '@/components/ui/button', + '@/components/ui/calendar', + '@/components/ui/card', + '@/components/ui/checkbox', + '@/components/ui/collapsible', + '@/components/ui/command', + '@/components/ui/context-menu', + '@/components/ui/dialog', + '@/components/ui/dialog-stack', + '@/components/ui/dropdown-menu', + '@/components/ui/input', + '@/components/ui/label', + '@/components/ui/menubar', + '@/components/ui/pagination', + '@/components/ui/popover', + '@/components/ui/select', + '@/components/ui/separator', + '@/components/ui/sheet', + '@/components/ui/sidebar', + '@/components/ui/skeleton', + '@/components/ui/slider', + '@/components/ui/sonner', + '@/components/ui/switch', + '@/components/ui/table', + '@/components/ui/textarea', + '@/components/ui/tooltip', + '@/components/modals/del-modal', + '@/lib/axios-compat', + '@inertiajs/react' + ], + }, + }, + resolve: { + alias: { + '@': path.resolve(import.meta.dirname, '../../../src/assets/js'), + }, + }, +}); diff --git a/fixture/apps/navigation/NavigationApp.ts b/fixture/apps/navigation/NavigationApp.ts new file mode 100644 index 00000000..afe09bf2 --- /dev/null +++ b/fixture/apps/navigation/NavigationApp.ts @@ -0,0 +1,145 @@ +import path from "path"; +import {AbstractAdminizerApp, AppSetupContext} from "../../../dist"; +import {NavigationCatalog} from "./NavigationCatalog"; +import {navigationModelName} from "./NavigationModel"; +import type {NavigationAppConfig} from "./NavigationTypes"; + +const NAVIGATION_ACCESS_TOKEN = "catalog-navigation"; + +export class NavigationApp extends AbstractAdminizerApp { + readonly name = "navigation"; + readonly version = "1.0.0"; + declare readonly config: NavigationAppConfig; + + constructor(config: NavigationAppConfig) { + super(); + this.config = { + ...config, + model: config.model ?? navigationModelName, + routePrefix: config.routePrefix ?? "", + componentFile: config.componentFile ?? path.resolve(import.meta.dirname, "NavigationCatalogTemplates.es.js"), + devComponentUrl: config.devComponentUrl ?? "/fixture/apps/navigation/NavigationCatalogTemplates.tsx", + }; + } + + setup(ctx: AppSetupContext): void { + let navigationCatalog: NavigationCatalog | undefined; + + ctx.accessRight({ + id: NAVIGATION_ACCESS_TOKEN, + name: "Navigation", + description: "Access to edit navigation catalogs", + department: "Catalog", + }); + + const catalogTemplates = ctx.asset({ + id: "catalog-templates", + filePath: this.config.componentFile, + devUrl: this.config.devComponentUrl, + }); + + ctx.model({ + name: this.config.model, + }); + + ctx.modelAccess({ + models: [ + this.config.model, + ...this.config.items.map((item) => item.model), + ], + }); + + ctx.config({ + models: { + [this.config.model.toLowerCase()]: { + add: false, + fields: { + createdAt: {visible: false}, + updatedAt: {visible: false}, + }, + navbar: { + visible: true, + }, + icon: "storage", + identifierField: "", + list: { + fields: { + tree: {visible: false}, + id: {visible: false}, + }, + }, + model: this.config.model.toLowerCase(), + remove: false, + title: this.config.model, + tools: [], + }, + }, + navbar: { + additionalLinks: this.config.sections.map((section) => ({ + id: `navigation-${section}`, + type: "self", + link: `${this.config.routePrefix}/catalog/navigation/${section}`, + title: `Nav ${section.charAt(0).toUpperCase()}${section.slice(1)}`, + icon: "menu" as any, + accessRightsToken: NAVIGATION_ACCESS_TOKEN, + })), + }, + }); + + ctx.catalog({ + id: "navigation", + templates: [ + { + id: "model-link-template", + type: "navigation.model-link", + component: catalogTemplates, + exportName: "NavigationModelLinkTemplate", + }, + { + id: "group-template", + type: "navigation.group", + component: catalogTemplates, + exportName: "NavigationGroupTemplate", + }, + { + id: "link-template", + type: "navigation.link", + component: catalogTemplates, + exportName: "NavigationLinkTemplate", + }, + ], + factory: async (runtime) => { + const catalog = new NavigationCatalog(runtime, this.config); + await catalog.ready(); + navigationCatalog = catalog; + return catalog; + }, + }); + + ctx.listener("model:updated", async (event: { + modelName: string; + record: Record; + }) => { + if (!navigationCatalog) { + return; + } + + const itemConfig = this.config.items.find((item) => + item.model.toLowerCase() === event.modelName.toLowerCase() + ); + if (!itemConfig) { + return; + } + + for (const section of this.config.sections) { + navigationCatalog.setId(section); + const navItem = navigationCatalog.itemTypes.find((item) => + item.type === event.modelName.toLowerCase() + ); + if (navItem) { + await navItem.updateModelItems(event.record.id, {record: event.record}, section); + } + } + }); + } +} diff --git a/fixture/apps/navigation/NavigationCatalog.ts b/fixture/apps/navigation/NavigationCatalog.ts new file mode 100644 index 00000000..e19082e7 --- /dev/null +++ b/fixture/apps/navigation/NavigationCatalog.ts @@ -0,0 +1,575 @@ +import { + AbstractCatalog, + AbstractGroup, + AbstractItem, + ActionHandler, + AppRuntime, + BaseItem, + Item, + ModelConfig, +} from "../../../dist"; +import {v4 as uuid} from "uuid"; +import {NavigationModel} from "./NavigationModel"; +import type {NavigationAppConfig} from "./NavigationTypes"; + +export interface NavItem extends Item { + urlPath?: string; + modelId?: string | number; + targetBlank?: boolean; + visible?: boolean; +} + +export class NavigationStorage { + protected storageMap: Map = new Map(); + public readonly ready: Promise; + + constructor( + private runtime: AppRuntime, + private id: string, + private model: string + ) { + this.ready = this.initModel(); + } + + private navigationModel() { + return this.runtime.models.get(this.model); + } + + public async initModel(): Promise { + const navigation = await this.navigationModel().findOne({where: {label: this.id}}); + if (navigation) { + await this.populateFromTree(navigation.tree as unknown as any[]); + return; + } + + await this.navigationModel().create({ + label: this.id, + tree: [] as any, + }); + } + + public getId(): string { + return this.id; + } + + public async buildTree(): Promise { + const rootElements = await this.findElementsByParentId(null, null); + const buildSubTree = async (elements: NavItem[]): Promise => { + const tree = []; + for (const element of elements) { + const children = await this.findElementsByParentId(element.id, null); + tree.push({ + ...element, + children: await buildSubTree(children), + }); + } + return tree; + }; + + const tree = await buildSubTree(rootElements); + const sortTree = (items: any[]) => { + items.sort((a, b) => a.sortOrder - b.sortOrder); + for (const item of items) { + if (item.children) { + sortTree(item.children); + } + } + }; + + sortTree(tree); + return tree; + } + + public async populateFromTree(tree: any[]): Promise { + const traverseTree = async (node: any): Promise => { + const {children, ...itemData} = node; + const item = { + ...itemData, + parentId: itemData.parentId === 0 ? null : itemData.parentId, + } as NavItem; + await this.setElement(item.id, item, true); + + if (children?.length) { + for (const child of children) { + await traverseTree(child); + } + } + }; + + for (const node of tree) { + await traverseTree(node); + } + } + + public async setElement(_id: string | number, item: NavItem, init = false): Promise { + this.storageMap.set(item.id, item); + if (!init) { + await this.saveToDB(); + } + return this.findElementById(item.id); + } + + public async removeElementById(id: string | number): Promise { + this.storageMap.delete(id); + await this.saveToDB(); + } + + public async findElementById(id: string | number): Promise { + return this.storageMap.get(id); + } + + public async findElementByModelId(modelId: string | number): Promise { + const elements: NavItem[] = []; + for (const item of this.storageMap.values()) { + if (item.modelId === modelId) { + elements.push(item); + } + } + return elements; + } + + public async saveToDB(): Promise { + const tree = await this.buildTree(); + await this.navigationModel().update( + {where: {label: this.id}}, + {tree: tree as any} + ); + } + + public async findElementsByParentId(parentId: string | number | null, type: string | null): Promise { + const elements: NavItem[] = []; + const normalizedParentId = parentId === 0 ? null : parentId; + for (const item of this.storageMap.values()) { + if (type === null && item.parentId === normalizedParentId) { + elements.push(item); + continue; + } + if (item.parentId === normalizedParentId && item.type === type) { + elements.push(item); + } + } + return elements; + } + + public async search(s: string, type: string): Promise { + const lowerCaseQuery = s.toLowerCase(); + return Array.from(this.storageMap.values()).filter((item) => + item.type === type && item.name.toLowerCase().includes(lowerCaseQuery) + ); + } +} + +export class NavigationStorageServices { + protected storages: NavigationStorage[] = []; + + public add(storage: NavigationStorage): void { + this.storages.push(storage); + } + + public get(id: string): NavigationStorage | undefined { + return this.storages.find((storage) => storage.getId() === id); + } + + public async ready(): Promise { + await Promise.all(this.storages.map((storage) => storage.ready)); + } +} + +export class NavigationCatalog extends AbstractCatalog { + readonly name = "Navigation"; + readonly slug = "navigation"; + public readonly icon = "box"; + public readonly actionHandlers: ActionHandler[] = []; + public idList: string[] = []; + public storageServices: NavigationStorageServices; + + constructor(private runtime: AppRuntime, config: NavigationAppConfig) { + const storageServices = new NavigationStorageServices(); + const items: BaseItem[] = config.items.map((configElement) => new NavigationItem( + runtime, + configElement.title, + configElement.model, + config.model, + configElement.urlPath as string, + storageServices + )); + + items.push(new NavigationGroup(config.groupField, storageServices)); + items.push(new LinkItem(storageServices)); + + super(createLegacyCatalogHost(), items); + + this.storageServices = storageServices; + for (const section of config.sections) { + this.storageServices.add(new NavigationStorage(runtime, section, config.model)); + } + + this.movingGroupsRootOnly = config.movingGroupsRootOnly; + this.idList = config.sections ?? []; + } + + async getIdList(): Promise { + return this.idList; + } + + async ready(): Promise { + await this.storageServices.ready(); + } +} + +class NavigationItem extends AbstractItem { + readonly allowedRoot = true; + readonly icon: string; + readonly name: string; + readonly type: string; + public readonly adminizer = createLegacyCatalogHost(); + public readonly actionHandlers: ActionHandler[] = []; + + constructor( + private runtime: AppRuntime, + name: string, + private model: string, + private navigationModel: string, + private urlPath: string, + storageServices: NavigationStorageServices + ) { + super(); + this.name = name; + this.type = model.toLowerCase(); + const configModel = runtime.config.getModelConfig(model) as ModelConfig; + this.icon = configModel?.icon ?? "file_present"; + (this as any).storageServices = storageServices; + } + + private contentModel() { + return this.runtime.models.get(this.model); + } + + async create(data: any, catalogId: string): Promise { + const storage = this.getStorage(catalogId); + let storageData: NavItem; + if (data._method === "select") { + const record = await this.contentModel().findOne({where: {id: data.record}}); + storageData = await this.dataPreparation({ + record, + parentId: data.parentId, + targetBlank: data.targetBlank, + visible: data.visible, + }, catalogId); + } else { + storageData = await this.dataPreparation(data, catalogId); + } + return await storage.setElement(data.id, storageData); + } + + protected async dataPreparation(data: any, catalogId: string, sortOrder?: number): Promise { + const storage = this.getStorage(catalogId); + const record = data.record; + const urlPath = this.renderUrlPath(record); + let parentId = data.parentId ? data.parentId : null; + if (parentId === 0) parentId = null; + + return { + id: uuid(), + modelId: record.id, + targetBlank: data.targetBlank ?? record.targetBlank, + visible: data.visible ?? record.visible, + isNavigation: true, + name: record.name ?? record.title ?? record.id, + parentId, + sortOrder: sortOrder ?? (await storage.findElementsByParentId(parentId, null)).length, + icon: this.icon, + type: this.type, + urlPath, + } as NavItem; + } + + async updateModelItems(modelId: string | number, data: any, catalogId: string): Promise { + const storage = this.getStorage(catalogId); + const items = await storage.findElementByModelId(modelId); + const response = []; + for (const item of items) { + item.name = data.record.name ?? data.record.title ?? data.record.id; + item.urlPath = this.renderUrlPath(data.record); + if (item.id === data.record.treeId) { + item.targetBlank = data.record.targetBlank; + item.visible = data.record.visible; + } + response.push(await storage.setElement(item.id, item)); + } + return response[0]; + } + + async update(itemId: string | number, data: any, catalogId: string): Promise { + return await this.getStorage(catalogId).setElement(itemId, data); + } + + async deleteItem(itemId: string | number, catalogId: string): Promise { + await this.getStorage(catalogId).removeElementById(itemId); + } + + async find(itemId: string | number, catalogId: string): Promise { + return await this.getStorage(catalogId).findElementById(itemId); + } + + async getAddTemplate(req: ReqType): Promise<{ + type: string; + data: { + items: { id: string; name: string }[]; + model: string; + labels?: Record; + }; + }> { + const itemsDB = await this.contentModel().find({}); + const items = itemsDB.map((item: any) => ({ + id: item.id, + name: item.name ?? item.title ?? item.id, + })); + return { + type: "navigation.model-link", + data: { + items, + model: this.model, + labels: { + selectTitle: `${req.i18n.__("Select")} ${req.i18n.__(this.name + "s")}`, + createTitle: `${req.i18n.__("create new")} ${req.i18n.__(this.name + "s")}`, + OR: req.i18n.__("OR"), + openInNewWindow: req.i18n.__("Open in a new window"), + visible: req.i18n.__("Visible"), + }, + }, + }; + } + + async getChilds(parentId: string | number | null, catalogId: string): Promise { + return await this.getStorage(catalogId).findElementsByParentId(parentId, this.type); + } + + async getEditTemplate(id: string | number, catalogId: string): Promise<{ + type: string; + data: { item: NavItem; model: string }; + }> { + return { + type: "navigation.model-link", + data: { + item: await this.find(id, catalogId), + model: this.model, + }, + }; + } + + async search(s: string, catalogId: string): Promise { + return await this.getStorage(catalogId).search(s, this.type); + } + + private getStorage(catalogId: string): NavigationStorage { + const storage = ((this as any).storageServices as NavigationStorageServices).get(catalogId); + if (!storage) { + throw new Error(`Navigation storage "${catalogId}" was not found`); + } + return storage; + } + + private renderUrlPath(record: any): string { + return this.urlPath.replace(/\$\{data\.record\.([^}]+)\}/g, (_match, field) => + encodeURIComponent(record?.[field] ?? "") + ); + } +} + +class NavigationGroup extends AbstractGroup { + readonly allowedRoot = true; + readonly name: string = "Group"; + readonly groupField: object[]; + public readonly adminizer = createLegacyCatalogHost(); + + constructor(groupField: object[], storageServices: NavigationStorageServices) { + super(); + this.groupField = groupField; + (this as any).storageServices = storageServices; + } + + async create(data: any, catalogId: string): Promise { + let storageData = await this.dataPreparation(data, catalogId); + delete data.name; + delete data.parentId; + storageData = {...storageData, ...data}; + return await this.getStorage(catalogId).setElement(storageData.id, storageData); + } + + protected async dataPreparation(data: any, catalogId: string, sortOrder?: number): Promise { + const storage = this.getStorage(catalogId); + let parentId = data.parentId ? data.parentId : null; + if (parentId === 0) parentId = null; + return { + id: uuid(), + name: data.name, + targetBlank: data.targetBlank, + visible: data.visible, + isNavigation: true, + parentId, + sortOrder: sortOrder ?? (await storage.findElementsByParentId(parentId, null)).length, + icon: this.icon, + type: this.type, + } as NavItem; + } + + async deleteItem(itemId: string | number, catalogId: string): Promise { + await this.getStorage(catalogId).removeElementById(itemId); + } + + async find(itemId: string | number, catalogId: string): Promise { + return await this.getStorage(catalogId).findElementById(itemId); + } + + async update(itemId: string | number, data: any, catalogId: string): Promise { + return await this.getStorage(catalogId).setElement(itemId, data); + } + + async updateModelItems(modelId: string | number, data: NavItem, catalogId: string): Promise { + return await this.getStorage(catalogId).setElement(modelId, data); + } + + getAddTemplate(req: ReqType): Promise<{ + type: string; + data: { + items?: { name: string; required: boolean }[] | Record[]; + model?: string; + labels?: Record; + }; + }> { + const items = this.groupField.map((field: any) => ({ + name: field.name, + label: field.label, + required: field.required, + })); + return Promise.resolve({ + type: "navigation.group", + data: { + items, + labels: { + openInNewWindow: req.i18n.__("Open in a new window"), + visible: req.i18n.__("Visible"), + title: req.i18n.__("Title"), + save: req.i18n.__("Save"), + }, + }, + }); + } + + async getEditTemplate(id: string | number, catalogId: string, req: ReqType): Promise<{ + type: string; + data: { + items?: { name: string; required: boolean }[] | Record[]; + item?: NavItem; + labels?: Record; + }; + }> { + const items = this.groupField.map((field: any) => ({ + name: field.name, + label: field.label, + required: field.required, + })); + return { + type: "navigation.group", + data: { + items, + item: await this.find(id, catalogId), + labels: { + openInNewWindow: req.i18n.__("Open in a new window"), + visible: req.i18n.__("Visible"), + title: req.i18n.__("Title"), + save: req.i18n.__("Save"), + }, + }, + }; + } + + async getChilds(parentId: string | number | null, catalogId: string): Promise { + return await this.getStorage(catalogId).findElementsByParentId(parentId, this.type); + } + + async search(s: string, catalogId: string): Promise { + return await this.getStorage(catalogId).search(s, this.type); + } + + private getStorage(catalogId: string): NavigationStorage { + const storage = ((this as any).storageServices as NavigationStorageServices).get(catalogId); + if (!storage) { + throw new Error(`Navigation storage "${catalogId}" was not found`); + } + return storage; + } +} + +class LinkItem extends NavigationGroup { + readonly allowedRoot = true; + readonly icon = "insert_link"; + readonly name: string = "Link"; + readonly type = "link"; + readonly isGroup = false; + + constructor(storageServices: NavigationStorageServices) { + super([], storageServices); + } + + getAddTemplate(req: ReqType): Promise<{ + type: string; + data: { + items?: { name: string; required: boolean; label: string }[]; + labels?: Record; + }; + }> { + return Promise.resolve({ + type: "navigation.link", + data: { + items: [{ + label: req.i18n.__("Link"), + name: "link", + required: true, + }], + labels: { + title: req.i18n.__("Title"), + openInNewWindow: req.i18n.__("Open in a new window"), + visible: req.i18n.__("Visible"), + save: req.i18n.__("Save"), + }, + }, + }); + } + + async getEditTemplate(id: string | number, catalogId: string, req: ReqType): Promise<{ + type: string; + data: { + items?: { name: string; required: boolean; label: string }[]; + item?: NavItem; + labels?: Record; + }; + }> { + return { + type: "navigation.link", + data: { + items: [{ + label: req.i18n.__("Link"), + name: "link", + required: true, + }], + item: await this.find(id, catalogId), + labels: { + openInNewWindow: req.i18n.__("Open in a new window"), + visible: req.i18n.__("Visible"), + title: req.i18n.__("Title"), + save: req.i18n.__("Save"), + }, + }, + }; + } +} + +function createLegacyCatalogHost(): any { + return { + accessRightsHelper: { + registerToken: (): void => undefined, + }, + }; +} diff --git a/fixture/apps/navigation/NavigationCatalogTemplates.es.js b/fixture/apps/navigation/NavigationCatalogTemplates.es.js new file mode 100644 index 00000000..461556be --- /dev/null +++ b/fixture/apps/navigation/NavigationCatalogTemplates.es.js @@ -0,0 +1,263 @@ +var b = { exports: {} }, f = {}; +var C; +function A() { + if (C) return f; + C = 1; + var n = /* @__PURE__ */ Symbol.for("react.transitional.element"), m = /* @__PURE__ */ Symbol.for("react.fragment"); + function c(i, s, t) { + var a = null; + if (t !== void 0 && (a = "" + t), s.key !== void 0 && (a = "" + s.key), "key" in s) { + t = {}; + for (var d in s) + d !== "key" && (t[d] = s[d]); + } else t = s; + return s = t.ref, { + $$typeof: n, + type: i, + key: a, + ref: s !== void 0 ? s : null, + props: t + }; + } + return f.Fragment = m, f.jsx = c, f.jsxs = c, f; +} +var N; +function M() { + return N || (N = 1, b.exports = A()), b.exports; +} +var e = M(); +const S = window.React.useEffect, p = window.React.useState, U = window.JSComponents.AddForm, y = window.UIComponents.Button, I = window.UIComponents.Checkbox, T = window.UIComponents.Input, v = window.UIComponents.Label, V = window.UIComponents.Select, P = window.UIComponents.SelectContent, $ = window.UIComponents.SelectItem, q = window.UIComponents.SelectTrigger, J = window.UIComponents.SelectValue, R = window.LucideReact.LoaderCircle, z = window.LucideReact.Plus, j = window.adminApi; +function G(n) { + return n.mode === "update" ? /* @__PURE__ */ e.jsx(O, { ...n }) : /* @__PURE__ */ e.jsx(D, { ...n }); +} +function D({ template: n, itemType: m, parentId: c, actions: i }) { + const [s, t] = p(!1), [a, d] = p(!1), [x, u] = p(!1), l = n.data, h = async (r) => { + try { + u(!0), (await j.post("", { + data: { + record: r, + parentId: c, + targetBlank: s, + visible: a, + _method: "select", + type: m + }, + _method: "createItem" + })).data && (i.close(), await i.reload(null)); + } catch (w) { + console.error(w); + } finally { + u(!1); + } + }; + return /* @__PURE__ */ e.jsxs("div", { className: "p-8", children: [ + /* @__PURE__ */ e.jsxs("div", { className: "flex gap-4 flex-col", children: [ + /* @__PURE__ */ e.jsxs("div", { className: `grid gap-4 ${l.items.length ? "" : "opacity-50 pointer-events-none"}`, children: [ + /* @__PURE__ */ e.jsx(v, { children: l.labels.selectTitle }), + /* @__PURE__ */ e.jsxs(V, { onValueChange: h, disabled: x, children: [ + /* @__PURE__ */ e.jsx(q, { className: "w-full max-w-[170px] cursor-pointer", children: /* @__PURE__ */ e.jsx(J, { placeholder: l.labels.selectTitle }) }), + /* @__PURE__ */ e.jsx(P, { className: "z-[1003]", children: l.items.map((r) => /* @__PURE__ */ e.jsx($, { value: r.id.toString(), children: r.name }, `${r.id}-${r.name}`)) }) + ] }) + ] }), + /* @__PURE__ */ e.jsx( + E, + { + labels: l.labels, + targetBlank: s, + visible: a, + onTargetBlankChange: t, + onVisibleChange: d + } + ) + ] }), + /* @__PURE__ */ e.jsx("div", { className: "mt-8", children: /* @__PURE__ */ e.jsx("span", { children: /* @__PURE__ */ e.jsx("b", { children: l.labels.OR }) }) }), + /* @__PURE__ */ e.jsxs(y, { className: "mt-8", onClick: () => i.openModelAdd(l.model), children: [ + /* @__PURE__ */ e.jsx(z, {}), + l.labels.createTitle + ] }) + ] }); +} +function O({ template: n, selectedItem: m, messages: c, actions: i }) { + const [s, t] = p(null), a = n.data.item, d = n.data.model ?? a.type ?? m?.type; + S(() => { + (async () => { + const l = await j.get(`${window.routePrefix}/model/${d}/edit/${a.modelId}?without_layout=true`); + t(l.data); + })().catch(console.error); + }, [a.modelId, d]); + const x = async (u, l, h) => { + const r = u[0]; + r.targetBlank = l, r.visible = h, r.treeId = a.id; + const w = await j.put("", { + type: a.type, + data: { record: r }, + modelId: a.modelId, + _method: "updateItem" + }); + i.close(), await i.reload(w.data.data); + }; + return s ? /* @__PURE__ */ e.jsx( + U, + { + page: s, + catalog: !0, + callback: x, + openNewWindowLabel: c["Open in a new window"], + visibleLable: c.Visible, + openNewWindow: a.targetBlank, + DnavVisible: a.visible, + isNavigation: !0 + } + ) : /* @__PURE__ */ e.jsx(R, { className: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-8 animate-spin" }); +} +function W(n) { + return /* @__PURE__ */ e.jsx(L, { ...n, templateType: "group" }); +} +function Y(n) { + return /* @__PURE__ */ e.jsx(L, { ...n, templateType: "link" }); +} +function L({ + mode: n, + template: m, + parentId: c, + actions: i, + templateType: s +}) { + const t = m.data, [a, d] = p({}), [x, u] = p(!1), [l, h] = p(!1), [r, w] = p(!1); + S(() => { + if (t.item) { + d({ + name: t.item.name || "", + ...Object.fromEntries(t.items.map((o) => [ + o.name, + t.item ? t.item[o.name] : "" + ])) + }), u(t.item.targetBlank || !1), h(t.item.visible || !1); + return; + } + d({ + name: "", + ...Object.fromEntries(t.items.map((o) => [o.name, ""])) + }), u(!1), h(!1); + }, [t.item, t.items]); + const k = (o) => { + const { name: g, value: _ } = o.target; + d((F) => ({ ...F, [g]: _ })); + }, B = async (o) => { + o.preventDefault(), w(!0); + try { + if (n === "update") { + const g = await j.put("", { + type: t.item?.type, + modelId: t.item?.id, + data: { + ...t.item, + ...a, + targetBlank: x, + visible: l + }, + _method: "updateItem" + }); + i.close(), await i.reload(g.data.data); + return; + } + await j.post("", { + data: { + ...a, + targetBlank: x, + visible: l, + parentId: c, + type: s + }, + _method: "createItem" + }), i.close(), await i.reload(null); + } catch (g) { + console.error(g); + } finally { + w(!1); + } + }; + return /* @__PURE__ */ e.jsxs("div", { className: "p-8", children: [ + /* @__PURE__ */ e.jsxs("form", { className: "grid gap-6", id: "navigation-group-link-template", onSubmit: B, children: [ + /* @__PURE__ */ e.jsx( + E, + { + labels: t.labels, + targetBlank: x, + visible: l, + onTargetBlankChange: u, + onVisibleChange: h + } + ), + /* @__PURE__ */ e.jsxs("div", { className: "grid gap-4", children: [ + /* @__PURE__ */ e.jsx(v, { htmlFor: "name", children: t.labels.title }), + /* @__PURE__ */ e.jsx( + T, + { + required: !0, + value: a.name || "", + name: "name", + placeholder: t.labels.title, + onChange: k + } + ) + ] }), + t.items.map((o) => /* @__PURE__ */ e.jsxs("div", { className: "grid gap-4", children: [ + /* @__PURE__ */ e.jsx(v, { children: o.label }), + /* @__PURE__ */ e.jsx( + T, + { + required: o.required, + value: a[o.name] || "", + name: o.name, + placeholder: o.label, + onChange: k + } + ) + ] }, o.name)) + ] }), + /* @__PURE__ */ e.jsxs(y, { className: "mt-8 w-fit", form: "navigation-group-link-template", type: "submit", disabled: r, children: [ + t.labels.save, + r && /* @__PURE__ */ e.jsx(R, { className: "h-4 w-4 animate-spin ml-2" }) + ] }) + ] }); +} +function E({ + labels: n, + targetBlank: m, + visible: c, + onTargetBlankChange: i, + onVisibleChange: s +}) { + return /* @__PURE__ */ e.jsxs(e.Fragment, { children: [ + /* @__PURE__ */ e.jsxs("div", { className: "flex gap-4 items-center", children: [ + /* @__PURE__ */ e.jsx( + I, + { + id: "targetBlank", + checked: m, + onCheckedChange: (t) => i(!!t), + className: "cursor-pointer size-5" + } + ), + /* @__PURE__ */ e.jsx(v, { htmlFor: "targetBlank", children: n.openInNewWindow }) + ] }), + /* @__PURE__ */ e.jsxs("div", { className: "flex gap-4 items-center", children: [ + /* @__PURE__ */ e.jsx( + I, + { + id: "visible", + checked: c, + onCheckedChange: (t) => s(!!t), + className: "cursor-pointer size-5" + } + ), + /* @__PURE__ */ e.jsx(v, { htmlFor: "visible", children: n.visible }) + ] }) + ] }); +} +export { + W as NavigationGroupTemplate, + Y as NavigationLinkTemplate, + G as NavigationModelLinkTemplate +}; diff --git a/fixture/apps/navigation/NavigationCatalogTemplates.tsx b/fixture/apps/navigation/NavigationCatalogTemplates.tsx new file mode 100644 index 00000000..51f180a3 --- /dev/null +++ b/fixture/apps/navigation/NavigationCatalogTemplates.tsx @@ -0,0 +1,305 @@ +import React, {useEffect, useState} from "react"; +import AddForm from "@/components/add-form"; +import {Button} from "@/components/ui/button"; +import {Checkbox} from "@/components/ui/checkbox"; +import {Input} from "@/components/ui/input"; +import {Label} from "@/components/ui/label"; +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"; +import type {AddCatalogProps, CatalogTemplateComponentProps} from "@/types"; +import {LoaderCircle, Plus} from "lucide-react"; + +const adminApi = window.adminApi; + +export function NavigationModelLinkTemplate(props: CatalogTemplateComponentProps) { + if (props.mode === "update") { + return ; + } + + return ; +} + +function NavigationModelLinkCreateTemplate({template, itemType, parentId, actions}: CatalogTemplateComponentProps) { + const [targetBlank, setTargetBlank] = useState(false); + const [visible, setVisible] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const data = template.data; + + const handleSelect = async (value: string) => { + try { + setIsSubmitting(true); + const res = await adminApi.post("", { + data: { + record: value, + parentId, + targetBlank, + visible, + _method: "select", + type: itemType, + }, + _method: "createItem", + }); + if (res.data) { + actions.close(); + await actions.reload(null); + } + } catch (e) { + console.error(e); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+
+ + +
+ +
+
+ {data.labels.OR} +
+ +
+ ); +} + +function NavigationModelLinkEditTemplate({template, selectedItem, messages, actions}: CatalogTemplateComponentProps) { + const [addProps, setAddProps] = useState(null); + const item = template.data.item; + const model = template.data.model ?? item.type ?? selectedItem?.type; + + useEffect(() => { + const loadModelForm = async () => { + const res = await adminApi.get(`${window.routePrefix}/model/${model}/edit/${item.modelId}?without_layout=true`); + setAddProps(res.data); + }; + loadModelForm().catch(console.error); + }, [item.modelId, model]); + + const updateModelLink = async (record: any, targetBlank?: boolean, visible?: boolean) => { + const updatedRecord = record[0]; + updatedRecord.targetBlank = targetBlank; + updatedRecord.visible = visible; + updatedRecord.treeId = item.id; + + const res = await adminApi.put("", { + type: item.type, + data: {record: updatedRecord}, + modelId: item.modelId, + _method: "updateItem", + }); + + actions.close(); + await actions.reload(res.data.data); + }; + + if (!addProps) { + return ; + } + + return ( + + ); +} + +export function NavigationGroupTemplate(props: CatalogTemplateComponentProps) { + return ; +} + +export function NavigationLinkTemplate(props: CatalogTemplateComponentProps) { + return ; +} + +function NavigationGroupLinkTemplate({ + mode, + template, + parentId, + actions, + templateType, +}: CatalogTemplateComponentProps & { templateType: "group" | "link" }) { + const data = template.data; + const [formData, setFormData] = useState>({}); + const [targetBlank, setTargetBlank] = useState(false); + const [visible, setVisible] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (data.item) { + setFormData({ + name: data.item.name || "", + ...Object.fromEntries(data.items.map((item: { name: string }) => [ + item.name, + data.item ? data.item[item.name] : "", + ])), + }); + setTargetBlank(data.item.targetBlank || false); + setVisible(data.item.visible || false); + return; + } + + setFormData({ + name: "", + ...Object.fromEntries(data.items.map((item: { name: string }) => [item.name, ""])), + }); + setTargetBlank(false); + setVisible(false); + }, [data.item, data.items]); + + const handleChange = (e: React.ChangeEvent) => { + const {name, value} = e.target; + setFormData((prev) => ({...prev, [name]: value})); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + try { + if (mode === "update") { + const res = await adminApi.put("", { + type: data.item?.type, + modelId: data.item?.id, + data: { + ...data.item, + ...formData, + targetBlank, + visible, + }, + _method: "updateItem", + }); + actions.close(); + await actions.reload(res.data.data); + return; + } + + await adminApi.post("", { + data: { + ...formData, + targetBlank, + visible, + parentId, + type: templateType, + }, + _method: "createItem", + }); + actions.close(); + await actions.reload(null); + } catch (e) { + console.error(e); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ ); +} + +function NavigationFlags({ + labels, + targetBlank, + visible, + onTargetBlankChange, + onVisibleChange, +}: { + labels: Record; + targetBlank: boolean; + visible: boolean; + onTargetBlankChange: (value: boolean) => void; + onVisibleChange: (value: boolean) => void; +}) { + return ( + <> +
+ onTargetBlankChange(!!checked)} + className="cursor-pointer size-5" + /> + +
+
+ onVisibleChange(!!checked)} + className="cursor-pointer size-5" + /> + +
+ + ); +} diff --git a/fixture/apps/navigation/NavigationModel.ts b/fixture/apps/navigation/NavigationModel.ts new file mode 100644 index 00000000..ffba33ae --- /dev/null +++ b/fixture/apps/navigation/NavigationModel.ts @@ -0,0 +1,45 @@ +import {DataTypes, Sequelize} from "sequelize"; + +export const navigationModelName = "Navigation"; + +export async function installNavigationSequelizeModel( + orm: Sequelize, + modelName: string = navigationModelName, + sync: boolean = false +): Promise { + let model = Object.values(orm.models).find( + (registeredModel) => registeredModel.name.toLowerCase() === modelName.toLowerCase() + ); + + if (!model) { + model = orm.define(modelName, { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + label: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + tree: { + type: DataTypes.JSON, + allowNull: false, + }, + }, { + tableName: modelName.toLowerCase(), + timestamps: true, + }); + } + + if (sync) { + await model.sync(); + } +} + +export interface NavigationModel { + id?: number; + label: string; + tree: Record; +} diff --git a/fixture/apps/navigation/NavigationTypes.ts b/fixture/apps/navigation/NavigationTypes.ts new file mode 100644 index 00000000..a77b6586 --- /dev/null +++ b/fixture/apps/navigation/NavigationTypes.ts @@ -0,0 +1,23 @@ +export interface NavigationItemConfig { + model: string; + title: string; + urlPath: string | ((value: any) => string); +} + +export interface NavigationGroupField { + name: string; + required: boolean; + label: string; +} + +export interface NavigationAppConfig { + model?: string; + routePrefix?: string; + componentFile?: string; + devComponentUrl?: string; + sections: string[]; + groupField: NavigationGroupField[]; + allowContentInGroup?: boolean; + items: NavigationItemConfig[]; + movingGroupsRootOnly?: boolean; +} diff --git a/fixture/apps/navigation/navigationConfig.ts b/fixture/apps/navigation/navigationConfig.ts new file mode 100644 index 00000000..ca841b0b --- /dev/null +++ b/fixture/apps/navigation/navigationConfig.ts @@ -0,0 +1,36 @@ +import {routePrefix} from "../../adminizerConfig"; +import type {NavigationAppConfig} from "./NavigationTypes"; + +export const navigationAppConfig: NavigationAppConfig = { + routePrefix, + items: [ + { + title: "Category", + model: "Category", + urlPath: "/catalog/category/${data.record.slug}", + }, + { + title: "Example", + model: "Example", + urlPath: `${routePrefix}/model/Example/\${data.record.id}`, + }, + ], + sections: ["header", "footer"], + groupField: [ + { + name: "link", + label: "Ссылка", + required: false, + }, + { + name: "test_field", + label: "Test", + required: false, + }, + { + name: "test_feild2", + label: "Test2", + required: false, + }, + ], +}; diff --git a/fixture/apps/navigation/vite.config.module.ts b/fixture/apps/navigation/vite.config.module.ts new file mode 100644 index 00000000..7b183cef --- /dev/null +++ b/fixture/apps/navigation/vite.config.module.ts @@ -0,0 +1,74 @@ +import {defineConfig} from "vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; +import {viteExternalsPlugin} from "vite-plugin-externals"; + +const uiExternals = { + react: "React", + "react-dom": "ReactDOM", + "lucide-react": "LucideReact", + sonner: "sonner", + "@/lib/axios-compat": "axios", + "@/components/add-form": ["JSComponents", "AddForm"], + "@/components/add-form.tsx": ["JSComponents", "AddForm"], + "@/components/ui/avatar": "UIComponents", + "@/components/ui/badge": "UIComponents", + "@/components/ui/breadcrumb": "UIComponents", + "@/components/ui/button": "UIComponents", + "@/components/ui/calendar": "UIComponents", + "@/components/ui/card": "UIComponents", + "@/components/ui/checkbox": "UIComponents", + "@/components/ui/collapsible": "UIComponents", + "@/components/ui/command": "UIComponents", + "@/components/ui/context-menu": "UIComponents", + "@/components/ui/dialog": "UIComponents", + "@/components/ui/dialog-stack": "UIComponents", + "@/components/ui/dropdown-menu": "UIComponents", + "@/components/ui/input": "UIComponents", + "@/components/ui/label": "UIComponents", + "@/components/ui/menubar": "UIComponents", + "@/components/ui/pagination": "UIComponents", + "@/components/ui/popover": "UIComponents", + "@/components/ui/select": "UIComponents", + "@/components/ui/separator": "UIComponents", + "@/components/ui/sheet": "UIComponents", + "@/components/ui/sidebar": "UIComponents", + "@/components/ui/skeleton": "UIComponents", + "@/components/ui/slider": "UIComponents", + "@/components/ui/sonner": "UIComponents", + "@/components/ui/switch": "UIComponents", + "@/components/ui/table": "UIComponents", + "@/components/ui/textarea": "UIComponents", + "@/components/ui/tooltip": "UIComponents", +}; + +export default defineConfig({ + define: { + "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "production"), + }, + plugins: [ + react(), + viteExternalsPlugin(uiExternals), + ], + build: { + outDir: path.resolve(import.meta.dirname, ""), + emptyOutDir: false, + lib: { + entry: path.resolve(import.meta.dirname, "NavigationCatalogTemplates"), + name: "NavigationCatalogTemplates", + formats: ["es"], + fileName: (format) => `NavigationCatalogTemplates.${format}.js`, + }, + rollupOptions: { + external: [ + "tailwindcss", + ...Object.keys(uiExternals), + ], + }, + }, + resolve: { + alias: { + "@": path.resolve(import.meta.dirname, "../../../src/assets/js"), + }, + }, +}); diff --git a/fixture/apps/notification-sender/NotificationSender.es.js b/fixture/apps/notification-sender/NotificationSender.es.js new file mode 100644 index 00000000..1b673c85 --- /dev/null +++ b/fixture/apps/notification-sender/NotificationSender.es.js @@ -0,0 +1,105 @@ +var x = { exports: {} }, c = {}; +var h; +function g() { + if (h) return c; + h = 1; + var d = /* @__PURE__ */ Symbol.for("react.transitional.element"), l = /* @__PURE__ */ Symbol.for("react.fragment"); + function a(i, t, n) { + var r = null; + if (n !== void 0 && (r = "" + n), t.key !== void 0 && (r = "" + t.key), "key" in t) { + n = {}; + for (var o in t) + o !== "key" && (n[o] = t[o]); + } else n = t; + return t = n.ref, { + $$typeof: d, + type: i, + key: r, + ref: t !== void 0 ? t : null, + props: n + }; + } + return c.Fragment = l, c.jsx = a, c.jsxs = a, c; +} +var w; +function R() { + return w || (w = 1, x.exports = g()), x.exports; +} +var e = R(); +const u = window.React.useState, I = window.UIComponents.Button, T = window.UIComponents.Select, k = window.UIComponents.SelectContent, j = window.UIComponents.SelectItem, U = window.UIComponents.SelectTrigger, N = window.UIComponents.SelectValue, b = window.axios, E = window.UIComponents.Textarea, p = window.UIComponents.Label, L = window.UIComponents.Checkbox, _ = window.UIComponents.Toaster, f = window.sonner.toast, A = window.LucideReact; +function F({ data: d }) { + const [l, a] = u(""), [i, t] = u(""), [n, r] = u(!1), [o, v] = u(!1), C = async (s) => { + s.preventDefault(), r(!0); + try { + const m = { + message: i, + ...o ? { sendToAll: !0 } : { userId: l } + }, S = await b.post("", m); + console.log(S.data), f.success("Сообщение успешно отправлено!"); + } catch (m) { + console.error(m), f.error("Ошибка при отправке сообщения."); + } finally { + r(!1); + } + }; + return /* @__PURE__ */ e.jsxs(e.Fragment, { children: [ + /* @__PURE__ */ e.jsx(_, { position: "top-center", richColors: !0, closeButton: !0 }), + /* @__PURE__ */ e.jsx("div", { className: "grid gap-4", children: /* @__PURE__ */ e.jsxs("form", { onSubmit: C, className: "flex flex-col gap-4 max-w-[400px]", children: [ + /* @__PURE__ */ e.jsxs("div", { className: "flex items-center gap-2", children: [ + /* @__PURE__ */ e.jsx( + L, + { + id: "send-to-all", + checked: o, + onCheckedChange: (s) => { + v(s), s && a(""); + } + } + ), + /* @__PURE__ */ e.jsx(p, { htmlFor: "send-to-all", children: "Всем пользователям" }) + ] }), + /* @__PURE__ */ e.jsxs("div", { children: [ + /* @__PURE__ */ e.jsx(p, { htmlFor: "user-select", className: "block mb-2 font-medium", children: "Выберите пользователя:" }), + /* @__PURE__ */ e.jsxs( + T, + { + onValueChange: (s) => a(s), + value: l, + disabled: o, + children: [ + /* @__PURE__ */ e.jsx(U, { id: "user-select", className: "w-full", children: /* @__PURE__ */ e.jsx(N, { placeholder: "-- Выберите --" }) }), + /* @__PURE__ */ e.jsx(k, { children: d?.users?.length > 0 ? d.users.map((s) => /* @__PURE__ */ e.jsx(j, { value: s.id.toString(), children: s.fullName }, s.id)) : /* @__PURE__ */ e.jsx(j, { value: "", disabled: !0, children: "Пользователей нет" }) }) + ] + } + ) + ] }), + /* @__PURE__ */ e.jsxs("div", { children: [ + /* @__PURE__ */ e.jsx(p, { htmlFor: "message-input", className: "block mb-2 font-medium", children: "Сообщение:" }), + /* @__PURE__ */ e.jsx( + E, + { + id: "message-input", + value: i, + onChange: (s) => t(s.target.value), + placeholder: "Введите сообщение..." + } + ) + ] }), + /* @__PURE__ */ e.jsxs("div", { className: "flex gap-2 items-center", children: [ + /* @__PURE__ */ e.jsx( + I, + { + type: "submit", + className: "w-fit", + disabled: n || !o && !l || !i.trim(), + children: n ? "Отправка..." : "Отправить" + } + ), + n && /* @__PURE__ */ e.jsx(A.Loader2, { className: "w-6 h-6 animate-spin" }) + ] }) + ] }) }) + ] }); +} +export { + F as default +}; diff --git a/modules/test/ComponentB.tsx b/fixture/apps/notification-sender/NotificationSender.tsx similarity index 97% rename from modules/test/ComponentB.tsx rename to fixture/apps/notification-sender/NotificationSender.tsx index 005273ee..66b32b13 100644 --- a/modules/test/ComponentB.tsx +++ b/fixture/apps/notification-sender/NotificationSender.tsx @@ -7,7 +7,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import axios from "axios"; +import axios from "@/lib/axios-compat"; import {Textarea} from "@/components/ui/textarea"; import {Label} from "@/components/ui/label"; import {Checkbox} from "@/components/ui/checkbox"; @@ -24,7 +24,7 @@ interface AppContentProps { }; } -export default function allUsersNotification({data}: AppContentProps) { +export default function NotificationSender({data}: AppContentProps) { const [selectedUser, setSelectedUser] = useState(""); const [inputMessage, setInputMessage] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -122,4 +122,4 @@ export default function allUsersNotification({data}: AppContentProps) { ); -} \ No newline at end of file +} diff --git a/fixture/apps/notification-sender/NotificationSenderApp.ts b/fixture/apps/notification-sender/NotificationSenderApp.ts new file mode 100644 index 00000000..cd40f79a --- /dev/null +++ b/fixture/apps/notification-sender/NotificationSenderApp.ts @@ -0,0 +1,118 @@ +import path from "path"; +import { + AbstractAdminizerApp, + AdminpanelIcon, + AppSetupContext, + User, +} from "../../../dist"; + +interface NotificationSenderAppConfig { + route: string; + sidebarId: string; + title: string; + icon: AdminpanelIcon; + section: string; + componentFile: string; + devComponentUrl: string; +} + +export class NotificationSenderApp extends AbstractAdminizerApp { + readonly name = "notification-sender"; + readonly version = "1.0.0"; + declare readonly config: NotificationSenderAppConfig; + + constructor(config: Partial = {}) { + super(); + this.config = { + route: "/notification-sender", + sidebarId: "notification-sender", + title: "Notification Sender", + icon: "360", + section: "Platform", + componentFile: path.resolve(import.meta.dirname, "NotificationSender.es.js"), + devComponentUrl: "/fixture/apps/notification-sender/NotificationSender.tsx", + ...config, + }; + } + + setup(ctx: AppSetupContext): void { + ctx.modelAccess({ + id: "users", + models: ["UserAP"], + }); + + const moduleComponent = ctx.asset({ + id: "component", + filePath: this.config.componentFile, + devUrl: this.config.devComponentUrl, + }); + + const pageUrl = ctx.controller({ + id: "page", + method: "get", + route: this.config.route, + middleware: this.renderModule(moduleComponent), + policies: [{type: "auth", mode: "ui"}], + }); + + ctx.controller({ + id: "send-notification", + method: "post", + route: this.config.route, + middleware: this.sendNotification, + policies: [{type: "auth", mode: "api"}], + }); + + ctx.config({ + navbar: { + additionalLinks: [{ + id: this.config.sidebarId, + link: pageUrl, + type: "self", + title: this.config.title, + icon: this.config.icon, + section: this.config.section, + }], + }, + }); + } + + private renderModule(moduleComponent: string): MiddlewareType { + return async (req, res) => { + const users = await req.runtime.models.get("UserAP").find({}); + + return req.Inertia.render({ + component: "module", + props: { + moduleComponent, + data: { + users, + }, + }, + }); + }; + } + + private sendNotification: MiddlewareType = async (req, res) => { + const rawUserId = req.body.userId; + const userId = req.body.sendToAll ? undefined : Number(rawUserId); + + if (!req.body.sendToAll && (!Number.isInteger(userId) || userId <= 0)) { + return res.status(400).json({ + error: "Invalid userId", + }); + } + + await req.runtime.notifications.send({ + title: "User message", + message: req.body.message, + notificationClass: "general", + channel: "", + ...(userId ? {userId} : {}), + }); + + return res.json({ + data: req.body, + }); + }; +} diff --git a/modules/test/vite.config.module.ts b/fixture/apps/notification-sender/vite.config.module.ts similarity index 91% rename from modules/test/vite.config.module.ts rename to fixture/apps/notification-sender/vite.config.module.ts index 9e4801b8..f23f9b58 100644 --- a/modules/test/vite.config.module.ts +++ b/fixture/apps/notification-sender/vite.config.module.ts @@ -44,18 +44,17 @@ export default defineConfig({ '@/components/ui/table': 'UIComponents', '@/components/ui/textarea': 'UIComponents', '@/components/ui/tooltip': 'UIComponents', + '@/lib/axios-compat': 'axios', }), ], build: { outDir: path.resolve(import.meta.dirname, ''), - // outDir: path.resolve(import.meta.dirname, 'dist/assets'), emptyOutDir: false, lib: { - // Library entrance point - entry: path.resolve(import.meta.dirname, 'ComponentB'), - name: 'ComponentB', + entry: path.resolve(import.meta.dirname, 'NotificationSender'), + name: 'NotificationSender', formats: ['es'], - fileName: (format) => `ComponentB.${format}.js`, + fileName: (format) => `NotificationSender.${format}.js`, }, rollupOptions: { external: [ @@ -89,12 +88,13 @@ export default defineConfig({ '@/components/ui/table', '@/components/ui/textarea', '@/components/ui/tooltip', + '@/lib/axios-compat', ], }, }, resolve: { alias: { - '@': path.resolve(import.meta.dirname, '../../src/assets/js'), + '@': path.resolve(import.meta.dirname, '../../../src/assets/js'), }, }, }); diff --git a/fixture/apps/quill-editor/ReactQuill.ts b/fixture/apps/quill-editor/ReactQuill.ts new file mode 100644 index 00000000..fce22af6 --- /dev/null +++ b/fixture/apps/quill-editor/ReactQuill.ts @@ -0,0 +1,47 @@ +import path from "path"; +import {AbstractAdminizerApp} from "../../../dist"; +import type {AppSetupContext, Config} from "../../../dist"; + +interface ReactQuillAppConfig { + config: Config; + componentFile: string; + componentDevUrl: string; + stylesheetFile: string; + stylesheetDevUrl: string; +} + +export class ReactQuillApp extends AbstractAdminizerApp { + readonly name = "react-quill"; + readonly version = "1.0.0"; + declare readonly config: ReactQuillAppConfig; + + constructor(config: Partial = {}) { + super(); + this.config = { + config: {}, + componentFile: path.resolve(import.meta.dirname, "react-quill-editor.es.js"), + componentDevUrl: "/fixture/apps/quill-editor/react-quill-editor.tsx", + stylesheetFile: path.resolve(import.meta.dirname, "react-quill-editor.css"), + stylesheetDevUrl: "/fixture/apps/quill-editor/react-quill-editor.css", + ...config, + }; + } + + setup(ctx: AppSetupContext): void { + ctx.control({ + type: "wysiwyg", + name: "react-quill", + config: this.config.config, + component: { + id: "editor", + filePath: this.config.componentFile, + devUrl: this.config.componentDevUrl, + }, + stylesheet: { + id: "editor-css", + filePath: this.config.stylesheetFile, + devUrl: this.config.stylesheetDevUrl, + }, + }); + } +} diff --git a/fixture/apps/quill-editor/react-quill-editor.css b/fixture/apps/quill-editor/react-quill-editor.css new file mode 100644 index 00000000..77289f7a --- /dev/null +++ b/fixture/apps/quill-editor/react-quill-editor.css @@ -0,0 +1 @@ +.ql-container{box-sizing:border-box;font-family:Helvetica,Arial,sans-serif;font-size:13px;height:100%;margin:0;position:relative}.ql-container.ql-disabled .ql-tooltip{visibility:hidden}.ql-container:not(.ql-disabled) li[data-list=checked]>.ql-ui,.ql-container:not(.ql-disabled) li[data-list=unchecked]>.ql-ui{cursor:pointer}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-clipboard p{margin:0;padding:0}.ql-editor{box-sizing:border-box;counter-reset:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;line-height:1.42;height:100%;outline:none;overflow-y:auto;padding:12px 15px;tab-size:4;-moz-tab-size:4;text-align:left;white-space:pre-wrap;word-wrap:break-word}.ql-editor>*{cursor:text}.ql-editor p,.ql-editor ol,.ql-editor pre,.ql-editor blockquote,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{margin:0;padding:0}@supports (counter-set:none){.ql-editor p,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{counter-set:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor p,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{counter-reset:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor table{border-collapse:collapse}.ql-editor td{border:1px solid #000;padding:2px 5px}.ql-editor ol{padding-left:1.5em}.ql-editor li{list-style-type:none;padding-left:1.5em;position:relative}.ql-editor li>.ql-ui:before{display:inline-block;margin-left:-1.5em;margin-right:.3em;text-align:right;white-space:nowrap;width:1.2em}.ql-editor li[data-list=checked]>.ql-ui,.ql-editor li[data-list=unchecked]>.ql-ui{color:#777}.ql-editor li[data-list=bullet]>.ql-ui:before{content:"•"}.ql-editor li[data-list=checked]>.ql-ui:before{content:"☑"}.ql-editor li[data-list=unchecked]>.ql-ui:before{content:"☐"}@supports (counter-set:none){.ql-editor li[data-list]{counter-set:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list]{counter-reset:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered]{counter-increment:list-0}.ql-editor li[data-list=ordered]>.ql-ui:before{content:counter(list-0,decimal) ". "}.ql-editor li[data-list=ordered].ql-indent-1{counter-increment:list-1}.ql-editor li[data-list=ordered].ql-indent-1>.ql-ui:before{content:counter(list-1,lower-alpha) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-1{counter-set:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-1{counter-reset:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-2{counter-increment:list-2}.ql-editor li[data-list=ordered].ql-indent-2>.ql-ui:before{content:counter(list-2,lower-roman) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-2{counter-set:list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-2{counter-reset:list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-3{counter-increment:list-3}.ql-editor li[data-list=ordered].ql-indent-3>.ql-ui:before{content:counter(list-3,decimal) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-3{counter-set:list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-3{counter-reset:list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-4{counter-increment:list-4}.ql-editor li[data-list=ordered].ql-indent-4>.ql-ui:before{content:counter(list-4,lower-alpha) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-4{counter-set:list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-4{counter-reset:list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-5{counter-increment:list-5}.ql-editor li[data-list=ordered].ql-indent-5>.ql-ui:before{content:counter(list-5,lower-roman) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-5{counter-set:list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-5{counter-reset:list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-6{counter-increment:list-6}.ql-editor li[data-list=ordered].ql-indent-6>.ql-ui:before{content:counter(list-6,decimal) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-6{counter-set:list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-6{counter-reset:list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-7{counter-increment:list-7}.ql-editor li[data-list=ordered].ql-indent-7>.ql-ui:before{content:counter(list-7,lower-alpha) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-7{counter-set:list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-7{counter-reset:list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-8{counter-increment:list-8}.ql-editor li[data-list=ordered].ql-indent-8>.ql-ui:before{content:counter(list-8,lower-roman) ". "}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-8{counter-set:list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-8{counter-reset:list-9}}.ql-editor li[data-list=ordered].ql-indent-9{counter-increment:list-9}.ql-editor li[data-list=ordered].ql-indent-9>.ql-ui:before{content:counter(list-9,decimal) ". "}.ql-editor .ql-indent-1:not(.ql-direction-rtl){padding-left:3em}.ql-editor li.ql-indent-1:not(.ql-direction-rtl){padding-left:4.5em}.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right{padding-right:3em}.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right{padding-right:4.5em}.ql-editor .ql-indent-2:not(.ql-direction-rtl){padding-left:6em}.ql-editor li.ql-indent-2:not(.ql-direction-rtl){padding-left:7.5em}.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right{padding-right:6em}.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right{padding-right:7.5em}.ql-editor .ql-indent-3:not(.ql-direction-rtl){padding-left:9em}.ql-editor li.ql-indent-3:not(.ql-direction-rtl){padding-left:10.5em}.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right{padding-right:9em}.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right{padding-right:10.5em}.ql-editor .ql-indent-4:not(.ql-direction-rtl){padding-left:12em}.ql-editor li.ql-indent-4:not(.ql-direction-rtl){padding-left:13.5em}.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right{padding-right:12em}.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right{padding-right:13.5em}.ql-editor .ql-indent-5:not(.ql-direction-rtl){padding-left:15em}.ql-editor li.ql-indent-5:not(.ql-direction-rtl){padding-left:16.5em}.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right{padding-right:15em}.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right{padding-right:16.5em}.ql-editor .ql-indent-6:not(.ql-direction-rtl){padding-left:18em}.ql-editor li.ql-indent-6:not(.ql-direction-rtl){padding-left:19.5em}.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right{padding-right:18em}.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right{padding-right:19.5em}.ql-editor .ql-indent-7:not(.ql-direction-rtl){padding-left:21em}.ql-editor li.ql-indent-7:not(.ql-direction-rtl){padding-left:22.5em}.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right{padding-right:21em}.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right{padding-right:22.5em}.ql-editor .ql-indent-8:not(.ql-direction-rtl){padding-left:24em}.ql-editor li.ql-indent-8:not(.ql-direction-rtl){padding-left:25.5em}.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right{padding-right:24em}.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right{padding-right:25.5em}.ql-editor .ql-indent-9:not(.ql-direction-rtl){padding-left:27em}.ql-editor li.ql-indent-9:not(.ql-direction-rtl){padding-left:28.5em}.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right{padding-right:27em}.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right{padding-right:28.5em}.ql-editor li.ql-direction-rtl{padding-right:1.5em}.ql-editor li.ql-direction-rtl>.ql-ui:before{margin-left:.3em;margin-right:-1.5em;text-align:left}.ql-editor table{table-layout:fixed;width:100%}.ql-editor table td{outline:none}.ql-editor .ql-code-block-container{font-family:monospace}.ql-editor .ql-video{display:block;max-width:100%}.ql-editor .ql-video.ql-align-center{margin:0 auto}.ql-editor .ql-video.ql-align-right{margin:0 0 0 auto}.ql-editor .ql-bg-black{background-color:#000}.ql-editor .ql-bg-red{background-color:#e60000}.ql-editor .ql-bg-orange{background-color:#f90}.ql-editor .ql-bg-yellow{background-color:#ff0}.ql-editor .ql-bg-green{background-color:#008a00}.ql-editor .ql-bg-blue{background-color:#06c}.ql-editor .ql-bg-purple{background-color:#93f}.ql-editor .ql-color-white{color:#fff}.ql-editor .ql-color-red{color:#e60000}.ql-editor .ql-color-orange{color:#f90}.ql-editor .ql-color-yellow{color:#ff0}.ql-editor .ql-color-green{color:#008a00}.ql-editor .ql-color-blue{color:#06c}.ql-editor .ql-color-purple{color:#93f}.ql-editor .ql-font-serif{font-family:Georgia,Times New Roman,serif}.ql-editor .ql-font-monospace{font-family:Monaco,Courier New,monospace}.ql-editor .ql-size-small{font-size:.75em}.ql-editor .ql-size-large{font-size:1.5em}.ql-editor .ql-size-huge{font-size:2.5em}.ql-editor .ql-direction-rtl{direction:rtl;text-align:inherit}.ql-editor .ql-align-center{text-align:center}.ql-editor .ql-align-justify{text-align:justify}.ql-editor .ql-align-right{text-align:right}.ql-editor .ql-ui{position:absolute}.ql-editor.ql-blank:before{color:#0009;content:attr(data-placeholder);font-style:italic;left:15px;pointer-events:none;position:absolute;right:15px}.ql-snow.ql-toolbar:after,.ql-snow .ql-toolbar:after{clear:both;content:"";display:table}.ql-snow.ql-toolbar button,.ql-snow .ql-toolbar button{background:none;border:none;cursor:pointer;display:inline-block;float:left;height:24px;padding:3px 5px;width:28px}.ql-snow.ql-toolbar button svg,.ql-snow .ql-toolbar button svg{float:left;height:100%}.ql-snow.ql-toolbar button:active:hover,.ql-snow .ql-toolbar button:active:hover{outline:none}.ql-snow.ql-toolbar input.ql-image[type=file],.ql-snow .ql-toolbar input.ql-image[type=file]{display:none}.ql-snow.ql-toolbar button:hover,.ql-snow .ql-toolbar button:hover,.ql-snow.ql-toolbar button:focus,.ql-snow .ql-toolbar button:focus,.ql-snow.ql-toolbar button.ql-active,.ql-snow .ql-toolbar button.ql-active,.ql-snow.ql-toolbar .ql-picker-label:hover,.ql-snow .ql-toolbar .ql-picker-label:hover,.ql-snow.ql-toolbar .ql-picker-label.ql-active,.ql-snow .ql-toolbar .ql-picker-label.ql-active,.ql-snow.ql-toolbar .ql-picker-item:hover,.ql-snow .ql-toolbar .ql-picker-item:hover,.ql-snow.ql-toolbar .ql-picker-item.ql-selected,.ql-snow .ql-toolbar .ql-picker-item.ql-selected{color:#06c}.ql-snow.ql-toolbar button:hover .ql-fill,.ql-snow .ql-toolbar button:hover .ql-fill,.ql-snow.ql-toolbar button:focus .ql-fill,.ql-snow .ql-toolbar button:focus .ql-fill,.ql-snow.ql-toolbar button.ql-active .ql-fill,.ql-snow .ql-toolbar button.ql-active .ql-fill,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill,.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill,.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill,.ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill{fill:#06c}.ql-snow.ql-toolbar button:hover .ql-stroke,.ql-snow .ql-toolbar button:hover .ql-stroke,.ql-snow.ql-toolbar button:focus .ql-stroke,.ql-snow .ql-toolbar button:focus .ql-stroke,.ql-snow.ql-toolbar button.ql-active .ql-stroke,.ql-snow .ql-toolbar button.ql-active .ql-stroke,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,.ql-snow.ql-toolbar button:hover .ql-stroke-miter,.ql-snow .ql-toolbar button:hover .ql-stroke-miter,.ql-snow.ql-toolbar button:focus .ql-stroke-miter,.ql-snow .ql-toolbar button:focus .ql-stroke-miter,.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter,.ql-snow .ql-toolbar button.ql-active .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter{stroke:#06c}@media(pointer:coarse){.ql-snow.ql-toolbar button:hover:not(.ql-active),.ql-snow .ql-toolbar button:hover:not(.ql-active){color:#444}.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill,.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill{fill:#444}.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke,.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter{stroke:#444}}.ql-snow,.ql-snow *{box-sizing:border-box}.ql-snow .ql-hidden{display:none}.ql-snow .ql-out-bottom,.ql-snow .ql-out-top{visibility:hidden}.ql-snow .ql-tooltip{position:absolute;transform:translateY(10px)}.ql-snow .ql-tooltip a{cursor:pointer;text-decoration:none}.ql-snow .ql-tooltip.ql-flip{transform:translateY(-10px)}.ql-snow .ql-formats{display:inline-block;vertical-align:middle}.ql-snow .ql-formats:after{clear:both;content:"";display:table}.ql-snow .ql-stroke{fill:none;stroke:#444;stroke-linecap:round;stroke-linejoin:round;stroke-width:2}.ql-snow .ql-stroke-miter{fill:none;stroke:#444;stroke-miterlimit:10;stroke-width:2}.ql-snow .ql-fill,.ql-snow .ql-stroke.ql-fill{fill:#444}.ql-snow .ql-empty{fill:none}.ql-snow .ql-even{fill-rule:evenodd}.ql-snow .ql-thin,.ql-snow .ql-stroke.ql-thin{stroke-width:1}.ql-snow .ql-transparent{opacity:.4}.ql-snow .ql-direction svg:last-child{display:none}.ql-snow .ql-direction.ql-active svg:last-child{display:inline}.ql-snow .ql-direction.ql-active svg:first-child{display:none}.ql-snow .ql-editor h1{font-size:2em}.ql-snow .ql-editor h2{font-size:1.5em}.ql-snow .ql-editor h3{font-size:1.17em}.ql-snow .ql-editor h4{font-size:1em}.ql-snow .ql-editor h5{font-size:.83em}.ql-snow .ql-editor h6{font-size:.67em}.ql-snow .ql-editor a{text-decoration:underline}.ql-snow .ql-editor blockquote{border-left:4px solid #ccc;margin-bottom:5px;margin-top:5px;padding-left:16px}.ql-snow .ql-editor code,.ql-snow .ql-editor .ql-code-block-container{background-color:#f0f0f0;border-radius:3px}.ql-snow .ql-editor .ql-code-block-container{margin-bottom:5px;margin-top:5px;padding:5px 10px}.ql-snow .ql-editor code{font-size:85%;padding:2px 4px}.ql-snow .ql-editor .ql-code-block-container{background-color:#23241f;color:#f8f8f2;overflow:visible}.ql-snow .ql-editor img{max-width:100%}.ql-snow .ql-picker{color:#444;display:inline-block;float:left;font-size:14px;font-weight:500;height:24px;position:relative;vertical-align:middle}.ql-snow .ql-picker-label{cursor:pointer;display:inline-block;height:100%;padding-left:8px;padding-right:2px;position:relative;width:100%}.ql-snow .ql-picker-label:before{display:inline-block;line-height:22px}.ql-snow .ql-picker-options{background-color:#fff;display:none;min-width:100%;padding:4px 8px;position:absolute;white-space:nowrap}.ql-snow .ql-picker-options .ql-picker-item{cursor:pointer;display:block;padding-bottom:5px;padding-top:5px}.ql-snow .ql-picker.ql-expanded .ql-picker-label{color:#ccc;z-index:2}.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill{fill:#ccc}.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke{stroke:#ccc}.ql-snow .ql-picker.ql-expanded .ql-picker-options{display:block;margin-top:-1px;top:100%;z-index:1}.ql-snow .ql-color-picker,.ql-snow .ql-icon-picker{width:28px}.ql-snow .ql-color-picker .ql-picker-label,.ql-snow .ql-icon-picker .ql-picker-label{padding:2px 4px}.ql-snow .ql-color-picker .ql-picker-label svg,.ql-snow .ql-icon-picker .ql-picker-label svg{right:4px}.ql-snow .ql-icon-picker .ql-picker-options{padding:4px 0}.ql-snow .ql-icon-picker .ql-picker-item{height:24px;width:24px;padding:2px 4px}.ql-snow .ql-color-picker .ql-picker-options{padding:3px 5px;width:152px}.ql-snow .ql-color-picker .ql-picker-item{border:1px solid transparent;float:left;height:16px;margin:2px;padding:0;width:16px}.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg{position:absolute;margin-top:-9px;right:0;top:50%;width:18px}.ql-snow .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=""]):before{content:attr(data-label)}.ql-snow .ql-picker.ql-header{width:98px}.ql-snow .ql-picker.ql-header .ql-picker-label:before,.ql-snow .ql-picker.ql-header .ql-picker-item:before{content:"Normal"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]:before{content:"Heading 1"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]:before{content:"Heading 2"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]:before{content:"Heading 3"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]:before{content:"Heading 4"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]:before{content:"Heading 5"}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]:before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]:before{content:"Heading 6"}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]:before{font-size:2em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]:before{font-size:1.5em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]:before{font-size:1.17em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]:before{font-size:1em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]:before{font-size:.83em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]:before{font-size:.67em}.ql-snow .ql-picker.ql-font{width:108px}.ql-snow .ql-picker.ql-font .ql-picker-label:before,.ql-snow .ql-picker.ql-font .ql-picker-item:before{content:"Sans Serif"}.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]:before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]:before{content:"Serif"}.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]:before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]:before{content:"Monospace"}.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]:before{font-family:Georgia,Times New Roman,serif}.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]:before{font-family:Monaco,Courier New,monospace}.ql-snow .ql-picker.ql-size{width:98px}.ql-snow .ql-picker.ql-size .ql-picker-label:before,.ql-snow .ql-picker.ql-size .ql-picker-item:before{content:"Normal"}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]:before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]:before{content:"Small"}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]:before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]:before{content:"Large"}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]:before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]:before{content:"Huge"}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]:before{font-size:10px}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]:before{font-size:18px}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]:before{font-size:32px}.ql-snow .ql-color-picker.ql-background .ql-picker-item{background-color:#fff}.ql-snow .ql-color-picker.ql-color .ql-picker-item{background-color:#000}.ql-code-block-container{position:relative}.ql-code-block-container .ql-ui{right:5px;top:5px}.ql-toolbar.ql-snow{border:1px solid #ccc;box-sizing:border-box;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;padding:8px}.ql-toolbar.ql-snow .ql-formats{margin-right:15px}.ql-toolbar.ql-snow .ql-picker-label{border:1px solid transparent}.ql-toolbar.ql-snow .ql-picker-options{border:1px solid transparent;box-shadow:#0003 0 2px 8px}.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label,.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options{border-color:#ccc}.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected,.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover{border-color:#000}.ql-toolbar.ql-snow+.ql-container.ql-snow{border-top:0}.ql-snow .ql-tooltip{background-color:#fff;border:1px solid #ccc;box-shadow:0 0 5px #ddd;color:#444;padding:5px 12px;white-space:nowrap}.ql-snow .ql-tooltip:before{content:"Visit URL:";line-height:26px;margin-right:8px}.ql-snow .ql-tooltip input[type=text]{display:none;border:1px solid #ccc;font-size:13px;height:26px;margin:0;padding:3px 5px;width:170px}.ql-snow .ql-tooltip a.ql-preview{display:inline-block;max-width:200px;overflow-x:hidden;text-overflow:ellipsis;vertical-align:top}.ql-snow .ql-tooltip a.ql-action:after{border-right:1px solid #ccc;content:"Edit";margin-left:16px;padding-right:8px}.ql-snow .ql-tooltip a.ql-remove:before{content:"Remove";margin-left:8px}.ql-snow .ql-tooltip a{line-height:26px}.ql-snow .ql-tooltip.ql-editing a.ql-preview,.ql-snow .ql-tooltip.ql-editing a.ql-remove{display:none}.ql-snow .ql-tooltip.ql-editing input[type=text]{display:inline-block}.ql-snow .ql-tooltip.ql-editing a.ql-action:after{border-right:0;content:"Save";padding-right:0}.ql-snow .ql-tooltip[data-mode=link]:before{content:"Enter link:"}.ql-snow .ql-tooltip[data-mode=formula]:before{content:"Enter formula:"}.ql-snow .ql-tooltip[data-mode=video]:before{content:"Enter video:"}.ql-snow a{color:#06c}.ql-container.ql-snow{border:1px solid #ccc} diff --git a/controls/wysiwyg/react-quill-editor.es.js b/fixture/apps/quill-editor/react-quill-editor.es.js similarity index 61% rename from controls/wysiwyg/react-quill-editor.es.js rename to fixture/apps/quill-editor/react-quill-editor.es.js index ac54074b..4411f6c2 100644 --- a/controls/wysiwyg/react-quill-editor.es.js +++ b/fixture/apps/quill-editor/react-quill-editor.es.js @@ -1,25 +1,13 @@ -var Ko = Object.defineProperty; -var Go = (r, t, e) => t in r ? Ko(r, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[t] = e; -var O = (r, t, e) => Go(r, typeof t != "symbol" ? t + "" : t, e); -var me = typeof globalThis < "u" ? globalThis : typeof window < "u" ? window : typeof global < "u" ? global : typeof self < "u" ? self : {}; -function Dl(r) { +var ue = typeof globalThis < "u" ? globalThis : typeof window < "u" ? window : typeof global < "u" ? global : typeof self < "u" ? self : {}; +function Nl(r) { return r && r.__esModule && Object.prototype.hasOwnProperty.call(r, "default") ? r.default : r; } -var _s = { exports: {} }, _n = {}; -/** - * @license React - * react-jsx-runtime.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -var Ci; -function Vo() { - if (Ci) return _n; - Ci = 1; - var r = Symbol.for("react.transitional.element"), t = Symbol.for("react.fragment"); +var vs = { exports: {} }, An = {}; +var mi; +function So() { + if (mi) return An; + mi = 1; + var r = /* @__PURE__ */ Symbol.for("react.transitional.element"), t = /* @__PURE__ */ Symbol.for("react.fragment"); function e(n, s, i) { var o = null; if (i !== void 0 && (o = "" + i), s.key !== void 0 && (o = "" + s.key), "key" in s) { @@ -35,61 +23,61 @@ function Vo() { props: i }; } - return _n.Fragment = t, _n.jsx = e, _n.jsxs = e, _n; + return An.Fragment = t, An.jsx = e, An.jsxs = e, An; } -var Ii; -function Wo() { - return Ii || (Ii = 1, _s.exports = Vo()), _s.exports; +var bi; +function _o() { + return bi || (bi = 1, vs.exports = So()), vs.exports; } -var Zo = Wo(), jl = typeof global == "object" && global && global.Object === Object && global, Yo = typeof self == "object" && self && self.Object === Object && self, ee = jl || Yo || Function("return this")(), Ee = ee.Symbol, $l = Object.prototype, Xo = $l.hasOwnProperty, Qo = $l.toString, qn = Ee ? Ee.toStringTag : void 0; -function Jo(r) { - var t = Xo.call(r, qn), e = r[qn]; +var pr = _o(), Tl = typeof global == "object" && global && global.Object === Object && global, Co = typeof self == "object" && self && self.Object === Object && self, Zt = Tl || Co || Function("return this")(), ge = Zt.Symbol, xl = Object.prototype, qo = xl.hasOwnProperty, Oo = xl.toString, wn = ge ? ge.toStringTag : void 0; +function Ro(r) { + var t = qo.call(r, wn), e = r[wn]; try { - r[qn] = void 0; + r[wn] = void 0; var n = !0; } catch { } - var s = Qo.call(r); - return n && (t ? r[qn] = e : delete r[qn]), s; + var s = Oo.call(r); + return n && (t ? r[wn] = e : delete r[wn]), s; } -var ta = Object.prototype, ea = ta.toString; -function na(r) { - return ea.call(r); +var Io = Object.prototype, ko = Io.toString; +function Bo(r) { + return ko.call(r); } -var ra = "[object Null]", sa = "[object Undefined]", Ri = Ee ? Ee.toStringTag : void 0; -function hn(r) { - return r == null ? r === void 0 ? sa : ra : Ri && Ri in Object(r) ? Jo(r) : na(r); +var Mo = "[object Null]", Do = "[object Undefined]", yi = ge ? ge.toStringTag : void 0; +function nn(r) { + return r == null ? r === void 0 ? Do : Mo : yi && yi in Object(r) ? Ro(r) : Bo(r); } -function oe(r) { +function ne(r) { return r != null && typeof r == "object"; } -var Oe = Array.isArray; -function Ae(r) { +var xe = Array.isArray; +function pe(r) { var t = typeof r; return r != null && (t == "object" || t == "function"); } -function Pl(r) { +function Ll(r) { return r; } -var ia = "[object AsyncFunction]", la = "[object Function]", oa = "[object GeneratorFunction]", aa = "[object Proxy]"; -function fi(r) { - if (!Ae(r)) +var jo = "[object AsyncFunction]", Po = "[object Function]", $o = "[object GeneratorFunction]", Uo = "[object Proxy]"; +function Ys(r) { + if (!pe(r)) return !1; - var t = hn(r); - return t == la || t == oa || t == ia || t == aa; + var t = nn(r); + return t == Po || t == $o || t == jo || t == Uo; } -var qs = ee["__core-js_shared__"], ki = function() { - var r = /[^.]+$/.exec(qs && qs.keys && qs.keys.IE_PROTO || ""); +var Es = Zt["__core-js_shared__"], vi = (function() { + var r = /[^.]+$/.exec(Es && Es.keys && Es.keys.IE_PROTO || ""); return r ? "Symbol(src)_1." + r : ""; -}(); -function ca(r) { - return !!ki && ki in r; +})(); +function Fo(r) { + return !!vi && vi in r; } -var ua = Function.prototype, ha = ua.toString; -function Re(r) { +var Ho = Function.prototype, zo = Ho.toString; +function Ce(r) { if (r != null) { try { - return ha.call(r); + return zo.call(r); } catch { } try { @@ -99,36 +87,36 @@ function Re(r) { } return ""; } -var fa = /[\\^$.*+?()[\]{}|]/g, da = /^\[object .+?Constructor\]$/, ga = Function.prototype, pa = Object.prototype, ma = ga.toString, ba = pa.hasOwnProperty, ya = RegExp( - "^" + ma.call(ba).replace(fa, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$" +var Ko = /[\\^$.*+?()[\]{}|]/g, Go = /^\[object .+?Constructor\]$/, Vo = Function.prototype, Wo = Object.prototype, Zo = Vo.toString, Yo = Wo.hasOwnProperty, Xo = RegExp( + "^" + Zo.call(Yo).replace(Ko, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$" ); -function va(r) { - if (!Ae(r) || ca(r)) +function Qo(r) { + if (!pe(r) || Fo(r)) return !1; - var t = fi(r) ? ya : da; - return t.test(Re(r)); + var t = Ys(r) ? Xo : Go; + return t.test(Ce(r)); } -function Ea(r, t) { - return r == null ? void 0 : r[t]; +function Jo(r, t) { + return r?.[t]; } -function ke(r, t) { - var e = Ea(r, t); - return va(e) ? e : void 0; +function qe(r, t) { + var e = Jo(r, t); + return Qo(e) ? e : void 0; } -var Hs = ke(ee, "WeakMap"), Bi = Object.create, Aa = /* @__PURE__ */ function() { +var ks = qe(Zt, "WeakMap"), Ei = Object.create, ta = /* @__PURE__ */ (function() { function r() { } return function(t) { - if (!Ae(t)) + if (!pe(t)) return {}; - if (Bi) - return Bi(t); + if (Ei) + return Ei(t); r.prototype = t; var e = new r(); return r.prototype = void 0, e; }; -}(); -function wa(r, t, e) { +})(); +function ea(r, t, e) { switch (e.length) { case 0: return r.call(t); @@ -141,568 +129,568 @@ function wa(r, t, e) { } return r.apply(t, e); } -function Na(r, t) { +function na(r, t) { var e = -1, n = r.length; for (t || (t = Array(n)); ++e < n; ) t[e] = r[e]; return t; } -var Ta = 800, xa = 16, La = Date.now; -function Sa(r) { +var ra = 800, sa = 16, ia = Date.now; +function la(r) { var t = 0, e = 0; return function() { - var n = La(), s = xa - (n - e); + var n = ia(), s = sa - (n - e); if (e = n, s > 0) { - if (++t >= Ta) + if (++t >= ra) return arguments[0]; } else t = 0; return r.apply(void 0, arguments); }; } -function _a(r) { +function oa(r) { return function() { return r; }; } -var _r = function() { +var br = (function() { try { - var r = ke(Object, "defineProperty"); + var r = qe(Object, "defineProperty"); return r({}, "", {}), r; } catch { } -}(), qa = _r ? function(r, t) { - return _r(r, "toString", { +})(), aa = br ? function(r, t) { + return br(r, "toString", { configurable: !0, enumerable: !1, - value: _a(t), + value: oa(t), writable: !0 }); -} : Pl, Oa = Sa(qa); -function Ca(r, t) { +} : Ll, ca = la(aa); +function ua(r, t) { for (var e = -1, n = r == null ? 0 : r.length; ++e < n && t(r[e], e, r) !== !1; ) ; return r; } -var Ia = 9007199254740991, Ra = /^(?:0|[1-9]\d*)$/; -function Ul(r, t) { +var ha = 9007199254740991, fa = /^(?:0|[1-9]\d*)$/; +function Sl(r, t) { var e = typeof r; - return t = t ?? Ia, !!t && (e == "number" || e != "symbol" && Ra.test(r)) && r > -1 && r % 1 == 0 && r < t; + return t = t ?? ha, !!t && (e == "number" || e != "symbol" && fa.test(r)) && r > -1 && r % 1 == 0 && r < t; } -function di(r, t, e) { - t == "__proto__" && _r ? _r(r, t, { +function Xs(r, t, e) { + t == "__proto__" && br ? br(r, t, { configurable: !0, enumerable: !0, value: e, writable: !0 }) : r[t] = e; } -function Kn(r, t) { +function Bn(r, t) { return r === t || r !== r && t !== t; } -var ka = Object.prototype, Ba = ka.hasOwnProperty; -function Fl(r, t, e) { +var da = Object.prototype, ga = da.hasOwnProperty; +function _l(r, t, e) { var n = r[t]; - (!(Ba.call(r, t) && Kn(n, e)) || e === void 0 && !(t in r)) && di(r, t, e); + (!(ga.call(r, t) && Bn(n, e)) || e === void 0 && !(t in r)) && Xs(r, t, e); } -function Ma(r, t, e, n) { +function pa(r, t, e, n) { var s = !e; e || (e = {}); for (var i = -1, o = t.length; ++i < o; ) { var a = t[i], u = void 0; - u === void 0 && (u = r[a]), s ? di(e, a, u) : Fl(e, a, u); + u === void 0 && (u = r[a]), s ? Xs(e, a, u) : _l(e, a, u); } return e; } -var Mi = Math.max; -function Da(r, t, e) { - return t = Mi(t === void 0 ? r.length - 1 : t, 0), function() { - for (var n = arguments, s = -1, i = Mi(n.length - t, 0), o = Array(i); ++s < i; ) +var Ai = Math.max; +function ma(r, t, e) { + return t = Ai(t === void 0 ? r.length - 1 : t, 0), function() { + for (var n = arguments, s = -1, i = Ai(n.length - t, 0), o = Array(i); ++s < i; ) o[s] = n[t + s]; s = -1; for (var a = Array(t + 1); ++s < t; ) a[s] = n[s]; - return a[t] = e(o), wa(r, this, a); + return a[t] = e(o), ea(r, this, a); }; } -function ja(r, t) { - return Oa(Da(r, t, Pl), r + ""); +function ba(r, t) { + return ca(ma(r, t, Ll), r + ""); } -var $a = 9007199254740991; -function Hl(r) { - return typeof r == "number" && r > -1 && r % 1 == 0 && r <= $a; +var ya = 9007199254740991; +function Cl(r) { + return typeof r == "number" && r > -1 && r % 1 == 0 && r <= ya; } -function Ir(r) { - return r != null && Hl(r.length) && !fi(r); +function Nr(r) { + return r != null && Cl(r.length) && !Ys(r); } -function Pa(r, t, e) { - if (!Ae(e)) +function va(r, t, e) { + if (!pe(e)) return !1; var n = typeof t; - return (n == "number" ? Ir(e) && Ul(t, e.length) : n == "string" && t in e) ? Kn(e[t], r) : !1; + return (n == "number" ? Nr(e) && Sl(t, e.length) : n == "string" && t in e) ? Bn(e[t], r) : !1; } -function Ua(r) { - return ja(function(t, e) { +function Ea(r) { + return ba(function(t, e) { var n = -1, s = e.length, i = s > 1 ? e[s - 1] : void 0, o = s > 2 ? e[2] : void 0; - for (i = r.length > 3 && typeof i == "function" ? (s--, i) : void 0, o && Pa(e[0], e[1], o) && (i = s < 3 ? void 0 : i, s = 1), t = Object(t); ++n < s; ) { + for (i = r.length > 3 && typeof i == "function" ? (s--, i) : void 0, o && va(e[0], e[1], o) && (i = s < 3 ? void 0 : i, s = 1), t = Object(t); ++n < s; ) { var a = e[n]; a && r(t, a, n, i); } return t; }); } -var Fa = Object.prototype; -function gi(r) { - var t = r && r.constructor, e = typeof t == "function" && t.prototype || Fa; +var Aa = Object.prototype; +function Qs(r) { + var t = r && r.constructor, e = typeof t == "function" && t.prototype || Aa; return r === e; } -function Ha(r, t) { +function wa(r, t) { for (var e = -1, n = Array(r); ++e < r; ) n[e] = t(e); return n; } -var za = "[object Arguments]"; -function Di(r) { - return oe(r) && hn(r) == za; +var Na = "[object Arguments]"; +function wi(r) { + return ne(r) && nn(r) == Na; } -var zl = Object.prototype, Ka = zl.hasOwnProperty, Ga = zl.propertyIsEnumerable, zs = Di(/* @__PURE__ */ function() { +var ql = Object.prototype, Ta = ql.hasOwnProperty, xa = ql.propertyIsEnumerable, Bs = wi(/* @__PURE__ */ (function() { return arguments; -}()) ? Di : function(r) { - return oe(r) && Ka.call(r, "callee") && !Ga.call(r, "callee"); +})()) ? wi : function(r) { + return ne(r) && Ta.call(r, "callee") && !xa.call(r, "callee"); }; -function Va() { +function La() { return !1; } -var Kl = typeof exports == "object" && exports && !exports.nodeType && exports, ji = Kl && typeof module == "object" && module && !module.nodeType && module, Wa = ji && ji.exports === Kl, $i = Wa ? ee.Buffer : void 0, Za = $i ? $i.isBuffer : void 0, jn = Za || Va, Ya = "[object Arguments]", Xa = "[object Array]", Qa = "[object Boolean]", Ja = "[object Date]", tc = "[object Error]", ec = "[object Function]", nc = "[object Map]", rc = "[object Number]", sc = "[object Object]", ic = "[object RegExp]", lc = "[object Set]", oc = "[object String]", ac = "[object WeakMap]", cc = "[object ArrayBuffer]", uc = "[object DataView]", hc = "[object Float32Array]", fc = "[object Float64Array]", dc = "[object Int8Array]", gc = "[object Int16Array]", pc = "[object Int32Array]", mc = "[object Uint8Array]", bc = "[object Uint8ClampedArray]", yc = "[object Uint16Array]", vc = "[object Uint32Array]", lt = {}; -lt[hc] = lt[fc] = lt[dc] = lt[gc] = lt[pc] = lt[mc] = lt[bc] = lt[yc] = lt[vc] = !0; -lt[Ya] = lt[Xa] = lt[cc] = lt[Qa] = lt[uc] = lt[Ja] = lt[tc] = lt[ec] = lt[nc] = lt[rc] = lt[sc] = lt[ic] = lt[lc] = lt[oc] = lt[ac] = !1; -function Ec(r) { - return oe(r) && Hl(r.length) && !!lt[hn(r)]; +var Ol = typeof exports == "object" && exports && !exports.nodeType && exports, Ni = Ol && typeof module == "object" && module && !module.nodeType && module, Sa = Ni && Ni.exports === Ol, Ti = Sa ? Zt.Buffer : void 0, _a = Ti ? Ti.isBuffer : void 0, Cn = _a || La, Ca = "[object Arguments]", qa = "[object Array]", Oa = "[object Boolean]", Ra = "[object Date]", Ia = "[object Error]", ka = "[object Function]", Ba = "[object Map]", Ma = "[object Number]", Da = "[object Object]", ja = "[object RegExp]", Pa = "[object Set]", $a = "[object String]", Ua = "[object WeakMap]", Fa = "[object ArrayBuffer]", Ha = "[object DataView]", za = "[object Float32Array]", Ka = "[object Float64Array]", Ga = "[object Int8Array]", Va = "[object Int16Array]", Wa = "[object Int32Array]", Za = "[object Uint8Array]", Ya = "[object Uint8ClampedArray]", Xa = "[object Uint16Array]", Qa = "[object Uint32Array]", it = {}; +it[za] = it[Ka] = it[Ga] = it[Va] = it[Wa] = it[Za] = it[Ya] = it[Xa] = it[Qa] = !0; +it[Ca] = it[qa] = it[Fa] = it[Oa] = it[Ha] = it[Ra] = it[Ia] = it[ka] = it[Ba] = it[Ma] = it[Da] = it[ja] = it[Pa] = it[$a] = it[Ua] = !1; +function Ja(r) { + return ne(r) && Cl(r.length) && !!it[nn(r)]; } -function pi(r) { +function Js(r) { return function(t) { return r(t); }; } -var Gl = typeof exports == "object" && exports && !exports.nodeType && exports, kn = Gl && typeof module == "object" && module && !module.nodeType && module, Ac = kn && kn.exports === Gl, Os = Ac && jl.process, ln = function() { +var Rl = typeof exports == "object" && exports && !exports.nodeType && exports, Sn = Rl && typeof module == "object" && module && !module.nodeType && module, tc = Sn && Sn.exports === Rl, As = tc && Tl.process, tn = (function() { try { - var r = kn && kn.require && kn.require("util").types; - return r || Os && Os.binding && Os.binding("util"); + var r = Sn && Sn.require && Sn.require("util").types; + return r || As && As.binding && As.binding("util"); } catch { } -}(), Pi = ln && ln.isTypedArray, mi = Pi ? pi(Pi) : Ec, wc = Object.prototype, Nc = wc.hasOwnProperty; -function Vl(r, t) { - var e = Oe(r), n = !e && zs(r), s = !e && !n && jn(r), i = !e && !n && !s && mi(r), o = e || n || s || i, a = o ? Ha(r.length, String) : [], u = a.length; +})(), xi = tn && tn.isTypedArray, ti = xi ? Js(xi) : Ja, ec = Object.prototype, nc = ec.hasOwnProperty; +function Il(r, t) { + var e = xe(r), n = !e && Bs(r), s = !e && !n && Cn(r), i = !e && !n && !s && ti(r), o = e || n || s || i, a = o ? wa(r.length, String) : [], u = a.length; for (var h in r) - (t || Nc.call(r, h)) && !(o && // Safari 9 has enumerable `arguments.length` in strict mode. + (t || nc.call(r, h)) && !(o && // Safari 9 has enumerable `arguments.length` in strict mode. (h == "length" || // Node.js 0.10 has enumerable non-index properties on buffers. s && (h == "offset" || h == "parent") || // PhantomJS 2 has enumerable non-index properties on typed arrays. i && (h == "buffer" || h == "byteLength" || h == "byteOffset") || // Skip index properties. - Ul(h, u))) && a.push(h); + Sl(h, u))) && a.push(h); return a; } -function Wl(r, t) { +function kl(r, t) { return function(e) { return r(t(e)); }; } -var Tc = Wl(Object.keys, Object), xc = Object.prototype, Lc = xc.hasOwnProperty; -function Sc(r) { - if (!gi(r)) - return Tc(r); +var rc = kl(Object.keys, Object), sc = Object.prototype, ic = sc.hasOwnProperty; +function lc(r) { + if (!Qs(r)) + return rc(r); var t = []; for (var e in Object(r)) - Lc.call(r, e) && e != "constructor" && t.push(e); + ic.call(r, e) && e != "constructor" && t.push(e); return t; } -function _c(r) { - return Ir(r) ? Vl(r) : Sc(r); +function oc(r) { + return Nr(r) ? Il(r) : lc(r); } -function qc(r) { +function ac(r) { var t = []; if (r != null) for (var e in Object(r)) t.push(e); return t; } -var Oc = Object.prototype, Cc = Oc.hasOwnProperty; -function Ic(r) { - if (!Ae(r)) - return qc(r); - var t = gi(r), e = []; +var cc = Object.prototype, uc = cc.hasOwnProperty; +function hc(r) { + if (!pe(r)) + return ac(r); + var t = Qs(r), e = []; for (var n in r) - n == "constructor" && (t || !Cc.call(r, n)) || e.push(n); + n == "constructor" && (t || !uc.call(r, n)) || e.push(n); return e; } -function Zl(r) { - return Ir(r) ? Vl(r, !0) : Ic(r); +function Bl(r) { + return Nr(r) ? Il(r, !0) : hc(r); } -var $n = ke(Object, "create"); -function Rc() { - this.__data__ = $n ? $n(null) : {}, this.size = 0; +var qn = qe(Object, "create"); +function fc() { + this.__data__ = qn ? qn(null) : {}, this.size = 0; } -function kc(r) { +function dc(r) { var t = this.has(r) && delete this.__data__[r]; return this.size -= t ? 1 : 0, t; } -var Bc = "__lodash_hash_undefined__", Mc = Object.prototype, Dc = Mc.hasOwnProperty; -function jc(r) { +var gc = "__lodash_hash_undefined__", pc = Object.prototype, mc = pc.hasOwnProperty; +function bc(r) { var t = this.__data__; - if ($n) { + if (qn) { var e = t[r]; - return e === Bc ? void 0 : e; + return e === gc ? void 0 : e; } - return Dc.call(t, r) ? t[r] : void 0; + return mc.call(t, r) ? t[r] : void 0; } -var $c = Object.prototype, Pc = $c.hasOwnProperty; -function Uc(r) { +var yc = Object.prototype, vc = yc.hasOwnProperty; +function Ec(r) { var t = this.__data__; - return $n ? t[r] !== void 0 : Pc.call(t, r); + return qn ? t[r] !== void 0 : vc.call(t, r); } -var Fc = "__lodash_hash_undefined__"; -function Hc(r, t) { +var Ac = "__lodash_hash_undefined__"; +function wc(r, t) { var e = this.__data__; - return this.size += this.has(r) ? 0 : 1, e[r] = $n && t === void 0 ? Fc : t, this; + return this.size += this.has(r) ? 0 : 1, e[r] = qn && t === void 0 ? Ac : t, this; } -function Ce(r) { +function Le(r) { var t = -1, e = r == null ? 0 : r.length; for (this.clear(); ++t < e; ) { var n = r[t]; this.set(n[0], n[1]); } } -Ce.prototype.clear = Rc; -Ce.prototype.delete = kc; -Ce.prototype.get = jc; -Ce.prototype.has = Uc; -Ce.prototype.set = Hc; -function zc() { +Le.prototype.clear = fc; +Le.prototype.delete = dc; +Le.prototype.get = bc; +Le.prototype.has = Ec; +Le.prototype.set = wc; +function Nc() { this.__data__ = [], this.size = 0; } -function Rr(r, t) { +function Tr(r, t) { for (var e = r.length; e--; ) - if (Kn(r[e][0], t)) + if (Bn(r[e][0], t)) return e; return -1; } -var Kc = Array.prototype, Gc = Kc.splice; -function Vc(r) { - var t = this.__data__, e = Rr(t, r); +var Tc = Array.prototype, xc = Tc.splice; +function Lc(r) { + var t = this.__data__, e = Tr(t, r); if (e < 0) return !1; var n = t.length - 1; - return e == n ? t.pop() : Gc.call(t, e, 1), --this.size, !0; + return e == n ? t.pop() : xc.call(t, e, 1), --this.size, !0; } -function Wc(r) { - var t = this.__data__, e = Rr(t, r); +function Sc(r) { + var t = this.__data__, e = Tr(t, r); return e < 0 ? void 0 : t[e][1]; } -function Zc(r) { - return Rr(this.__data__, r) > -1; +function _c(r) { + return Tr(this.__data__, r) > -1; } -function Yc(r, t) { - var e = this.__data__, n = Rr(e, r); +function Cc(r, t) { + var e = this.__data__, n = Tr(e, r); return n < 0 ? (++this.size, e.push([r, t])) : e[n][1] = t, this; } -function ue(r) { +function se(r) { var t = -1, e = r == null ? 0 : r.length; for (this.clear(); ++t < e; ) { var n = r[t]; this.set(n[0], n[1]); } } -ue.prototype.clear = zc; -ue.prototype.delete = Vc; -ue.prototype.get = Wc; -ue.prototype.has = Zc; -ue.prototype.set = Yc; -var Pn = ke(ee, "Map"); -function Xc() { +se.prototype.clear = Nc; +se.prototype.delete = Lc; +se.prototype.get = Sc; +se.prototype.has = _c; +se.prototype.set = Cc; +var On = qe(Zt, "Map"); +function qc() { this.size = 0, this.__data__ = { - hash: new Ce(), - map: new (Pn || ue)(), - string: new Ce() + hash: new Le(), + map: new (On || se)(), + string: new Le() }; } -function Qc(r) { +function Oc(r) { var t = typeof r; return t == "string" || t == "number" || t == "symbol" || t == "boolean" ? r !== "__proto__" : r === null; } -function kr(r, t) { +function xr(r, t) { var e = r.__data__; - return Qc(t) ? e[typeof t == "string" ? "string" : "hash"] : e.map; + return Oc(t) ? e[typeof t == "string" ? "string" : "hash"] : e.map; } -function Jc(r) { - var t = kr(this, r).delete(r); +function Rc(r) { + var t = xr(this, r).delete(r); return this.size -= t ? 1 : 0, t; } -function tu(r) { - return kr(this, r).get(r); +function Ic(r) { + return xr(this, r).get(r); } -function eu(r) { - return kr(this, r).has(r); +function kc(r) { + return xr(this, r).has(r); } -function nu(r, t) { - var e = kr(this, r), n = e.size; +function Bc(r, t) { + var e = xr(this, r), n = e.size; return e.set(r, t), this.size += e.size == n ? 0 : 1, this; } -function Be(r) { +function Oe(r) { var t = -1, e = r == null ? 0 : r.length; for (this.clear(); ++t < e; ) { var n = r[t]; this.set(n[0], n[1]); } } -Be.prototype.clear = Xc; -Be.prototype.delete = Jc; -Be.prototype.get = tu; -Be.prototype.has = eu; -Be.prototype.set = nu; -function ru(r, t) { +Oe.prototype.clear = qc; +Oe.prototype.delete = Rc; +Oe.prototype.get = Ic; +Oe.prototype.has = kc; +Oe.prototype.set = Bc; +function Mc(r, t) { for (var e = -1, n = t.length, s = r.length; ++e < n; ) r[s + e] = t[e]; return r; } -var Yl = Wl(Object.getPrototypeOf, Object), su = "[object Object]", iu = Function.prototype, lu = Object.prototype, Xl = iu.toString, ou = lu.hasOwnProperty, au = Xl.call(Object); -function cu(r) { - if (!oe(r) || hn(r) != su) +var Ml = kl(Object.getPrototypeOf, Object), Dc = "[object Object]", jc = Function.prototype, Pc = Object.prototype, Dl = jc.toString, $c = Pc.hasOwnProperty, Uc = Dl.call(Object); +function Fc(r) { + if (!ne(r) || nn(r) != Dc) return !1; - var t = Yl(r); + var t = Ml(r); if (t === null) return !0; - var e = ou.call(t, "constructor") && t.constructor; - return typeof e == "function" && e instanceof e && Xl.call(e) == au; + var e = $c.call(t, "constructor") && t.constructor; + return typeof e == "function" && e instanceof e && Dl.call(e) == Uc; } -function uu() { - this.__data__ = new ue(), this.size = 0; +function Hc() { + this.__data__ = new se(), this.size = 0; } -function hu(r) { +function zc(r) { var t = this.__data__, e = t.delete(r); return this.size = t.size, e; } -function fu(r) { +function Kc(r) { return this.__data__.get(r); } -function du(r) { +function Gc(r) { return this.__data__.has(r); } -var gu = 200; -function pu(r, t) { +var Vc = 200; +function Wc(r, t) { var e = this.__data__; - if (e instanceof ue) { + if (e instanceof se) { var n = e.__data__; - if (!Pn || n.length < gu - 1) + if (!On || n.length < Vc - 1) return n.push([r, t]), this.size = ++e.size, this; - e = this.__data__ = new Be(n); + e = this.__data__ = new Oe(n); } return e.set(r, t), this.size = e.size, this; } -function Xt(r) { - var t = this.__data__ = new ue(r); +function Vt(r) { + var t = this.__data__ = new se(r); this.size = t.size; } -Xt.prototype.clear = uu; -Xt.prototype.delete = hu; -Xt.prototype.get = fu; -Xt.prototype.has = du; -Xt.prototype.set = pu; -var Ql = typeof exports == "object" && exports && !exports.nodeType && exports, Ui = Ql && typeof module == "object" && module && !module.nodeType && module, mu = Ui && Ui.exports === Ql, Fi = mu ? ee.Buffer : void 0, Hi = Fi ? Fi.allocUnsafe : void 0; -function Jl(r, t) { +Vt.prototype.clear = Hc; +Vt.prototype.delete = zc; +Vt.prototype.get = Kc; +Vt.prototype.has = Gc; +Vt.prototype.set = Wc; +var jl = typeof exports == "object" && exports && !exports.nodeType && exports, Li = jl && typeof module == "object" && module && !module.nodeType && module, Zc = Li && Li.exports === jl, Si = Zc ? Zt.Buffer : void 0, _i = Si ? Si.allocUnsafe : void 0; +function Pl(r, t) { if (t) return r.slice(); - var e = r.length, n = Hi ? Hi(e) : new r.constructor(e); + var e = r.length, n = _i ? _i(e) : new r.constructor(e); return r.copy(n), n; } -function bu(r, t) { +function Yc(r, t) { for (var e = -1, n = r == null ? 0 : r.length, s = 0, i = []; ++e < n; ) { var o = r[e]; t(o, e, r) && (i[s++] = o); } return i; } -function yu() { +function Xc() { return []; } -var vu = Object.prototype, Eu = vu.propertyIsEnumerable, zi = Object.getOwnPropertySymbols, Au = zi ? function(r) { - return r == null ? [] : (r = Object(r), bu(zi(r), function(t) { - return Eu.call(r, t); +var Qc = Object.prototype, Jc = Qc.propertyIsEnumerable, Ci = Object.getOwnPropertySymbols, tu = Ci ? function(r) { + return r == null ? [] : (r = Object(r), Yc(Ci(r), function(t) { + return Jc.call(r, t); })); -} : yu; -function wu(r, t, e) { +} : Xc; +function eu(r, t, e) { var n = t(r); - return Oe(r) ? n : ru(n, e(r)); + return xe(r) ? n : Mc(n, e(r)); } -function Ks(r) { - return wu(r, _c, Au); +function Ms(r) { + return eu(r, oc, tu); } -var Gs = ke(ee, "DataView"), Vs = ke(ee, "Promise"), Ws = ke(ee, "Set"), Ki = "[object Map]", Nu = "[object Object]", Gi = "[object Promise]", Vi = "[object Set]", Wi = "[object WeakMap]", Zi = "[object DataView]", Tu = Re(Gs), xu = Re(Pn), Lu = Re(Vs), Su = Re(Ws), _u = Re(Hs), jt = hn; -(Gs && jt(new Gs(new ArrayBuffer(1))) != Zi || Pn && jt(new Pn()) != Ki || Vs && jt(Vs.resolve()) != Gi || Ws && jt(new Ws()) != Vi || Hs && jt(new Hs()) != Wi) && (jt = function(r) { - var t = hn(r), e = t == Nu ? r.constructor : void 0, n = e ? Re(e) : ""; +var Ds = qe(Zt, "DataView"), js = qe(Zt, "Promise"), Ps = qe(Zt, "Set"), qi = "[object Map]", nu = "[object Object]", Oi = "[object Promise]", Ri = "[object Set]", Ii = "[object WeakMap]", ki = "[object DataView]", ru = Ce(Ds), su = Ce(On), iu = Ce(js), lu = Ce(Ps), ou = Ce(ks), Mt = nn; +(Ds && Mt(new Ds(new ArrayBuffer(1))) != ki || On && Mt(new On()) != qi || js && Mt(js.resolve()) != Oi || Ps && Mt(new Ps()) != Ri || ks && Mt(new ks()) != Ii) && (Mt = function(r) { + var t = nn(r), e = t == nu ? r.constructor : void 0, n = e ? Ce(e) : ""; if (n) switch (n) { - case Tu: - return Zi; - case xu: - return Ki; - case Lu: - return Gi; - case Su: - return Vi; - case _u: - return Wi; + case ru: + return ki; + case su: + return qi; + case iu: + return Oi; + case lu: + return Ri; + case ou: + return Ii; } return t; }); -var qu = Object.prototype, Ou = qu.hasOwnProperty; -function Cu(r) { +var au = Object.prototype, cu = au.hasOwnProperty; +function uu(r) { var t = r.length, e = new r.constructor(t); - return t && typeof r[0] == "string" && Ou.call(r, "index") && (e.index = r.index, e.input = r.input), e; + return t && typeof r[0] == "string" && cu.call(r, "index") && (e.index = r.index, e.input = r.input), e; } -var qr = ee.Uint8Array; -function bi(r) { +var yr = Zt.Uint8Array; +function ei(r) { var t = new r.constructor(r.byteLength); - return new qr(t).set(new qr(r)), t; + return new yr(t).set(new yr(r)), t; } -function Iu(r, t) { - var e = bi(r.buffer); +function hu(r, t) { + var e = ei(r.buffer); return new r.constructor(e, r.byteOffset, r.byteLength); } -var Ru = /\w*$/; -function ku(r) { - var t = new r.constructor(r.source, Ru.exec(r)); +var fu = /\w*$/; +function du(r) { + var t = new r.constructor(r.source, fu.exec(r)); return t.lastIndex = r.lastIndex, t; } -var Yi = Ee ? Ee.prototype : void 0, Xi = Yi ? Yi.valueOf : void 0; -function Bu(r) { - return Xi ? Object(Xi.call(r)) : {}; +var Bi = ge ? ge.prototype : void 0, Mi = Bi ? Bi.valueOf : void 0; +function gu(r) { + return Mi ? Object(Mi.call(r)) : {}; } -function to(r, t) { - var e = t ? bi(r.buffer) : r.buffer; +function $l(r, t) { + var e = t ? ei(r.buffer) : r.buffer; return new r.constructor(e, r.byteOffset, r.length); } -var Mu = "[object Boolean]", Du = "[object Date]", ju = "[object Map]", $u = "[object Number]", Pu = "[object RegExp]", Uu = "[object Set]", Fu = "[object String]", Hu = "[object Symbol]", zu = "[object ArrayBuffer]", Ku = "[object DataView]", Gu = "[object Float32Array]", Vu = "[object Float64Array]", Wu = "[object Int8Array]", Zu = "[object Int16Array]", Yu = "[object Int32Array]", Xu = "[object Uint8Array]", Qu = "[object Uint8ClampedArray]", Ju = "[object Uint16Array]", th = "[object Uint32Array]"; -function eh(r, t, e) { +var pu = "[object Boolean]", mu = "[object Date]", bu = "[object Map]", yu = "[object Number]", vu = "[object RegExp]", Eu = "[object Set]", Au = "[object String]", wu = "[object Symbol]", Nu = "[object ArrayBuffer]", Tu = "[object DataView]", xu = "[object Float32Array]", Lu = "[object Float64Array]", Su = "[object Int8Array]", _u = "[object Int16Array]", Cu = "[object Int32Array]", qu = "[object Uint8Array]", Ou = "[object Uint8ClampedArray]", Ru = "[object Uint16Array]", Iu = "[object Uint32Array]"; +function ku(r, t, e) { var n = r.constructor; switch (t) { - case zu: - return bi(r); - case Mu: - case Du: + case Nu: + return ei(r); + case pu: + case mu: return new n(+r); - case Ku: - return Iu(r); - case Gu: - case Vu: - case Wu: - case Zu: - case Yu: - case Xu: - case Qu: - case Ju: - case th: - return to(r, e); - case ju: + case Tu: + return hu(r); + case xu: + case Lu: + case Su: + case _u: + case Cu: + case qu: + case Ou: + case Ru: + case Iu: + return $l(r, e); + case bu: return new n(); - case $u: - case Fu: + case yu: + case Au: return new n(r); - case Pu: - return ku(r); - case Uu: + case vu: + return du(r); + case Eu: return new n(); - case Hu: - return Bu(r); + case wu: + return gu(r); } } -function eo(r) { - return typeof r.constructor == "function" && !gi(r) ? Aa(Yl(r)) : {}; +function Ul(r) { + return typeof r.constructor == "function" && !Qs(r) ? ta(Ml(r)) : {}; } -var nh = "[object Map]"; -function rh(r) { - return oe(r) && jt(r) == nh; +var Bu = "[object Map]"; +function Mu(r) { + return ne(r) && Mt(r) == Bu; } -var Qi = ln && ln.isMap, sh = Qi ? pi(Qi) : rh, ih = "[object Set]"; -function lh(r) { - return oe(r) && jt(r) == ih; +var Di = tn && tn.isMap, Du = Di ? Js(Di) : Mu, ju = "[object Set]"; +function Pu(r) { + return ne(r) && Mt(r) == ju; } -var Ji = ln && ln.isSet, oh = Ji ? pi(Ji) : lh, ah = 1, no = "[object Arguments]", ch = "[object Array]", uh = "[object Boolean]", hh = "[object Date]", fh = "[object Error]", ro = "[object Function]", dh = "[object GeneratorFunction]", gh = "[object Map]", ph = "[object Number]", so = "[object Object]", mh = "[object RegExp]", bh = "[object Set]", yh = "[object String]", vh = "[object Symbol]", Eh = "[object WeakMap]", Ah = "[object ArrayBuffer]", wh = "[object DataView]", Nh = "[object Float32Array]", Th = "[object Float64Array]", xh = "[object Int8Array]", Lh = "[object Int16Array]", Sh = "[object Int32Array]", _h = "[object Uint8Array]", qh = "[object Uint8ClampedArray]", Oh = "[object Uint16Array]", Ch = "[object Uint32Array]", st = {}; -st[no] = st[ch] = st[Ah] = st[wh] = st[uh] = st[hh] = st[Nh] = st[Th] = st[xh] = st[Lh] = st[Sh] = st[gh] = st[ph] = st[so] = st[mh] = st[bh] = st[yh] = st[vh] = st[_h] = st[qh] = st[Oh] = st[Ch] = !0; -st[fh] = st[ro] = st[Eh] = !1; -function xr(r, t, e, n, s, i) { - var o, a = t & ah; +var ji = tn && tn.isSet, $u = ji ? Js(ji) : Pu, Uu = 1, Fl = "[object Arguments]", Fu = "[object Array]", Hu = "[object Boolean]", zu = "[object Date]", Ku = "[object Error]", Hl = "[object Function]", Gu = "[object GeneratorFunction]", Vu = "[object Map]", Wu = "[object Number]", zl = "[object Object]", Zu = "[object RegExp]", Yu = "[object Set]", Xu = "[object String]", Qu = "[object Symbol]", Ju = "[object WeakMap]", th = "[object ArrayBuffer]", eh = "[object DataView]", nh = "[object Float32Array]", rh = "[object Float64Array]", sh = "[object Int8Array]", ih = "[object Int16Array]", lh = "[object Int32Array]", oh = "[object Uint8Array]", ah = "[object Uint8ClampedArray]", ch = "[object Uint16Array]", uh = "[object Uint32Array]", rt = {}; +rt[Fl] = rt[Fu] = rt[th] = rt[eh] = rt[Hu] = rt[zu] = rt[nh] = rt[rh] = rt[sh] = rt[ih] = rt[lh] = rt[Vu] = rt[Wu] = rt[zl] = rt[Zu] = rt[Yu] = rt[Xu] = rt[Qu] = rt[oh] = rt[ah] = rt[ch] = rt[uh] = !0; +rt[Ku] = rt[Hl] = rt[Ju] = !1; +function mr(r, t, e, n, s, i) { + var o, a = t & Uu; if (o !== void 0) return o; - if (!Ae(r)) + if (!pe(r)) return r; - var u = Oe(r); + var u = xe(r); if (u) - o = Cu(r); + o = uu(r); else { - var h = jt(r), p = h == ro || h == dh; - if (jn(r)) - return Jl(r, a); - if (h == so || h == no || p && !s) - o = p ? {} : eo(r); + var h = Mt(r), p = h == Hl || h == Gu; + if (Cn(r)) + return Pl(r, a); + if (h == zl || h == Fl || p && !s) + o = p ? {} : Ul(r); else { - if (!st[h]) + if (!rt[h]) return s ? r : {}; - o = eh(r, h, a); + o = ku(r, h, a); } } - i || (i = new Xt()); + i || (i = new Vt()); var v = i.get(r); if (v) return v; - i.set(r, o), oh(r) ? r.forEach(function(y) { - o.add(xr(y, t, e, y, r, i)); - }) : sh(r) && r.forEach(function(y, A) { - o.set(A, xr(y, t, e, A, r, i)); + i.set(r, o), $u(r) ? r.forEach(function(y) { + o.add(mr(y, t, e, y, r, i)); + }) : Du(r) && r.forEach(function(y, w) { + o.set(w, mr(y, t, e, w, r, i)); }); - var f = Ks, m = u ? void 0 : f(r); - return Ca(m || r, function(y, A) { - m && (A = y, y = r[A]), Fl(o, A, xr(y, t, e, A, r, i)); + var f = Ms, m = u ? void 0 : f(r); + return ua(m || r, function(y, w) { + m && (w = y, y = r[w]), _l(o, w, mr(y, t, e, w, r, i)); }), o; } -var Ih = 1, Rh = 4; -function nn(r) { - return xr(r, Ih | Rh); +var hh = 1, fh = 4; +function Xe(r) { + return mr(r, hh | fh); } -var kh = "__lodash_hash_undefined__"; -function Bh(r) { - return this.__data__.set(r, kh), this; +var dh = "__lodash_hash_undefined__"; +function gh(r) { + return this.__data__.set(r, dh), this; } -function Mh(r) { +function ph(r) { return this.__data__.has(r); } -function Or(r) { +function vr(r) { var t = -1, e = r == null ? 0 : r.length; - for (this.__data__ = new Be(); ++t < e; ) + for (this.__data__ = new Oe(); ++t < e; ) this.add(r[t]); } -Or.prototype.add = Or.prototype.push = Bh; -Or.prototype.has = Mh; -function Dh(r, t) { +vr.prototype.add = vr.prototype.push = gh; +vr.prototype.has = ph; +function mh(r, t) { for (var e = -1, n = r == null ? 0 : r.length; ++e < n; ) if (t(r[e], e, r)) return !0; return !1; } -function jh(r, t) { +function bh(r, t) { return r.has(t); } -var $h = 1, Ph = 2; -function io(r, t, e, n, s, i) { - var o = e & $h, a = r.length, u = t.length; +var yh = 1, vh = 2; +function Kl(r, t, e, n, s, i) { + var o = e & yh, a = r.length, u = t.length; if (a != u && !(o && u > a)) return !1; var h = i.get(r), p = i.get(t); if (h && p) return h == t && p == r; - var v = -1, f = !0, m = e & Ph ? new Or() : void 0; + var v = -1, f = !0, m = e & vh ? new vr() : void 0; for (i.set(r, t), i.set(t, r); ++v < a; ) { - var y = r[v], A = t[v]; + var y = r[v], w = t[v]; if (n) - var N = o ? n(A, y, v, t, r, i) : n(y, A, v, r, t, i); + var N = o ? n(w, y, v, t, r, i) : n(y, w, v, r, t, i); if (N !== void 0) { if (N) continue; @@ -710,125 +698,125 @@ function io(r, t, e, n, s, i) { break; } if (m) { - if (!Dh(t, function(x, C) { - if (!jh(m, C) && (y === x || s(y, x, e, n, i))) - return m.push(C); + if (!mh(t, function(x, q) { + if (!bh(m, q) && (y === x || s(y, x, e, n, i))) + return m.push(q); })) { f = !1; break; } - } else if (!(y === A || s(y, A, e, n, i))) { + } else if (!(y === w || s(y, w, e, n, i))) { f = !1; break; } } return i.delete(r), i.delete(t), f; } -function Uh(r) { +function Eh(r) { var t = -1, e = Array(r.size); return r.forEach(function(n, s) { e[++t] = [s, n]; }), e; } -function Fh(r) { +function Ah(r) { var t = -1, e = Array(r.size); return r.forEach(function(n) { e[++t] = n; }), e; } -var Hh = 1, zh = 2, Kh = "[object Boolean]", Gh = "[object Date]", Vh = "[object Error]", Wh = "[object Map]", Zh = "[object Number]", Yh = "[object RegExp]", Xh = "[object Set]", Qh = "[object String]", Jh = "[object Symbol]", tf = "[object ArrayBuffer]", ef = "[object DataView]", tl = Ee ? Ee.prototype : void 0, Cs = tl ? tl.valueOf : void 0; -function nf(r, t, e, n, s, i, o) { +var wh = 1, Nh = 2, Th = "[object Boolean]", xh = "[object Date]", Lh = "[object Error]", Sh = "[object Map]", _h = "[object Number]", Ch = "[object RegExp]", qh = "[object Set]", Oh = "[object String]", Rh = "[object Symbol]", Ih = "[object ArrayBuffer]", kh = "[object DataView]", Pi = ge ? ge.prototype : void 0, ws = Pi ? Pi.valueOf : void 0; +function Bh(r, t, e, n, s, i, o) { switch (e) { - case ef: + case kh: if (r.byteLength != t.byteLength || r.byteOffset != t.byteOffset) return !1; r = r.buffer, t = t.buffer; - case tf: - return !(r.byteLength != t.byteLength || !i(new qr(r), new qr(t))); - case Kh: - case Gh: - case Zh: - return Kn(+r, +t); - case Vh: + case Ih: + return !(r.byteLength != t.byteLength || !i(new yr(r), new yr(t))); + case Th: + case xh: + case _h: + return Bn(+r, +t); + case Lh: return r.name == t.name && r.message == t.message; - case Yh: - case Qh: + case Ch: + case Oh: return r == t + ""; - case Wh: - var a = Uh; - case Xh: - var u = n & Hh; - if (a || (a = Fh), r.size != t.size && !u) + case Sh: + var a = Eh; + case qh: + var u = n & wh; + if (a || (a = Ah), r.size != t.size && !u) return !1; var h = o.get(r); if (h) return h == t; - n |= zh, o.set(r, t); - var p = io(a(r), a(t), n, s, i, o); + n |= Nh, o.set(r, t); + var p = Kl(a(r), a(t), n, s, i, o); return o.delete(r), p; - case Jh: - if (Cs) - return Cs.call(r) == Cs.call(t); + case Rh: + if (ws) + return ws.call(r) == ws.call(t); } return !1; } -var rf = 1, sf = Object.prototype, lf = sf.hasOwnProperty; -function of(r, t, e, n, s, i) { - var o = e & rf, a = Ks(r), u = a.length, h = Ks(t), p = h.length; +var Mh = 1, Dh = Object.prototype, jh = Dh.hasOwnProperty; +function Ph(r, t, e, n, s, i) { + var o = e & Mh, a = Ms(r), u = a.length, h = Ms(t), p = h.length; if (u != p && !o) return !1; for (var v = u; v--; ) { var f = a[v]; - if (!(o ? f in t : lf.call(t, f))) + if (!(o ? f in t : jh.call(t, f))) return !1; } var m = i.get(r), y = i.get(t); if (m && y) return m == t && y == r; - var A = !0; + var w = !0; i.set(r, t), i.set(t, r); for (var N = o; ++v < u; ) { f = a[v]; - var x = r[f], C = t[f]; + var x = r[f], q = t[f]; if (n) - var M = o ? n(C, x, f, t, r, i) : n(x, C, f, r, t, i); - if (!(M === void 0 ? x === C || s(x, C, e, n, i) : M)) { - A = !1; + var B = o ? n(q, x, f, t, r, i) : n(x, q, f, r, t, i); + if (!(B === void 0 ? x === q || s(x, q, e, n, i) : B)) { + w = !1; break; } N || (N = f == "constructor"); } - if (A && !N) { - var z = r.constructor, U = t.constructor; - z != U && "constructor" in r && "constructor" in t && !(typeof z == "function" && z instanceof z && typeof U == "function" && U instanceof U) && (A = !1); + if (w && !N) { + var H = r.constructor, $ = t.constructor; + H != $ && "constructor" in r && "constructor" in t && !(typeof H == "function" && H instanceof H && typeof $ == "function" && $ instanceof $) && (w = !1); } - return i.delete(r), i.delete(t), A; + return i.delete(r), i.delete(t), w; } -var af = 1, el = "[object Arguments]", nl = "[object Array]", pr = "[object Object]", cf = Object.prototype, rl = cf.hasOwnProperty; -function uf(r, t, e, n, s, i) { - var o = Oe(r), a = Oe(t), u = o ? nl : jt(r), h = a ? nl : jt(t); - u = u == el ? pr : u, h = h == el ? pr : h; - var p = u == pr, v = h == pr, f = u == h; - if (f && jn(r)) { - if (!jn(t)) +var $h = 1, $i = "[object Arguments]", Ui = "[object Array]", ir = "[object Object]", Uh = Object.prototype, Fi = Uh.hasOwnProperty; +function Fh(r, t, e, n, s, i) { + var o = xe(r), a = xe(t), u = o ? Ui : Mt(r), h = a ? Ui : Mt(t); + u = u == $i ? ir : u, h = h == $i ? ir : h; + var p = u == ir, v = h == ir, f = u == h; + if (f && Cn(r)) { + if (!Cn(t)) return !1; o = !0, p = !1; } if (f && !p) - return i || (i = new Xt()), o || mi(r) ? io(r, t, e, n, s, i) : nf(r, t, u, e, n, s, i); - if (!(e & af)) { - var m = p && rl.call(r, "__wrapped__"), y = v && rl.call(t, "__wrapped__"); + return i || (i = new Vt()), o || ti(r) ? Kl(r, t, e, n, s, i) : Bh(r, t, u, e, n, s, i); + if (!(e & $h)) { + var m = p && Fi.call(r, "__wrapped__"), y = v && Fi.call(t, "__wrapped__"); if (m || y) { - var A = m ? r.value() : r, N = y ? t.value() : t; - return i || (i = new Xt()), s(A, N, e, n, i); + var w = m ? r.value() : r, N = y ? t.value() : t; + return i || (i = new Vt()), s(w, N, e, n, i); } } - return f ? (i || (i = new Xt()), of(r, t, e, n, s, i)) : !1; + return f ? (i || (i = new Vt()), Ph(r, t, e, n, s, i)) : !1; } -function lo(r, t, e, n, s) { - return r === t ? !0 : r == null || t == null || !oe(r) && !oe(t) ? r !== r && t !== t : uf(r, t, e, n, lo, s); +function Gl(r, t, e, n, s) { + return r === t ? !0 : r == null || t == null || !ne(r) && !ne(t) ? r !== r && t !== t : Fh(r, t, e, n, Gl, s); } -function hf(r) { +function Hh(r) { return function(t, e, n) { for (var s = -1, i = Object(t), o = n(t), a = o.length; a--; ) { var u = o[++s]; @@ -838,57 +826,57 @@ function hf(r) { return t; }; } -var ff = hf(); -function Zs(r, t, e) { - (e !== void 0 && !Kn(r[t], e) || e === void 0 && !(t in r)) && di(r, t, e); +var zh = Hh(); +function $s(r, t, e) { + (e !== void 0 && !Bn(r[t], e) || e === void 0 && !(t in r)) && Xs(r, t, e); } -function df(r) { - return oe(r) && Ir(r); +function Kh(r) { + return ne(r) && Nr(r); } -function Ys(r, t) { +function Us(r, t) { if (!(t === "constructor" && typeof r[t] == "function") && t != "__proto__") return r[t]; } -function gf(r) { - return Ma(r, Zl(r)); +function Gh(r) { + return pa(r, Bl(r)); } -function pf(r, t, e, n, s, i, o) { - var a = Ys(r, e), u = Ys(t, e), h = o.get(u); +function Vh(r, t, e, n, s, i, o) { + var a = Us(r, e), u = Us(t, e), h = o.get(u); if (h) { - Zs(r, e, h); + $s(r, e, h); return; } var p = i ? i(a, u, e + "", r, t, o) : void 0, v = p === void 0; if (v) { - var f = Oe(u), m = !f && jn(u), y = !f && !m && mi(u); - p = u, f || m || y ? Oe(a) ? p = a : df(a) ? p = Na(a) : m ? (v = !1, p = Jl(u, !0)) : y ? (v = !1, p = to(u, !0)) : p = [] : cu(u) || zs(u) ? (p = a, zs(a) ? p = gf(a) : (!Ae(a) || fi(a)) && (p = eo(u))) : v = !1; + var f = xe(u), m = !f && Cn(u), y = !f && !m && ti(u); + p = u, f || m || y ? xe(a) ? p = a : Kh(a) ? p = na(a) : m ? (v = !1, p = Pl(u, !0)) : y ? (v = !1, p = $l(u, !0)) : p = [] : Fc(u) || Bs(u) ? (p = a, Bs(a) ? p = Gh(a) : (!pe(a) || Ys(a)) && (p = Ul(u))) : v = !1; } - v && (o.set(u, p), s(p, u, n, i, o), o.delete(u)), Zs(r, e, p); + v && (o.set(u, p), s(p, u, n, i, o), o.delete(u)), $s(r, e, p); } -function oo(r, t, e, n, s) { - r !== t && ff(t, function(i, o) { - if (s || (s = new Xt()), Ae(i)) - pf(r, t, o, e, oo, n, s); +function Vl(r, t, e, n, s) { + r !== t && zh(t, function(i, o) { + if (s || (s = new Vt()), pe(i)) + Vh(r, t, o, e, Vl, n, s); else { - var a = n ? n(Ys(r, o), i, o + "", r, t, s) : void 0; - a === void 0 && (a = i), Zs(r, o, a); + var a = n ? n(Us(r, o), i, o + "", r, t, s) : void 0; + a === void 0 && (a = i), $s(r, o, a); } - }, Zl); + }, Bl); } -function pe(r, t) { - return lo(r, t); +function ce(r, t) { + return Gl(r, t); } -var ve = Ua(function(r, t, e) { - oo(r, t, e); -}), D = /* @__PURE__ */ ((r) => (r[r.TYPE = 3] = "TYPE", r[r.LEVEL = 12] = "LEVEL", r[r.ATTRIBUTE = 13] = "ATTRIBUTE", r[r.BLOT = 14] = "BLOT", r[r.INLINE = 7] = "INLINE", r[r.BLOCK = 11] = "BLOCK", r[r.BLOCK_BLOT = 10] = "BLOCK_BLOT", r[r.INLINE_BLOT = 6] = "INLINE_BLOT", r[r.BLOCK_ATTRIBUTE = 9] = "BLOCK_ATTRIBUTE", r[r.INLINE_ATTRIBUTE = 5] = "INLINE_ATTRIBUTE", r[r.ANY = 15] = "ANY", r))(D || {}); -class Jt { +var fe = Ea(function(r, t, e) { + Vl(r, t, e); +}), M = /* @__PURE__ */ ((r) => (r[r.TYPE = 3] = "TYPE", r[r.LEVEL = 12] = "LEVEL", r[r.ATTRIBUTE = 13] = "ATTRIBUTE", r[r.BLOT = 14] = "BLOT", r[r.INLINE = 7] = "INLINE", r[r.BLOCK = 11] = "BLOCK", r[r.BLOCK_BLOT = 10] = "BLOCK_BLOT", r[r.INLINE_BLOT = 6] = "INLINE_BLOT", r[r.BLOCK_ATTRIBUTE = 9] = "BLOCK_ATTRIBUTE", r[r.INLINE_ATTRIBUTE = 5] = "INLINE_ATTRIBUTE", r[r.ANY = 15] = "ANY", r))(M || {}); +class Wt { constructor(t, e, n = {}) { this.attrName = t, this.keyName = e; - const s = D.TYPE & D.ATTRIBUTE; + const s = M.TYPE & M.ATTRIBUTE; this.scope = n.scope != null ? ( // Ignore type bits, force attribute bit - n.scope & D.LEVEL | s - ) : D.ATTRIBUTE, n.whitelist != null && (this.whitelist = n.whitelist); + n.scope & M.LEVEL | s + ) : M.ATTRIBUTE, n.whitelist != null && (this.whitelist = n.whitelist); } static keys(t) { return Array.from(t.attributes).map((e) => e.name); @@ -907,12 +895,12 @@ class Jt { return this.canAdd(t, e) && e ? e : ""; } } -class rn extends Error { +class Qe extends Error { constructor(t) { t = "[Parchment] " + t, super(t), this.message = t, this.name = this.constructor.name; } } -const ao = class Xs { +const Wl = class Fs { constructor() { this.attributes = {}, this.classes = {}, this.tags = {}, this.types = {}; } @@ -935,27 +923,27 @@ const ao = class Xs { create(t, e, n) { const s = this.query(e); if (s == null) - throw new rn(`Unable to create ${e} blot`); + throw new Qe(`Unable to create ${e} blot`); const i = s, o = ( // @ts-expect-error Fix me later e instanceof Node || e.nodeType === Node.TEXT_NODE ? e : i.create(n) ), a = new i(t, o, n); - return Xs.blots.set(a.domNode, a), a; + return Fs.blots.set(a.domNode, a), a; } find(t, e = !1) { - return Xs.find(t, e); + return Fs.find(t, e); } - query(t, e = D.ANY) { + query(t, e = M.ANY) { let n; - return typeof t == "string" ? n = this.types[t] || this.attributes[t] : t instanceof Text || t.nodeType === Node.TEXT_NODE ? n = this.types.text : typeof t == "number" ? t & D.LEVEL & D.BLOCK ? n = this.types.block : t & D.LEVEL & D.INLINE && (n = this.types.inline) : t instanceof Element && ((t.getAttribute("class") || "").split(/\s+/).some((s) => (n = this.classes[s], !!n)), n = n || this.tags[t.tagName]), n == null ? null : "scope" in n && e & D.LEVEL & n.scope && e & D.TYPE & n.scope ? n : null; + return typeof t == "string" ? n = this.types[t] || this.attributes[t] : t instanceof Text || t.nodeType === Node.TEXT_NODE ? n = this.types.text : typeof t == "number" ? t & M.LEVEL & M.BLOCK ? n = this.types.block : t & M.LEVEL & M.INLINE && (n = this.types.inline) : t instanceof Element && ((t.getAttribute("class") || "").split(/\s+/).some((s) => (n = this.classes[s], !!n)), n = n || this.tags[t.tagName]), n == null ? null : "scope" in n && e & M.LEVEL & n.scope && e & M.TYPE & n.scope ? n : null; } register(...t) { return t.map((e) => { const n = "blotName" in e, s = "attrName" in e; if (!n && !s) - throw new rn("Invalid definition"); + throw new Qe("Invalid definition"); if (n && e.blotName === "abstract") - throw new rn("Cannot register abstract class"); + throw new Qe("Cannot register abstract class"); const i = n ? e.blotName : s ? e.attrName : void 0; return this.types[i] = e, s ? typeof e.keyName == "string" && (this.attributes[e.keyName] = e) : n && (e.className && (this.classes[e.className] = e), e.tagName && (Array.isArray(e.tagName) ? e.tagName = e.tagName.map((o) => o.toUpperCase()) : e.tagName = e.tagName.toUpperCase(), (Array.isArray(e.tagName) ? e.tagName : [e.tagName]).forEach((o) => { (this.tags[o] == null || e.className == null) && (this.tags[o] = e); @@ -963,12 +951,12 @@ const ao = class Xs { }); } }; -ao.blots = /* @__PURE__ */ new WeakMap(); -let on = ao; -function sl(r, t) { +Wl.blots = /* @__PURE__ */ new WeakMap(); +let en = Wl; +function Hi(r, t) { return (r.getAttribute("class") || "").split(/\s+/).filter((e) => e.indexOf(`${t}-`) === 0); } -class mf extends Jt { +class Wh extends Wt { static keys(t) { return (t.getAttribute("class") || "").split(/\s+/).map((e) => e.split("-").slice(0, -1).join("-")); } @@ -976,37 +964,37 @@ class mf extends Jt { return this.canAdd(t, e) ? (this.remove(t), t.classList.add(`${this.keyName}-${e}`), !0) : !1; } remove(t) { - sl(t, this.keyName).forEach((e) => { + Hi(t, this.keyName).forEach((e) => { t.classList.remove(e); }), t.classList.length === 0 && t.removeAttribute("class"); } value(t) { - const e = (sl(t, this.keyName)[0] || "").slice(this.keyName.length + 1); + const e = (Hi(t, this.keyName)[0] || "").slice(this.keyName.length + 1); return this.canAdd(t, e) ? e : ""; } } -const Ft = mf; -function Is(r) { +const Ut = Wh; +function Ns(r) { const t = r.split("-"), e = t.slice(1).map((n) => n[0].toUpperCase() + n.slice(1)).join(""); return t[0] + e; } -class bf extends Jt { +class Zh extends Wt { static keys(t) { return (t.getAttribute("style") || "").split(";").map((e) => e.split(":")[0].trim()); } add(t, e) { - return this.canAdd(t, e) ? (t.style[Is(this.keyName)] = e, !0) : !1; + return this.canAdd(t, e) ? (t.style[Ns(this.keyName)] = e, !0) : !1; } remove(t) { - t.style[Is(this.keyName)] = "", t.getAttribute("style") || t.removeAttribute("style"); + t.style[Ns(this.keyName)] = "", t.getAttribute("style") || t.removeAttribute("style"); } value(t) { - const e = t.style[Is(this.keyName)]; + const e = t.style[Ns(this.keyName)]; return this.canAdd(t, e) ? e : ""; } } -const we = bf; -class yf { +const me = Zh; +class Yh { constructor(t) { this.attributes = {}, this.domNode = t, this.build(); } @@ -1015,13 +1003,13 @@ class yf { } build() { this.attributes = {}; - const t = on.find(this.domNode); + const t = en.find(this.domNode); if (t == null) return; - const e = Jt.keys(this.domNode), n = Ft.keys(this.domNode), s = we.keys(this.domNode); + const e = Wt.keys(this.domNode), n = Ut.keys(this.domNode), s = me.keys(this.domNode); e.concat(n).concat(s).forEach((i) => { - const o = t.scroll.query(i, D.ATTRIBUTE); - o instanceof Jt && (this.attributes[o.attrName] = o); + const o = t.scroll.query(i, M.ATTRIBUTE); + o instanceof Wt && (this.attributes[o.attrName] = o); }); } copy(t) { @@ -1042,13 +1030,13 @@ class yf { ); } } -const Br = yf, co = class { +const Lr = Yh, Zl = class { constructor(t, e) { - this.scroll = t, this.domNode = e, on.blots.set(e, this), this.prev = null, this.next = null; + this.scroll = t, this.domNode = e, en.blots.set(e, this), this.prev = null, this.next = null; } static create(t) { if (this.tagName == null) - throw new rn("Blot definition missing tagName"); + throw new Qe("Blot definition missing tagName"); let e, n; return Array.isArray(this.tagName) ? (typeof t == "string" ? (n = t.toUpperCase(), parseInt(n, 10).toString() === n && (n = parseInt(n, 10))) : typeof t == "number" && (n = t), typeof n == "number" ? e = document.createElement(this.tagName[n - 1]) : n && this.tagName.indexOf(n) > -1 ? e = document.createElement(n) : e = document.createElement(this.tagName[0])) : e = document.createElement(this.tagName), this.className && e.classList.add(this.className), e; } @@ -1063,16 +1051,16 @@ const Br = yf, co = class { return this.scroll.create(t); } detach() { - this.parent != null && this.parent.removeChild(this), on.blots.delete(this.domNode); + this.parent != null && this.parent.removeChild(this), en.blots.delete(this.domNode); } deleteAt(t, e) { this.isolate(t, e).remove(); } formatAt(t, e, n, s) { const i = this.isolate(t, e); - if (this.scroll.query(n, D.BLOT) != null && s) + if (this.scroll.query(n, M.BLOT) != null && s) i.wrap(n, s); - else if (this.scroll.query(n, D.ATTRIBUTE) != null) { + else if (this.scroll.query(n, M.ATTRIBUTE) != null) { const o = this.scroll.create(this.statics.scope); i.wrap(o), o.format(n, s); } @@ -1111,13 +1099,13 @@ const Br = yf, co = class { wrap(t, e) { const n = typeof t == "string" ? this.scroll.create(t, e) : t; if (this.parent != null && this.parent.insertBefore(n, this.next || void 0), typeof n.appendChild != "function") - throw new rn(`Cannot wrap ${t}`); + throw new Qe(`Cannot wrap ${t}`); return n.appendChild(this), n; } }; -co.blotName = "abstract"; -let uo = co; -const ho = class extends uo { +Zl.blotName = "abstract"; +let Yl = Zl; +const Xl = class extends Yl { /** * Returns the value represented by domNode if it is this Blot's type * No checking that domNode can represent this Blot type is required so @@ -1152,10 +1140,10 @@ const ho = class extends uo { }; } }; -ho.scope = D.INLINE_BLOT; -let vf = ho; -const Et = vf; -class Ef { +Xl.scope = M.INLINE_BLOT; +let Xh = Xl; +const vt = Xh; +class Qh { constructor() { this.head = null, this.tail = null, this.length = 0; } @@ -1257,20 +1245,20 @@ class Ef { return e; } } -function il(r, t) { +function zi(r, t) { const e = t.find(r); if (e) return e; try { return t.create(r); } catch { - const n = t.create(D.INLINE); + const n = t.create(M.INLINE); return Array.from(r.childNodes).forEach((s) => { n.domNode.appendChild(s); }), r.parentNode && r.parentNode.replaceChild(n.domNode, r), n.attach(), n; } } -const fo = class ge extends uo { +const Ql = class ae extends Yl { constructor(t, e) { super(t, e), this.uiNode = null, this.build(); } @@ -1283,18 +1271,18 @@ const fo = class ge extends uo { }); } attachUI(t) { - this.uiNode != null && this.uiNode.remove(), this.uiNode = t, ge.uiClass && this.uiNode.classList.add(ge.uiClass), this.uiNode.setAttribute("contenteditable", "false"), this.domNode.insertBefore(this.uiNode, this.domNode.firstChild); + this.uiNode != null && this.uiNode.remove(), this.uiNode = t, ae.uiClass && this.uiNode.classList.add(ae.uiClass), this.uiNode.setAttribute("contenteditable", "false"), this.domNode.insertBefore(this.uiNode, this.domNode.firstChild); } /** * Called during construction, should fill its own children LinkedList. */ build() { - this.children = new Ef(), Array.from(this.domNode.childNodes).filter((t) => t !== this.uiNode).reverse().forEach((t) => { + this.children = new Qh(), Array.from(this.domNode.childNodes).filter((t) => t !== this.uiNode).reverse().forEach((t) => { try { - const e = il(t, this.scroll); + const e = zi(t, this.scroll); this.insertBefore(e, this.children.head || void 0); } catch (e) { - if (e instanceof rn) + if (e instanceof Qe) return; throw e; } @@ -1309,7 +1297,7 @@ const fo = class ge extends uo { } descendant(t, e = 0) { const [n, s] = this.children.find(e); - return t.blotName == null && t(n) || t.blotName != null && n instanceof t ? [n, s] : n instanceof ge ? n.descendant(t, s) : [null, -1]; + return t.blotName == null && t(n) || t.blotName != null && n instanceof t ? [n, s] : n instanceof ae ? n.descendant(t, s) : [null, -1]; } descendants(t, e = 0, n = Number.MAX_VALUE) { let s = [], i = n; @@ -1317,7 +1305,7 @@ const fo = class ge extends uo { e, n, (o, a, u) => { - (t.blotName == null && t(o) || t.blotName != null && o instanceof t) && s.push(o), o instanceof ge && (s = s.concat( + (t.blotName == null && t(o) || t.blotName != null && o instanceof t) && s.push(o), o instanceof ae && (s = s.concat( o.descendants(t, a, i) )), i -= u; } @@ -1333,7 +1321,7 @@ const fo = class ge extends uo { this.children.forEach((e) => { t || this.statics.allowedChildren.some( (n) => e instanceof n - ) || (e.statics.scope === D.BLOCK_BLOT ? (e.next != null && this.splitAfter(e), e.prev != null && this.splitAfter(e.prev), e.parent.unwrap(), t = !0) : e instanceof ge ? e.unwrap() : e.remove()); + ) || (e.statics.scope === M.BLOCK_BLOT ? (e.next != null && this.splitAfter(e), e.prev != null && this.splitAfter(e.prev), e.parent.unwrap(), t = !0) : e instanceof ae ? e.unwrap() : e.remove()); }); } formatAt(t, e, n, s) { @@ -1373,14 +1361,14 @@ const fo = class ge extends uo { } path(t, e = !1) { const [n, s] = this.children.find(t, e), i = [[this, t]]; - return n instanceof ge ? i.concat(n.path(s, e)) : (n != null && i.push([n, s]), i); + return n instanceof ae ? i.concat(n.path(s, e)) : (n != null && i.push([n, s]), i); } removeChild(t) { this.children.remove(t); } replaceWith(t, e) { const n = typeof t == "string" ? this.scroll.create(t, e) : t; - return n instanceof ge && this.moveChildren(n), super.replaceWith(n); + return n instanceof ae && this.moveChildren(n), super.replaceWith(n); } split(t, e = !1) { if (!e) { @@ -1417,15 +1405,15 @@ const fo = class ge extends uo { }), n.filter((i) => i.parentNode === this.domNode && i !== this.uiNode).sort((i, o) => i === o ? 0 : i.compareDocumentPosition(o) & Node.DOCUMENT_POSITION_FOLLOWING ? 1 : -1).forEach((i) => { let o = null; i.nextSibling != null && (o = this.scroll.find(i.nextSibling)); - const a = il(i, this.scroll); + const a = zi(i, this.scroll); (a.next !== o || a.next == null) && (a.parent != null && a.parent.removeChild(this), this.insertBefore(a, o || void 0)); }), this.enforceAllowedChildren(); } }; -fo.uiClass = ""; -let Af = fo; -const Pt = Af; -function wf(r, t) { +Ql.uiClass = ""; +let Jh = Ql; +const Pt = Jh; +function tf(r, t) { if (Object.keys(r).length !== Object.keys(t).length) return !1; for (const e in r) @@ -1433,12 +1421,12 @@ function wf(r, t) { return !1; return !0; } -const Ye = class Xe extends Pt { +const Ge = class Ve extends Pt { static create(t) { return super.create(t); } static formats(t, e) { - const n = e.query(Xe.blotName); + const n = e.query(Ve.blotName); if (!(n != null && t.tagName === n.tagName)) { if (typeof this.tagName == "string") return !0; @@ -1447,18 +1435,18 @@ const Ye = class Xe extends Pt { } } constructor(t, e) { - super(t, e), this.attributes = new Br(this.domNode); + super(t, e), this.attributes = new Lr(this.domNode); } format(t, e) { if (t === this.statics.blotName && !e) this.children.forEach((n) => { - n instanceof Xe || (n = n.wrap(Xe.blotName, !0)), this.attributes.copy(n); + n instanceof Ve || (n = n.wrap(Ve.blotName, !0)), this.attributes.copy(n); }), this.unwrap(); else { - const n = this.scroll.query(t, D.INLINE); + const n = this.scroll.query(t, M.INLINE); if (n == null) return; - n instanceof Jt ? this.attributes.attribute(n, e) : e && (t !== this.statics.blotName || this.formats()[t] !== e) && this.replaceWith(t, e); + n instanceof Wt ? this.attributes.attribute(n, e) : e && (t !== this.statics.blotName || this.formats()[t] !== e) && this.replaceWith(t, e); } } formats() { @@ -1466,7 +1454,7 @@ const Ye = class Xe extends Pt { return e != null && (t[this.statics.blotName] = e), t; } formatAt(t, e, n, s) { - this.formats()[n] != null || this.scroll.query(n, D.ATTRIBUTE) ? this.isolate(t, e).format(n, s) : super.formatAt(t, e, n, s); + this.formats()[n] != null || this.scroll.query(n, M.ATTRIBUTE) ? this.isolate(t, e).format(n, s) : super.formatAt(t, e, n, s); } optimize(t) { super.optimize(t); @@ -1474,7 +1462,7 @@ const Ye = class Xe extends Pt { if (Object.keys(e).length === 0) return this.unwrap(); const n = this.next; - n instanceof Xe && n.prev === this && wf(e, n.formats()) && (n.moveChildren(this), n.remove()); + n instanceof Ve && n.prev === this && tf(e, n.formats()) && (n.moveChildren(this), n.remove()); } replaceWith(t, e) { const n = super.replaceWith(t, e); @@ -1487,17 +1475,17 @@ const Ye = class Xe extends Pt { } wrap(t, e) { const n = super.wrap(t, e); - return n instanceof Xe && this.attributes.move(n), n; + return n instanceof Ve && this.attributes.move(n), n; } }; -Ye.allowedChildren = [Ye, Et], Ye.blotName = "inline", Ye.scope = D.INLINE_BLOT, Ye.tagName = "SPAN"; -let Nf = Ye; -const yi = Nf, Qe = class Qs extends Pt { +Ge.allowedChildren = [Ge, vt], Ge.blotName = "inline", Ge.scope = M.INLINE_BLOT, Ge.tagName = "SPAN"; +let ef = Ge; +const ni = ef, We = class Hs extends Pt { static create(t) { return super.create(t); } static formats(t, e) { - const n = e.query(Qs.blotName); + const n = e.query(Hs.blotName); if (!(n != null && t.tagName === n.tagName)) { if (typeof this.tagName == "string") return !0; @@ -1506,21 +1494,21 @@ const yi = Nf, Qe = class Qs extends Pt { } } constructor(t, e) { - super(t, e), this.attributes = new Br(this.domNode); + super(t, e), this.attributes = new Lr(this.domNode); } format(t, e) { - const n = this.scroll.query(t, D.BLOCK); - n != null && (n instanceof Jt ? this.attributes.attribute(n, e) : t === this.statics.blotName && !e ? this.replaceWith(Qs.blotName) : e && (t !== this.statics.blotName || this.formats()[t] !== e) && this.replaceWith(t, e)); + const n = this.scroll.query(t, M.BLOCK); + n != null && (n instanceof Wt ? this.attributes.attribute(n, e) : t === this.statics.blotName && !e ? this.replaceWith(Hs.blotName) : e && (t !== this.statics.blotName || this.formats()[t] !== e) && this.replaceWith(t, e)); } formats() { const t = this.attributes.values(), e = this.statics.formats(this.domNode, this.scroll); return e != null && (t[this.statics.blotName] = e), t; } formatAt(t, e, n, s) { - this.scroll.query(n, D.BLOCK) != null ? this.format(n, s) : super.formatAt(t, e, n, s); + this.scroll.query(n, M.BLOCK) != null ? this.format(n, s) : super.formatAt(t, e, n, s); } insertAt(t, e, n) { - if (n == null || this.scroll.query(e, D.INLINE) != null) + if (n == null || this.scroll.query(e, M.INLINE) != null) super.insertAt(t, e, n); else { const s = this.split(t); @@ -1541,13 +1529,13 @@ const yi = Nf, Qe = class Qs extends Pt { ) && this.attributes.build(); } }; -Qe.blotName = "block", Qe.scope = D.BLOCK_BLOT, Qe.tagName = "P", Qe.allowedChildren = [ - yi, - Qe, - Et +We.blotName = "block", We.scope = M.BLOCK_BLOT, We.tagName = "P", We.allowedChildren = [ + ni, + We, + vt ]; -let Tf = Qe; -const Un = Tf, Js = class extends Pt { +let nf = We; +const Rn = nf, zs = class extends Pt { checkMerge() { return this.next !== null && this.next.statics.blotName === this.statics.blotName; } @@ -1564,10 +1552,10 @@ const Un = Tf, Js = class extends Pt { super.optimize(t), this.children.length > 0 && this.next != null && this.checkMerge() && (this.next.moveChildren(this), this.next.remove()); } }; -Js.blotName = "container", Js.scope = D.BLOCK_BLOT; -let xf = Js; -const Mr = xf; -class Lf extends Et { +zs.blotName = "container", zs.scope = M.BLOCK_BLOT; +let rf = zs; +const Sr = rf; +class sf extends vt { static formats(t, e) { } format(t, e) { @@ -1580,17 +1568,17 @@ class Lf extends Et { return this.statics.formats(this.domNode, this.scroll); } } -const Lt = Lf, Sf = { +const Lt = sf, lf = { attributes: !0, characterData: !0, characterDataOldValue: !0, childList: !0, subtree: !0 -}, _f = 100, Je = class extends Pt { +}, of = 100, Ze = class extends Pt { constructor(t, e) { super(null, e), this.registry = t, this.scroll = this, this.build(), this.observer = new MutationObserver((n) => { this.update(n); - }), this.observer.observe(this.domNode, Sf), this.attach(); + }), this.observer.observe(this.domNode, lf), this.attach(); } create(t, e) { return this.registry.create(this, t, e); @@ -1599,7 +1587,7 @@ const Lt = Lf, Sf = { const n = this.registry.find(t, e); return n ? n.scroll === this ? n : e ? this.find(n.scroll.domNode.parentNode, !0) : null : null; } - query(t, e = D.ANY) { + query(t, e = M.ANY) { return this.registry.query(t, e); } register(...t) { @@ -1635,7 +1623,7 @@ const Lt = Lf, Sf = { }; let a = t; for (let u = 0; a.length > 0; u += 1) { - if (u >= _f) + if (u >= of) throw new Error("[Parchment] Maximum optimize iterations reached"); for (a.forEach((h) => { const p = this.find(h.target, !0); @@ -1660,9 +1648,9 @@ const Lt = Lf, Sf = { }), e.mutationsMap = n, n.has(this.domNode) && super.update(n.get(this.domNode), e), this.optimize(t, e); } }; -Je.blotName = "scroll", Je.defaultChild = Un, Je.allowedChildren = [Un, Mr], Je.scope = D.BLOCK_BLOT, Je.tagName = "DIV"; -let qf = Je; -const vi = qf, ti = class go extends Et { +Ze.blotName = "scroll", Ze.defaultChild = Rn, Ze.allowedChildren = [Rn, Sr], Ze.scope = M.BLOCK_BLOT, Ze.tagName = "DIV"; +let af = Ze; +const ri = af, Ks = class Jl extends vt { static create(t) { return document.createTextNode(t); } @@ -1685,7 +1673,7 @@ const vi = qf, ti = class go extends Et { return this.text.length; } optimize(t) { - super.optimize(t), this.text = this.statics.value(this.domNode), this.text.length === 0 ? this.remove() : this.next instanceof go && this.next.prev === this && (this.insertAt(this.length(), this.next.value()), this.next.remove()); + super.optimize(t), this.text = this.statics.value(this.domNode), this.text.length === 0 ? this.remove() : this.next instanceof Jl && this.next.prev === this && (this.insertAt(this.length(), this.next.value()), this.next.remove()); } position(t, e = !1) { return [this.domNode, t]; @@ -1707,31 +1695,31 @@ const vi = qf, ti = class go extends Et { return this.text; } }; -ti.blotName = "text", ti.scope = D.INLINE_BLOT; -let Of = ti; -const Cr = Of, Cf = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +Ks.blotName = "text", Ks.scope = M.INLINE_BLOT; +let cf = Ks; +const Er = cf, uf = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, - Attributor: Jt, - AttributorStore: Br, - BlockBlot: Un, - ClassAttributor: Ft, - ContainerBlot: Mr, + Attributor: Wt, + AttributorStore: Lr, + BlockBlot: Rn, + ClassAttributor: Ut, + ContainerBlot: Sr, EmbedBlot: Lt, - InlineBlot: yi, - LeafBlot: Et, + InlineBlot: ni, + LeafBlot: vt, ParentBlot: Pt, - Registry: on, - Scope: D, - ScrollBlot: vi, - StyleAttributor: we, - TextBlot: Cr + Registry: en, + Scope: M, + ScrollBlot: ri, + StyleAttributor: me, + TextBlot: Er }, Symbol.toStringTag, { value: "Module" })); -var mr = { exports: {} }, Rs, ll; -function If() { - if (ll) return Rs; - ll = 1; +var lr = { exports: {} }, Ts, Ki; +function hf() { + if (Ki) return Ts; + Ki = 1; var r = -1, t = 1, e = 0; - function n(g, L, b, q, S) { + function n(g, L, b, C, S) { if (g === L) return g ? [[e, g]] : []; if (b != null) { @@ -1739,12 +1727,12 @@ function If() { if (T) return T; } - var _ = a(g, L), k = g.substring(0, _); + var _ = a(g, L), I = g.substring(0, _); g = g.substring(_), L = L.substring(_), _ = h(g, L); - var j = g.substring(g.length - _); + var D = g.substring(g.length - _); g = g.substring(0, g.length - _), L = L.substring(0, L.length - _); - var I = s(g, L); - return k && I.unshift([e, k]), j && I.push([e, j]), C(I, S), q && v(I), I; + var O = s(g, L); + return I && O.unshift([e, I]), D && O.push([e, D]), q(O, S), C && v(O), O; } function s(g, L) { var b; @@ -1752,12 +1740,12 @@ function If() { return [[t, L]]; if (!L) return [[r, g]]; - var q = g.length > L.length ? g : L, S = g.length > L.length ? L : g, T = q.indexOf(S); + var C = g.length > L.length ? g : L, S = g.length > L.length ? L : g, T = C.indexOf(S); if (T !== -1) return b = [ - [t, q.substring(0, T)], + [t, C.substring(0, T)], [e, S], - [t, q.substring(T + S.length)] + [t, C.substring(T + S.length)] ], g.length > L.length && (b[0][0] = b[2][0] = r), b; if (S.length === 1) return [ @@ -1766,49 +1754,49 @@ function If() { ]; var _ = p(g, L); if (_) { - var k = _[0], j = _[1], I = _[2], K = _[3], F = _[4], V = n(k, I), W = n(j, K); - return V.concat([[e, F]], W); + var I = _[0], D = _[1], O = _[2], z = _[3], U = _[4], G = n(I, O), V = n(D, z); + return G.concat([[e, U]], V); } return i(g, L); } function i(g, L) { - for (var b = g.length, q = L.length, S = Math.ceil((b + q) / 2), T = S, _ = 2 * S, k = new Array(_), j = new Array(_), I = 0; I < _; I++) - k[I] = -1, j[I] = -1; - k[T + 1] = 0, j[T + 1] = 0; - for (var K = b - q, F = K % 2 !== 0, V = 0, W = 0, P = 0, it = 0, ot = 0; ot < S; ot++) { - for (var G = -ot + V; G <= ot - W; G += 2) { - var Q = T + G, X; - G === -ot || G !== ot && k[Q - 1] < k[Q + 1] ? X = k[Q + 1] : X = k[Q - 1] + 1; - for (var J = X - G; X < b && J < q && g.charAt(X) === L.charAt(J); ) - X++, J++; - if (k[Q] = X, X > b) - W += 2; - else if (J > q) + for (var b = g.length, C = L.length, S = Math.ceil((b + C) / 2), T = S, _ = 2 * S, I = new Array(_), D = new Array(_), O = 0; O < _; O++) + I[O] = -1, D[O] = -1; + I[T + 1] = 0, D[T + 1] = 0; + for (var z = b - C, U = z % 2 !== 0, G = 0, V = 0, P = 0, st = 0, lt = 0; lt < S; lt++) { + for (var K = -lt + G; K <= lt - V; K += 2) { + var X = T + K, Y; + K === -lt || K !== lt && I[X - 1] < I[X + 1] ? Y = I[X + 1] : Y = I[X - 1] + 1; + for (var Q = Y - K; Y < b && Q < C && g.charAt(Y) === L.charAt(Q); ) + Y++, Q++; + if (I[X] = Y, Y > b) V += 2; - else if (F) { - var et = T + K - G; - if (et >= 0 && et < _ && j[et] !== -1) { - var nt = b - j[et]; - if (X >= nt) - return o(g, L, X, J); + else if (Q > C) + G += 2; + else if (U) { + var tt = T + z - K; + if (tt >= 0 && tt < _ && D[tt] !== -1) { + var et = b - D[tt]; + if (Y >= et) + return o(g, L, Y, Q); } } } - for (var ct = -ot + P; ct <= ot - it; ct += 2) { - var et = T + ct, nt; - ct === -ot || ct !== ot && j[et - 1] < j[et + 1] ? nt = j[et + 1] : nt = j[et - 1] + 1; - for (var mt = nt - ct; nt < b && mt < q && g.charAt(b - nt - 1) === L.charAt(q - mt - 1); ) - nt++, mt++; - if (j[et] = nt, nt > b) - it += 2; - else if (mt > q) + for (var at = -lt + P; at <= lt - st; at += 2) { + var tt = T + at, et; + at === -lt || at !== lt && D[tt - 1] < D[tt + 1] ? et = D[tt + 1] : et = D[tt - 1] + 1; + for (var pt = et - at; et < b && pt < C && g.charAt(b - et - 1) === L.charAt(C - pt - 1); ) + et++, pt++; + if (D[tt] = et, et > b) + st += 2; + else if (pt > C) P += 2; - else if (!F) { - var Q = T + K - ct; - if (Q >= 0 && Q < _ && k[Q] !== -1) { - var X = k[Q], J = T + X - Q; - if (nt = b - nt, X >= nt) - return o(g, L, X, J); + else if (!U) { + var X = T + z - at; + if (X >= 0 && X < _ && I[X] !== -1) { + var Y = I[X], Q = T + Y - X; + if (et = b - et, Y >= et) + return o(g, L, Y, Q); } } } @@ -1818,133 +1806,133 @@ function If() { [t, L] ]; } - function o(g, L, b, q) { - var S = g.substring(0, b), T = L.substring(0, q), _ = g.substring(b), k = L.substring(q), j = n(S, T), I = n(_, k); - return j.concat(I); + function o(g, L, b, C) { + var S = g.substring(0, b), T = L.substring(0, C), _ = g.substring(b), I = L.substring(C), D = n(S, T), O = n(_, I); + return D.concat(O); } function a(g, L) { if (!g || !L || g.charAt(0) !== L.charAt(0)) return 0; - for (var b = 0, q = Math.min(g.length, L.length), S = q, T = 0; b < S; ) - g.substring(T, S) == L.substring(T, S) ? (b = S, T = b) : q = S, S = Math.floor((q - b) / 2 + b); - return M(g.charCodeAt(S - 1)) && S--, S; + for (var b = 0, C = Math.min(g.length, L.length), S = C, T = 0; b < S; ) + g.substring(T, S) == L.substring(T, S) ? (b = S, T = b) : C = S, S = Math.floor((C - b) / 2 + b); + return B(g.charCodeAt(S - 1)) && S--, S; } function u(g, L) { - var b = g.length, q = L.length; - if (b == 0 || q == 0) + var b = g.length, C = L.length; + if (b == 0 || C == 0) return 0; - b > q ? g = g.substring(b - q) : b < q && (L = L.substring(0, b)); - var S = Math.min(b, q); + b > C ? g = g.substring(b - C) : b < C && (L = L.substring(0, b)); + var S = Math.min(b, C); if (g == L) return S; for (var T = 0, _ = 1; ; ) { - var k = g.substring(S - _), j = L.indexOf(k); - if (j == -1) + var I = g.substring(S - _), D = L.indexOf(I); + if (D == -1) return T; - _ += j, (j == 0 || g.substring(S - _) == L.substring(0, _)) && (T = _, _++); + _ += D, (D == 0 || g.substring(S - _) == L.substring(0, _)) && (T = _, _++); } } function h(g, L) { if (!g || !L || g.slice(-1) !== L.slice(-1)) return 0; - for (var b = 0, q = Math.min(g.length, L.length), S = q, T = 0; b < S; ) - g.substring(g.length - S, g.length - T) == L.substring(L.length - S, L.length - T) ? (b = S, T = b) : q = S, S = Math.floor((q - b) / 2 + b); - return z(g.charCodeAt(g.length - S)) && S--, S; + for (var b = 0, C = Math.min(g.length, L.length), S = C, T = 0; b < S; ) + g.substring(g.length - S, g.length - T) == L.substring(L.length - S, L.length - T) ? (b = S, T = b) : C = S, S = Math.floor((C - b) / 2 + b); + return H(g.charCodeAt(g.length - S)) && S--, S; } function p(g, L) { - var b = g.length > L.length ? g : L, q = g.length > L.length ? L : g; - if (b.length < 4 || q.length * 2 < b.length) + var b = g.length > L.length ? g : L, C = g.length > L.length ? L : g; + if (b.length < 4 || C.length * 2 < b.length) return null; - function S(W, P, it) { - for (var ot = W.substring(it, it + Math.floor(W.length / 4)), G = -1, Q = "", X, J, et, nt; (G = P.indexOf(ot, G + 1)) !== -1; ) { - var ct = a( - W.substring(it), - P.substring(G) - ), mt = h( - W.substring(0, it), - P.substring(0, G) + function S(V, P, st) { + for (var lt = V.substring(st, st + Math.floor(V.length / 4)), K = -1, X = "", Y, Q, tt, et; (K = P.indexOf(lt, K + 1)) !== -1; ) { + var at = a( + V.substring(st), + P.substring(K) + ), pt = h( + V.substring(0, st), + P.substring(0, K) ); - Q.length < mt + ct && (Q = P.substring(G - mt, G) + P.substring(G, G + ct), X = W.substring(0, it - mt), J = W.substring(it + ct), et = P.substring(0, G - mt), nt = P.substring(G + ct)); + X.length < pt + at && (X = P.substring(K - pt, K) + P.substring(K, K + at), Y = V.substring(0, st - pt), Q = V.substring(st + at), tt = P.substring(0, K - pt), et = P.substring(K + at)); } - return Q.length * 2 >= W.length ? [ - X, - J, + return X.length * 2 >= V.length ? [ + Y, + Q, + tt, et, - nt, - Q + X ] : null; } var T = S( b, - q, + C, Math.ceil(b.length / 4) ), _ = S( b, - q, + C, Math.ceil(b.length / 2) - ), k; + ), I; if (!T && !_) return null; - _ ? T ? k = T[4].length > _[4].length ? T : _ : k = _ : k = T; - var j, I, K, F; - g.length > L.length ? (j = k[0], I = k[1], K = k[2], F = k[3]) : (K = k[0], F = k[1], j = k[2], I = k[3]); - var V = k[4]; - return [j, I, K, F, V]; + _ ? T ? I = T[4].length > _[4].length ? T : _ : I = _ : I = T; + var D, O, z, U; + g.length > L.length ? (D = I[0], O = I[1], z = I[2], U = I[3]) : (z = I[0], U = I[1], D = I[2], O = I[3]); + var G = I[4]; + return [D, O, z, U, G]; } function v(g) { - for (var L = !1, b = [], q = 0, S = null, T = 0, _ = 0, k = 0, j = 0, I = 0; T < g.length; ) - g[T][0] == e ? (b[q++] = T, _ = j, k = I, j = 0, I = 0, S = g[T][1]) : (g[T][0] == t ? j += g[T][1].length : I += g[T][1].length, S && S.length <= Math.max(_, k) && S.length <= Math.max(j, I) && (g.splice(b[q - 1], 0, [ + for (var L = !1, b = [], C = 0, S = null, T = 0, _ = 0, I = 0, D = 0, O = 0; T < g.length; ) + g[T][0] == e ? (b[C++] = T, _ = D, I = O, D = 0, O = 0, S = g[T][1]) : (g[T][0] == t ? D += g[T][1].length : O += g[T][1].length, S && S.length <= Math.max(_, I) && S.length <= Math.max(D, O) && (g.splice(b[C - 1], 0, [ r, S - ]), g[b[q - 1] + 1][0] = t, q--, q--, T = q > 0 ? b[q - 1] : -1, _ = 0, k = 0, j = 0, I = 0, S = null, L = !0)), T++; - for (L && C(g), x(g), T = 1; T < g.length; ) { + ]), g[b[C - 1] + 1][0] = t, C--, C--, T = C > 0 ? b[C - 1] : -1, _ = 0, I = 0, D = 0, O = 0, S = null, L = !0)), T++; + for (L && q(g), x(g), T = 1; T < g.length; ) { if (g[T - 1][0] == r && g[T][0] == t) { - var K = g[T - 1][1], F = g[T][1], V = u(K, F), W = u(F, K); - V >= W ? (V >= K.length / 2 || V >= F.length / 2) && (g.splice(T, 0, [ + var z = g[T - 1][1], U = g[T][1], G = u(z, U), V = u(U, z); + G >= V ? (G >= z.length / 2 || G >= U.length / 2) && (g.splice(T, 0, [ e, - F.substring(0, V) - ]), g[T - 1][1] = K.substring( + U.substring(0, G) + ]), g[T - 1][1] = z.substring( 0, - K.length - V - ), g[T + 1][1] = F.substring(V), T++) : (W >= K.length / 2 || W >= F.length / 2) && (g.splice(T, 0, [ + z.length - G + ), g[T + 1][1] = U.substring(G), T++) : (V >= z.length / 2 || V >= U.length / 2) && (g.splice(T, 0, [ e, - K.substring(0, W) - ]), g[T - 1][0] = t, g[T - 1][1] = F.substring( + z.substring(0, V) + ]), g[T - 1][0] = t, g[T - 1][1] = U.substring( 0, - F.length - W - ), g[T + 1][0] = r, g[T + 1][1] = K.substring(W), T++), T++; + U.length - V + ), g[T + 1][0] = r, g[T + 1][1] = z.substring(V), T++), T++; } T++; } } - var f = /[^a-zA-Z0-9]/, m = /\s/, y = /[\r\n]/, A = /\n\r?\n$/, N = /^\r?\n\r?\n/; + var f = /[^a-zA-Z0-9]/, m = /\s/, y = /[\r\n]/, w = /\n\r?\n$/, N = /^\r?\n\r?\n/; function x(g) { - function L(W, P) { - if (!W || !P) + function L(V, P) { + if (!V || !P) return 6; - var it = W.charAt(W.length - 1), ot = P.charAt(0), G = it.match(f), Q = ot.match(f), X = G && it.match(m), J = Q && ot.match(m), et = X && it.match(y), nt = J && ot.match(y), ct = et && W.match(A), mt = nt && P.match(N); - return ct || mt ? 5 : et || nt ? 4 : G && !X && J ? 3 : X || J ? 2 : G || Q ? 1 : 0; + var st = V.charAt(V.length - 1), lt = P.charAt(0), K = st.match(f), X = lt.match(f), Y = K && st.match(m), Q = X && lt.match(m), tt = Y && st.match(y), et = Q && lt.match(y), at = tt && V.match(w), pt = et && P.match(N); + return at || pt ? 5 : tt || et ? 4 : K && !Y && Q ? 3 : Y || Q ? 2 : K || X ? 1 : 0; } for (var b = 1; b < g.length - 1; ) { if (g[b - 1][0] == e && g[b + 1][0] == e) { - var q = g[b - 1][1], S = g[b][1], T = g[b + 1][1], _ = h(q, S); + var C = g[b - 1][1], S = g[b][1], T = g[b + 1][1], _ = h(C, S); if (_) { - var k = S.substring(S.length - _); - q = q.substring(0, q.length - _), S = k + S.substring(0, S.length - _), T = k + T; + var I = S.substring(S.length - _); + C = C.substring(0, C.length - _), S = I + S.substring(0, S.length - _), T = I + T; } - for (var j = q, I = S, K = T, F = L(q, S) + L(S, T); S.charAt(0) === T.charAt(0); ) { - q += S.charAt(0), S = S.substring(1) + T.charAt(0), T = T.substring(1); - var V = L(q, S) + L(S, T); - V >= F && (F = V, j = q, I = S, K = T); + for (var D = C, O = S, z = T, U = L(C, S) + L(S, T); S.charAt(0) === T.charAt(0); ) { + C += S.charAt(0), S = S.substring(1) + T.charAt(0), T = T.substring(1); + var G = L(C, S) + L(S, T); + G >= U && (U = G, D = C, O = S, z = T); } - g[b - 1][1] != j && (j ? g[b - 1][1] = j : (g.splice(b - 1, 1), b--), g[b][1] = I, K ? g[b + 1][1] = K : (g.splice(b + 1, 1), b--)); + g[b - 1][1] != D && (D ? g[b - 1][1] = D : (g.splice(b - 1, 1), b--), g[b][1] = O, z ? g[b + 1][1] = z : (g.splice(b + 1, 1), b--)); } b++; } } - function C(g, L) { + function q(g, L) { g.push([e, ""]); - for (var b = 0, q = 0, S = 0, T = "", _ = "", k; b < g.length; ) { + for (var b = 0, C = 0, S = 0, T = "", _ = "", I; b < g.length; ) { if (b < g.length - 1 && !g[b][1]) { g.splice(b, 1); continue; @@ -1954,25 +1942,25 @@ function If() { S++, _ += g[b][1], b++; break; case r: - q++, T += g[b][1], b++; + C++, T += g[b][1], b++; break; case e: - var j = b - S - q - 1; + var D = b - S - C - 1; if (L) { - if (j >= 0 && rt(g[j][1])) { - var I = g[j][1].slice(-1); - if (g[j][1] = g[j][1].slice( + if (D >= 0 && nt(g[D][1])) { + var O = g[D][1].slice(-1); + if (g[D][1] = g[D][1].slice( 0, -1 - ), T = I + T, _ = I + _, !g[j][1]) { - g.splice(j, 1), b--; - var K = j - 1; - g[K] && g[K][0] === t && (S++, _ = g[K][1] + _, K--), g[K] && g[K][0] === r && (q++, T = g[K][1] + T, K--), j = K; + ), T = O + T, _ = O + _, !g[D][1]) { + g.splice(D, 1), b--; + var z = D - 1; + g[z] && g[z][0] === t && (S++, _ = g[z][1] + _, z--), g[z] && g[z][0] === r && (C++, T = g[z][1] + T, z--), D = z; } } - if (U(g[b][1])) { - var I = g[b][1].charAt(0); - g[b][1] = g[b][1].slice(1), T += I, _ += I; + if ($(g[b][1])) { + var O = g[b][1].charAt(0); + g[b][1] = g[b][1].slice(1), T += O, _ += O; } } if (b < g.length - 1 && !g[b][1]) { @@ -1980,153 +1968,153 @@ function If() { break; } if (T.length > 0 || _.length > 0) { - T.length > 0 && _.length > 0 && (k = a(_, T), k !== 0 && (j >= 0 ? g[j][1] += _.substring( + T.length > 0 && _.length > 0 && (I = a(_, T), I !== 0 && (D >= 0 ? g[D][1] += _.substring( 0, - k + I ) : (g.splice(0, 0, [ e, - _.substring(0, k) - ]), b++), _ = _.substring(k), T = T.substring(k)), k = h(_, T), k !== 0 && (g[b][1] = _.substring(_.length - k) + g[b][1], _ = _.substring( + _.substring(0, I) + ]), b++), _ = _.substring(I), T = T.substring(I)), I = h(_, T), I !== 0 && (g[b][1] = _.substring(_.length - I) + g[b][1], _ = _.substring( 0, - _.length - k + _.length - I ), T = T.substring( 0, - T.length - k + T.length - I ))); - var F = S + q; - T.length === 0 && _.length === 0 ? (g.splice(b - F, F), b = b - F) : T.length === 0 ? (g.splice(b - F, F, [t, _]), b = b - F + 1) : _.length === 0 ? (g.splice(b - F, F, [r, T]), b = b - F + 1) : (g.splice( - b - F, - F, + var U = S + C; + T.length === 0 && _.length === 0 ? (g.splice(b - U, U), b = b - U) : T.length === 0 ? (g.splice(b - U, U, [t, _]), b = b - U + 1) : _.length === 0 ? (g.splice(b - U, U, [r, T]), b = b - U + 1) : (g.splice( + b - U, + U, [r, T], [t, _] - ), b = b - F + 2); + ), b = b - U + 2); } - b !== 0 && g[b - 1][0] === e ? (g[b - 1][1] += g[b][1], g.splice(b, 1)) : b++, S = 0, q = 0, T = "", _ = ""; + b !== 0 && g[b - 1][0] === e ? (g[b - 1][1] += g[b][1], g.splice(b, 1)) : b++, S = 0, C = 0, T = "", _ = ""; break; } } g[g.length - 1][1] === "" && g.pop(); - var V = !1; + var G = !1; for (b = 1; b < g.length - 1; ) g[b - 1][0] === e && g[b + 1][0] === e && (g[b][1].substring( g[b][1].length - g[b - 1][1].length ) === g[b - 1][1] ? (g[b][1] = g[b - 1][1] + g[b][1].substring( 0, g[b][1].length - g[b - 1][1].length - ), g[b + 1][1] = g[b - 1][1] + g[b + 1][1], g.splice(b - 1, 1), V = !0) : g[b][1].substring(0, g[b + 1][1].length) == g[b + 1][1] && (g[b - 1][1] += g[b + 1][1], g[b][1] = g[b][1].substring(g[b + 1][1].length) + g[b + 1][1], g.splice(b + 1, 1), V = !0)), b++; - V && C(g, L); + ), g[b + 1][1] = g[b - 1][1] + g[b + 1][1], g.splice(b - 1, 1), G = !0) : g[b][1].substring(0, g[b + 1][1].length) == g[b + 1][1] && (g[b - 1][1] += g[b + 1][1], g[b][1] = g[b][1].substring(g[b + 1][1].length) + g[b + 1][1], g.splice(b + 1, 1), G = !0)), b++; + G && q(g, L); } - function M(g) { + function B(g) { return g >= 55296 && g <= 56319; } - function z(g) { + function H(g) { return g >= 56320 && g <= 57343; } - function U(g) { - return z(g.charCodeAt(0)); + function $(g) { + return H(g.charCodeAt(0)); } - function rt(g) { - return M(g.charCodeAt(g.length - 1)); + function nt(g) { + return B(g.charCodeAt(g.length - 1)); } - function at(g) { + function ot(g) { for (var L = [], b = 0; b < g.length; b++) g[b][1].length > 0 && L.push(g[b]); return L; } - function ft(g, L, b, q) { - return rt(g) || U(q) ? null : at([ + function ht(g, L, b, C) { + return nt(g) || $(C) ? null : ot([ [e, g], [r, L], [t, b], - [e, q] + [e, C] ]); } function Rt(g, L, b) { - var q = typeof b == "number" ? { index: b, length: 0 } : b.oldRange, S = typeof b == "number" ? null : b.newRange, T = g.length, _ = L.length; - if (q.length === 0 && (S === null || S.length === 0)) { - var k = q.index, j = g.slice(0, k), I = g.slice(k), K = S ? S.index : null; + var C = typeof b == "number" ? { index: b, length: 0 } : b.oldRange, S = typeof b == "number" ? null : b.newRange, T = g.length, _ = L.length; + if (C.length === 0 && (S === null || S.length === 0)) { + var I = C.index, D = g.slice(0, I), O = g.slice(I), z = S ? S.index : null; t: { - var F = k + _ - T; - if (K !== null && K !== F || F < 0 || F > _) + var U = I + _ - T; + if (z !== null && z !== U || U < 0 || U > _) break t; - var V = L.slice(0, F), W = L.slice(F); - if (W !== I) + var G = L.slice(0, U), V = L.slice(U); + if (V !== O) break t; - var P = Math.min(k, F), it = j.slice(0, P), ot = V.slice(0, P); - if (it !== ot) + var P = Math.min(I, U), st = D.slice(0, P), lt = G.slice(0, P); + if (st !== lt) break t; - var G = j.slice(P), Q = V.slice(P); - return ft(it, G, Q, I); + var K = D.slice(P), X = G.slice(P); + return ht(st, K, X, O); } t: { - if (K !== null && K !== k) + if (z !== null && z !== I) break t; - var X = k, V = L.slice(0, X), W = L.slice(X); - if (V !== j) + var Y = I, G = L.slice(0, Y), V = L.slice(Y); + if (G !== D) break t; - var J = Math.min(T - X, _ - X), et = I.slice(I.length - J), nt = W.slice(W.length - J); - if (et !== nt) + var Q = Math.min(T - Y, _ - Y), tt = O.slice(O.length - Q), et = V.slice(V.length - Q); + if (tt !== et) break t; - var G = I.slice(0, I.length - J), Q = W.slice(0, W.length - J); - return ft(j, G, Q, et); + var K = O.slice(0, O.length - Q), X = V.slice(0, V.length - Q); + return ht(D, K, X, tt); } } - if (q.length > 0 && S && S.length === 0) + if (C.length > 0 && S && S.length === 0) t: { - var it = g.slice(0, q.index), et = g.slice(q.index + q.length), P = it.length, J = et.length; - if (_ < P + J) + var st = g.slice(0, C.index), tt = g.slice(C.index + C.length), P = st.length, Q = tt.length; + if (_ < P + Q) break t; - var ot = L.slice(0, P), nt = L.slice(_ - J); - if (it !== ot || et !== nt) + var lt = L.slice(0, P), et = L.slice(_ - Q); + if (st !== lt || tt !== et) break t; - var G = g.slice(P, T - J), Q = L.slice(P, _ - J); - return ft(it, G, Q, et); + var K = g.slice(P, T - Q), X = L.slice(P, _ - Q); + return ht(st, K, X, tt); } return null; } - function xt(g, L, b, q) { - return n(g, L, b, q, !0); + function xt(g, L, b, C) { + return n(g, L, b, C, !0); } - return xt.INSERT = t, xt.DELETE = r, xt.EQUAL = e, Rs = xt, Rs; + return xt.INSERT = t, xt.DELETE = r, xt.EQUAL = e, Ts = xt, Ts; } -var Cn = { exports: {} }; -Cn.exports; -var ol; -function po() { - return ol || (ol = 1, function(r, t) { - var e = 200, n = "__lodash_hash_undefined__", s = 9007199254740991, i = "[object Arguments]", o = "[object Array]", a = "[object Boolean]", u = "[object Date]", h = "[object Error]", p = "[object Function]", v = "[object GeneratorFunction]", f = "[object Map]", m = "[object Number]", y = "[object Object]", A = "[object Promise]", N = "[object RegExp]", x = "[object Set]", C = "[object String]", M = "[object Symbol]", z = "[object WeakMap]", U = "[object ArrayBuffer]", rt = "[object DataView]", at = "[object Float32Array]", ft = "[object Float64Array]", Rt = "[object Int8Array]", xt = "[object Int16Array]", g = "[object Int32Array]", L = "[object Uint8Array]", b = "[object Uint8ClampedArray]", q = "[object Uint16Array]", S = "[object Uint32Array]", T = /[\\^$.*+?()[\]{}|]/g, _ = /\w*$/, k = /^\[object .+?Constructor\]$/, j = /^(?:0|[1-9]\d*)$/, I = {}; - I[i] = I[o] = I[U] = I[rt] = I[a] = I[u] = I[at] = I[ft] = I[Rt] = I[xt] = I[g] = I[f] = I[m] = I[y] = I[N] = I[x] = I[C] = I[M] = I[L] = I[b] = I[q] = I[S] = !0, I[h] = I[p] = I[z] = !1; - var K = typeof me == "object" && me && me.Object === Object && me, F = typeof self == "object" && self && self.Object === Object && self, V = K || F || Function("return this")(), W = t && !t.nodeType && t, P = W && !0 && r && !r.nodeType && r, it = P && P.exports === W; - function ot(l, c) { +var Tn = { exports: {} }; +Tn.exports; +var Gi; +function to() { + return Gi || (Gi = 1, (function(r, t) { + var e = 200, n = "__lodash_hash_undefined__", s = 9007199254740991, i = "[object Arguments]", o = "[object Array]", a = "[object Boolean]", u = "[object Date]", h = "[object Error]", p = "[object Function]", v = "[object GeneratorFunction]", f = "[object Map]", m = "[object Number]", y = "[object Object]", w = "[object Promise]", N = "[object RegExp]", x = "[object Set]", q = "[object String]", B = "[object Symbol]", H = "[object WeakMap]", $ = "[object ArrayBuffer]", nt = "[object DataView]", ot = "[object Float32Array]", ht = "[object Float64Array]", Rt = "[object Int8Array]", xt = "[object Int16Array]", g = "[object Int32Array]", L = "[object Uint8Array]", b = "[object Uint8ClampedArray]", C = "[object Uint16Array]", S = "[object Uint32Array]", T = /[\\^$.*+?()[\]{}|]/g, _ = /\w*$/, I = /^\[object .+?Constructor\]$/, D = /^(?:0|[1-9]\d*)$/, O = {}; + O[i] = O[o] = O[$] = O[nt] = O[a] = O[u] = O[ot] = O[ht] = O[Rt] = O[xt] = O[g] = O[f] = O[m] = O[y] = O[N] = O[x] = O[q] = O[B] = O[L] = O[b] = O[C] = O[S] = !0, O[h] = O[p] = O[H] = !1; + var z = typeof ue == "object" && ue && ue.Object === Object && ue, U = typeof self == "object" && self && self.Object === Object && self, G = z || U || Function("return this")(), V = t && !t.nodeType && t, P = V && !0 && r && !r.nodeType && r, st = P && P.exports === V; + function lt(l, c) { return l.set(c[0], c[1]), l; } - function G(l, c) { + function K(l, c) { return l.add(c), l; } - function Q(l, c) { + function X(l, c) { for (var d = -1, E = l ? l.length : 0; ++d < E && c(l[d], d, l) !== !1; ) ; return l; } - function X(l, c) { - for (var d = -1, E = c.length, H = l.length; ++d < E; ) - l[H + d] = c[d]; + function Y(l, c) { + for (var d = -1, E = c.length, F = l.length; ++d < E; ) + l[F + d] = c[d]; return l; } - function J(l, c, d, E) { - for (var H = -1, $ = l ? l.length : 0; ++H < $; ) - d = c(d, l[H], H, l); + function Q(l, c, d, E) { + for (var F = -1, j = l ? l.length : 0; ++F < j; ) + d = c(d, l[F], F, l); return d; } - function et(l, c) { + function tt(l, c) { for (var d = -1, E = Array(l); ++d < l; ) E[d] = c(d); return E; } - function nt(l, c) { - return l == null ? void 0 : l[c]; + function et(l, c) { + return l?.[c]; } - function ct(l) { + function at(l) { var c = !1; if (l != null && typeof l.toString != "function") try { @@ -2135,356 +2123,356 @@ function po() { } return c; } - function mt(l) { + function pt(l) { var c = -1, d = Array(l.size); - return l.forEach(function(E, H) { - d[++c] = [H, E]; + return l.forEach(function(E, F) { + d[++c] = [F, E]; }), d; } - function fn(l, c) { + function sn(l, c) { return function(d) { return l(c(d)); }; } - function Yn(l) { + function $n(l) { var c = -1, d = Array(l.size); return l.forEach(function(E) { d[++c] = E; }), d; } - var Ur = Array.prototype, Fr = Function.prototype, $e = Object.prototype, dn = V["__core-js_shared__"], Xn = function() { - var l = /[^.]+$/.exec(dn && dn.keys && dn.keys.IE_PROTO || ""); + var Rr = Array.prototype, Ir = Function.prototype, Be = Object.prototype, ln = G["__core-js_shared__"], Un = (function() { + var l = /[^.]+$/.exec(ln && ln.keys && ln.keys.IE_PROTO || ""); return l ? "Symbol(src)_1." + l : ""; - }(), Qn = Fr.toString, Kt = $e.hasOwnProperty, Pe = $e.toString, Hr = RegExp( - "^" + Qn.call(Kt).replace(T, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$" - ), Ne = it ? V.Buffer : void 0, Ue = V.Symbol, gn = V.Uint8Array, St = fn(Object.getPrototypeOf, Object), Jn = Object.create, tr = $e.propertyIsEnumerable, zr = Ur.splice, pn = Object.getOwnPropertySymbols, Fe = Ne ? Ne.isBuffer : void 0, er = fn(Object.keys, Object), He = Bt(V, "DataView"), Te = Bt(V, "Map"), kt = Bt(V, "Promise"), ze = Bt(V, "Set"), mn = Bt(V, "WeakMap"), xe = Bt(Object, "create"), bn = wt(He), Le = wt(Te), yn = wt(kt), vn = wt(ze), En = wt(mn), fe = Ue ? Ue.prototype : void 0, nr = fe ? fe.valueOf : void 0; - function ne(l) { + })(), Fn = Ir.toString, Ht = Be.hasOwnProperty, Me = Be.toString, kr = RegExp( + "^" + Fn.call(Ht).replace(T, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$" + ), be = st ? G.Buffer : void 0, De = G.Symbol, on = G.Uint8Array, St = sn(Object.getPrototypeOf, Object), Hn = Object.create, zn = Be.propertyIsEnumerable, Br = Rr.splice, an = Object.getOwnPropertySymbols, je = be ? be.isBuffer : void 0, Kn = sn(Object.keys, Object), Pe = kt(G, "DataView"), ye = kt(G, "Map"), It = kt(G, "Promise"), $e = kt(G, "Set"), cn = kt(G, "WeakMap"), ve = kt(Object, "create"), un = Et(Pe), Ee = Et(ye), hn = Et(It), fn = Et($e), dn = Et(cn), le = De ? De.prototype : void 0, Gn = le ? le.valueOf : void 0; + function Xt(l) { var c = -1, d = l ? l.length : 0; for (this.clear(); ++c < d; ) { var E = l[c]; this.set(E[0], E[1]); } } - function Kr() { - this.__data__ = xe ? xe(null) : {}; + function Mr() { + this.__data__ = ve ? ve(null) : {}; } - function Gr(l) { + function Dr(l) { return this.has(l) && delete this.__data__[l]; } - function Vr(l) { + function jr(l) { var c = this.__data__; - if (xe) { + if (ve) { var d = c[l]; return d === n ? void 0 : d; } - return Kt.call(c, l) ? c[l] : void 0; + return Ht.call(c, l) ? c[l] : void 0; } - function rr(l) { + function Vn(l) { var c = this.__data__; - return xe ? c[l] !== void 0 : Kt.call(c, l); + return ve ? c[l] !== void 0 : Ht.call(c, l); } - function An(l, c) { + function gn(l, c) { var d = this.__data__; - return d[l] = xe && c === void 0 ? n : c, this; + return d[l] = ve && c === void 0 ? n : c, this; } - ne.prototype.clear = Kr, ne.prototype.delete = Gr, ne.prototype.get = Vr, ne.prototype.has = rr, ne.prototype.set = An; - function dt(l) { + Xt.prototype.clear = Mr, Xt.prototype.delete = Dr, Xt.prototype.get = jr, Xt.prototype.has = Vn, Xt.prototype.set = gn; + function ft(l) { var c = -1, d = l ? l.length : 0; for (this.clear(); ++c < d; ) { var E = l[c]; this.set(E[0], E[1]); } } - function Wr() { + function Pr() { this.__data__ = []; } - function Zr(l) { - var c = this.__data__, d = Ge(c, l); + function $r(l) { + var c = this.__data__, d = Fe(c, l); if (d < 0) return !1; var E = c.length - 1; - return d == E ? c.pop() : zr.call(c, d, 1), !0; + return d == E ? c.pop() : Br.call(c, d, 1), !0; } - function Yr(l) { - var c = this.__data__, d = Ge(c, l); + function Ur(l) { + var c = this.__data__, d = Fe(c, l); return d < 0 ? void 0 : c[d][1]; } - function Xr(l) { - return Ge(this.__data__, l) > -1; + function Fr(l) { + return Fe(this.__data__, l) > -1; } - function Qr(l, c) { - var d = this.__data__, E = Ge(d, l); + function Hr(l, c) { + var d = this.__data__, E = Fe(d, l); return E < 0 ? d.push([l, c]) : d[E][1] = c, this; } - dt.prototype.clear = Wr, dt.prototype.delete = Zr, dt.prototype.get = Yr, dt.prototype.has = Xr, dt.prototype.set = Qr; - function bt(l) { + ft.prototype.clear = Pr, ft.prototype.delete = $r, ft.prototype.get = Ur, ft.prototype.has = Fr, ft.prototype.set = Hr; + function mt(l) { var c = -1, d = l ? l.length : 0; for (this.clear(); ++c < d; ) { var E = l[c]; this.set(E[0], E[1]); } } - function Jr() { + function zr() { this.__data__ = { - hash: new ne(), - map: new (Te || dt)(), - string: new ne() + hash: new Xt(), + map: new (ye || ft)(), + string: new Xt() }; } - function ts(l) { - return _e(this, l).delete(l); + function Kr(l) { + return we(this, l).delete(l); } - function es(l) { - return _e(this, l).get(l); + function Gr(l) { + return we(this, l).get(l); } - function ns(l) { - return _e(this, l).has(l); + function Vr(l) { + return we(this, l).has(l); } - function rs(l, c) { - return _e(this, l).set(l, c), this; + function Wr(l, c) { + return we(this, l).set(l, c), this; } - bt.prototype.clear = Jr, bt.prototype.delete = ts, bt.prototype.get = es, bt.prototype.has = ns, bt.prototype.set = rs; + mt.prototype.clear = zr, mt.prototype.delete = Kr, mt.prototype.get = Gr, mt.prototype.has = Vr, mt.prototype.set = Wr; function Nt(l) { - this.__data__ = new dt(l); + this.__data__ = new ft(l); } - function ss() { - this.__data__ = new dt(); + function Zr() { + this.__data__ = new ft(); } - function is(l) { + function Yr(l) { return this.__data__.delete(l); } - function ls(l) { + function Xr(l) { return this.__data__.get(l); } - function os(l) { + function Qr(l) { return this.__data__.has(l); } - function as(l, c) { + function Jr(l, c) { var d = this.__data__; - if (d instanceof dt) { + if (d instanceof ft) { var E = d.__data__; - if (!Te || E.length < e - 1) + if (!ye || E.length < e - 1) return E.push([l, c]), this; - d = this.__data__ = new bt(E); + d = this.__data__ = new mt(E); } return d.set(l, c), this; } - Nt.prototype.clear = ss, Nt.prototype.delete = is, Nt.prototype.get = ls, Nt.prototype.has = os, Nt.prototype.set = as; - function Ke(l, c) { - var d = xn(l) || We(l) ? et(l.length, String) : [], E = d.length, H = !!E; - for (var $ in l) - Kt.call(l, $) && !(H && ($ == "length" || ws($, E))) && d.push($); + Nt.prototype.clear = Zr, Nt.prototype.delete = Yr, Nt.prototype.get = Xr, Nt.prototype.has = Qr, Nt.prototype.set = Jr; + function Ue(l, c) { + var d = yn(l) || ze(l) ? tt(l.length, String) : [], E = d.length, F = !!E; + for (var j in l) + Ht.call(l, j) && !(F && (j == "length" || ds(j, E))) && d.push(j); return d; } - function sr(l, c, d) { + function Wn(l, c, d) { var E = l[c]; - (!(Kt.call(l, c) && cr(E, d)) || d === void 0 && !(c in l)) && (l[c] = d); + (!(Ht.call(l, c) && Jn(E, d)) || d === void 0 && !(c in l)) && (l[c] = d); } - function Ge(l, c) { + function Fe(l, c) { for (var d = l.length; d--; ) - if (cr(l[d][0], c)) + if (Jn(l[d][0], c)) return d; return -1; } - function Gt(l, c) { - return l && Tn(c, Sn(c), l); + function zt(l, c) { + return l && bn(c, En(c), l); } - function wn(l, c, d, E, H, $, Y) { - var Z; - if (E && (Z = $ ? E(l, H, $, Y) : E(l)), Z !== void 0) - return Z; - if (!Wt(l)) + function pn(l, c, d, E, F, j, Z) { + var W; + if (E && (W = j ? E(l, F, j, Z) : E(l)), W !== void 0) + return W; + if (!Gt(l)) return l; - var ut = xn(l); - if (ut) { - if (Z = Es(l), !c) - return bs(l, Z); + var ct = yn(l); + if (ct) { + if (W = hs(l), !c) + return as(l, W); } else { - var tt = se(l), yt = tt == p || tt == v; - if (ur(l)) - return Ve(l, c); - if (tt == y || tt == i || yt && !$) { - if (ct(l)) - return $ ? l : {}; - if (Z = Vt(yt ? {} : l), !c) - return ys(l, Gt(Z, l)); + var J = Jt(l), bt = J == p || J == v; + if (tr(l)) + return He(l, c); + if (J == y || J == i || bt && !j) { + if (at(l)) + return j ? l : {}; + if (W = Kt(bt ? {} : l), !c) + return cs(l, zt(W, l)); } else { - if (!I[tt]) - return $ ? l : {}; - Z = As(l, tt, wn, c); + if (!O[J]) + return j ? l : {}; + W = fs(l, J, pn, c); } } - Y || (Y = new Nt()); - var Tt = Y.get(l); + Z || (Z = new Nt()); + var Tt = Z.get(l); if (Tt) return Tt; - if (Y.set(l, Z), !ut) - var ht = d ? vs(l) : Sn(l); - return Q(ht || l, function(vt, gt) { - ht && (gt = vt, vt = l[gt]), sr(Z, gt, wn(vt, c, d, E, gt, l, Y)); - }), Z; + if (Z.set(l, W), !ct) + var ut = d ? us(l) : En(l); + return X(ut || l, function(yt, dt) { + ut && (dt = yt, yt = l[dt]), Wn(W, dt, pn(yt, c, d, E, dt, l, Z)); + }), W; } - function cs(l) { - return Wt(l) ? Jn(l) : {}; + function ts(l) { + return Gt(l) ? Hn(l) : {}; } - function us(l, c, d) { + function es(l, c, d) { var E = c(l); - return xn(l) ? E : X(E, d(l)); + return yn(l) ? E : Y(E, d(l)); } - function hs(l) { - return Pe.call(l); + function ns(l) { + return Me.call(l); } - function fs(l) { - if (!Wt(l) || Ts(l)) + function rs(l) { + if (!Gt(l) || ps(l)) return !1; - var c = Ln(l) || ct(l) ? Hr : k; - return c.test(wt(l)); + var c = vn(l) || at(l) ? kr : I; + return c.test(Et(l)); } - function ds(l) { - if (!or(l)) - return er(l); + function ss(l) { + if (!Xn(l)) + return Kn(l); var c = []; for (var d in Object(l)) - Kt.call(l, d) && d != "constructor" && c.push(d); + Ht.call(l, d) && d != "constructor" && c.push(d); return c; } - function Ve(l, c) { + function He(l, c) { if (c) return l.slice(); var d = new l.constructor(l.length); return l.copy(d), d; } - function Nn(l) { + function mn(l) { var c = new l.constructor(l.byteLength); - return new gn(c).set(new gn(l)), c; + return new on(c).set(new on(l)), c; } - function Se(l, c) { - var d = c ? Nn(l.buffer) : l.buffer; + function Ae(l, c) { + var d = c ? mn(l.buffer) : l.buffer; return new l.constructor(d, l.byteOffset, l.byteLength); } - function ir(l, c, d) { - var E = c ? d(mt(l), !0) : mt(l); - return J(E, ot, new l.constructor()); + function Zn(l, c, d) { + var E = c ? d(pt(l), !0) : pt(l); + return Q(E, lt, new l.constructor()); } - function lr(l) { + function Yn(l) { var c = new l.constructor(l.source, _.exec(l)); return c.lastIndex = l.lastIndex, c; } - function gs(l, c, d) { - var E = c ? d(Yn(l), !0) : Yn(l); - return J(E, G, new l.constructor()); + function is(l, c, d) { + var E = c ? d($n(l), !0) : $n(l); + return Q(E, K, new l.constructor()); } - function ps(l) { - return nr ? Object(nr.call(l)) : {}; + function ls(l) { + return Gn ? Object(Gn.call(l)) : {}; } - function ms(l, c) { - var d = c ? Nn(l.buffer) : l.buffer; + function os(l, c) { + var d = c ? mn(l.buffer) : l.buffer; return new l.constructor(d, l.byteOffset, l.length); } - function bs(l, c) { + function as(l, c) { var d = -1, E = l.length; for (c || (c = Array(E)); ++d < E; ) c[d] = l[d]; return c; } - function Tn(l, c, d, E) { + function bn(l, c, d, E) { d || (d = {}); - for (var H = -1, $ = c.length; ++H < $; ) { - var Y = c[H], Z = void 0; - sr(d, Y, Z === void 0 ? l[Y] : Z); + for (var F = -1, j = c.length; ++F < j; ) { + var Z = c[F], W = void 0; + Wn(d, Z, W === void 0 ? l[Z] : W); } return d; } - function ys(l, c) { - return Tn(l, re(l), c); + function cs(l, c) { + return bn(l, Qt(l), c); } - function vs(l) { - return us(l, Sn, re); + function us(l) { + return es(l, En, Qt); } - function _e(l, c) { + function we(l, c) { var d = l.__data__; - return Ns(c) ? d[typeof c == "string" ? "string" : "hash"] : d.map; + return gs(c) ? d[typeof c == "string" ? "string" : "hash"] : d.map; } - function Bt(l, c) { - var d = nt(l, c); - return fs(d) ? d : void 0; + function kt(l, c) { + var d = et(l, c); + return rs(d) ? d : void 0; } - var re = pn ? fn(pn, Object) : Ls, se = hs; - (He && se(new He(new ArrayBuffer(1))) != rt || Te && se(new Te()) != f || kt && se(kt.resolve()) != A || ze && se(new ze()) != x || mn && se(new mn()) != z) && (se = function(l) { - var c = Pe.call(l), d = c == y ? l.constructor : void 0, E = d ? wt(d) : void 0; + var Qt = an ? sn(an, Object) : bs, Jt = ns; + (Pe && Jt(new Pe(new ArrayBuffer(1))) != nt || ye && Jt(new ye()) != f || It && Jt(It.resolve()) != w || $e && Jt(new $e()) != x || cn && Jt(new cn()) != H) && (Jt = function(l) { + var c = Me.call(l), d = c == y ? l.constructor : void 0, E = d ? Et(d) : void 0; if (E) switch (E) { - case bn: - return rt; - case Le: + case un: + return nt; + case Ee: return f; - case yn: - return A; - case vn: + case hn: + return w; + case fn: return x; - case En: - return z; + case dn: + return H; } return c; }); - function Es(l) { + function hs(l) { var c = l.length, d = l.constructor(c); - return c && typeof l[0] == "string" && Kt.call(l, "index") && (d.index = l.index, d.input = l.input), d; + return c && typeof l[0] == "string" && Ht.call(l, "index") && (d.index = l.index, d.input = l.input), d; } - function Vt(l) { - return typeof l.constructor == "function" && !or(l) ? cs(St(l)) : {}; + function Kt(l) { + return typeof l.constructor == "function" && !Xn(l) ? ts(St(l)) : {}; } - function As(l, c, d, E) { - var H = l.constructor; + function fs(l, c, d, E) { + var F = l.constructor; switch (c) { - case U: - return Nn(l); + case $: + return mn(l); case a: case u: - return new H(+l); - case rt: - return Se(l, E); - case at: - case ft: + return new F(+l); + case nt: + return Ae(l, E); + case ot: + case ht: case Rt: case xt: case g: case L: case b: - case q: + case C: case S: - return ms(l, E); + return os(l, E); case f: - return ir(l, E, d); + return Zn(l, E, d); case m: - case C: - return new H(l); + case q: + return new F(l); case N: - return lr(l); + return Yn(l); case x: - return gs(l, E, d); - case M: - return ps(l); + return is(l, E, d); + case B: + return ls(l); } } - function ws(l, c) { - return c = c ?? s, !!c && (typeof l == "number" || j.test(l)) && l > -1 && l % 1 == 0 && l < c; + function ds(l, c) { + return c = c ?? s, !!c && (typeof l == "number" || D.test(l)) && l > -1 && l % 1 == 0 && l < c; } - function Ns(l) { + function gs(l) { var c = typeof l; return c == "string" || c == "number" || c == "symbol" || c == "boolean" ? l !== "__proto__" : l === null; } - function Ts(l) { - return !!Xn && Xn in l; + function ps(l) { + return !!Un && Un in l; } - function or(l) { - var c = l && l.constructor, d = typeof c == "function" && c.prototype || $e; + function Xn(l) { + var c = l && l.constructor, d = typeof c == "function" && c.prototype || Be; return l === d; } - function wt(l) { + function Et(l) { if (l != null) { try { - return Qn.call(l); + return Fn.call(l); } catch { } try { @@ -2494,179 +2482,179 @@ function po() { } return ""; } - function ar(l) { - return wn(l, !0, !0); + function Qn(l) { + return pn(l, !0, !0); } - function cr(l, c) { + function Jn(l, c) { return l === c || l !== l && c !== c; } - function We(l) { - return xs(l) && Kt.call(l, "callee") && (!tr.call(l, "callee") || Pe.call(l) == i); + function ze(l) { + return ms(l) && Ht.call(l, "callee") && (!zn.call(l, "callee") || Me.call(l) == i); } - var xn = Array.isArray; - function Ze(l) { - return l != null && hr(l.length) && !Ln(l); + var yn = Array.isArray; + function Ke(l) { + return l != null && er(l.length) && !vn(l); } - function xs(l) { - return fr(l) && Ze(l); + function ms(l) { + return nr(l) && Ke(l); } - var ur = Fe || Ss; - function Ln(l) { - var c = Wt(l) ? Pe.call(l) : ""; + var tr = je || ys; + function vn(l) { + var c = Gt(l) ? Me.call(l) : ""; return c == p || c == v; } - function hr(l) { + function er(l) { return typeof l == "number" && l > -1 && l % 1 == 0 && l <= s; } - function Wt(l) { + function Gt(l) { var c = typeof l; return !!l && (c == "object" || c == "function"); } - function fr(l) { + function nr(l) { return !!l && typeof l == "object"; } - function Sn(l) { - return Ze(l) ? Ke(l) : ds(l); + function En(l) { + return Ke(l) ? Ue(l) : ss(l); } - function Ls() { + function bs() { return []; } - function Ss() { + function ys() { return !1; } - r.exports = ar; - }(Cn, Cn.exports)), Cn.exports; -} -var In = { exports: {} }; -In.exports; -var al; -function mo() { - return al || (al = 1, function(r, t) { - var e = 200, n = "__lodash_hash_undefined__", s = 1, i = 2, o = 9007199254740991, a = "[object Arguments]", u = "[object Array]", h = "[object AsyncFunction]", p = "[object Boolean]", v = "[object Date]", f = "[object Error]", m = "[object Function]", y = "[object GeneratorFunction]", A = "[object Map]", N = "[object Number]", x = "[object Null]", C = "[object Object]", M = "[object Promise]", z = "[object Proxy]", U = "[object RegExp]", rt = "[object Set]", at = "[object String]", ft = "[object Symbol]", Rt = "[object Undefined]", xt = "[object WeakMap]", g = "[object ArrayBuffer]", L = "[object DataView]", b = "[object Float32Array]", q = "[object Float64Array]", S = "[object Int8Array]", T = "[object Int16Array]", _ = "[object Int32Array]", k = "[object Uint8Array]", j = "[object Uint8ClampedArray]", I = "[object Uint16Array]", K = "[object Uint32Array]", F = /[\\^$.*+?()[\]{}|]/g, V = /^\[object .+?Constructor\]$/, W = /^(?:0|[1-9]\d*)$/, P = {}; - P[b] = P[q] = P[S] = P[T] = P[_] = P[k] = P[j] = P[I] = P[K] = !0, P[a] = P[u] = P[g] = P[p] = P[L] = P[v] = P[f] = P[m] = P[A] = P[N] = P[C] = P[U] = P[rt] = P[at] = P[xt] = !1; - var it = typeof me == "object" && me && me.Object === Object && me, ot = typeof self == "object" && self && self.Object === Object && self, G = it || ot || Function("return this")(), Q = t && !t.nodeType && t, X = Q && !0 && r && !r.nodeType && r, J = X && X.exports === Q, et = J && it.process, nt = function() { + r.exports = Qn; + })(Tn, Tn.exports)), Tn.exports; +} +var xn = { exports: {} }; +xn.exports; +var Vi; +function eo() { + return Vi || (Vi = 1, (function(r, t) { + var e = 200, n = "__lodash_hash_undefined__", s = 1, i = 2, o = 9007199254740991, a = "[object Arguments]", u = "[object Array]", h = "[object AsyncFunction]", p = "[object Boolean]", v = "[object Date]", f = "[object Error]", m = "[object Function]", y = "[object GeneratorFunction]", w = "[object Map]", N = "[object Number]", x = "[object Null]", q = "[object Object]", B = "[object Promise]", H = "[object Proxy]", $ = "[object RegExp]", nt = "[object Set]", ot = "[object String]", ht = "[object Symbol]", Rt = "[object Undefined]", xt = "[object WeakMap]", g = "[object ArrayBuffer]", L = "[object DataView]", b = "[object Float32Array]", C = "[object Float64Array]", S = "[object Int8Array]", T = "[object Int16Array]", _ = "[object Int32Array]", I = "[object Uint8Array]", D = "[object Uint8ClampedArray]", O = "[object Uint16Array]", z = "[object Uint32Array]", U = /[\\^$.*+?()[\]{}|]/g, G = /^\[object .+?Constructor\]$/, V = /^(?:0|[1-9]\d*)$/, P = {}; + P[b] = P[C] = P[S] = P[T] = P[_] = P[I] = P[D] = P[O] = P[z] = !0, P[a] = P[u] = P[g] = P[p] = P[L] = P[v] = P[f] = P[m] = P[w] = P[N] = P[q] = P[$] = P[nt] = P[ot] = P[xt] = !1; + var st = typeof ue == "object" && ue && ue.Object === Object && ue, lt = typeof self == "object" && self && self.Object === Object && self, K = st || lt || Function("return this")(), X = t && !t.nodeType && t, Y = X && !0 && r && !r.nodeType && r, Q = Y && Y.exports === X, tt = Q && st.process, et = (function() { try { - return et && et.binding && et.binding("util"); + return tt && tt.binding && tt.binding("util"); } catch { } - }(), ct = nt && nt.isTypedArray; - function mt(l, c) { - for (var d = -1, E = l == null ? 0 : l.length, H = 0, $ = []; ++d < E; ) { - var Y = l[d]; - c(Y, d, l) && ($[H++] = Y); + })(), at = et && et.isTypedArray; + function pt(l, c) { + for (var d = -1, E = l == null ? 0 : l.length, F = 0, j = []; ++d < E; ) { + var Z = l[d]; + c(Z, d, l) && (j[F++] = Z); } - return $; + return j; } - function fn(l, c) { - for (var d = -1, E = c.length, H = l.length; ++d < E; ) - l[H + d] = c[d]; + function sn(l, c) { + for (var d = -1, E = c.length, F = l.length; ++d < E; ) + l[F + d] = c[d]; return l; } - function Yn(l, c) { + function $n(l, c) { for (var d = -1, E = l == null ? 0 : l.length; ++d < E; ) if (c(l[d], d, l)) return !0; return !1; } - function Ur(l, c) { + function Rr(l, c) { for (var d = -1, E = Array(l); ++d < l; ) E[d] = c(d); return E; } - function Fr(l) { + function Ir(l) { return function(c) { return l(c); }; } - function $e(l, c) { + function Be(l, c) { return l.has(c); } - function dn(l, c) { - return l == null ? void 0 : l[c]; + function ln(l, c) { + return l?.[c]; } - function Xn(l) { + function Un(l) { var c = -1, d = Array(l.size); - return l.forEach(function(E, H) { - d[++c] = [H, E]; + return l.forEach(function(E, F) { + d[++c] = [F, E]; }), d; } - function Qn(l, c) { + function Fn(l, c) { return function(d) { return l(c(d)); }; } - function Kt(l) { + function Ht(l) { var c = -1, d = Array(l.size); return l.forEach(function(E) { d[++c] = E; }), d; } - var Pe = Array.prototype, Hr = Function.prototype, Ne = Object.prototype, Ue = G["__core-js_shared__"], gn = Hr.toString, St = Ne.hasOwnProperty, Jn = function() { - var l = /[^.]+$/.exec(Ue && Ue.keys && Ue.keys.IE_PROTO || ""); + var Me = Array.prototype, kr = Function.prototype, be = Object.prototype, De = K["__core-js_shared__"], on = kr.toString, St = be.hasOwnProperty, Hn = (function() { + var l = /[^.]+$/.exec(De && De.keys && De.keys.IE_PROTO || ""); return l ? "Symbol(src)_1." + l : ""; - }(), tr = Ne.toString, zr = RegExp( - "^" + gn.call(St).replace(F, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$" - ), pn = J ? G.Buffer : void 0, Fe = G.Symbol, er = G.Uint8Array, He = Ne.propertyIsEnumerable, Te = Pe.splice, kt = Fe ? Fe.toStringTag : void 0, ze = Object.getOwnPropertySymbols, mn = pn ? pn.isBuffer : void 0, xe = Qn(Object.keys, Object), bn = re(G, "DataView"), Le = re(G, "Map"), yn = re(G, "Promise"), vn = re(G, "Set"), En = re(G, "WeakMap"), fe = re(Object, "create"), nr = wt(bn), ne = wt(Le), Kr = wt(yn), Gr = wt(vn), Vr = wt(En), rr = Fe ? Fe.prototype : void 0, An = rr ? rr.valueOf : void 0; - function dt(l) { + })(), zn = be.toString, Br = RegExp( + "^" + on.call(St).replace(U, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$" + ), an = Q ? K.Buffer : void 0, je = K.Symbol, Kn = K.Uint8Array, Pe = be.propertyIsEnumerable, ye = Me.splice, It = je ? je.toStringTag : void 0, $e = Object.getOwnPropertySymbols, cn = an ? an.isBuffer : void 0, ve = Fn(Object.keys, Object), un = Qt(K, "DataView"), Ee = Qt(K, "Map"), hn = Qt(K, "Promise"), fn = Qt(K, "Set"), dn = Qt(K, "WeakMap"), le = Qt(Object, "create"), Gn = Et(un), Xt = Et(Ee), Mr = Et(hn), Dr = Et(fn), jr = Et(dn), Vn = je ? je.prototype : void 0, gn = Vn ? Vn.valueOf : void 0; + function ft(l) { var c = -1, d = l == null ? 0 : l.length; for (this.clear(); ++c < d; ) { var E = l[c]; this.set(E[0], E[1]); } } - function Wr() { - this.__data__ = fe ? fe(null) : {}, this.size = 0; + function Pr() { + this.__data__ = le ? le(null) : {}, this.size = 0; } - function Zr(l) { + function $r(l) { var c = this.has(l) && delete this.__data__[l]; return this.size -= c ? 1 : 0, c; } - function Yr(l) { + function Ur(l) { var c = this.__data__; - if (fe) { + if (le) { var d = c[l]; return d === n ? void 0 : d; } return St.call(c, l) ? c[l] : void 0; } - function Xr(l) { + function Fr(l) { var c = this.__data__; - return fe ? c[l] !== void 0 : St.call(c, l); + return le ? c[l] !== void 0 : St.call(c, l); } - function Qr(l, c) { + function Hr(l, c) { var d = this.__data__; - return this.size += this.has(l) ? 0 : 1, d[l] = fe && c === void 0 ? n : c, this; + return this.size += this.has(l) ? 0 : 1, d[l] = le && c === void 0 ? n : c, this; } - dt.prototype.clear = Wr, dt.prototype.delete = Zr, dt.prototype.get = Yr, dt.prototype.has = Xr, dt.prototype.set = Qr; - function bt(l) { + ft.prototype.clear = Pr, ft.prototype.delete = $r, ft.prototype.get = Ur, ft.prototype.has = Fr, ft.prototype.set = Hr; + function mt(l) { var c = -1, d = l == null ? 0 : l.length; for (this.clear(); ++c < d; ) { var E = l[c]; this.set(E[0], E[1]); } } - function Jr() { + function zr() { this.__data__ = [], this.size = 0; } - function ts(l) { - var c = this.__data__, d = Ve(c, l); + function Kr(l) { + var c = this.__data__, d = He(c, l); if (d < 0) return !1; var E = c.length - 1; - return d == E ? c.pop() : Te.call(c, d, 1), --this.size, !0; + return d == E ? c.pop() : ye.call(c, d, 1), --this.size, !0; } - function es(l) { - var c = this.__data__, d = Ve(c, l); + function Gr(l) { + var c = this.__data__, d = He(c, l); return d < 0 ? void 0 : c[d][1]; } - function ns(l) { - return Ve(this.__data__, l) > -1; + function Vr(l) { + return He(this.__data__, l) > -1; } - function rs(l, c) { - var d = this.__data__, E = Ve(d, l); + function Wr(l, c) { + var d = this.__data__, E = He(d, l); return E < 0 ? (++this.size, d.push([l, c])) : d[E][1] = c, this; } - bt.prototype.clear = Jr, bt.prototype.delete = ts, bt.prototype.get = es, bt.prototype.has = ns, bt.prototype.set = rs; + mt.prototype.clear = zr, mt.prototype.delete = Kr, mt.prototype.get = Gr, mt.prototype.has = Vr, mt.prototype.set = Wr; function Nt(l) { var c = -1, d = l == null ? 0 : l.length; for (this.clear(); ++c < d; ) { @@ -2674,297 +2662,297 @@ function mo() { this.set(E[0], E[1]); } } - function ss() { + function Zr() { this.size = 0, this.__data__ = { - hash: new dt(), - map: new (Le || bt)(), - string: new dt() + hash: new ft(), + map: new (Ee || mt)(), + string: new ft() }; } - function is(l) { - var c = Bt(this, l).delete(l); + function Yr(l) { + var c = kt(this, l).delete(l); return this.size -= c ? 1 : 0, c; } - function ls(l) { - return Bt(this, l).get(l); + function Xr(l) { + return kt(this, l).get(l); } - function os(l) { - return Bt(this, l).has(l); + function Qr(l) { + return kt(this, l).has(l); } - function as(l, c) { - var d = Bt(this, l), E = d.size; + function Jr(l, c) { + var d = kt(this, l), E = d.size; return d.set(l, c), this.size += d.size == E ? 0 : 1, this; } - Nt.prototype.clear = ss, Nt.prototype.delete = is, Nt.prototype.get = ls, Nt.prototype.has = os, Nt.prototype.set = as; - function Ke(l) { + Nt.prototype.clear = Zr, Nt.prototype.delete = Yr, Nt.prototype.get = Xr, Nt.prototype.has = Qr, Nt.prototype.set = Jr; + function Ue(l) { var c = -1, d = l == null ? 0 : l.length; for (this.__data__ = new Nt(); ++c < d; ) this.add(l[c]); } - function sr(l) { + function Wn(l) { return this.__data__.set(l, n), this; } - function Ge(l) { + function Fe(l) { return this.__data__.has(l); } - Ke.prototype.add = Ke.prototype.push = sr, Ke.prototype.has = Ge; - function Gt(l) { - var c = this.__data__ = new bt(l); + Ue.prototype.add = Ue.prototype.push = Wn, Ue.prototype.has = Fe; + function zt(l) { + var c = this.__data__ = new mt(l); this.size = c.size; } - function wn() { - this.__data__ = new bt(), this.size = 0; + function pn() { + this.__data__ = new mt(), this.size = 0; } - function cs(l) { + function ts(l) { var c = this.__data__, d = c.delete(l); return this.size = c.size, d; } - function us(l) { + function es(l) { return this.__data__.get(l); } - function hs(l) { + function ns(l) { return this.__data__.has(l); } - function fs(l, c) { + function rs(l, c) { var d = this.__data__; - if (d instanceof bt) { + if (d instanceof mt) { var E = d.__data__; - if (!Le || E.length < e - 1) + if (!Ee || E.length < e - 1) return E.push([l, c]), this.size = ++d.size, this; d = this.__data__ = new Nt(E); } return d.set(l, c), this.size = d.size, this; } - Gt.prototype.clear = wn, Gt.prototype.delete = cs, Gt.prototype.get = us, Gt.prototype.has = hs, Gt.prototype.set = fs; - function ds(l, c) { - var d = We(l), E = !d && cr(l), H = !d && !E && Ze(l), $ = !d && !E && !H && fr(l), Y = d || E || H || $, Z = Y ? Ur(l.length, String) : [], ut = Z.length; - for (var tt in l) - St.call(l, tt) && !(Y && // Safari 9 has enumerable `arguments.length` in strict mode. - (tt == "length" || // Node.js 0.10 has enumerable non-index properties on buffers. - H && (tt == "offset" || tt == "parent") || // PhantomJS 2 has enumerable non-index properties on typed arrays. - $ && (tt == "buffer" || tt == "byteLength" || tt == "byteOffset") || // Skip index properties. - As(tt, ut))) && Z.push(tt); - return Z; - } - function Ve(l, c) { + zt.prototype.clear = pn, zt.prototype.delete = ts, zt.prototype.get = es, zt.prototype.has = ns, zt.prototype.set = rs; + function ss(l, c) { + var d = ze(l), E = !d && Jn(l), F = !d && !E && Ke(l), j = !d && !E && !F && nr(l), Z = d || E || F || j, W = Z ? Rr(l.length, String) : [], ct = W.length; + for (var J in l) + St.call(l, J) && !(Z && // Safari 9 has enumerable `arguments.length` in strict mode. + (J == "length" || // Node.js 0.10 has enumerable non-index properties on buffers. + F && (J == "offset" || J == "parent") || // PhantomJS 2 has enumerable non-index properties on typed arrays. + j && (J == "buffer" || J == "byteLength" || J == "byteOffset") || // Skip index properties. + fs(J, ct))) && W.push(J); + return W; + } + function He(l, c) { for (var d = l.length; d--; ) - if (ar(l[d][0], c)) + if (Qn(l[d][0], c)) return d; return -1; } - function Nn(l, c, d) { + function mn(l, c, d) { var E = c(l); - return We(l) ? E : fn(E, d(l)); + return ze(l) ? E : sn(E, d(l)); } - function Se(l) { - return l == null ? l === void 0 ? Rt : x : kt && kt in Object(l) ? se(l) : or(l); + function Ae(l) { + return l == null ? l === void 0 ? Rt : x : It && It in Object(l) ? Jt(l) : Xn(l); } - function ir(l) { - return Wt(l) && Se(l) == a; + function Zn(l) { + return Gt(l) && Ae(l) == a; } - function lr(l, c, d, E, H) { - return l === c ? !0 : l == null || c == null || !Wt(l) && !Wt(c) ? l !== l && c !== c : gs(l, c, d, E, lr, H); + function Yn(l, c, d, E, F) { + return l === c ? !0 : l == null || c == null || !Gt(l) && !Gt(c) ? l !== l && c !== c : is(l, c, d, E, Yn, F); } - function gs(l, c, d, E, H, $) { - var Y = We(l), Z = We(c), ut = Y ? u : Vt(l), tt = Z ? u : Vt(c); - ut = ut == a ? C : ut, tt = tt == a ? C : tt; - var yt = ut == C, Tt = tt == C, ht = ut == tt; - if (ht && Ze(l)) { - if (!Ze(c)) + function is(l, c, d, E, F, j) { + var Z = ze(l), W = ze(c), ct = Z ? u : Kt(l), J = W ? u : Kt(c); + ct = ct == a ? q : ct, J = J == a ? q : J; + var bt = ct == q, Tt = J == q, ut = ct == J; + if (ut && Ke(l)) { + if (!Ke(c)) return !1; - Y = !0, yt = !1; + Z = !0, bt = !1; } - if (ht && !yt) - return $ || ($ = new Gt()), Y || fr(l) ? Tn(l, c, d, E, H, $) : ys(l, c, ut, d, E, H, $); + if (ut && !bt) + return j || (j = new zt()), Z || nr(l) ? bn(l, c, d, E, F, j) : cs(l, c, ct, d, E, F, j); if (!(d & s)) { - var vt = yt && St.call(l, "__wrapped__"), gt = Tt && St.call(c, "__wrapped__"); - if (vt || gt) { - var de = vt ? l.value() : l, ie = gt ? c.value() : c; - return $ || ($ = new Gt()), H(de, ie, d, E, $); + var yt = bt && St.call(l, "__wrapped__"), dt = Tt && St.call(c, "__wrapped__"); + if (yt || dt) { + var oe = yt ? l.value() : l, te = dt ? c.value() : c; + return j || (j = new zt()), F(oe, te, d, E, j); } } - return ht ? ($ || ($ = new Gt()), vs(l, c, d, E, H, $)) : !1; + return ut ? (j || (j = new zt()), us(l, c, d, E, F, j)) : !1; } - function ps(l) { - if (!hr(l) || Ns(l)) + function ls(l) { + if (!er(l) || gs(l)) return !1; - var c = ur(l) ? zr : V; - return c.test(wt(l)); + var c = tr(l) ? Br : G; + return c.test(Et(l)); } - function ms(l) { - return Wt(l) && Ln(l.length) && !!P[Se(l)]; + function os(l) { + return Gt(l) && vn(l.length) && !!P[Ae(l)]; } - function bs(l) { - if (!Ts(l)) - return xe(l); + function as(l) { + if (!ps(l)) + return ve(l); var c = []; for (var d in Object(l)) St.call(l, d) && d != "constructor" && c.push(d); return c; } - function Tn(l, c, d, E, H, $) { - var Y = d & s, Z = l.length, ut = c.length; - if (Z != ut && !(Y && ut > Z)) + function bn(l, c, d, E, F, j) { + var Z = d & s, W = l.length, ct = c.length; + if (W != ct && !(Z && ct > W)) return !1; - var tt = $.get(l); - if (tt && $.get(c)) - return tt == c; - var yt = -1, Tt = !0, ht = d & i ? new Ke() : void 0; - for ($.set(l, c), $.set(c, l); ++yt < Z; ) { - var vt = l[yt], gt = c[yt]; + var J = j.get(l); + if (J && j.get(c)) + return J == c; + var bt = -1, Tt = !0, ut = d & i ? new Ue() : void 0; + for (j.set(l, c), j.set(c, l); ++bt < W; ) { + var yt = l[bt], dt = c[bt]; if (E) - var de = Y ? E(gt, vt, yt, c, l, $) : E(vt, gt, yt, l, c, $); - if (de !== void 0) { - if (de) + var oe = Z ? E(dt, yt, bt, c, l, j) : E(yt, dt, bt, l, c, j); + if (oe !== void 0) { + if (oe) continue; Tt = !1; break; } - if (ht) { - if (!Yn(c, function(ie, qe) { - if (!$e(ht, qe) && (vt === ie || H(vt, ie, d, E, $))) - return ht.push(qe); + if (ut) { + if (!$n(c, function(te, Ne) { + if (!Be(ut, Ne) && (yt === te || F(yt, te, d, E, j))) + return ut.push(Ne); })) { Tt = !1; break; } - } else if (!(vt === gt || H(vt, gt, d, E, $))) { + } else if (!(yt === dt || F(yt, dt, d, E, j))) { Tt = !1; break; } } - return $.delete(l), $.delete(c), Tt; + return j.delete(l), j.delete(c), Tt; } - function ys(l, c, d, E, H, $, Y) { + function cs(l, c, d, E, F, j, Z) { switch (d) { case L: if (l.byteLength != c.byteLength || l.byteOffset != c.byteOffset) return !1; l = l.buffer, c = c.buffer; case g: - return !(l.byteLength != c.byteLength || !$(new er(l), new er(c))); + return !(l.byteLength != c.byteLength || !j(new Kn(l), new Kn(c))); case p: case v: case N: - return ar(+l, +c); + return Qn(+l, +c); case f: return l.name == c.name && l.message == c.message; - case U: - case at: + case $: + case ot: return l == c + ""; - case A: - var Z = Xn; - case rt: - var ut = E & s; - if (Z || (Z = Kt), l.size != c.size && !ut) + case w: + var W = Un; + case nt: + var ct = E & s; + if (W || (W = Ht), l.size != c.size && !ct) return !1; - var tt = Y.get(l); - if (tt) - return tt == c; - E |= i, Y.set(l, c); - var yt = Tn(Z(l), Z(c), E, H, $, Y); - return Y.delete(l), yt; - case ft: - if (An) - return An.call(l) == An.call(c); + var J = Z.get(l); + if (J) + return J == c; + E |= i, Z.set(l, c); + var bt = bn(W(l), W(c), E, F, j, Z); + return Z.delete(l), bt; + case ht: + if (gn) + return gn.call(l) == gn.call(c); } return !1; } - function vs(l, c, d, E, H, $) { - var Y = d & s, Z = _e(l), ut = Z.length, tt = _e(c), yt = tt.length; - if (ut != yt && !Y) + function us(l, c, d, E, F, j) { + var Z = d & s, W = we(l), ct = W.length, J = we(c), bt = J.length; + if (ct != bt && !Z) return !1; - for (var Tt = ut; Tt--; ) { - var ht = Z[Tt]; - if (!(Y ? ht in c : St.call(c, ht))) + for (var Tt = ct; Tt--; ) { + var ut = W[Tt]; + if (!(Z ? ut in c : St.call(c, ut))) return !1; } - var vt = $.get(l); - if (vt && $.get(c)) - return vt == c; - var gt = !0; - $.set(l, c), $.set(c, l); - for (var de = Y; ++Tt < ut; ) { - ht = Z[Tt]; - var ie = l[ht], qe = c[ht]; + var yt = j.get(l); + if (yt && j.get(c)) + return yt == c; + var dt = !0; + j.set(l, c), j.set(c, l); + for (var oe = Z; ++Tt < ct; ) { + ut = W[Tt]; + var te = l[ut], Ne = c[ut]; if (E) - var Oi = Y ? E(qe, ie, ht, c, l, $) : E(ie, qe, ht, l, c, $); - if (!(Oi === void 0 ? ie === qe || H(ie, qe, d, E, $) : Oi)) { - gt = !1; + var pi = Z ? E(Ne, te, ut, c, l, j) : E(te, Ne, ut, l, c, j); + if (!(pi === void 0 ? te === Ne || F(te, Ne, d, E, j) : pi)) { + dt = !1; break; } - de || (de = ht == "constructor"); + oe || (oe = ut == "constructor"); } - if (gt && !de) { - var dr = l.constructor, gr = c.constructor; - dr != gr && "constructor" in l && "constructor" in c && !(typeof dr == "function" && dr instanceof dr && typeof gr == "function" && gr instanceof gr) && (gt = !1); + if (dt && !oe) { + var rr = l.constructor, sr = c.constructor; + rr != sr && "constructor" in l && "constructor" in c && !(typeof rr == "function" && rr instanceof rr && typeof sr == "function" && sr instanceof sr) && (dt = !1); } - return $.delete(l), $.delete(c), gt; + return j.delete(l), j.delete(c), dt; } - function _e(l) { - return Nn(l, Sn, Es); + function we(l) { + return mn(l, En, hs); } - function Bt(l, c) { + function kt(l, c) { var d = l.__data__; - return ws(c) ? d[typeof c == "string" ? "string" : "hash"] : d.map; + return ds(c) ? d[typeof c == "string" ? "string" : "hash"] : d.map; } - function re(l, c) { - var d = dn(l, c); - return ps(d) ? d : void 0; + function Qt(l, c) { + var d = ln(l, c); + return ls(d) ? d : void 0; } - function se(l) { - var c = St.call(l, kt), d = l[kt]; + function Jt(l) { + var c = St.call(l, It), d = l[It]; try { - l[kt] = void 0; + l[It] = void 0; var E = !0; } catch { } - var H = tr.call(l); - return E && (c ? l[kt] = d : delete l[kt]), H; + var F = zn.call(l); + return E && (c ? l[It] = d : delete l[It]), F; } - var Es = ze ? function(l) { - return l == null ? [] : (l = Object(l), mt(ze(l), function(c) { - return He.call(l, c); + var hs = $e ? function(l) { + return l == null ? [] : (l = Object(l), pt($e(l), function(c) { + return Pe.call(l, c); })); - } : Ls, Vt = Se; - (bn && Vt(new bn(new ArrayBuffer(1))) != L || Le && Vt(new Le()) != A || yn && Vt(yn.resolve()) != M || vn && Vt(new vn()) != rt || En && Vt(new En()) != xt) && (Vt = function(l) { - var c = Se(l), d = c == C ? l.constructor : void 0, E = d ? wt(d) : ""; + } : bs, Kt = Ae; + (un && Kt(new un(new ArrayBuffer(1))) != L || Ee && Kt(new Ee()) != w || hn && Kt(hn.resolve()) != B || fn && Kt(new fn()) != nt || dn && Kt(new dn()) != xt) && (Kt = function(l) { + var c = Ae(l), d = c == q ? l.constructor : void 0, E = d ? Et(d) : ""; if (E) switch (E) { - case nr: + case Gn: return L; - case ne: - return A; - case Kr: - return M; - case Gr: - return rt; - case Vr: + case Xt: + return w; + case Mr: + return B; + case Dr: + return nt; + case jr: return xt; } return c; }); - function As(l, c) { - return c = c ?? o, !!c && (typeof l == "number" || W.test(l)) && l > -1 && l % 1 == 0 && l < c; + function fs(l, c) { + return c = c ?? o, !!c && (typeof l == "number" || V.test(l)) && l > -1 && l % 1 == 0 && l < c; } - function ws(l) { + function ds(l) { var c = typeof l; return c == "string" || c == "number" || c == "symbol" || c == "boolean" ? l !== "__proto__" : l === null; } - function Ns(l) { - return !!Jn && Jn in l; + function gs(l) { + return !!Hn && Hn in l; } - function Ts(l) { - var c = l && l.constructor, d = typeof c == "function" && c.prototype || Ne; + function ps(l) { + var c = l && l.constructor, d = typeof c == "function" && c.prototype || be; return l === d; } - function or(l) { - return tr.call(l); + function Xn(l) { + return zn.call(l); } - function wt(l) { + function Et(l) { if (l != null) { try { - return gn.call(l); + return on.call(l); } catch { } try { @@ -2974,57 +2962,57 @@ function mo() { } return ""; } - function ar(l, c) { + function Qn(l, c) { return l === c || l !== l && c !== c; } - var cr = ir(/* @__PURE__ */ function() { + var Jn = Zn(/* @__PURE__ */ (function() { return arguments; - }()) ? ir : function(l) { - return Wt(l) && St.call(l, "callee") && !He.call(l, "callee"); - }, We = Array.isArray; - function xn(l) { - return l != null && Ln(l.length) && !ur(l); - } - var Ze = mn || Ss; - function xs(l, c) { - return lr(l, c); - } - function ur(l) { - if (!hr(l)) + })()) ? Zn : function(l) { + return Gt(l) && St.call(l, "callee") && !Pe.call(l, "callee"); + }, ze = Array.isArray; + function yn(l) { + return l != null && vn(l.length) && !tr(l); + } + var Ke = cn || ys; + function ms(l, c) { + return Yn(l, c); + } + function tr(l) { + if (!er(l)) return !1; - var c = Se(l); - return c == m || c == y || c == h || c == z; + var c = Ae(l); + return c == m || c == y || c == h || c == H; } - function Ln(l) { + function vn(l) { return typeof l == "number" && l > -1 && l % 1 == 0 && l <= o; } - function hr(l) { + function er(l) { var c = typeof l; return l != null && (c == "object" || c == "function"); } - function Wt(l) { + function Gt(l) { return l != null && typeof l == "object"; } - var fr = ct ? Fr(ct) : ms; - function Sn(l) { - return xn(l) ? ds(l) : bs(l); + var nr = at ? Ir(at) : os; + function En(l) { + return yn(l) ? ss(l) : as(l); } - function Ls() { + function bs() { return []; } - function Ss() { + function ys() { return !1; } - r.exports = xs; - }(In, In.exports)), In.exports; + r.exports = ms; + })(xn, xn.exports)), xn.exports; } -var br = {}, cl; -function Rf() { - if (cl) return br; - cl = 1, Object.defineProperty(br, "__esModule", { value: !0 }); - const r = po(), t = mo(); +var or = {}, Wi; +function ff() { + if (Wi) return or; + Wi = 1, Object.defineProperty(or, "__esModule", { value: !0 }); + const r = to(), t = eo(); var e; - return function(n) { + return (function(n) { function s(u = {}, h = {}, p = !1) { typeof u != "object" && (u = {}), typeof h != "object" && (h = {}); let v = r(h); @@ -3057,25 +3045,25 @@ function Rf() { return Object.keys(v).length > 0 ? v : void 0; } n.transform = a; - }(e || (e = {})), br.default = e, br; + })(e || (e = {})), or.default = e, or; } -var yr = {}, ul; -function bo() { - if (ul) return yr; - ul = 1, Object.defineProperty(yr, "__esModule", { value: !0 }); +var ar = {}, Zi; +function no() { + if (Zi) return ar; + Zi = 1, Object.defineProperty(ar, "__esModule", { value: !0 }); var r; - return function(t) { + return (function(t) { function e(n) { return typeof n.delete == "number" ? n.delete : typeof n.retain == "number" ? n.retain : typeof n.retain == "object" && n.retain !== null ? 1 : typeof n.insert == "string" ? n.insert.length : 1; } t.length = e; - }(r || (r = {})), yr.default = r, yr; + })(r || (r = {})), ar.default = r, ar; } -var vr = {}, hl; -function kf() { - if (hl) return vr; - hl = 1, Object.defineProperty(vr, "__esModule", { value: !0 }); - const r = bo(); +var cr = {}, Yi; +function df() { + if (Yi) return cr; + Yi = 1, Object.defineProperty(cr, "__esModule", { value: !0 }); + const r = no(); class t { constructor(n) { this.ops = n, this.index = 0, this.offset = 0; @@ -3118,17 +3106,17 @@ function kf() { } else return []; } } - return vr.default = t, vr; + return cr.default = t, cr; } -var fl; -function Bf() { - return fl || (fl = 1, function(r, t) { +var Xi; +function gf() { + return Xi || (Xi = 1, (function(r, t) { Object.defineProperty(t, "__esModule", { value: !0 }), t.AttributeMap = t.OpIterator = t.Op = void 0; - const e = If(), n = po(), s = mo(), i = Rf(); + const e = hf(), n = to(), s = eo(), i = ff(); t.AttributeMap = i.default; - const o = bo(); + const o = no(); t.Op = o.default; - const a = kf(); + const a = df(); t.OpIterator = a.default; const u = "\0", h = (v, f) => { if (typeof v != "object" || v === null) @@ -3200,8 +3188,8 @@ function Bf() { } partition(f) { const m = [], y = []; - return this.forEach((A) => { - (f(A) ? m : y).push(A); + return this.forEach((w) => { + (f(w) ? m : y).push(w); }), [m, y]; } reduce(f, m) { @@ -3214,48 +3202,48 @@ function Bf() { return this.reduce((f, m) => f + o.default.length(m), 0); } slice(f = 0, m = 1 / 0) { - const y = [], A = new a.default(this.ops); + const y = [], w = new a.default(this.ops); let N = 0; - for (; N < m && A.hasNext(); ) { + for (; N < m && w.hasNext(); ) { let x; - N < f ? x = A.next(f - N) : (x = A.next(m - N), y.push(x)), N += o.default.length(x); + N < f ? x = w.next(f - N) : (x = w.next(m - N), y.push(x)), N += o.default.length(x); } return new p(y); } compose(f) { - const m = new a.default(this.ops), y = new a.default(f.ops), A = [], N = y.peek(); + const m = new a.default(this.ops), y = new a.default(f.ops), w = [], N = y.peek(); if (N != null && typeof N.retain == "number" && N.attributes == null) { - let C = N.retain; - for (; m.peekType() === "insert" && m.peekLength() <= C; ) - C -= m.peekLength(), A.push(m.next()); - N.retain - C > 0 && y.next(N.retain - C); + let q = N.retain; + for (; m.peekType() === "insert" && m.peekLength() <= q; ) + q -= m.peekLength(), w.push(m.next()); + N.retain - q > 0 && y.next(N.retain - q); } - const x = new p(A); + const x = new p(w); for (; m.hasNext() || y.hasNext(); ) if (y.peekType() === "insert") x.push(y.next()); else if (m.peekType() === "delete") x.push(m.next()); else { - const C = Math.min(m.peekLength(), y.peekLength()), M = m.next(C), z = y.next(C); - if (z.retain) { - const U = {}; - if (typeof M.retain == "number") - U.retain = typeof z.retain == "number" ? C : z.retain; - else if (typeof z.retain == "number") - M.retain == null ? U.insert = M.insert : U.retain = M.retain; + const q = Math.min(m.peekLength(), y.peekLength()), B = m.next(q), H = y.next(q); + if (H.retain) { + const $ = {}; + if (typeof B.retain == "number") + $.retain = typeof H.retain == "number" ? q : H.retain; + else if (typeof H.retain == "number") + B.retain == null ? $.insert = B.insert : $.retain = B.retain; else { - const at = M.retain == null ? "insert" : "retain", [ft, Rt, xt] = h(M[at], z.retain), g = p.getHandler(ft); - U[at] = { - [ft]: g.compose(Rt, xt, at === "retain") + const ot = B.retain == null ? "insert" : "retain", [ht, Rt, xt] = h(B[ot], H.retain), g = p.getHandler(ht); + $[ot] = { + [ht]: g.compose(Rt, xt, ot === "retain") }; } - const rt = i.default.compose(M.attributes, z.attributes, typeof M.retain == "number"); - if (rt && (U.attributes = rt), x.push(U), !y.hasNext() && s(x.ops[x.ops.length - 1], U)) { - const at = new p(m.rest()); - return x.concat(at).chop(); + const nt = i.default.compose(B.attributes, H.attributes, typeof B.retain == "number"); + if (nt && ($.attributes = nt), x.push($), !y.hasNext() && s(x.ops[x.ops.length - 1], $)) { + const ot = new p(m.rest()); + return x.concat(ot).chop(); } - } else typeof z.delete == "number" && (typeof M.retain == "number" || typeof M.retain == "object" && M.retain !== null) && x.push(z); + } else typeof H.delete == "number" && (typeof B.retain == "number" || typeof B.retain == "object" && B.retain !== null) && x.push(H); } return x.chop(); } @@ -3266,69 +3254,69 @@ function Bf() { diff(f, m) { if (this.ops === f.ops) return new p(); - const y = [this, f].map((M) => M.map((z) => { - if (z.insert != null) - return typeof z.insert == "string" ? z.insert : u; - const U = M === f ? "on" : "with"; - throw new Error("diff() called " + U + " non-document"); - }).join("")), A = new p(), N = e(y[0], y[1], m, !0), x = new a.default(this.ops), C = new a.default(f.ops); - return N.forEach((M) => { - let z = M[1].length; - for (; z > 0; ) { - let U = 0; - switch (M[0]) { + const y = [this, f].map((B) => B.map((H) => { + if (H.insert != null) + return typeof H.insert == "string" ? H.insert : u; + const $ = B === f ? "on" : "with"; + throw new Error("diff() called " + $ + " non-document"); + }).join("")), w = new p(), N = e(y[0], y[1], m, !0), x = new a.default(this.ops), q = new a.default(f.ops); + return N.forEach((B) => { + let H = B[1].length; + for (; H > 0; ) { + let $ = 0; + switch (B[0]) { case e.INSERT: - U = Math.min(C.peekLength(), z), A.push(C.next(U)); + $ = Math.min(q.peekLength(), H), w.push(q.next($)); break; case e.DELETE: - U = Math.min(z, x.peekLength()), x.next(U), A.delete(U); + $ = Math.min(H, x.peekLength()), x.next($), w.delete($); break; case e.EQUAL: - U = Math.min(x.peekLength(), C.peekLength(), z); - const rt = x.next(U), at = C.next(U); - s(rt.insert, at.insert) ? A.retain(U, i.default.diff(rt.attributes, at.attributes)) : A.push(at).delete(U); + $ = Math.min(x.peekLength(), q.peekLength(), H); + const nt = x.next($), ot = q.next($); + s(nt.insert, ot.insert) ? w.retain($, i.default.diff(nt.attributes, ot.attributes)) : w.push(ot).delete($); break; } - z -= U; + H -= $; } - }), A.chop(); + }), w.chop(); } eachLine(f, m = ` `) { const y = new a.default(this.ops); - let A = new p(), N = 0; + let w = new p(), N = 0; for (; y.hasNext(); ) { if (y.peekType() !== "insert") return; - const x = y.peek(), C = o.default.length(x) - y.peekLength(), M = typeof x.insert == "string" ? x.insert.indexOf(m, C) - C : -1; - if (M < 0) - A.push(y.next()); - else if (M > 0) - A.push(y.next(M)); + const x = y.peek(), q = o.default.length(x) - y.peekLength(), B = typeof x.insert == "string" ? x.insert.indexOf(m, q) - q : -1; + if (B < 0) + w.push(y.next()); + else if (B > 0) + w.push(y.next(B)); else { - if (f(A, y.next(1).attributes || {}, N) === !1) + if (f(w, y.next(1).attributes || {}, N) === !1) return; - N += 1, A = new p(); + N += 1, w = new p(); } } - A.length() > 0 && f(A, {}, N); + w.length() > 0 && f(w, {}, N); } invert(f) { const m = new p(); - return this.reduce((y, A) => { - if (A.insert) - m.delete(o.default.length(A)); + return this.reduce((y, w) => { + if (w.insert) + m.delete(o.default.length(w)); else { - if (typeof A.retain == "number" && A.attributes == null) - return m.retain(A.retain), y + A.retain; - if (A.delete || typeof A.retain == "number") { - const N = A.delete || A.retain; - return f.slice(y, y + N).forEach((C) => { - A.delete ? m.push(C) : A.retain && A.attributes && m.retain(o.default.length(C), i.default.invert(A.attributes, C.attributes)); + if (typeof w.retain == "number" && w.attributes == null) + return m.retain(w.retain), y + w.retain; + if (w.delete || typeof w.retain == "number") { + const N = w.delete || w.retain; + return f.slice(y, y + N).forEach((q) => { + w.delete ? m.push(q) : w.retain && w.attributes && m.retain(o.default.length(q), i.default.invert(w.attributes, q.attributes)); }), y + N; - } else if (typeof A.retain == "object" && A.retain !== null) { - const N = f.slice(y, y + 1), x = new a.default(N.ops).next(), [C, M, z] = h(A.retain, x.insert), U = p.getHandler(C); - return m.retain({ [C]: U.invert(M, z) }, i.default.invert(A.attributes, x.attributes)), y + 1; + } else if (typeof w.retain == "object" && w.retain !== null) { + const N = f.slice(y, y + 1), x = new a.default(N.ops).next(), [q, B, H] = h(w.retain, x.insert), $ = p.getHandler(q); + return m.retain({ [q]: $.invert(B, H) }, i.default.invert(w.attributes, x.attributes)), y + 1; } } return y; @@ -3337,31 +3325,31 @@ function Bf() { transform(f, m = !1) { if (m = !!m, typeof f == "number") return this.transformPosition(f, m); - const y = f, A = new a.default(this.ops), N = new a.default(y.ops), x = new p(); - for (; A.hasNext() || N.hasNext(); ) - if (A.peekType() === "insert" && (m || N.peekType() !== "insert")) - x.retain(o.default.length(A.next())); + const y = f, w = new a.default(this.ops), N = new a.default(y.ops), x = new p(); + for (; w.hasNext() || N.hasNext(); ) + if (w.peekType() === "insert" && (m || N.peekType() !== "insert")) + x.retain(o.default.length(w.next())); else if (N.peekType() === "insert") x.push(N.next()); else { - const C = Math.min(A.peekLength(), N.peekLength()), M = A.next(C), z = N.next(C); - if (M.delete) + const q = Math.min(w.peekLength(), N.peekLength()), B = w.next(q), H = N.next(q); + if (B.delete) continue; - if (z.delete) - x.push(z); + if (H.delete) + x.push(H); else { - const U = M.retain, rt = z.retain; - let at = typeof rt == "object" && rt !== null ? rt : C; - if (typeof U == "object" && U !== null && typeof rt == "object" && rt !== null) { - const ft = Object.keys(U)[0]; - if (ft === Object.keys(rt)[0]) { - const Rt = p.getHandler(ft); - Rt && (at = { - [ft]: Rt.transform(U[ft], rt[ft], m) + const $ = B.retain, nt = H.retain; + let ot = typeof nt == "object" && nt !== null ? nt : q; + if (typeof $ == "object" && $ !== null && typeof nt == "object" && nt !== null) { + const ht = Object.keys($)[0]; + if (ht === Object.keys(nt)[0]) { + const Rt = p.getHandler(ht); + Rt && (ot = { + [ht]: Rt.transform($[ht], nt[ht], m) }); } } - x.retain(at, i.default.transform(M.attributes, z.attributes, m)); + x.retain(ot, i.default.transform(B.attributes, H.attributes, m)); } } return x.chop(); @@ -3369,24 +3357,24 @@ function Bf() { transformPosition(f, m = !1) { m = !!m; const y = new a.default(this.ops); - let A = 0; - for (; y.hasNext() && A <= f; ) { + let w = 0; + for (; y.hasNext() && w <= f; ) { const N = y.peekLength(), x = y.peekType(); if (y.next(), x === "delete") { - f -= Math.min(N, f - A); + f -= Math.min(N, f - w); continue; - } else x === "insert" && (A < f || !m) && (f += N); - A += N; + } else x === "insert" && (w < f || !m) && (f += N); + w += N; } return f; } } p.Op = o.default, p.OpIterator = a.default, p.AttributeMap = i.default, p.handlers = {}, t.default = p, r.exports = p, r.exports.default = p; - }(mr, mr.exports)), mr.exports; + })(lr, lr.exports)), lr.exports; } -var It = Bf(); -const B = /* @__PURE__ */ Dl(It); -class Ht extends Lt { +var Ot = gf(); +const k = /* @__PURE__ */ Nl(Ot); +class Ft extends Lt { static value() { } optimize() { @@ -3399,129 +3387,126 @@ class Ht extends Lt { return ""; } } -Ht.blotName = "break"; -Ht.tagName = "BR"; -let Ut = class extends Cr { +Ft.blotName = "break"; +Ft.tagName = "BR"; +let $t = class extends Er { }; -const Mf = { +const pf = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; -function Dr(r) { - return r.replace(/[&<>"']/g, (t) => Mf[t]); -} -const Yt = class Yt extends yi { +function _r(r) { + return r.replace(/[&<>"']/g, (t) => pf[t]); +} +class At extends ni { + static allowedChildren = [At, Ft, Lt, $t]; + // Lower index means deeper in the DOM tree, since not found (-1) is for embeds + static order = [ + "cursor", + "inline", + // Must be lower + "link", + // Chrome wants to be lower + "underline", + "strike", + "italic", + "bold", + "script", + "code" + // Must be higher + ]; static compare(t, e) { - const n = Yt.order.indexOf(t), s = Yt.order.indexOf(e); + const n = At.order.indexOf(t), s = At.order.indexOf(e); return n >= 0 || s >= 0 ? n - s : t === e ? 0 : t < e ? -1 : 1; } formatAt(t, e, n, s) { - if (Yt.compare(this.statics.blotName, n) < 0 && this.scroll.query(n, D.BLOT)) { + if (At.compare(this.statics.blotName, n) < 0 && this.scroll.query(n, M.BLOT)) { const i = this.isolate(t, e); s && i.wrap(n, s); } else super.formatAt(t, e, n, s); } optimize(t) { - if (super.optimize(t), this.parent instanceof Yt && Yt.compare(this.statics.blotName, this.parent.statics.blotName) > 0) { + if (super.optimize(t), this.parent instanceof At && At.compare(this.statics.blotName, this.parent.statics.blotName) > 0) { const e = this.parent.isolate(this.offset(), this.length()); this.moveChildren(e), e.wrap(this); } } -}; -O(Yt, "allowedChildren", [Yt, Ht, Lt, Ut]), // Lower index means deeper in the DOM tree, since not found (-1) is for embeds -O(Yt, "order", [ - "cursor", - "inline", - // Must be lower - "link", - // Chrome wants to be lower - "underline", - "strike", - "italic", - "bold", - "script", - "code" - // Must be higher -]); -let te = Yt; -const dl = 1; -class pt extends Un { - constructor() { - super(...arguments); - O(this, "cache", {}); - } +} +const Qi = 1; +class gt extends Rn { + cache = {}; delta() { - return this.cache.delta == null && (this.cache.delta = yo(this)), this.cache.delta; + return this.cache.delta == null && (this.cache.delta = ro(this)), this.cache.delta; } - deleteAt(e, n) { - super.deleteAt(e, n), this.cache = {}; + deleteAt(t, e) { + super.deleteAt(t, e), this.cache = {}; } - formatAt(e, n, s, i) { - n <= 0 || (this.scroll.query(s, D.BLOCK) ? e + n === this.length() && this.format(s, i) : super.formatAt(e, Math.min(n, this.length() - e - 1), s, i), this.cache = {}); + formatAt(t, e, n, s) { + e <= 0 || (this.scroll.query(n, M.BLOCK) ? t + e === this.length() && this.format(n, s) : super.formatAt(t, Math.min(e, this.length() - t - 1), n, s), this.cache = {}); } - insertAt(e, n, s) { - if (s != null) { - super.insertAt(e, n, s), this.cache = {}; + insertAt(t, e, n) { + if (n != null) { + super.insertAt(t, e, n), this.cache = {}; return; } - if (n.length === 0) return; - const i = n.split(` -`), o = i.shift(); - o.length > 0 && (e < this.length() - 1 || this.children.tail == null ? super.insertAt(Math.min(e, this.length() - 1), o) : this.children.tail.insertAt(this.children.tail.length(), o), this.cache = {}); - let a = this; - i.reduce((u, h) => (a = a.split(u, !0), a.insertAt(0, h), h.length), e + o.length); + if (e.length === 0) return; + const s = e.split(` +`), i = s.shift(); + i.length > 0 && (t < this.length() - 1 || this.children.tail == null ? super.insertAt(Math.min(t, this.length() - 1), i) : this.children.tail.insertAt(this.children.tail.length(), i), this.cache = {}); + let o = this; + s.reduce((a, u) => (o = o.split(a, !0), o.insertAt(0, u), u.length), t + i.length); } - insertBefore(e, n) { + insertBefore(t, e) { const { - head: s + head: n } = this.children; - super.insertBefore(e, n), s instanceof Ht && s.remove(), this.cache = {}; + super.insertBefore(t, e), n instanceof Ft && n.remove(), this.cache = {}; } length() { - return this.cache.length == null && (this.cache.length = super.length() + dl), this.cache.length; + return this.cache.length == null && (this.cache.length = super.length() + Qi), this.cache.length; } - moveChildren(e, n) { - super.moveChildren(e, n), this.cache = {}; + moveChildren(t, e) { + super.moveChildren(t, e), this.cache = {}; } - optimize(e) { - super.optimize(e), this.cache = {}; + optimize(t) { + super.optimize(t), this.cache = {}; } - path(e) { - return super.path(e, !0); + path(t) { + return super.path(t, !0); } - removeChild(e) { - super.removeChild(e), this.cache = {}; + removeChild(t) { + super.removeChild(t), this.cache = {}; } - split(e) { - let n = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : !1; - if (n && (e === 0 || e >= this.length() - dl)) { - const i = this.clone(); - return e === 0 ? (this.parent.insertBefore(i, this), this) : (this.parent.insertBefore(i, this.next), i); + split(t) { + let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : !1; + if (e && (t === 0 || t >= this.length() - Qi)) { + const s = this.clone(); + return t === 0 ? (this.parent.insertBefore(s, this), this) : (this.parent.insertBefore(s, this.next), s); } - const s = super.split(e, n); - return this.cache = {}, s; + const n = super.split(t, e); + return this.cache = {}, n; } } -pt.blotName = "block"; -pt.tagName = "P"; -pt.defaultChild = Ht; -pt.allowedChildren = [Ht, te, Lt, Ut]; -class Ct extends Lt { +gt.blotName = "block"; +gt.tagName = "P"; +gt.defaultChild = Ft; +gt.allowedChildren = [Ft, At, Lt, $t]; +class qt extends Lt { attach() { - super.attach(), this.attributes = new Br(this.domNode); + super.attach(), this.attributes = new Lr(this.domNode); } delta() { - return new B().insert(this.value(), { + return new k().insert(this.value(), { ...this.formats(), ...this.attributes.values() }); } format(t, e) { - const n = this.scroll.query(t, D.BLOCK_ATTRIBUTE); + const n = this.scroll.query(t, M.BLOCK_ATTRIBUTE); n != null && this.attributes.attribute(n, e); } formatAt(t, e, n, s) { @@ -3534,7 +3519,7 @@ class Ct extends Lt { } const s = e.split(` `), i = s.pop(), o = s.map((u) => { - const h = this.scroll.create(pt.blotName); + const h = this.scroll.create(gt.blotName); return h.insertAt(0, u), h; }), a = this.split(t); o.forEach((u) => { @@ -3542,25 +3527,29 @@ class Ct extends Lt { }), i && this.parent.insertBefore(this.scroll.create("text", i), a); } } -Ct.scope = D.BLOCK_BLOT; -function yo(r) { +qt.scope = M.BLOCK_BLOT; +function ro(r) { let t = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : !0; - return r.descendants(Et).reduce((e, n) => n.length() === 0 ? e : e.insert(n.value(), qt(n, {}, t)), new B()).insert(` -`, qt(r)); + return r.descendants(vt).reduce((e, n) => n.length() === 0 ? e : e.insert(n.value(), _t(n, {}, t)), new k()).insert(` +`, _t(r)); } -function qt(r) { +function _t(r) { let t = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}, e = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : !0; return r == null || ("formats" in r && typeof r.formats == "function" && (t = { ...t, ...r.formats() - }, e && delete t["code-token"]), r.parent == null || r.parent.statics.blotName === "scroll" || r.parent.statics.scope !== r.statics.scope) ? t : qt(r.parent, t, e); + }, e && delete t["code-token"]), r.parent == null || r.parent.statics.blotName === "scroll" || r.parent.statics.scope !== r.statics.scope) ? t : _t(r.parent, t, e); } -const _t = class _t extends Lt { +class Dt extends Lt { + static blotName = "cursor"; + static className = "ql-cursor"; + static tagName = "span"; + static CONTENTS = "\uFEFF"; // Zero width no break space static value() { } constructor(t, e, n) { - super(t, e), this.selection = n, this.textNode = document.createTextNode(_t.CONTENTS), this.domNode.appendChild(this.textNode), this.savedLength = 0; + super(t, e), this.selection = n, this.textNode = document.createTextNode(Dt.CONTENTS), this.domNode.appendChild(this.textNode), this.savedLength = 0; } detach() { this.parent != null && this.parent.removeChild(this); @@ -3571,9 +3560,9 @@ const _t = class _t extends Lt { return; } let n = this, s = 0; - for (; n != null && n.statics.scope !== D.BLOCK_BLOT; ) + for (; n != null && n.statics.scope !== M.BLOCK_BLOT; ) s += n.offset(n.parent), n = n.parent; - n != null && (this.savedLength = _t.CONTENTS.length, n.optimize(), n.formatAt(s, _t.CONTENTS.length, t, e), this.savedLength = 0); + n != null && (this.savedLength = Dt.CONTENTS.length, n.optimize(), n.formatAt(s, Dt.CONTENTS.length, t, e), this.savedLength = 0); } index(t, e) { return t === this.textNode ? 0 : super.index(t, e); @@ -3592,10 +3581,10 @@ const _t = class _t extends Lt { const t = this.selection.getNativeRange(); for (; this.domNode.lastChild != null && this.domNode.lastChild !== this.textNode; ) this.domNode.parentNode.insertBefore(this.domNode.lastChild, this.domNode); - const e = this.prev instanceof Ut ? this.prev : null, n = e ? e.length() : 0, s = this.next instanceof Ut ? this.next : null, i = s ? s.text : "", { + const e = this.prev instanceof $t ? this.prev : null, n = e ? e.length() : 0, s = this.next instanceof $t ? this.next : null, i = s ? s.text : "", { textNode: o - } = this, a = o.data.split(_t.CONTENTS).join(""); - o.data = _t.CONTENTS; + } = this, a = o.data.split(Dt.CONTENTS).join(""); + o.data = Dt.CONTENTS; let u; if (e) u = e, (a || s) && (e.insertAt(e.length(), a + i), s && s.remove()); @@ -3640,7 +3629,7 @@ const _t = class _t extends Lt { } = this; for (; e; ) { if (e.domNode.tagName === "A") { - this.savedLength = _t.CONTENTS.length, e.isolate(this.offset(e), this.length()).unwrap(), this.savedLength = 0; + this.savedLength = Dt.CONTENTS.length, e.isolate(this.offset(e), this.length()).unwrap(), this.savedLength = 0; break; } e = e.parent; @@ -3649,12 +3638,10 @@ const _t = class _t extends Lt { value() { return ""; } -}; -O(_t, "blotName", "cursor"), O(_t, "className", "ql-cursor"), O(_t, "tagName", "span"), O(_t, "CONTENTS", "\uFEFF"); -let an = _t; -var ks = { exports: {} }, gl; -function Df() { - return gl || (gl = 1, function(r) { +} +var xs = { exports: {} }, Ji; +function mf() { + return Ji || (Ji = 1, (function(r) { var t = Object.prototype.hasOwnProperty, e = "~"; function n() { } @@ -3691,9 +3678,9 @@ function Df() { var p = e ? e + h : h, v = this._events[p]; return v ? v.fn ? 1 : v.length : 0; }, a.prototype.emit = function(h, p, v, f, m, y) { - var A = e ? e + h : h; - if (!this._events[A]) return !1; - var N = this._events[A], x = arguments.length, C, M; + var w = e ? e + h : h; + if (!this._events[w]) return !1; + var N = this._events[w], x = arguments.length, q, B; if (N.fn) { switch (N.once && this.removeListener(h, N.fn, void 0, !0), x) { case 1: @@ -3709,29 +3696,29 @@ function Df() { case 6: return N.fn.call(N.context, p, v, f, m, y), !0; } - for (M = 1, C = new Array(x - 1); M < x; M++) - C[M - 1] = arguments[M]; - N.fn.apply(N.context, C); + for (B = 1, q = new Array(x - 1); B < x; B++) + q[B - 1] = arguments[B]; + N.fn.apply(N.context, q); } else { - var z = N.length, U; - for (M = 0; M < z; M++) - switch (N[M].once && this.removeListener(h, N[M].fn, void 0, !0), x) { + var H = N.length, $; + for (B = 0; B < H; B++) + switch (N[B].once && this.removeListener(h, N[B].fn, void 0, !0), x) { case 1: - N[M].fn.call(N[M].context); + N[B].fn.call(N[B].context); break; case 2: - N[M].fn.call(N[M].context, p); + N[B].fn.call(N[B].context, p); break; case 3: - N[M].fn.call(N[M].context, p, v); + N[B].fn.call(N[B].context, p, v); break; case 4: - N[M].fn.call(N[M].context, p, v, f); + N[B].fn.call(N[B].context, p, v, f); break; default: - if (!C) for (U = 1, C = new Array(x - 1); U < x; U++) - C[U - 1] = arguments[U]; - N[M].fn.apply(N[M].context, C); + if (!q) for ($ = 1, q = new Array(x - 1); $ < x; $++) + q[$ - 1] = arguments[$]; + N[B].fn.apply(N[B].context, q); } } return !0; @@ -3748,8 +3735,8 @@ function Df() { if (y.fn) y.fn === p && (!f || y.once) && (!v || y.context === v) && o(this, m); else { - for (var A = 0, N = [], x = y.length; A < x; A++) - (y[A].fn !== p || f && !y[A].once || v && y[A].context !== v) && N.push(y[A]); + for (var w = 0, N = [], x = y.length; w < x; w++) + (y[w].fn !== p || f && !y[w].once || v && y[w].context !== v) && N.push(y[w]); N.length ? this._events[m] = N.length === 1 ? N[0] : N : o(this, m); } return this; @@ -3757,44 +3744,64 @@ function Df() { var p; return h ? (p = e ? e + h : h, this._events[p] && o(this, p)) : (this._events = new n(), this._eventsCount = 0), this; }, a.prototype.off = a.prototype.removeListener, a.prototype.addListener = a.prototype.on, a.prefixed = e, a.EventEmitter = a, r.exports = a; - }(ks)), ks.exports; + })(xs)), xs.exports; } -var jf = Df(); -const $f = /* @__PURE__ */ Dl(jf), ei = /* @__PURE__ */ new WeakMap(), ni = ["error", "warn", "log", "info"]; -let ri = "warn"; -function vo(r) { - if (ri && ni.indexOf(r) <= ni.indexOf(ri)) { +var bf = mf(); +const yf = /* @__PURE__ */ Nl(bf), Gs = /* @__PURE__ */ new WeakMap(), Vs = ["error", "warn", "log", "info"]; +let Ws = "warn"; +function so(r) { + if (Ws && Vs.indexOf(r) <= Vs.indexOf(Ws)) { for (var t = arguments.length, e = new Array(t > 1 ? t - 1 : 0), n = 1; n < t; n++) e[n - 1] = arguments[n]; console[r](...e); } } -function he(r) { - return ni.reduce((t, e) => (t[e] = vo.bind(console, e, r), t), {}); +function ie(r) { + return Vs.reduce((t, e) => (t[e] = so.bind(console, e, r), t), {}); } -he.level = (r) => { - ri = r; +ie.level = (r) => { + Ws = r; }; -vo.level = he.level; -const Bs = he("quill:events"), Pf = ["selectionchange", "mousedown", "mouseup", "click"]; -Pf.forEach((r) => { +so.level = ie.level; +const Ls = ie("quill:events"), vf = ["selectionchange", "mousedown", "mouseup", "click"]; +vf.forEach((r) => { document.addEventListener(r, function() { for (var t = arguments.length, e = new Array(t), n = 0; n < t; n++) e[n] = arguments[n]; Array.from(document.querySelectorAll(".ql-container")).forEach((s) => { - const i = ei.get(s); + const i = Gs.get(s); i && i.emitter && i.emitter.handleDOM(...e); }); }); }); -class R extends $f { +class R extends yf { + static events = { + EDITOR_CHANGE: "editor-change", + SCROLL_BEFORE_UPDATE: "scroll-before-update", + SCROLL_BLOT_MOUNT: "scroll-blot-mount", + SCROLL_BLOT_UNMOUNT: "scroll-blot-unmount", + SCROLL_OPTIMIZE: "scroll-optimize", + SCROLL_UPDATE: "scroll-update", + SCROLL_EMBED_UPDATE: "scroll-embed-update", + SELECTION_CHANGE: "selection-change", + TEXT_CHANGE: "text-change", + COMPOSITION_BEFORE_START: "composition-before-start", + COMPOSITION_START: "composition-start", + COMPOSITION_BEFORE_END: "composition-before-end", + COMPOSITION_END: "composition-end" + }; + static sources = { + API: "api", + SILENT: "silent", + USER: "user" + }; constructor() { - super(), this.domListeners = {}, this.on("error", Bs.error); + super(), this.domListeners = {}, this.on("error", Ls.error); } emit() { for (var t = arguments.length, e = new Array(t), n = 0; n < t; n++) e[n] = arguments[n]; - return Bs.log.call(Bs, ...e), super.emit(...e); + return Ls.log.call(Ls, ...e), super.emit(...e); } handleDOM(t) { for (var e = arguments.length, n = new Array(e > 1 ? e - 1 : 0), s = 1; s < e; s++) @@ -3814,35 +3821,16 @@ class R extends $f { }); } } -O(R, "events", { - EDITOR_CHANGE: "editor-change", - SCROLL_BEFORE_UPDATE: "scroll-before-update", - SCROLL_BLOT_MOUNT: "scroll-blot-mount", - SCROLL_BLOT_UNMOUNT: "scroll-blot-unmount", - SCROLL_OPTIMIZE: "scroll-optimize", - SCROLL_UPDATE: "scroll-update", - SCROLL_EMBED_UPDATE: "scroll-embed-update", - SELECTION_CHANGE: "selection-change", - TEXT_CHANGE: "text-change", - COMPOSITION_BEFORE_START: "composition-before-start", - COMPOSITION_START: "composition-start", - COMPOSITION_BEFORE_END: "composition-before-end", - COMPOSITION_END: "composition-end" -}), O(R, "sources", { - API: "api", - SILENT: "silent", - USER: "user" -}); -const Ms = he("quill:selection"); -class Ie { +const Ss = ie("quill:selection"); +class Se { constructor(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 0; this.index = t, this.length = e; } } -class Uf { +class Ef { constructor(t, e) { - this.emitter = e, this.scroll = t, this.composing = !1, this.mouseDown = !1, this.root = this.scroll.domNode, this.cursor = this.scroll.create("cursor", this), this.savedRange = new Ie(0, 0), this.lastRange = this.savedRange, this.lastNative = null, this.handleComposition(), this.handleDragging(), this.emitter.listenDOM("selectionchange", document, () => { + this.emitter = e, this.scroll = t, this.composing = !1, this.mouseDown = !1, this.root = this.scroll.domNode, this.cursor = this.scroll.create("cursor", this), this.savedRange = new Se(0, 0), this.lastRange = this.savedRange, this.lastNative = null, this.handleComposition(), this.handleDragging(), this.emitter.listenDOM("selectionchange", document, () => { !this.mouseDown && !this.composing && setTimeout(this.update.bind(this, R.sources.USER), 1); }), this.emitter.on(R.events.SCROLL_BEFORE_UPDATE, () => { if (!this.hasFocus()) return; @@ -3895,11 +3883,11 @@ class Uf { format(t, e) { this.scroll.update(); const n = this.getNativeRange(); - if (!(n == null || !n.native.collapsed || this.scroll.query(t, D.BLOCK))) { + if (!(n == null || !n.native.collapsed || this.scroll.query(t, M.BLOCK))) { if (n.start.node !== this.cursor.textNode) { const s = this.scroll.find(n.start.node, !1); if (s == null) return; - if (s instanceof Et) { + if (s instanceof vt) { const i = s.split(n.start.offset); s.parent.insertBefore(this.cursor, i); } else @@ -3950,7 +3938,7 @@ class Uf { const e = t.getRangeAt(0); if (e == null) return null; const n = this.normalizeNative(e); - return Ms.info("getNativeRange", n), n; + return Ss.info("getNativeRange", n), n; } getRange() { const t = this.scroll.domNode; @@ -3960,19 +3948,19 @@ class Uf { return e == null ? [null, null] : [this.normalizedToRange(e), e]; } hasFocus() { - return document.activeElement === this.root || document.activeElement != null && Ds(this.root, document.activeElement); + return document.activeElement === this.root || document.activeElement != null && _s(this.root, document.activeElement); } normalizedToRange(t) { const e = [[t.start.node, t.start.offset]]; t.native.collapsed || e.push([t.end.node, t.end.offset]); const n = e.map((o) => { const [a, u] = o, h = this.scroll.find(a, !0), p = h.offset(this.scroll); - return u === 0 ? p : h instanceof Et ? p + h.index(a, u) : p + h.length(); + return u === 0 ? p : h instanceof vt ? p + h.index(a, u) : p + h.length(); }), s = Math.min(Math.max(...n), this.scroll.length() - 1), i = Math.min(s, ...n); - return new Ie(i, s - i); + return new Se(i, s - i); } normalizeNative(t) { - if (!Ds(this.root, t.startContainer) || !t.collapsed && !Ds(this.root, t.endContainer)) + if (!_s(this.root, t.startContainer) || !t.collapsed && !_s(this.root, t.endContainer)) return null; const e = { start: { @@ -4010,7 +3998,7 @@ class Uf { } setNativeRange(t, e) { let n = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : t, s = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : e, i = arguments.length > 4 && arguments[4] !== void 0 ? arguments[4] : !1; - if (Ms.info("setNativeRange", t, e, n, s), t != null && (this.root.parentNode == null || t.parentNode == null || // @ts-expect-error Fix me later + if (Ss.info("setNativeRange", t, e, n, s), t != null && (this.root.parentNode == null || t.parentNode == null || // @ts-expect-error Fix me later n.parentNode == null)) return; const o = document.getSelection(); @@ -4032,7 +4020,7 @@ class Uf { } setRange(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : !1, n = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : R.sources.API; - if (typeof e == "string" && (n = e, e = !1), Ms.info("setRange", t), t != null) { + if (typeof e == "string" && (n = e, e = !1), Ss.info("setRange", t), t != null) { const s = this.rangeToNative(t); this.setNativeRange(...s, e); } else @@ -4042,17 +4030,17 @@ class Uf { update() { let t = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : R.sources.USER; const e = this.lastRange, [n, s] = this.getRange(); - if (this.lastRange = n, this.lastNative = s, this.lastRange != null && (this.savedRange = this.lastRange), !pe(e, this.lastRange)) { + if (this.lastRange = n, this.lastNative = s, this.lastRange != null && (this.savedRange = this.lastRange), !ce(e, this.lastRange)) { if (!this.composing && s != null && s.native.collapsed && s.start.node !== this.cursor.textNode) { const o = this.cursor.restore(); o && this.setNativeRange(o.startNode, o.startOffset, o.endNode, o.endOffset); } - const i = [R.events.SELECTION_CHANGE, nn(this.lastRange), nn(e), t]; + const i = [R.events.SELECTION_CHANGE, Xe(this.lastRange), Xe(e), t]; this.emitter.emit(R.events.EDITOR_CHANGE, ...i), t !== R.sources.SILENT && this.emitter.emit(...i); } } } -function Ds(r, t) { +function _s(r, t) { try { t.parentNode; } catch { @@ -4060,8 +4048,8 @@ function Ds(r, t) { } return r.contains(t); } -const Ff = /^[ -~]*$/; -class Hf { +const Af = /^[ -~]*$/; +class wf { constructor(t) { this.scroll = t, this.delta = this.getDelta(); } @@ -4069,38 +4057,38 @@ class Hf { this.scroll.update(); let e = this.scroll.length(); this.scroll.batchStart(); - const n = pl(t), s = new B(); - return Kf(n.ops.slice()).reduce((o, a) => { - const u = It.Op.length(a); + const n = tl(t), s = new k(); + return Tf(n.ops.slice()).reduce((o, a) => { + const u = Ot.Op.length(a); let h = a.attributes || {}, p = !1, v = !1; if (a.insert != null) { if (s.retain(u), typeof a.insert == "string") { const y = a.insert; v = !y.endsWith(` -`) && (e <= o || !!this.scroll.descendant(Ct, o)[0]), this.scroll.insertAt(o, y); - const [A, N] = this.scroll.line(o); - let x = ve({}, qt(A)); - if (A instanceof pt) { - const [C] = A.descendant(Et, N); - C && (x = ve(x, qt(C))); +`) && (e <= o || !!this.scroll.descendant(qt, o)[0]), this.scroll.insertAt(o, y); + const [w, N] = this.scroll.line(o); + let x = fe({}, _t(w)); + if (w instanceof gt) { + const [q] = w.descendant(vt, N); + q && (x = fe(x, _t(q))); } - h = It.AttributeMap.diff(x, h) || {}; + h = Ot.AttributeMap.diff(x, h) || {}; } else if (typeof a.insert == "object") { const y = Object.keys(a.insert)[0]; if (y == null) return o; - const A = this.scroll.query(y, D.INLINE) != null; - if (A) - (e <= o || this.scroll.descendant(Ct, o)[0]) && (v = !0); + const w = this.scroll.query(y, M.INLINE) != null; + if (w) + (e <= o || this.scroll.descendant(qt, o)[0]) && (v = !0); else if (o > 0) { - const [N, x] = this.scroll.descendant(Et, o - 1); - N instanceof Ut ? N.value()[x] !== ` -` && (p = !0) : N instanceof Lt && N.statics.scope === D.INLINE_BLOT && (p = !0); + const [N, x] = this.scroll.descendant(vt, o - 1); + N instanceof $t ? N.value()[x] !== ` +` && (p = !0) : N instanceof Lt && N.statics.scope === M.INLINE_BLOT && (p = !0); } - if (this.scroll.insertAt(o, y, a.insert[y]), A) { - const [N] = this.scroll.descendant(Et, o); + if (this.scroll.insertAt(o, y, a.insert[y]), w) { + const [N] = this.scroll.descendant(vt, o); if (N) { - const x = ve({}, qt(N)); - h = It.AttributeMap.diff(x, h) || {}; + const x = fe({}, _t(N)); + h = Ot.AttributeMap.diff(x, h) || {}; } } } @@ -4115,10 +4103,10 @@ class Hf { }); const f = p ? 1 : 0, m = v ? 1 : 0; return e += f + m, s.retain(f), s.delete(m), o + u + f + m; - }, 0), s.reduce((o, a) => typeof a.delete == "number" ? (this.scroll.deleteAt(o, a.delete), o) : o + It.Op.length(a), 0), this.scroll.batchEnd(), this.scroll.optimize(), this.update(n); + }, 0), s.reduce((o, a) => typeof a.delete == "number" ? (this.scroll.deleteAt(o, a.delete), o) : o + Ot.Op.length(a), 0), this.scroll.batchEnd(), this.scroll.optimize(), this.update(n); } deleteText(t, e) { - return this.scroll.deleteAt(t, e), this.update(new B().retain(t).delete(e)); + return this.scroll.deleteAt(t, e), this.update(new k().retain(t).delete(e)); } formatLine(t, e) { let n = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {}; @@ -4127,7 +4115,7 @@ class Hf { o.format(i, n[i]); }); }), this.scroll.optimize(); - const s = new B().retain(t).retain(e, nn(n)); + const s = new k().retain(t).retain(e, Xe(n)); return this.update(s); } formatText(t, e) { @@ -4135,29 +4123,29 @@ class Hf { Object.keys(n).forEach((i) => { this.scroll.formatAt(t, e, i, n[i]); }); - const s = new B().retain(t).retain(e, nn(n)); + const s = new k().retain(t).retain(e, Xe(n)); return this.update(s); } getContents(t, e) { return this.delta.slice(t, t + e); } getDelta() { - return this.scroll.lines().reduce((t, e) => t.concat(e.delta()), new B()); + return this.scroll.lines().reduce((t, e) => t.concat(e.delta()), new k()); } getFormat(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 0, n = [], s = []; e === 0 ? this.scroll.path(t).forEach((a) => { const [u] = a; - u instanceof pt ? n.push(u) : u instanceof Et && s.push(u); - }) : (n = this.scroll.lines(t, e), s = this.scroll.descendants(Et, t, e)); + u instanceof gt ? n.push(u) : u instanceof vt && s.push(u); + }) : (n = this.scroll.lines(t, e), s = this.scroll.descendants(vt, t, e)); const [i, o] = [n, s].map((a) => { const u = a.shift(); if (u == null) return {}; - let h = qt(u); + let h = _t(u); for (; Object.keys(h).length > 0; ) { const p = a.shift(); if (p == null) return h; - h = zf(qt(p), h); + h = Nf(_t(p), h); } return h; }); @@ -4170,7 +4158,7 @@ class Hf { const [n, s] = this.scroll.line(t); if (n) { const i = n.length(); - return n.length() >= s + e && !(s === 0 && e === i) ? Fn(n, s, e, !0) : Fn(this.scroll, t, e, !0); + return n.length() >= s + e && !(s === 0 && e === i) ? In(n, s, e, !0) : In(this.scroll, t, e, !0); } return ""; } @@ -4178,11 +4166,11 @@ class Hf { return this.getContents(t, e).filter((n) => typeof n.insert == "string").map((n) => n.insert).join(""); } insertContents(t, e) { - const n = pl(e), s = new B().retain(t).concat(n); + const n = tl(e), s = new k().retain(t).concat(n); return this.scroll.insertContents(t, n), this.update(s); } insertEmbed(t, e, n) { - return this.scroll.insertAt(t, e, n), this.update(new B().retain(t).insert({ + return this.scroll.insertAt(t, e, n), this.update(new k().retain(t).insert({ [e]: n })); } @@ -4192,43 +4180,43 @@ class Hf { `).replace(/\r/g, ` `), this.scroll.insertAt(t, e), Object.keys(n).forEach((s) => { this.scroll.formatAt(t, e.length, s, n[s]); - }), this.update(new B().retain(t).insert(e, nn(n))); + }), this.update(new k().retain(t).insert(e, Xe(n))); } isBlank() { if (this.scroll.children.length === 0) return !0; if (this.scroll.children.length > 1) return !1; const t = this.scroll.children.head; - if ((t == null ? void 0 : t.statics.blotName) !== pt.blotName) return !1; + if (t?.statics.blotName !== gt.blotName) return !1; const e = t; - return e.children.length > 1 ? !1 : e.children.head instanceof Ht; + return e.children.length > 1 ? !1 : e.children.head instanceof Ft; } removeFormat(t, e) { const n = this.getText(t, e), [s, i] = this.scroll.line(t + e); - let o = 0, a = new B(); + let o = 0, a = new k(); s != null && (o = s.length() - i, a = s.delta().slice(i, i + o - 1).insert(` `)); - const h = this.getContents(t, e + o).diff(new B().insert(n).concat(a)), p = new B().retain(t).concat(h); + const h = this.getContents(t, e + o).diff(new k().insert(n).concat(a)), p = new k().retain(t).concat(h); return this.applyDelta(p); } update(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : [], n = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : void 0; const s = this.delta; if (e.length === 1 && e[0].type === "characterData" && // @ts-expect-error Fix me later - e[0].target.data.match(Ff) && this.scroll.find(e[0].target)) { - const i = this.scroll.find(e[0].target), o = qt(i), a = i.offset(this.scroll), u = e[0].oldValue.replace(an.CONTENTS, ""), h = new B().insert(u), p = new B().insert(i.value()), v = n && { - oldRange: ml(n.oldRange, -a), - newRange: ml(n.newRange, -a) + e[0].target.data.match(Af) && this.scroll.find(e[0].target)) { + const i = this.scroll.find(e[0].target), o = _t(i), a = i.offset(this.scroll), u = e[0].oldValue.replace(Dt.CONTENTS, ""), h = new k().insert(u), p = new k().insert(i.value()), v = n && { + oldRange: el(n.oldRange, -a), + newRange: el(n.newRange, -a) }; - t = new B().retain(a).concat(h.diff(p, v)).reduce((m, y) => y.insert ? m.insert(y.insert, o) : m.push(y), new B()), this.delta = s.compose(t); + t = new k().retain(a).concat(h.diff(p, v)).reduce((m, y) => y.insert ? m.insert(y.insert, o) : m.push(y), new k()), this.delta = s.compose(t); } else - this.delta = this.getDelta(), (!t || !pe(s.compose(t), this.delta)) && (t = s.diff(this.delta, n)); + this.delta = this.getDelta(), (!t || !ce(s.compose(t), this.delta)) && (t = s.diff(this.delta, n)); return t; } } -function tn(r, t, e) { +function Ye(r, t, e) { if (r.length === 0) { - const [m] = js(e.pop()); - return t <= 0 ? `` : `${tn([], t - 1, e)}`; + const [m] = Cs(e.pop()); + return t <= 0 ? `` : `${Ye([], t - 1, e)}`; } const [{ child: n, @@ -4236,21 +4224,21 @@ function tn(r, t, e) { length: i, indent: o, type: a - }, ...u] = r, [h, p] = js(a); + }, ...u] = r, [h, p] = Cs(a); if (o > t) - return e.push(a), o === t + 1 ? `<${h}>${Fn(n, s, i)}${tn(u, o, e)}` : `<${h}>
  • ${tn(r, t + 1, e)}`; + return e.push(a), o === t + 1 ? `<${h}>${In(n, s, i)}${Ye(u, o, e)}` : `<${h}>
  • ${Ye(r, t + 1, e)}`; const v = e[e.length - 1]; if (o === t && a === v) - return `
  • ${Fn(n, s, i)}${tn(u, o, e)}`; - const [f] = js(e.pop()); - return `${tn(r, t - 1, e)}`; + return `${In(n, s, i)}${Ye(u, o, e)}`; + const [f] = Cs(e.pop()); + return `${Ye(r, t - 1, e)}`; } -function Fn(r, t, e) { +function In(r, t, e) { let n = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : !1; if ("html" in r && typeof r.html == "function") return r.html(t, e); - if (r instanceof Ut) - return Dr(r.value().slice(t, t + e)).replaceAll(" ", " "); + if (r instanceof $t) + return _r(r.value().slice(t, t + e)).replaceAll(" ", " "); if (r instanceof Pt) { if (r.statics.blotName === "list-container") { const h = []; @@ -4263,11 +4251,11 @@ function Fn(r, t, e) { indent: m.indent || 0, type: m.list }); - }), tn(h, -1, []); + }), Ye(h, -1, []); } const s = []; if (r.children.forEachAt(t, e, (h, p, v) => { - s.push(Fn(h, p, v)); + s.push(In(h, p, v)); }), n || r.statics.blotName === "list") return s.join(""); const { @@ -4278,14 +4266,14 @@ function Fn(r, t, e) { } return r.domNode instanceof Element ? r.domNode.outerHTML : ""; } -function zf(r, t) { +function Nf(r, t) { return Object.keys(t).reduce((e, n) => { if (r[n] == null) return e; const s = t[n]; return s === r[n] ? e[n] = s : Array.isArray(s) ? s.indexOf(r[n]) < 0 ? e[n] = s.concat([r[n]]) : e[n] = s : e[n] = [s, r[n]], e; }, {}); } -function js(r) { +function Cs(r) { const t = r === "ordered" ? "ol" : "ul"; switch (r) { case "checked": @@ -4296,7 +4284,7 @@ function js(r) { return [t, ""]; } } -function pl(r) { +function tl(r) { return r.reduce((t, e) => { if (typeof e.insert == "string") { const n = e.insert.replace(/\r\n/g, ` @@ -4305,16 +4293,16 @@ function pl(r) { return t.insert(n, e.attributes); } return t.push(e); - }, new B()); + }, new k()); } -function ml(r, t) { +function el(r, t) { let { index: e, length: n } = r; - return new Ie(e + t, n); + return new Se(e + t, n); } -function Kf(r) { +function Tf(r) { const t = []; return r.forEach((e) => { typeof e.insert == "string" ? e.insert.split(` @@ -4330,28 +4318,28 @@ function Kf(r) { }) : t.push(e); }), t; } -class zt { +class Yt { + static DEFAULTS = {}; constructor(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}; this.quill = t, this.options = e; } } -O(zt, "DEFAULTS", {}); -const Er = "\uFEFF"; -class Ei extends Lt { +const ur = "\uFEFF"; +class si extends Lt { constructor(t, e) { super(t, e), this.contentNode = document.createElement("span"), this.contentNode.setAttribute("contenteditable", "false"), Array.from(this.domNode.childNodes).forEach((n) => { this.contentNode.appendChild(n); - }), this.leftGuard = document.createTextNode(Er), this.rightGuard = document.createTextNode(Er), this.domNode.appendChild(this.leftGuard), this.domNode.appendChild(this.contentNode), this.domNode.appendChild(this.rightGuard); + }), this.leftGuard = document.createTextNode(ur), this.rightGuard = document.createTextNode(ur), this.domNode.appendChild(this.leftGuard), this.domNode.appendChild(this.contentNode), this.domNode.appendChild(this.rightGuard); } index(t, e) { return t === this.leftGuard ? 0 : t === this.rightGuard ? 1 : super.index(t, e); } restore(t) { let e = null, n; - const s = t.data.split(Er).join(""); + const s = t.data.split(ur).join(""); if (t === this.leftGuard) - if (this.prev instanceof Ut) { + if (this.prev instanceof $t) { const i = this.prev.length(); this.prev.insertAt(i, s), e = { startNode: this.prev.domNode, @@ -4362,14 +4350,14 @@ class Ei extends Lt { startNode: n, startOffset: s.length }; - else t === this.rightGuard && (this.next instanceof Ut ? (this.next.insertAt(0, s), e = { + else t === this.rightGuard && (this.next instanceof $t ? (this.next.insertAt(0, s), e = { startNode: this.next.domNode, startOffset: s.length }) : (n = document.createTextNode(s), this.parent.insertBefore(this.scroll.create(n), this.next), e = { startNode: n, startOffset: s.length })); - return t.data = Er, e; + return t.data = ur, e; } update(t, e) { t.forEach((n) => { @@ -4380,9 +4368,9 @@ class Ei extends Lt { }); } } -class Gf { +class xf { + isComposing = !1; constructor(t, e) { - O(this, "isComposing", !1); this.scroll = t, this.emitter = e, this.setupListeners(); } setupListeners() { @@ -4396,15 +4384,21 @@ class Gf { } handleCompositionStart(t) { const e = t.target instanceof Node ? this.scroll.find(t.target, !0) : null; - e && !(e instanceof Ei) && (this.emitter.emit(R.events.COMPOSITION_BEFORE_START, t), this.scroll.batchStart(), this.emitter.emit(R.events.COMPOSITION_START, t), this.isComposing = !0); + e && !(e instanceof si) && (this.emitter.emit(R.events.COMPOSITION_BEFORE_START, t), this.scroll.batchStart(), this.emitter.emit(R.events.COMPOSITION_START, t), this.isComposing = !0); } handleCompositionEnd(t) { this.emitter.emit(R.events.COMPOSITION_BEFORE_END, t), this.scroll.batchEnd(), this.emitter.emit(R.events.COMPOSITION_END, t), this.isComposing = !1; } } -const Mn = class Mn { +class rn { + static DEFAULTS = { + modules: {} + }; + static themes = { + default: rn + }; + modules = {}; constructor(t, e) { - O(this, "modules", {}); this.quill = t, this.options = e; } init() { @@ -4416,14 +4410,8 @@ const Mn = class Mn { const e = this.quill.constructor.import(`modules/${t}`); return this.modules[t] = new e(this.quill, this.options.modules[t] || {}), this.modules[t]; } -}; -O(Mn, "DEFAULTS", { - modules: {} -}), O(Mn, "themes", { - default: Mn -}); -let cn = Mn; -const Vf = (r) => r.parentElement || r.getRootNode().host || null, Wf = (r) => { +} +const Lf = (r) => r.parentElement || r.getRootNode().host || null, Sf = (r) => { const t = r.getBoundingClientRect(), e = "offsetWidth" in r && Math.abs(t.width) / r.offsetWidth || 1, n = "offsetHeight" in r && Math.abs(t.height) / r.offsetHeight || 1; return { top: t.top, @@ -4431,42 +4419,41 @@ const Vf = (r) => r.parentElement || r.getRootNode().host || null, Wf = (r) => { bottom: t.top + r.clientHeight * n, left: t.left }; -}, Ar = (r) => { +}, hr = (r) => { const t = parseInt(r, 10); return Number.isNaN(t) ? 0 : t; -}, bl = (r, t, e, n, s, i) => r < e && t > n ? 0 : r < e ? -(e - r + s) : t > n ? t - r > n - e ? r + s - e : t - n + i : 0, Zf = (r, t) => { - var i, o, a; +}, nl = (r, t, e, n, s, i) => r < e && t > n ? 0 : r < e ? -(e - r + s) : t > n ? t - r > n - e ? r + s - e : t - n + i : 0, _f = (r, t) => { const e = r.ownerDocument; let n = t, s = r; for (; s; ) { - const u = s === e.body, h = u ? { + const i = s === e.body, o = i ? { top: 0, - right: ((i = window.visualViewport) == null ? void 0 : i.width) ?? e.documentElement.clientWidth, - bottom: ((o = window.visualViewport) == null ? void 0 : o.height) ?? e.documentElement.clientHeight, + right: window.visualViewport?.width ?? e.documentElement.clientWidth, + bottom: window.visualViewport?.height ?? e.documentElement.clientHeight, left: 0 - } : Wf(s), p = getComputedStyle(s), v = bl(n.left, n.right, h.left, h.right, Ar(p.scrollPaddingLeft), Ar(p.scrollPaddingRight)), f = bl(n.top, n.bottom, h.top, h.bottom, Ar(p.scrollPaddingTop), Ar(p.scrollPaddingBottom)); - if (v || f) - if (u) - (a = e.defaultView) == null || a.scrollBy(v, f); + } : Sf(s), a = getComputedStyle(s), u = nl(n.left, n.right, o.left, o.right, hr(a.scrollPaddingLeft), hr(a.scrollPaddingRight)), h = nl(n.top, n.bottom, o.top, o.bottom, hr(a.scrollPaddingTop), hr(a.scrollPaddingBottom)); + if (u || h) + if (i) + e.defaultView?.scrollBy(u, h); else { const { - scrollLeft: m, - scrollTop: y + scrollLeft: p, + scrollTop: v } = s; - f && (s.scrollTop += f), v && (s.scrollLeft += v); - const A = s.scrollLeft - m, N = s.scrollTop - y; + h && (s.scrollTop += h), u && (s.scrollLeft += u); + const f = s.scrollLeft - p, m = s.scrollTop - v; n = { - left: n.left - A, - top: n.top - N, - right: n.right - A, - bottom: n.bottom - N + left: n.left - f, + top: n.top - m, + right: n.right - f, + bottom: n.bottom - m }; } - s = u || p.position === "fixed" ? null : Vf(s); + s = i || a.position === "fixed" ? null : Lf(s); } -}, Yf = 100, Xf = ["block", "break", "cursor", "inline", "scroll", "text"], Qf = (r, t, e) => { - const n = new on(); - return Xf.forEach((s) => { +}, Cf = 100, qf = ["block", "break", "cursor", "inline", "scroll", "text"], Of = (r, t, e) => { + const n = new en(); + return qf.forEach((s) => { const i = t.query(s); i && n.register(i); }), r.forEach((s) => { @@ -4474,23 +4461,45 @@ const Vf = (r) => r.parentElement || r.getRootNode().host || null, Wf = (r) => { i || e.error(`Cannot register "${s}" specified in "formats" config. Are you sure it was registered?`); let o = 0; for (; i; ) - if (n.register(i), i = "blotName" in i ? i.requiredContainer ?? null : null, o += 1, o > Yf) { + if (n.register(i), i = "blotName" in i ? i.requiredContainer ?? null : null, o += 1, o > Cf) { e.error(`Cycle detected in registering blot requiredContainer: "${s}"`); break; } }), n; -}, sn = he("quill"), wr = new on(); +}, Je = ie("quill"), fr = new en(); Pt.uiClass = "ql-ui"; -const Dt = class Dt { +class A { + static DEFAULTS = { + bounds: null, + modules: { + clipboard: !0, + keyboard: !0, + history: !0, + uploader: !0 + }, + placeholder: "", + readOnly: !1, + registry: fr, + theme: "default" + }; + static events = R.events; + static sources = R.sources; + static version = "2.0.3"; + static imports = { + delta: k, + parchment: uf, + "core/module": Yt, + "core/theme": rn + }; static debug(t) { - t === !0 && (t = "log"), he.level(t); + t === !0 && (t = "log"), ie.level(t); } static find(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : !1; - return ei.get(t) || wr.find(t, e); + return Gs.get(t) || fr.find(t, e); } static import(t) { - return this.imports[t] == null && sn.error(`Cannot import ${t}. Are you sure it was registered?`), this.imports[t]; + return this.imports[t] == null && Je.error(`Cannot import ${t}. Are you sure it was registered?`), this.imports[t]; } static register() { if (typeof (arguments.length <= 0 ? void 0 : arguments[0]) != "string") { @@ -4500,42 +4509,42 @@ const Dt = class Dt { }); } else { const t = arguments.length <= 0 ? void 0 : arguments[0], e = arguments.length <= 1 ? void 0 : arguments[1], n = !!(!(arguments.length <= 2) && arguments[2]); - this.imports[t] != null && !n && sn.warn(`Overwriting ${t} with`, e), this.imports[t] = e, (t.startsWith("blots/") || t.startsWith("formats/")) && e && typeof e != "boolean" && e.blotName !== "abstract" && wr.register(e), typeof e.register == "function" && e.register(wr); + this.imports[t] != null && !n && Je.warn(`Overwriting ${t} with`, e), this.imports[t] = e, (t.startsWith("blots/") || t.startsWith("formats/")) && e && typeof e != "boolean" && e.blotName !== "abstract" && fr.register(e), typeof e.register == "function" && e.register(fr); } } constructor(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}; - if (this.options = Jf(t, e), this.container = this.options.container, this.container == null) { - sn.error("Invalid Quill container", t); + if (this.options = Rf(t, e), this.container = this.options.container, this.container == null) { + Je.error("Invalid Quill container", t); return; } - this.options.debug && Dt.debug(this.options.debug); + this.options.debug && A.debug(this.options.debug); const n = this.container.innerHTML.trim(); - this.container.classList.add("ql-container"), this.container.innerHTML = "", ei.set(this.container, this), this.root = this.addContainer("ql-editor"), this.root.classList.add("ql-blank"), this.emitter = new R(); - const s = vi.blotName, i = this.options.registry.query(s); + this.container.classList.add("ql-container"), this.container.innerHTML = "", Gs.set(this.container, this), this.root = this.addContainer("ql-editor"), this.root.classList.add("ql-blank"), this.emitter = new R(); + const s = ri.blotName, i = this.options.registry.query(s); if (!i || !("blotName" in i)) throw new Error(`Cannot initialize Quill without "${s}" blot`); if (this.scroll = new i(this.options.registry, this.root, { emitter: this.emitter - }), this.editor = new Hf(this.scroll), this.selection = new Uf(this.scroll, this.emitter), this.composition = new Gf(this.scroll, this.emitter), this.theme = new this.options.theme(this, this.options), this.keyboard = this.theme.addModule("keyboard"), this.clipboard = this.theme.addModule("clipboard"), this.history = this.theme.addModule("history"), this.uploader = this.theme.addModule("uploader"), this.theme.addModule("input"), this.theme.addModule("uiNode"), this.theme.init(), this.emitter.on(R.events.EDITOR_CHANGE, (o) => { + }), this.editor = new wf(this.scroll), this.selection = new Ef(this.scroll, this.emitter), this.composition = new xf(this.scroll, this.emitter), this.theme = new this.options.theme(this, this.options), this.keyboard = this.theme.addModule("keyboard"), this.clipboard = this.theme.addModule("clipboard"), this.history = this.theme.addModule("history"), this.uploader = this.theme.addModule("uploader"), this.theme.addModule("input"), this.theme.addModule("uiNode"), this.theme.init(), this.emitter.on(R.events.EDITOR_CHANGE, (o) => { o === R.events.TEXT_CHANGE && this.root.classList.toggle("ql-blank", this.editor.isBlank()); }), this.emitter.on(R.events.SCROLL_UPDATE, (o, a) => { const u = this.selection.lastRange, [h] = this.selection.getRange(), p = u && h ? { oldRange: u, newRange: h } : void 0; - Mt.call(this, () => this.editor.update(null, a, p), o); + Bt.call(this, () => this.editor.update(null, a, p), o); }), this.emitter.on(R.events.SCROLL_EMBED_UPDATE, (o, a) => { const u = this.selection.lastRange, [h] = this.selection.getRange(), p = u && h ? { oldRange: u, newRange: h } : void 0; - Mt.call(this, () => { - const v = new B().retain(o.offset(this)).retain({ + Bt.call(this, () => { + const v = new k().retain(o.offset(this)).retain({ [o.statics.blotName]: a }); return this.editor.update(v, [], p); - }, Dt.sources.USER); + }, A.sources.USER); }), n) { const o = this.clipboard.convert({ html: `${n}


    `, @@ -4558,7 +4567,7 @@ const Dt = class Dt { this.selection.setRange(null); } deleteText(t, e, n) { - return [t, e, , n] = le(t, e, n), Mt.call(this, () => this.editor.deleteText(t, e), n, t, -1 * e); + return [t, e, , n] = ee(t, e, n), Bt.call(this, () => this.editor.deleteText(t, e), n, t, -1 * e); } disable() { this.enable(!1); @@ -4578,11 +4587,11 @@ const Dt = class Dt { } format(t, e) { let n = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : R.sources.API; - return Mt.call(this, () => { + return Bt.call(this, () => { const s = this.getSelection(!0); - let i = new B(); + let i = new k(); if (s == null) return i; - if (this.scroll.query(t, D.BLOCK)) + if (this.scroll.query(t, M.BLOCK)) i = this.editor.formatLine(s.index, s.length, { [t]: e }); @@ -4598,25 +4607,25 @@ const Dt = class Dt { } formatLine(t, e, n, s, i) { let o; - return [t, e, o, i] = le( + return [t, e, o, i] = ee( t, e, // @ts-expect-error n, s, i - ), Mt.call(this, () => this.editor.formatLine(t, e, o), i, t, 0); + ), Bt.call(this, () => this.editor.formatLine(t, e, o), i, t, 0); } formatText(t, e, n, s, i) { let o; - return [t, e, o, i] = le( + return [t, e, o, i] = ee( // @ts-expect-error t, e, n, s, i - ), Mt.call(this, () => this.editor.formatText(t, e, o), i, t, 0); + ), Bt.call(this, () => this.editor.formatText(t, e, o), i, t, 0); } getBounds(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 0, n = null; @@ -4633,7 +4642,7 @@ const Dt = class Dt { } getContents() { let t = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : 0, e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : this.getLength() - t; - return [t, e] = le(t, e), this.editor.getContents(t, e); + return [t, e] = ee(t, e), this.editor.getContents(t, e); } getFormat() { let t = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : this.getSelection(!0), e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 0; @@ -4659,26 +4668,26 @@ const Dt = class Dt { return this.theme.modules[t]; } getSelection() { - return (arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : !1) && this.focus(), this.update(), this.selection.getRange()[0]; + return arguments.length > 0 && arguments[0] !== void 0 && arguments[0] && this.focus(), this.update(), this.selection.getRange()[0]; } getSemanticHTML() { let t = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : 0, e = arguments.length > 1 ? arguments[1] : void 0; - return typeof t == "number" && (e = e ?? this.getLength() - t), [t, e] = le(t, e), this.editor.getHTML(t, e); + return typeof t == "number" && (e = e ?? this.getLength() - t), [t, e] = ee(t, e), this.editor.getHTML(t, e); } getText() { let t = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : 0, e = arguments.length > 1 ? arguments[1] : void 0; - return typeof t == "number" && (e = e ?? this.getLength() - t), [t, e] = le(t, e), this.editor.getText(t, e); + return typeof t == "number" && (e = e ?? this.getLength() - t), [t, e] = ee(t, e), this.editor.getText(t, e); } hasFocus() { return this.selection.hasFocus(); } insertEmbed(t, e, n) { - let s = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : Dt.sources.API; - return Mt.call(this, () => this.editor.insertEmbed(t, e, n), s, t); + let s = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : A.sources.API; + return Bt.call(this, () => this.editor.insertEmbed(t, e, n), s, t); } insertText(t, e, n, s, i) { let o; - return [t, , o, i] = le(t, 0, n, s, i), Mt.call(this, () => this.editor.insertText(t, e, o), i, t, e.length); + return [t, , o, i] = ee(t, 0, n, s, i), Bt.call(this, () => this.editor.insertText(t, e, o), i, t, e.length); } isEnabled() { return this.scroll.isEnabled(); @@ -4693,10 +4702,10 @@ const Dt = class Dt { return this.emitter.once(...arguments); } removeFormat(t, e, n) { - return [t, e, , n] = le(t, e, n), Mt.call(this, () => this.editor.removeFormat(t, e), n, t); + return [t, e, , n] = ee(t, e, n), Bt.call(this, () => this.editor.removeFormat(t, e), n, t); } scrollRectIntoView(t) { - Zf(this.root, t); + _f(this.root, t); } /** * @deprecated Use Quill#scrollSelectionIntoView() instead. @@ -4714,18 +4723,18 @@ const Dt = class Dt { } setContents(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : R.sources.API; - return Mt.call(this, () => { - t = new B(t); + return Bt.call(this, () => { + t = new k(t); const n = this.getLength(), s = this.editor.deleteText(0, n), i = this.editor.insertContents(0, t), o = this.editor.deleteText(this.getLength() - 1, 1); return s.compose(i).compose(o); }, e); } setSelection(t, e, n) { - t == null ? this.selection.setRange(null, e || Dt.sources.API) : ([t, e, , n] = le(t, e, n), this.selection.setRange(new Ie(Math.max(0, t), e), n), n !== R.sources.SILENT && this.scrollSelectionIntoView()); + t == null ? this.selection.setRange(null, e || A.sources.API) : ([t, e, , n] = ee(t, e, n), this.selection.setRange(new Se(Math.max(0, t), e), n), n !== R.sources.SILENT && this.scrollSelectionIntoView()); } setText(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : R.sources.API; - const n = new B().insert(t); + const n = new k().insert(t); return this.setContents(n, e); } update() { @@ -4735,32 +4744,13 @@ const Dt = class Dt { } updateContents(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : R.sources.API; - return Mt.call(this, () => (t = new B(t), this.editor.applyDelta(t)), e, !0); + return Bt.call(this, () => (t = new k(t), this.editor.applyDelta(t)), e, !0); } -}; -O(Dt, "DEFAULTS", { - bounds: null, - modules: { - clipboard: !0, - keyboard: !0, - history: !0, - uploader: !0 - }, - placeholder: "", - readOnly: !1, - registry: wr, - theme: "default" -}), O(Dt, "events", R.events), O(Dt, "sources", R.sources), O(Dt, "version", "2.0.3"), O(Dt, "imports", { - delta: B, - parchment: Cf, - "core/module": zt, - "core/theme": cn -}); -let w = Dt; -function yl(r) { +} +function rl(r) { return typeof r == "string" ? document.querySelector(r) : r; } -function $s(r) { +function qs(r) { return Object.entries(r ?? {}).reduce((t, e) => { let [n, s] = e; return { @@ -4769,87 +4759,92 @@ function $s(r) { }; }, {}); } -function vl(r) { +function sl(r) { return Object.fromEntries(Object.entries(r).filter((t) => t[1] !== void 0)); } -function Jf(r, t) { - const e = yl(r); +function Rf(r, t) { + const e = rl(r); if (!e) throw new Error("Invalid Quill container"); - const s = !t.theme || t.theme === w.DEFAULTS.theme ? cn : w.import(`themes/${t.theme}`); + const s = !t.theme || t.theme === A.DEFAULTS.theme ? rn : A.import(`themes/${t.theme}`); if (!s) throw new Error(`Invalid theme ${t.theme}. Did you register it?`); const { modules: i, ...o - } = w.DEFAULTS, { + } = A.DEFAULTS, { modules: a, ...u } = s.DEFAULTS; - let h = $s(t.modules); + let h = qs(t.modules); h != null && h.toolbar && h.toolbar.constructor !== Object && (h = { ...h, toolbar: { container: h.toolbar } }); - const p = ve({}, $s(i), $s(a), h), v = { + const p = fe({}, qs(i), qs(a), h), v = { ...o, - ...vl(u), - ...vl(t) + ...sl(u), + ...sl(t) }; let f = t.registry; - return f ? t.formats && sn.warn('Ignoring "formats" option because "registry" is specified') : f = t.formats ? Qf(t.formats, v.registry, sn) : v.registry, { + return f ? t.formats && Je.warn('Ignoring "formats" option because "registry" is specified') : f = t.formats ? Of(t.formats, v.registry, Je) : v.registry, { ...v, registry: f, container: e, theme: s, modules: Object.entries(p).reduce((m, y) => { - let [A, N] = y; + let [w, N] = y; if (!N) return m; - const x = w.import(`modules/${A}`); - return x == null ? (sn.error(`Cannot load ${A} module. Are you sure you registered it?`), m) : { + const x = A.import(`modules/${w}`); + return x == null ? (Je.error(`Cannot load ${w} module. Are you sure you registered it?`), m) : { ...m, // @ts-expect-error - [A]: ve({}, x.DEFAULTS || {}, N) + [w]: fe({}, x.DEFAULTS || {}, N) }; }, {}), - bounds: yl(v.bounds) + bounds: rl(v.bounds) }; } -function Mt(r, t, e, n) { +function Bt(r, t, e, n) { if (!this.isEnabled() && t === R.sources.USER && !this.allowReadOnlyEdits) - return new B(); + return new k(); let s = e == null ? null : this.getSelection(); const i = this.editor.delta, o = r(); - if (s != null && (e === !0 && (e = s.index), n == null ? s = El(s, o, t) : n !== 0 && (s = El(s, e, n, t)), this.setSelection(s, R.sources.SILENT)), o.length() > 0) { + if (s != null && (e === !0 && (e = s.index), n == null ? s = il(s, o, t) : n !== 0 && (s = il(s, e, n, t)), this.setSelection(s, R.sources.SILENT)), o.length() > 0) { const a = [R.events.TEXT_CHANGE, o, i, t]; this.emitter.emit(R.events.EDITOR_CHANGE, ...a), t !== R.sources.SILENT && this.emitter.emit(...a); } return o; } -function le(r, t, e, n, s) { +function ee(r, t, e, n, s) { let i = {}; return typeof r.index == "number" && typeof r.length == "number" ? typeof t != "number" ? (s = n, n = e, e = t, t = r.length, r = r.index) : (t = r.length, r = r.index) : typeof t != "number" && (s = n, n = e, e = t, t = 0), typeof e == "object" ? (i = e, s = n) : typeof e == "string" && (n != null ? i[e] = n : s = e), s = s || R.sources.API, [r, t, i, s]; } -function El(r, t, e, n) { +function il(r, t, e, n) { const s = typeof e == "number" ? e : 0; if (r == null) return null; let i, o; return t && typeof t.transformPosition == "function" ? [i, o] = [r.index, r.index + r.length].map((a) => ( // @ts-expect-error -- TODO: add a better type guard around `index` t.transformPosition(a, n !== R.sources.USER) - )) : [i, o] = [r.index, r.index + r.length].map((a) => a < t || a === t && n === R.sources.USER ? a : s >= 0 ? a + s : Math.max(t, a + s)), new Ie(i, o - i); + )) : [i, o] = [r.index, r.index + r.length].map((a) => a < t || a === t && n === R.sources.USER ? a : s >= 0 ? a + s : Math.max(t, a + s)), new Se(i, o - i); } -class Me extends Mr { +class Re extends Sr { } -function Al(r) { - return r instanceof pt || r instanceof Ct; +function ll(r) { + return r instanceof gt || r instanceof qt; } -function wl(r) { +function ol(r) { return typeof r.updateContent == "function"; } -class en extends vi { +class If extends ri { + static blotName = "scroll"; + static className = "ql-editor"; + static tagName = "DIV"; + static defaultChild = gt; + static allowedChildren = [gt, qt, Re]; constructor(t, e, n) { let { emitter: s @@ -4876,11 +4871,11 @@ class en extends vi { deleteAt(t, e) { const [n, s] = this.line(t), [i] = this.line(t + e); if (super.deleteAt(t, e), i != null && n !== i && s > 0) { - if (n instanceof Ct || i instanceof Ct) { + if (n instanceof qt || i instanceof qt) { this.optimize(); return; } - const o = i.children.head instanceof Ht ? null : i.children.head; + const o = i.children.head instanceof Ft ? null : i.children.head; n.moveChildren(i, o), n.remove(); } this.optimize(); @@ -4894,7 +4889,7 @@ class en extends vi { } insertAt(t, e, n) { if (t >= this.length()) - if (n == null || this.scroll.query(e, D.BLOCK) == null) { + if (n == null || this.scroll.query(e, M.BLOCK) == null) { const s = this.scroll.create(this.statics.defaultChild.blotName); this.appendChild(s), n == null && e.endsWith(` `) ? s.insertAt(0, e.slice(0, -1), n) : s.insertAt(0, e, n); @@ -4907,27 +4902,27 @@ class en extends vi { this.optimize(); } insertBefore(t, e) { - if (t.statics.scope === D.INLINE_BLOT) { + if (t.statics.scope === M.INLINE_BLOT) { const n = this.scroll.create(this.statics.defaultChild.blotName); n.appendChild(t), super.insertBefore(n, e); } else super.insertBefore(t, e); } insertContents(t, e) { - const n = this.deltaToRenderBlocks(e.concat(new B().insert(` + const n = this.deltaToRenderBlocks(e.concat(new k().insert(` `))), s = n.pop(); if (s == null) return; this.batchStart(); const i = n.shift(); if (i) { - const u = i.type === "block" && (i.delta.length() === 0 || !this.descendant(Ct, t)[0] && t < this.length()), h = i.type === "block" ? i.delta : new B().insert({ + const u = i.type === "block" && (i.delta.length() === 0 || !this.descendant(qt, t)[0] && t < this.length()), h = i.type === "block" ? i.delta : new k().insert({ [i.key]: i.value }); - Ps(this, t, h); + Os(this, t, h); const p = i.type === "block" ? 1 : 0, v = t + h.length() + p; u && this.insertAt(v - 1, ` `); - const f = qt(this.line(t)[0]), m = It.AttributeMap.diff(f, i.attributes) || {}; + const f = _t(this.line(t)[0]), m = Ot.AttributeMap.diff(f, i.attributes) || {}; Object.keys(m).forEach((y) => { this.formatAt(v - 1, 1, y, m[y]); }), t = v; @@ -4936,7 +4931,7 @@ class en extends vi { if (n.length && (o && (o = o.split(a), a = 0), n.forEach((u) => { if (u.type === "block") { const h = this.createBlock(u.attributes, o || void 0); - Ps(h, 0, u.delta); + Os(h, 0, u.delta); } else { const h = this.create(u.key, u.value); this.insertBefore(h, o || void 0), Object.keys(u.attributes).forEach((p) => { @@ -4945,7 +4940,7 @@ class en extends vi { } })), s.type === "block" && s.delta.length()) { const u = o ? o.offset(o.scroll) + a : this.length(); - Ps(this, u, s.delta); + Os(this, u, s.delta); } this.batchEnd(), this.optimize(); } @@ -4957,17 +4952,17 @@ class en extends vi { if (!e) return [null, -1]; const [n, s] = e; - return n instanceof Et ? [n, s] : [null, -1]; + return n instanceof vt ? [n, s] : [null, -1]; } line(t) { - return t === this.length() ? this.line(t - 1) : this.descendant(Al, t); + return t === this.length() ? this.line(t - 1) : this.descendant(ll, t); } lines() { let t = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : 0, e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : Number.MAX_VALUE; const n = (s, i, o) => { let a = [], u = o; return s.children.forEachAt(i, o, (h, p, v) => { - Al(h) ? a.push(h) : h instanceof Mr && (a = a.concat(n(h, p, u))), u -= v; + ll(h) ? a.push(h) : h instanceof Sr && (a = a.concat(n(h, p, u))), u -= v; }), a; }; return n(this, t, e); @@ -4992,21 +4987,21 @@ class en extends vi { target: s } = n; const i = this.find(s, !0); - return i && !wl(i); + return i && !ol(i); }), t.length > 0 && this.emitter.emit(R.events.SCROLL_BEFORE_UPDATE, e, t), super.update(t.concat([])), t.length > 0 && this.emitter.emit(R.events.SCROLL_UPDATE, e, t); } updateEmbedAt(t, e, n) { - const [s] = this.descendant((i) => i instanceof Ct, t); - s && s.statics.blotName === e && wl(s) && s.updateContent(n); + const [s] = this.descendant((i) => i instanceof qt, t); + s && s.statics.blotName === e && ol(s) && s.updateContent(n); } handleDragStart(t) { t.preventDefault(); } deltaToRenderBlocks(t) { const e = []; - let n = new B(); + let n = new k(); return t.forEach((s) => { - const i = s == null ? void 0 : s.insert; + const i = s?.insert; if (i) if (typeof i == "string") { const o = i.split(` @@ -5016,18 +5011,18 @@ class en extends vi { type: "block", delta: n, attributes: s.attributes ?? {} - }), n = new B(); + }), n = new k(); }); const a = o[o.length - 1]; a && n.insert(a, s.attributes); } else { const o = Object.keys(i)[0]; if (!o) return; - this.query(o, D.INLINE) ? n.push(s) : (n.length() && e.push({ + this.query(o, M.INLINE) ? n.push(s) : (n.length() && e.push({ type: "block", delta: n, attributes: {} - }), n = new B(), e.push({ + }), n = new k(), e.push({ type: "blockEmbed", key: o, value: i[o], @@ -5045,7 +5040,7 @@ class en extends vi { const s = {}; Object.entries(t).forEach((a) => { let [u, h] = a; - this.query(u, D.BLOCK & D.BLOT) != null ? n = u : s[u] = h; + this.query(u, M.BLOCK & M.BLOT) != null ? n = u : s[u] = h; }); const i = this.create(n || this.statics.defaultChild.blotName, n ? t[n] : void 0); this.insertBefore(i, e || void 0); @@ -5056,23 +5051,22 @@ class en extends vi { }), i; } } -O(en, "blotName", "scroll"), O(en, "className", "ql-editor"), O(en, "tagName", "DIV"), O(en, "defaultChild", pt), O(en, "allowedChildren", [pt, Ct, Me]); -function Ps(r, t, e) { +function Os(r, t, e) { e.reduce((n, s) => { - const i = It.Op.length(s); + const i = Ot.Op.length(s); let o = s.attributes || {}; if (s.insert != null) { if (typeof s.insert == "string") { const a = s.insert; r.insertAt(n, a); - const [u] = r.descendant(Et, n), h = qt(u); - o = It.AttributeMap.diff(h, o) || {}; + const [u] = r.descendant(vt, n), h = _t(u); + o = Ot.AttributeMap.diff(h, o) || {}; } else if (typeof s.insert == "object") { const a = Object.keys(s.insert)[0]; if (a == null) return n; - if (r.insertAt(n, a, s.insert[a]), r.scroll.query(a, D.INLINE) != null) { - const [h] = r.descendant(Et, n), p = qt(h); - o = It.AttributeMap.diff(p, o) || {}; + if (r.insertAt(n, a, s.insert[a]), r.scroll.query(a, M.INLINE) != null) { + const [h] = r.descendant(vt, n), p = _t(h); + o = Ot.AttributeMap.diff(p, o) || {}; } } } @@ -5081,26 +5075,26 @@ function Ps(r, t, e) { }), n + i; }, t); } -const Ai = { - scope: D.BLOCK, +const ii = { + scope: M.BLOCK, whitelist: ["right", "center", "justify"] -}, td = new Jt("align", "align", Ai), Eo = new Ft("align", "ql-align", Ai), Ao = new we("align", "text-align", Ai); -class wo extends we { +}, kf = new Wt("align", "align", ii), io = new Ut("align", "ql-align", ii), lo = new me("align", "text-align", ii); +class oo extends me { value(t) { let e = super.value(t); return e.startsWith("rgb(") ? (e = e.replace(/^[^\d]+/, "").replace(/[^\d]+$/, ""), `#${e.split(",").map((s) => `00${parseInt(s, 10).toString(16)}`.slice(-2)).join("")}`) : e; } } -const ed = new Ft("color", "ql-color", { - scope: D.INLINE -}), wi = new wo("color", "color", { - scope: D.INLINE -}), nd = new Ft("background", "ql-bg", { - scope: D.INLINE -}), Ni = new wo("background", "background-color", { - scope: D.INLINE +const Bf = new Ut("color", "ql-color", { + scope: M.INLINE +}), li = new oo("color", "color", { + scope: M.INLINE +}), Mf = new Ut("background", "ql-bg", { + scope: M.INLINE +}), oi = new oo("background", "background-color", { + scope: M.INLINE }); -class De extends Me { +class Ie extends Re { static create(t) { const e = super.create(t); return e.setAttribute("spellcheck", "false"), e; @@ -5111,49 +5105,49 @@ class De extends Me { } html(t, e) { return `
    -${Dr(this.code(t, e))}
    +${_r(this.code(t, e))}
     
    `; } } -class At extends pt { +class wt extends gt { + static TAB = " "; static register() { - w.register(De); - } -} -O(At, "TAB", " "); -class Ti extends te { -} -Ti.blotName = "code"; -Ti.tagName = "CODE"; -At.blotName = "code-block"; -At.className = "ql-code-block"; -At.tagName = "DIV"; -De.blotName = "code-block-container"; -De.className = "ql-code-block-container"; -De.tagName = "DIV"; -De.allowedChildren = [At]; -At.allowedChildren = [Ut, Ht, an]; -At.requiredContainer = De; -const xi = { - scope: D.BLOCK, + A.register(Ie); + } +} +class ai extends At { +} +ai.blotName = "code"; +ai.tagName = "CODE"; +wt.blotName = "code-block"; +wt.className = "ql-code-block"; +wt.tagName = "DIV"; +Ie.blotName = "code-block-container"; +Ie.className = "ql-code-block-container"; +Ie.tagName = "DIV"; +Ie.allowedChildren = [wt]; +wt.allowedChildren = [$t, Ft, Dt]; +wt.requiredContainer = Ie; +const ci = { + scope: M.BLOCK, whitelist: ["rtl"] -}, No = new Jt("direction", "dir", xi), To = new Ft("direction", "ql-direction", xi), xo = new we("direction", "direction", xi), Lo = { - scope: D.INLINE, +}, ao = new Wt("direction", "dir", ci), co = new Ut("direction", "ql-direction", ci), uo = new me("direction", "direction", ci), ho = { + scope: M.INLINE, whitelist: ["serif", "monospace"] -}, So = new Ft("font", "ql-font", Lo); -class rd extends we { +}, fo = new Ut("font", "ql-font", ho); +class Df extends me { value(t) { return super.value(t).replace(/["']/g, ""); } } -const _o = new rd("font", "font-family", Lo), qo = new Ft("size", "ql-size", { - scope: D.INLINE, +const go = new Df("font", "font-family", ho), po = new Ut("size", "ql-size", { + scope: M.INLINE, whitelist: ["small", "large", "huge"] -}), Oo = new we("size", "font-size", { - scope: D.INLINE, +}), mo = new me("size", "font-size", { + scope: M.INLINE, whitelist: ["10px", "18px", "32px"] -}), sd = he("quill:keyboard"), id = /Mac/i.test(navigator.platform) ? "metaKey" : "ctrlKey"; -class jr extends zt { +}), jf = ie("quill:keyboard"), Pf = /Mac/i.test(navigator.platform) ? "metaKey" : "ctrlKey"; +class Cr extends Yt { static match(t, e) { return ["altKey", "ctrlKey", "metaKey", "shiftKey"].some((n) => !!e[n] !== t[n] && e[n] !== null) ? !1 : e.key === t.key || e.key === t.which; } @@ -5208,9 +5202,9 @@ class jr extends zt { } addBinding(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}, n = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {}; - const s = od(t); + const s = Uf(t); if (s == null) { - sd.warn("Attempted to add invalid keyboard binding", s); + jf.warn("Attempted to add invalid keyboard binding", s); return; } typeof e == "function" && (e = { @@ -5230,13 +5224,13 @@ class jr extends zt { listen() { this.quill.root.addEventListener("keydown", (t) => { if (t.defaultPrevented || t.isComposing || t.keyCode === 229 && (t.key === "Enter" || t.key === "Backspace")) return; - const s = (this.bindings[t.key] || []).concat(this.bindings[t.which] || []).filter((x) => jr.match(t, x)); + const s = (this.bindings[t.key] || []).concat(this.bindings[t.which] || []).filter((x) => Cr.match(t, x)); if (s.length === 0) return; - const i = w.find(t.target, !0); + const i = A.find(t.target, !0); if (i && i.scroll !== this.quill.scroll) return; const o = this.quill.getSelection(); if (o == null || !this.quill.hasFocus()) return; - const [a, u] = this.quill.getLine(o.index), [h, p] = this.quill.getLeaf(o.index), [v, f] = o.length === 0 ? [h, p] : this.quill.getLeaf(o.index + o.length), m = h instanceof Cr ? h.value().slice(0, p) : "", y = v instanceof Cr ? v.value().slice(f) : "", A = { + const [a, u] = this.quill.getLine(o.index), [h, p] = this.quill.getLeaf(o.index), [v, f] = o.length === 0 ? [h, p] : this.quill.getLeaf(o.index + o.length), m = h instanceof Er ? h.value().slice(0, p) : "", y = v instanceof Er ? v.value().slice(f) : "", w = { collapsed: o.length === 0, // @ts-expect-error Fix me later empty: o.length === 0 && a.length() <= 1, @@ -5248,14 +5242,14 @@ class jr extends zt { event: t }; s.some((x) => { - if (x.collapsed != null && x.collapsed !== A.collapsed || x.empty != null && x.empty !== A.empty || x.offset != null && x.offset !== A.offset) + if (x.collapsed != null && x.collapsed !== w.collapsed || x.empty != null && x.empty !== w.empty || x.offset != null && x.offset !== w.offset) return !1; if (Array.isArray(x.format)) { - if (x.format.every((C) => A.format[C] == null)) + if (x.format.every((q) => w.format[q] == null)) return !1; - } else if (typeof x.format == "object" && !Object.keys(x.format).every((C) => x.format[C] === !0 ? A.format[C] != null : x.format[C] === !1 ? A.format[C] == null : pe(x.format[C], A.format[C]))) + } else if (typeof x.format == "object" && !Object.keys(x.format).every((q) => x.format[q] === !0 ? w.format[q] != null : x.format[q] === !1 ? w.format[q] == null : ce(x.format[q], w.format[q]))) return !1; - return x.prefix != null && !x.prefix.test(A.prefix) || x.suffix != null && !x.suffix.test(A.suffix) ? !1 : x.handler.call(this, o, A, x) !== !0; + return x.prefix != null && !x.prefix.test(w.prefix) || x.suffix != null && !x.suffix.test(w.suffix) ? !1 : x.handler.call(this, o, w, x) !== !0; }) && t.preventDefault(); }); } @@ -5264,57 +5258,57 @@ class jr extends zt { if (t.index === 0 || this.quill.getLength() <= 1) return; let s = {}; const [i] = this.quill.getLine(t.index); - let o = new B().retain(t.index - n).delete(n); + let o = new k().retain(t.index - n).delete(n); if (e.offset === 0) { const [a] = this.quill.getLine(t.index - 1); if (a && !(a.statics.blotName === "block" && a.length() <= 1)) { const h = i.formats(), p = this.quill.getFormat(t.index - 1, 1); - if (s = It.AttributeMap.diff(h, p) || {}, Object.keys(s).length > 0) { - const v = new B().retain(t.index + i.length() - 2).retain(1, s); + if (s = Ot.AttributeMap.diff(h, p) || {}, Object.keys(s).length > 0) { + const v = new k().retain(t.index + i.length() - 2).retain(1, s); o = o.compose(v); } } } - this.quill.updateContents(o, w.sources.USER), this.quill.focus(); + this.quill.updateContents(o, A.sources.USER), this.quill.focus(); } handleDelete(t, e) { const n = /^[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(e.suffix) ? 2 : 1; if (t.index >= this.quill.getLength() - n) return; let s = {}; const [i] = this.quill.getLine(t.index); - let o = new B().retain(t.index).delete(n); + let o = new k().retain(t.index).delete(n); if (e.offset >= i.length() - 1) { const [a] = this.quill.getLine(t.index + 1); if (a) { const u = i.formats(), h = this.quill.getFormat(t.index, 1); - s = It.AttributeMap.diff(u, h) || {}, Object.keys(s).length > 0 && (o = o.retain(a.length() - 1).retain(1, s)); + s = Ot.AttributeMap.diff(u, h) || {}, Object.keys(s).length > 0 && (o = o.retain(a.length() - 1).retain(1, s)); } } - this.quill.updateContents(o, w.sources.USER), this.quill.focus(); + this.quill.updateContents(o, A.sources.USER), this.quill.focus(); } handleDeleteRange(t) { - Li({ + ui({ range: t, quill: this.quill }), this.quill.focus(); } handleEnter(t, e) { - const n = Object.keys(e.format).reduce((i, o) => (this.quill.scroll.query(o, D.BLOCK) && !Array.isArray(e.format[o]) && (i[o] = e.format[o]), i), {}), s = new B().retain(t.index).delete(t.length).insert(` + const n = Object.keys(e.format).reduce((i, o) => (this.quill.scroll.query(o, M.BLOCK) && !Array.isArray(e.format[o]) && (i[o] = e.format[o]), i), {}), s = new k().retain(t.index).delete(t.length).insert(` `, n); - this.quill.updateContents(s, w.sources.USER), this.quill.setSelection(t.index + 1, w.sources.SILENT), this.quill.focus(); + this.quill.updateContents(s, A.sources.USER), this.quill.setSelection(t.index + 1, A.sources.SILENT), this.quill.focus(); } } -const ld = { +const $f = { bindings: { - bold: Us("bold"), - italic: Us("italic"), - underline: Us("underline"), + bold: Rs("bold"), + italic: Rs("italic"), + underline: Rs("underline"), indent: { // highlight tab or tab at beginning of list, indent or blockquote key: "Tab", format: ["blockquote", "indent", "list"], handler(r, t) { - return t.collapsed && t.offset !== 0 ? !0 : (this.quill.format("indent", "+1", w.sources.USER), !1); + return t.collapsed && t.offset !== 0 ? !0 : (this.quill.format("indent", "+1", A.sources.USER), !1); } }, outdent: { @@ -5323,7 +5317,7 @@ const ld = { format: ["blockquote", "indent", "list"], // highlight tab or tab at beginning of list, indent or blockquote handler(r, t) { - return t.collapsed && t.offset !== 0 ? !0 : (this.quill.format("indent", "-1", w.sources.USER), !1); + return t.collapsed && t.offset !== 0 ? !0 : (this.quill.format("indent", "-1", A.sources.USER), !1); } }, "outdent backspace": { @@ -5336,18 +5330,18 @@ const ld = { format: ["indent", "list"], offset: 0, handler(r, t) { - t.format.indent != null ? this.quill.format("indent", "-1", w.sources.USER) : t.format.list != null && this.quill.format("list", !1, w.sources.USER); + t.format.indent != null ? this.quill.format("indent", "-1", A.sources.USER) : t.format.list != null && this.quill.format("list", !1, A.sources.USER); } }, - "indent code-block": Nl(!0), - "outdent code-block": Nl(!1), + "indent code-block": al(!0), + "outdent code-block": al(!1), "remove tab": { key: "Tab", shiftKey: !0, collapsed: !0, prefix: /\t$/, handler(r) { - this.quill.deleteText(r.index - 1, 1, w.sources.USER); + this.quill.deleteText(r.index - 1, 1, A.sources.USER); } }, tab: { @@ -5355,8 +5349,8 @@ const ld = { handler(r, t) { if (t.format.table) return !0; this.quill.history.cutoff(); - const e = new B().retain(r.index).delete(r.length).insert(" "); - return this.quill.updateContents(e, w.sources.USER), this.quill.history.cutoff(), this.quill.setSelection(r.index + 1, w.sources.SILENT), !1; + const e = new k().retain(r.index).delete(r.length).insert(" "); + return this.quill.updateContents(e, A.sources.USER), this.quill.history.cutoff(), this.quill.setSelection(r.index + 1, A.sources.SILENT), !1; } }, "blockquote empty enter": { @@ -5365,7 +5359,7 @@ const ld = { format: ["blockquote"], empty: !0, handler() { - this.quill.format("blockquote", !1, w.sources.USER); + this.quill.format("blockquote", !1, A.sources.USER); } }, "list empty enter": { @@ -5377,7 +5371,7 @@ const ld = { const e = { list: !1 }; - t.format.indent && (e.indent = !1), this.quill.formatLine(r.index, r.length, e, w.sources.USER); + t.format.indent && (e.indent = !1), this.quill.formatLine(r.index, r.length, e, A.sources.USER); } }, "checklist enter": { @@ -5391,11 +5385,11 @@ const ld = { // @ts-expect-error Fix me later ...t.formats(), list: "checked" - }, s = new B().retain(r.index).insert(` + }, s = new k().retain(r.index).insert(` `, n).retain(t.length() - e - 1).retain(1, { list: "unchecked" }); - this.quill.updateContents(s, w.sources.USER), this.quill.setSelection(r.index + 1, w.sources.SILENT), this.quill.scrollSelectionIntoView(); + this.quill.updateContents(s, A.sources.USER), this.quill.setSelection(r.index + 1, A.sources.SILENT), this.quill.scrollSelectionIntoView(); } }, "header enter": { @@ -5404,11 +5398,11 @@ const ld = { format: ["header"], suffix: /^$/, handler(r, t) { - const [e, n] = this.quill.getLine(r.index), s = new B().retain(r.index).insert(` + const [e, n] = this.quill.getLine(r.index), s = new k().retain(r.index).insert(` `, t.format).retain(e.length() - n - 1).retain(1, { header: null }); - this.quill.updateContents(s, w.sources.USER), this.quill.setSelection(r.index + 1, w.sources.SILENT), this.quill.scrollSelectionIntoView(); + this.quill.updateContents(s, A.sources.USER), this.quill.setSelection(r.index + 1, A.sources.SILENT), this.quill.scrollSelectionIntoView(); } }, "table backspace": { @@ -5434,18 +5428,18 @@ const ld = { handler(r) { const t = this.quill.getModule("table"); if (t) { - const [e, n, s, i] = t.getTable(r), o = ad(e, n, s, i); + const [e, n, s, i] = t.getTable(r), o = Ff(e, n, s, i); if (o == null) return; let a = e.offset(); if (o < 0) { - const u = new B().retain(a).insert(` + const u = new k().retain(a).insert(` `); - this.quill.updateContents(u, w.sources.USER), this.quill.setSelection(r.index + 1, r.length, w.sources.SILENT); + this.quill.updateContents(u, A.sources.USER), this.quill.setSelection(r.index + 1, r.length, A.sources.SILENT); } else if (o > 0) { a += e.length(); - const u = new B().retain(a).insert(` + const u = new k().retain(a).insert(` `); - this.quill.updateContents(u, w.sources.USER), this.quill.setSelection(a, w.sources.USER); + this.quill.updateContents(u, A.sources.USER), this.quill.setSelection(a, A.sources.USER); } } } @@ -5459,7 +5453,7 @@ const ld = { event: e, line: n } = t, s = n.offset(this.quill.scroll); - e.shiftKey ? this.quill.setSelection(s - 1, w.sources.USER) : this.quill.setSelection(s + n.length(), w.sources.USER); + e.shiftKey ? this.quill.setSelection(s - 1, A.sources.USER) : this.quill.setSelection(s + n.length(), A.sources.USER); } }, "list autofill": { @@ -5494,11 +5488,11 @@ const ld = { default: i = "ordered"; } - this.quill.insertText(r.index, " ", w.sources.USER), this.quill.history.cutoff(); - const o = new B().retain(r.index - s).delete(e + 1).retain(n.length() - 2 - s).retain(1, { + this.quill.insertText(r.index, " ", A.sources.USER), this.quill.history.cutoff(); + const o = new k().retain(r.index - s).delete(e + 1).retain(n.length() - 2 - s).retain(1, { list: i }); - return this.quill.updateContents(o, w.sources.USER), this.quill.history.cutoff(), this.quill.setSelection(r.index - e, w.sources.SILENT), !1; + return this.quill.updateContents(o, A.sources.USER), this.quill.history.cutoff(), this.quill.setSelection(r.index - e, A.sources.SILENT), !1; } }, "code exit": { @@ -5512,24 +5506,24 @@ const ld = { let n = 2, s = t; for (; s != null && s.length() <= 1 && s.formats()["code-block"]; ) if (s = s.prev, n -= 1, n <= 0) { - const i = new B().retain(r.index + t.length() - e - 2).retain(1, { + const i = new k().retain(r.index + t.length() - e - 2).retain(1, { "code-block": null }).delete(1); - return this.quill.updateContents(i, w.sources.USER), this.quill.setSelection(r.index - 1, w.sources.SILENT), !1; + return this.quill.updateContents(i, A.sources.USER), this.quill.setSelection(r.index - 1, A.sources.SILENT), !1; } return !0; } }, - "embed left": Nr("ArrowLeft", !1), - "embed left shift": Nr("ArrowLeft", !0), - "embed right": Nr("ArrowRight", !1), - "embed right shift": Nr("ArrowRight", !0), - "table down": Tl(!1), - "table up": Tl(!0) + "embed left": dr("ArrowLeft", !1), + "embed left shift": dr("ArrowLeft", !0), + "embed right": dr("ArrowRight", !1), + "embed right shift": dr("ArrowRight", !0), + "table down": cl(!1), + "table up": cl(!0) } }; -jr.DEFAULTS = ld; -function Nl(r) { +Cr.DEFAULTS = $f; +function al(r) { return { key: "Tab", shiftKey: !r, @@ -5544,7 +5538,7 @@ function Nl(r) { TAB: i } = s; if (t.length === 0 && !n.shiftKey) { - this.quill.insertText(t.index, i, w.sources.USER), this.quill.setSelection(t.index + i.length, w.sources.SILENT); + this.quill.insertText(t.index, i, A.sources.USER), this.quill.setSelection(t.index + i.length, A.sources.SILENT); return; } const o = t.length === 0 ? this.quill.getLines(t.index, 1) : this.quill.getLines(t); @@ -5554,11 +5548,11 @@ function Nl(r) { } = t; o.forEach((h, p) => { r ? (h.insertAt(0, i), p === 0 ? a += i.length : u += i.length) : h.domNode.textContent.startsWith(i) && (h.deleteAt(0, i.length), p === 0 ? a -= i.length : u -= i.length); - }), this.quill.update(w.sources.USER), this.quill.setSelection(a, u, w.sources.SILENT); + }), this.quill.update(A.sources.USER), this.quill.setSelection(a, u, A.sources.SILENT); } }; } -function Nr(r, t) { +function dr(r, t) { return { key: r, shiftKey: t, @@ -5570,20 +5564,20 @@ function Nr(r, t) { } = n; r === "ArrowRight" && (s += n.length + 1); const [i] = this.quill.getLeaf(s); - return i instanceof Lt ? (r === "ArrowLeft" ? t ? this.quill.setSelection(n.index - 1, n.length + 1, w.sources.USER) : this.quill.setSelection(n.index - 1, w.sources.USER) : t ? this.quill.setSelection(n.index, n.length + 1, w.sources.USER) : this.quill.setSelection(n.index + n.length + 1, w.sources.USER), !1) : !0; + return i instanceof Lt ? (r === "ArrowLeft" ? t ? this.quill.setSelection(n.index - 1, n.length + 1, A.sources.USER) : this.quill.setSelection(n.index - 1, A.sources.USER) : t ? this.quill.setSelection(n.index, n.length + 1, A.sources.USER) : this.quill.setSelection(n.index + n.length + 1, A.sources.USER), !1) : !0; } }; } -function Us(r) { +function Rs(r) { return { key: r[0], shortKey: !0, handler(t, e) { - this.quill.format(r, !e.format[r], w.sources.USER); + this.quill.format(r, !e.format[r], A.sources.USER); } }; } -function Tl(r) { +function cl(r) { return { key: r ? "ArrowUp" : "ArrowDown", collapsed: !0, @@ -5596,28 +5590,28 @@ function Tl(r) { for (; a.prev != null; ) a = a.prev, o = o.next; const u = o.offset(this.quill.scroll) + Math.min(e.offset, o.length() - 1); - this.quill.setSelection(u, 0, w.sources.USER); + this.quill.setSelection(u, 0, A.sources.USER); } } else { const o = s.table()[n]; - o != null && (r ? this.quill.setSelection(o.offset(this.quill.scroll) + o.length() - 1, 0, w.sources.USER) : this.quill.setSelection(o.offset(this.quill.scroll), 0, w.sources.USER)); + o != null && (r ? this.quill.setSelection(o.offset(this.quill.scroll) + o.length() - 1, 0, A.sources.USER) : this.quill.setSelection(o.offset(this.quill.scroll), 0, A.sources.USER)); } return !1; } }; } -function od(r) { +function Uf(r) { if (typeof r == "string" || typeof r == "number") r = { key: r }; else if (typeof r == "object") - r = nn(r); + r = Xe(r); else return null; - return r.shortKey && (r[id] = r.shortKey, delete r.shortKey), r; + return r.shortKey && (r[Pf] = r.shortKey, delete r.shortKey), r; } -function Li(r) { +function ui(r) { let { quill: t, range: e @@ -5626,85 +5620,79 @@ function Li(r) { let s = {}; if (n.length > 1) { const i = n[0].formats(), o = n[n.length - 1].formats(); - s = It.AttributeMap.diff(o, i) || {}; + s = Ot.AttributeMap.diff(o, i) || {}; } - t.deleteText(e, w.sources.USER), Object.keys(s).length > 0 && t.formatLine(e.index, 1, s, w.sources.USER), t.setSelection(e.index, w.sources.SILENT); + t.deleteText(e, A.sources.USER), Object.keys(s).length > 0 && t.formatLine(e.index, 1, s, A.sources.USER), t.setSelection(e.index, A.sources.SILENT); } -function ad(r, t, e, n) { +function Ff(r, t, e, n) { return t.prev == null && t.next == null ? e.prev == null && e.next == null ? n === 0 ? -1 : 1 : e.prev == null ? -1 : 1 : t.prev == null ? -1 : t.next == null ? 1 : null; } -const cd = /font-weight:\s*normal/, ud = ["P", "OL", "UL"], xl = (r) => r && ud.includes(r.tagName), hd = (r) => { - Array.from(r.querySelectorAll("br")).filter((t) => xl(t.previousElementSibling) && xl(t.nextElementSibling)).forEach((t) => { - var e; - (e = t.parentNode) == null || e.removeChild(t); +const Hf = /font-weight:\s*normal/, zf = ["P", "OL", "UL"], ul = (r) => r && zf.includes(r.tagName), Kf = (r) => { + Array.from(r.querySelectorAll("br")).filter((t) => ul(t.previousElementSibling) && ul(t.nextElementSibling)).forEach((t) => { + t.parentNode?.removeChild(t); }); -}, fd = (r) => { - Array.from(r.querySelectorAll('b[style*="font-weight"]')).filter((t) => { - var e; - return (e = t.getAttribute("style")) == null ? void 0 : e.match(cd); - }).forEach((t) => { - var n; +}, Gf = (r) => { + Array.from(r.querySelectorAll('b[style*="font-weight"]')).filter((t) => t.getAttribute("style")?.match(Hf)).forEach((t) => { const e = r.createDocumentFragment(); - e.append(...t.childNodes), (n = t.parentNode) == null || n.replaceChild(e, t); + e.append(...t.childNodes), t.parentNode?.replaceChild(e, t); }); }; -function dd(r) { - r.querySelector('[id^="docs-internal-guid-"]') && (fd(r), hd(r)); +function Vf(r) { + r.querySelector('[id^="docs-internal-guid-"]') && (Gf(r), Kf(r)); } -const gd = /\bmso-list:[^;]*ignore/i, pd = /\bmso-list:[^;]*\bl(\d+)/i, md = /\bmso-list:[^;]*\blevel(\d+)/i, bd = (r, t) => { - const e = r.getAttribute("style"), n = e == null ? void 0 : e.match(pd); +const Wf = /\bmso-list:[^;]*ignore/i, Zf = /\bmso-list:[^;]*\bl(\d+)/i, Yf = /\bmso-list:[^;]*\blevel(\d+)/i, Xf = (r, t) => { + const e = r.getAttribute("style"), n = e?.match(Zf); if (!n) return null; - const s = Number(n[1]), i = e == null ? void 0 : e.match(md), o = i ? Number(i[1]) : 1, a = new RegExp(`@list l${s}:level${o}\\s*\\{[^\\}]*mso-level-number-format:\\s*([\\w-]+)`, "i"), u = t.match(a), h = u && u[1] === "bullet" ? "bullet" : "ordered"; + const s = Number(n[1]), i = e?.match(Yf), o = i ? Number(i[1]) : 1, a = new RegExp(`@list l${s}:level${o}\\s*\\{[^\\}]*mso-level-number-format:\\s*([\\w-]+)`, "i"), u = t.match(a), h = u && u[1] === "bullet" ? "bullet" : "ordered"; return { id: s, indent: o, type: h, element: r }; -}, yd = (r) => { - var o, a; +}, Qf = (r) => { const t = Array.from(r.querySelectorAll("[style*=mso-list]")), e = [], n = []; - t.forEach((u) => { - (u.getAttribute("style") || "").match(gd) ? e.push(u) : n.push(u); - }), e.forEach((u) => { - var h; - return (h = u.parentNode) == null ? void 0 : h.removeChild(u); - }); - const s = r.documentElement.innerHTML, i = n.map((u) => bd(u, s)).filter((u) => u); + t.forEach((o) => { + (o.getAttribute("style") || "").match(Wf) ? e.push(o) : n.push(o); + }), e.forEach((o) => o.parentNode?.removeChild(o)); + const s = r.documentElement.innerHTML, i = n.map((o) => Xf(o, s)).filter((o) => o); for (; i.length; ) { - const u = []; - let h = i.shift(); - for (; h; ) - u.push(h), h = i.length && ((o = i[0]) == null ? void 0 : o.element) === h.element.nextElementSibling && // Different id means the next item doesn't belong to this group. - i[0].id === h.id ? i.shift() : null; - const p = document.createElement("ul"); - u.forEach((m) => { - const y = document.createElement("li"); - y.setAttribute("data-list", m.type), m.indent > 1 && y.setAttribute("class", `ql-indent-${m.indent - 1}`), y.innerHTML = m.element.innerHTML, p.appendChild(y); + const o = []; + let a = i.shift(); + for (; a; ) + o.push(a), a = i.length && i[0]?.element === a.element.nextElementSibling && // Different id means the next item doesn't belong to this group. + i[0].id === a.id ? i.shift() : null; + const u = document.createElement("ul"); + o.forEach((v) => { + const f = document.createElement("li"); + f.setAttribute("data-list", v.type), v.indent > 1 && f.setAttribute("class", `ql-indent-${v.indent - 1}`), f.innerHTML = v.element.innerHTML, u.appendChild(f); }); - const v = (a = u[0]) == null ? void 0 : a.element, { - parentNode: f - } = v ?? {}; - v && (f == null || f.replaceChild(p, v)), u.slice(1).forEach((m) => { + const h = o[0]?.element, { + parentNode: p + } = h ?? {}; + h && p?.replaceChild(u, h), o.slice(1).forEach((v) => { let { - element: y - } = m; - f == null || f.removeChild(y); + element: f + } = v; + p?.removeChild(f); }); } }; -function vd(r) { - r.documentElement.getAttribute("xmlns:w") === "urn:schemas-microsoft-com:office:word" && yd(r); +function Jf(r) { + r.documentElement.getAttribute("xmlns:w") === "urn:schemas-microsoft-com:office:word" && Qf(r); } -const Ed = [vd, dd], Ad = (r) => { - r.documentElement && Ed.forEach((t) => { +const td = [Jf, Vf], ed = (r) => { + r.documentElement && td.forEach((t) => { t(r); }); -}, wd = he("quill:clipboard"), Nd = [[Node.TEXT_NODE, Bd], [Node.TEXT_NODE, Sl], ["br", _d], [Node.ELEMENT_NODE, Sl], [Node.ELEMENT_NODE, Sd], [Node.ELEMENT_NODE, Ld], [Node.ELEMENT_NODE, Rd], ["li", Cd], ["ol, ul", Id], ["pre", qd], ["tr", kd], ["b", Fs("bold")], ["i", Fs("italic")], ["strike", Fs("strike")], ["style", Od]], Td = [td, No].reduce((r, t) => (r[t.keyName] = t, r), {}), Ll = [Ao, Ni, wi, xo, _o, Oo].reduce((r, t) => (r[t.keyName] = t, r), {}); -class Co extends zt { +}, nd = ie("quill:clipboard"), rd = [[Node.TEXT_NODE, md], [Node.TEXT_NODE, fl], ["br", cd], [Node.ELEMENT_NODE, fl], [Node.ELEMENT_NODE, ad], [Node.ELEMENT_NODE, od], [Node.ELEMENT_NODE, gd], ["li", fd], ["ol, ul", dd], ["pre", ud], ["tr", pd], ["b", Is("bold")], ["i", Is("italic")], ["strike", Is("strike")], ["style", hd]], sd = [kf, ao].reduce((r, t) => (r[t.keyName] = t, r), {}), hl = [lo, oi, li, uo, go, mo].reduce((r, t) => (r[t.keyName] = t, r), {}); +class id extends Yt { + static DEFAULTS = { + matchers: [] + }; constructor(t, e) { - super(t, e), this.quill.root.addEventListener("copy", (n) => this.onCaptureCopy(n, !1)), this.quill.root.addEventListener("cut", (n) => this.onCaptureCopy(n, !0)), this.quill.root.addEventListener("paste", this.onCapturePaste.bind(this)), this.matchers = [], Nd.concat(this.options.matchers ?? []).forEach((n) => { + super(t, e), this.quill.root.addEventListener("copy", (n) => this.onCaptureCopy(n, !1)), this.quill.root.addEventListener("cut", (n) => this.onCaptureCopy(n, !0)), this.quill.root.addEventListener("paste", this.onCapturePaste.bind(this)), this.matchers = [], rd.concat(this.options.matchers ?? []).forEach((n) => { let [s, i] = n; this.addMatcher(s, i); }); @@ -5717,43 +5705,42 @@ class Co extends zt { html: e, text: n } = t, s = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}; - if (s[At.blotName]) - return new B().insert(n || "", { - [At.blotName]: s[At.blotName] + if (s[wt.blotName]) + return new k().insert(n || "", { + [wt.blotName]: s[wt.blotName] }); if (!e) - return new B().insert(n || "", s); + return new k().insert(n || "", s); const i = this.convertHTML(e); - return Gn(i, ` -`) && (i.ops[i.ops.length - 1].attributes == null || s.table) ? i.compose(new B().retain(i.length() - 1).delete(1)) : i; + return Mn(i, ` +`) && (i.ops[i.ops.length - 1].attributes == null || s.table) ? i.compose(new k().retain(i.length() - 1).delete(1)) : i; } normalizeHTML(t) { - Ad(t); + ed(t); } convertHTML(t) { const e = new DOMParser().parseFromString(t, "text/html"); this.normalizeHTML(e); const n = e.body, s = /* @__PURE__ */ new WeakMap(), [i, o] = this.prepareMatching(n, s); - return Si(this.quill.scroll, n, i, o, s); + return hi(this.quill.scroll, n, i, o, s); } dangerouslyPasteHTML(t, e) { - let n = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : w.sources.API; + let n = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : A.sources.API; if (typeof t == "string") { const s = this.convert({ html: t, text: "" }); - this.quill.setContents(s, e), this.quill.setSelection(0, w.sources.SILENT); + this.quill.setContents(s, e), this.quill.setSelection(0, A.sources.SILENT); } else { const s = this.convert({ html: e, text: "" }); - this.quill.updateContents(new B().retain(t).concat(s), n), this.quill.setSelection(t + s.length(), w.sources.SILENT); + this.quill.updateContents(new k().retain(t).concat(s), n), this.quill.setSelection(t + s.length(), A.sources.SILENT); } } onCaptureCopy(t) { - var o, a; let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : !1; if (t.defaultPrevented) return; t.preventDefault(); @@ -5763,7 +5750,7 @@ class Co extends zt { html: s, text: i } = this.onCopy(n, e); - (o = t.clipboardData) == null || o.setData("text/plain", i), (a = t.clipboardData) == null || a.setData("text/html", s), e && Li({ + t.clipboardData?.setData("text/plain", i), t.clipboardData?.setData("text/html", s), e && ui({ range: n, quill: this.quill }); @@ -5776,25 +5763,24 @@ class Co extends zt { `); } onCapturePaste(t) { - var o, a, u, h, p; if (t.defaultPrevented || !this.quill.isEnabled()) return; t.preventDefault(); const e = this.quill.getSelection(!0); if (e == null) return; - const n = (o = t.clipboardData) == null ? void 0 : o.getData("text/html"); - let s = (a = t.clipboardData) == null ? void 0 : a.getData("text/plain"); + const n = t.clipboardData?.getData("text/html"); + let s = t.clipboardData?.getData("text/plain"); if (!n && !s) { - const v = (u = t.clipboardData) == null ? void 0 : u.getData("text/uri-list"); - v && (s = this.normalizeURIList(v)); + const o = t.clipboardData?.getData("text/uri-list"); + o && (s = this.normalizeURIList(o)); } - const i = Array.from(((h = t.clipboardData) == null ? void 0 : h.files) || []); + const i = Array.from(t.clipboardData?.files || []); if (!n && i.length > 0) { this.quill.uploader.upload(e, i); return; } if (n && i.length > 0) { - const v = new DOMParser().parseFromString(n, "text/html"); - if (v.body.childElementCount === 1 && ((p = v.body.firstElementChild) == null ? void 0 : p.tagName) === "IMG") { + const o = new DOMParser().parseFromString(n, "text/html"); + if (o.body.childElementCount === 1 && o.body.firstElementChild?.tagName === "IMG") { this.quill.uploader.upload(e, i); return; } @@ -5820,12 +5806,12 @@ class Co extends zt { text: n, html: s }, i); - wd.log("onPaste", o, { + nd.log("onPaste", o, { text: n, html: s }); - const a = new B().retain(t.index).delete(t.length).concat(o); - this.quill.updateContents(a, w.sources.USER), this.quill.setSelection(a.length() - t.length, w.sources.SILENT), this.quill.scrollSelectionIntoView(); + const a = new k().retain(t.index).delete(t.length).concat(o); + this.quill.updateContents(a, A.sources.USER), this.quill.setSelection(a.length() - t.length, A.sources.SILENT), this.quill.scrollSelectionIntoView(); } prepareMatching(t, e) { const n = [], s = []; @@ -5840,21 +5826,14 @@ class Co extends zt { break; default: Array.from(t.querySelectorAll(o)).forEach((u) => { - if (e.has(u)) { - const h = e.get(u); - h == null || h.push(a); - } else - e.set(u, [a]); + e.has(u) ? e.get(u)?.push(a) : e.set(u, [a]); }); break; } }), [n, s]; } } -O(Co, "DEFAULTS", { - matchers: [] -}); -function je(r, t, e, n) { +function ke(r, t, e, n) { return n.query(t) ? r.reduce((s, i) => { if (!i.insert) return s; if (i.attributes && i.attributes[t]) @@ -5866,9 +5845,9 @@ function je(r, t, e, n) { ...o, ...i.attributes }); - }, new B()) : r; + }, new k()) : r; } -function Gn(r, t) { +function Mn(r, t) { let e = ""; for (let n = r.ops.length - 1; n >= 0 && e.length < t.length; --n) { const s = r.ops[n]; @@ -5877,66 +5856,66 @@ function Gn(r, t) { } return e.slice(-1 * t.length) === t; } -function be(r, t) { +function he(r, t) { if (!(r instanceof Element)) return !1; const e = t.query(r); return e && e.prototype instanceof Lt ? !1 : ["address", "article", "blockquote", "canvas", "dd", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "iframe", "li", "main", "nav", "ol", "output", "p", "pre", "section", "table", "td", "tr", "ul", "video"].includes(r.tagName.toLowerCase()); } -function xd(r, t) { - return r.previousElementSibling && r.nextElementSibling && !be(r.previousElementSibling, t) && !be(r.nextElementSibling, t); +function ld(r, t) { + return r.previousElementSibling && r.nextElementSibling && !he(r.previousElementSibling, t) && !he(r.nextElementSibling, t); } -const Tr = /* @__PURE__ */ new WeakMap(); -function Io(r) { - return r == null ? !1 : (Tr.has(r) || (r.tagName === "PRE" ? Tr.set(r, !0) : Tr.set(r, Io(r.parentNode))), Tr.get(r)); +const gr = /* @__PURE__ */ new WeakMap(); +function bo(r) { + return r == null ? !1 : (gr.has(r) || (r.tagName === "PRE" ? gr.set(r, !0) : gr.set(r, bo(r.parentNode))), gr.get(r)); } -function Si(r, t, e, n, s) { - return t.nodeType === t.TEXT_NODE ? n.reduce((i, o) => o(t, i, r), new B()) : t.nodeType === t.ELEMENT_NODE ? Array.from(t.childNodes || []).reduce((i, o) => { - let a = Si(r, o, e, n, s); +function hi(r, t, e, n, s) { + return t.nodeType === t.TEXT_NODE ? n.reduce((i, o) => o(t, i, r), new k()) : t.nodeType === t.ELEMENT_NODE ? Array.from(t.childNodes || []).reduce((i, o) => { + let a = hi(r, o, e, n, s); return o.nodeType === t.ELEMENT_NODE && (a = e.reduce((u, h) => h(o, u, r), a), a = (s.get(o) || []).reduce((u, h) => h(o, u, r), a)), i.concat(a); - }, new B()) : new B(); + }, new k()) : new k(); } -function Fs(r) { - return (t, e, n) => je(e, r, !0, n); +function Is(r) { + return (t, e, n) => ke(e, r, !0, n); } -function Ld(r, t, e) { - const n = Jt.keys(r), s = Ft.keys(r), i = we.keys(r), o = {}; +function od(r, t, e) { + const n = Wt.keys(r), s = Ut.keys(r), i = me.keys(r), o = {}; return n.concat(s).concat(i).forEach((a) => { - let u = e.query(a, D.ATTRIBUTE); - u != null && (o[u.attrName] = u.value(r), o[u.attrName]) || (u = Td[a], u != null && (u.attrName === a || u.keyName === a) && (o[u.attrName] = u.value(r) || void 0), u = Ll[a], u != null && (u.attrName === a || u.keyName === a) && (u = Ll[a], o[u.attrName] = u.value(r) || void 0)); + let u = e.query(a, M.ATTRIBUTE); + u != null && (o[u.attrName] = u.value(r), o[u.attrName]) || (u = sd[a], u != null && (u.attrName === a || u.keyName === a) && (o[u.attrName] = u.value(r) || void 0), u = hl[a], u != null && (u.attrName === a || u.keyName === a) && (u = hl[a], o[u.attrName] = u.value(r) || void 0)); }), Object.entries(o).reduce((a, u) => { let [h, p] = u; - return je(a, h, p, e); + return ke(a, h, p, e); }, t); } -function Sd(r, t, e) { +function ad(r, t, e) { const n = e.query(r); if (n == null) return t; if (n.prototype instanceof Lt) { const s = {}, i = n.value(r); if (i != null) - return s[n.blotName] = i, new B().insert(s, n.formats(r, e)); - } else if (n.prototype instanceof Un && !Gn(t, ` + return s[n.blotName] = i, new k().insert(s, n.formats(r, e)); + } else if (n.prototype instanceof Rn && !Mn(t, ` `) && t.insert(` `), "blotName" in n && "formats" in n && typeof n.formats == "function") - return je(t, n.blotName, n.formats(r, e), e); + return ke(t, n.blotName, n.formats(r, e), e); return t; } -function _d(r, t) { - return Gn(t, ` +function cd(r, t) { + return Mn(t, ` `) || t.insert(` `), t; } -function qd(r, t, e) { +function ud(r, t, e) { const n = e.query("code-block"), s = n && "formats" in n && typeof n.formats == "function" ? n.formats(r, e) : !0; - return je(t, "code-block", s, e); + return ke(t, "code-block", s, e); } -function Od() { - return new B(); +function hd() { + return new k(); } -function Cd(r, t, e) { +function fd(r, t, e) { const n = e.query(r); if (n == null || // @ts-expect-error - n.blotName !== "list" || !Gn(t, ` + n.blotName !== "list" || !Mn(t, ` `)) return t; let s = -1, i = r.parentNode; @@ -5945,28 +5924,28 @@ function Cd(r, t, e) { return s <= 0 ? t : t.reduce((o, a) => a.insert ? a.attributes && typeof a.attributes.indent == "number" ? o.push(a) : o.insert(a.insert, { indent: s, ...a.attributes || {} - }) : o, new B()); + }) : o, new k()); } -function Id(r, t, e) { +function dd(r, t, e) { const n = r; let s = n.tagName === "OL" ? "ordered" : "bullet"; const i = n.getAttribute("data-checked"); - return i && (s = i === "true" ? "checked" : "unchecked"), je(t, "list", s, e); + return i && (s = i === "true" ? "checked" : "unchecked"), ke(t, "list", s, e); } -function Sl(r, t, e) { - if (!Gn(t, ` +function fl(r, t, e) { + if (!Mn(t, ` `)) { - if (be(r, e) && (r.childNodes.length > 0 || r instanceof HTMLParagraphElement)) + if (he(r, e) && (r.childNodes.length > 0 || r instanceof HTMLParagraphElement)) return t.insert(` `); if (t.length() > 0 && r.nextSibling) { let n = r.nextSibling; for (; n != null; ) { - if (be(n, e)) + if (he(n, e)) return t.insert(` `); const s = e.query(n); - if (s && s.prototype instanceof Ct) + if (s && s.prototype instanceof qt) return t.insert(` `); n = n.firstChild; @@ -5975,49 +5954,50 @@ function Sl(r, t, e) { } return t; } -function Rd(r, t, e) { - var i; +function gd(r, t, e) { const n = {}, s = r.style || {}; - return s.fontStyle === "italic" && (n.italic = !0), s.textDecoration === "underline" && (n.underline = !0), s.textDecoration === "line-through" && (n.strike = !0), ((i = s.fontWeight) != null && i.startsWith("bold") || // @ts-expect-error Fix me later - parseInt(s.fontWeight, 10) >= 700) && (n.bold = !0), t = Object.entries(n).reduce((o, a) => { - let [u, h] = a; - return je(o, u, h, e); - }, t), parseFloat(s.textIndent || 0) > 0 ? new B().insert(" ").concat(t) : t; -} -function kd(r, t, e) { - var s, i; - const n = ((s = r.parentElement) == null ? void 0 : s.tagName) === "TABLE" ? r.parentElement : (i = r.parentElement) == null ? void 0 : i.parentElement; + return s.fontStyle === "italic" && (n.italic = !0), s.textDecoration === "underline" && (n.underline = !0), s.textDecoration === "line-through" && (n.strike = !0), (s.fontWeight?.startsWith("bold") || // @ts-expect-error Fix me later + parseInt(s.fontWeight, 10) >= 700) && (n.bold = !0), t = Object.entries(n).reduce((i, o) => { + let [a, u] = o; + return ke(i, a, u, e); + }, t), parseFloat(s.textIndent || 0) > 0 ? new k().insert(" ").concat(t) : t; +} +function pd(r, t, e) { + const n = r.parentElement?.tagName === "TABLE" ? r.parentElement : r.parentElement?.parentElement; if (n != null) { - const a = Array.from(n.querySelectorAll("tr")).indexOf(r) + 1; - return je(t, "table", a, e); + const i = Array.from(n.querySelectorAll("tr")).indexOf(r) + 1; + return ke(t, "table", i, e); } return t; } -function Bd(r, t, e) { - var s; +function md(r, t, e) { let n = r.data; - if (((s = r.parentElement) == null ? void 0 : s.tagName) === "O:P") + if (r.parentElement?.tagName === "O:P") return t.insert(n.trim()); - if (!Io(r)) { + if (!bo(r)) { if (n.trim().length === 0 && n.includes(` -`) && !xd(r, e)) +`) && !ld(r, e)) return t; - n = n.replace(/[^\S\u00a0]/g, " "), n = n.replace(/ {2,}/g, " "), (r.previousSibling == null && r.parentElement != null && be(r.parentElement, e) || r.previousSibling instanceof Element && be(r.previousSibling, e)) && (n = n.replace(/^ /, "")), (r.nextSibling == null && r.parentElement != null && be(r.parentElement, e) || r.nextSibling instanceof Element && be(r.nextSibling, e)) && (n = n.replace(/ $/, "")), n = n.replaceAll(" ", " "); + n = n.replace(/[^\S\u00a0]/g, " "), n = n.replace(/ {2,}/g, " "), (r.previousSibling == null && r.parentElement != null && he(r.parentElement, e) || r.previousSibling instanceof Element && he(r.previousSibling, e)) && (n = n.replace(/^ /, "")), (r.nextSibling == null && r.parentElement != null && he(r.parentElement, e) || r.nextSibling instanceof Element && he(r.nextSibling, e)) && (n = n.replace(/ $/, "")), n = n.replaceAll(" ", " "); } return t.insert(n); } -class Ro extends zt { - constructor(e, n) { - super(e, n); - O(this, "lastRecorded", 0); - O(this, "ignoreChange", !1); - O(this, "stack", { - undo: [], - redo: [] - }); - O(this, "currentRange", null); - this.quill.on(w.events.EDITOR_CHANGE, (s, i, o, a) => { - s === w.events.SELECTION_CHANGE ? i && a !== w.sources.SILENT && (this.currentRange = i) : s === w.events.TEXT_CHANGE && (this.ignoreChange || (!this.options.userOnly || a === w.sources.USER ? this.record(i, o) : this.transform(i)), this.currentRange = si(this.currentRange, i)); +class bd extends Yt { + static DEFAULTS = { + delay: 1e3, + maxStack: 100, + userOnly: !1 + }; + lastRecorded = 0; + ignoreChange = !1; + stack = { + undo: [], + redo: [] + }; + currentRange = null; + constructor(t, e) { + super(t, e), this.quill.on(A.events.EDITOR_CHANGE, (n, s, i, o) => { + n === A.events.SELECTION_CHANGE ? s && o !== A.sources.SILENT && (this.currentRange = s) : n === A.events.TEXT_CHANGE && (this.ignoreChange || (!this.options.userOnly || o === A.sources.USER ? this.record(s, i) : this.transform(s)), this.currentRange = Zs(this.currentRange, s)); }), this.quill.keyboard.addBinding({ key: "z", shortKey: !0 @@ -6028,19 +6008,19 @@ class Ro extends zt { }, this.redo.bind(this)), /Win/i.test(navigator.platform) && this.quill.keyboard.addBinding({ key: "y", shortKey: !0 - }, this.redo.bind(this)), this.quill.root.addEventListener("beforeinput", (s) => { - s.inputType === "historyUndo" ? (this.undo(), s.preventDefault()) : s.inputType === "historyRedo" && (this.redo(), s.preventDefault()); + }, this.redo.bind(this)), this.quill.root.addEventListener("beforeinput", (n) => { + n.inputType === "historyUndo" ? (this.undo(), n.preventDefault()) : n.inputType === "historyRedo" && (this.redo(), n.preventDefault()); }); } - change(e, n) { - if (this.stack[e].length === 0) return; - const s = this.stack[e].pop(); - if (!s) return; - const i = this.quill.getContents(), o = s.delta.invert(i); - this.stack[n].push({ - delta: o, - range: si(s.range, o) - }), this.lastRecorded = 0, this.ignoreChange = !0, this.quill.updateContents(s.delta, w.sources.USER), this.ignoreChange = !1, this.restoreSelection(s); + change(t, e) { + if (this.stack[t].length === 0) return; + const n = this.stack[t].pop(); + if (!n) return; + const s = this.quill.getContents(), i = n.delta.invert(s); + this.stack[e].push({ + delta: i, + range: Zs(n.range, i) + }), this.lastRecorded = 0, this.ignoreChange = !0, this.quill.updateContents(n.delta, A.sources.USER), this.ignoreChange = !1, this.restoreSelection(n); } clear() { this.stack = { @@ -6051,68 +6031,63 @@ class Ro extends zt { cutoff() { this.lastRecorded = 0; } - record(e, n) { - if (e.ops.length === 0) return; + record(t, e) { + if (t.ops.length === 0) return; this.stack.redo = []; - let s = e.invert(n), i = this.currentRange; - const o = Date.now(); + let n = t.invert(e), s = this.currentRange; + const i = Date.now(); if ( // @ts-expect-error Fix me later - this.lastRecorded + this.options.delay > o && this.stack.undo.length > 0 + this.lastRecorded + this.options.delay > i && this.stack.undo.length > 0 ) { - const a = this.stack.undo.pop(); - a && (s = s.compose(a.delta), i = a.range); + const o = this.stack.undo.pop(); + o && (n = n.compose(o.delta), s = o.range); } else - this.lastRecorded = o; - s.length() !== 0 && (this.stack.undo.push({ - delta: s, - range: i + this.lastRecorded = i; + n.length() !== 0 && (this.stack.undo.push({ + delta: n, + range: s }), this.stack.undo.length > this.options.maxStack && this.stack.undo.shift()); } redo() { this.change("redo", "undo"); } - transform(e) { - _l(this.stack.undo, e), _l(this.stack.redo, e); + transform(t) { + dl(this.stack.undo, t), dl(this.stack.redo, t); } undo() { this.change("undo", "redo"); } - restoreSelection(e) { - if (e.range) - this.quill.setSelection(e.range, w.sources.USER); + restoreSelection(t) { + if (t.range) + this.quill.setSelection(t.range, A.sources.USER); else { - const n = Dd(this.quill.scroll, e.delta); - this.quill.setSelection(n, w.sources.USER); + const e = vd(this.quill.scroll, t.delta); + this.quill.setSelection(e, A.sources.USER); } } } -O(Ro, "DEFAULTS", { - delay: 1e3, - maxStack: 100, - userOnly: !1 -}); -function _l(r, t) { +function dl(r, t) { let e = t; for (let n = r.length - 1; n >= 0; n -= 1) { const s = r[n]; r[n] = { delta: e.transform(s.delta, !0), - range: s.range && si(s.range, e) + range: s.range && Zs(s.range, e) }, e = s.delta.transform(e), r[n].delta.length() === 0 && r.splice(n, 1); } } -function Md(r, t) { +function yd(r, t) { const e = t.ops[t.ops.length - 1]; return e == null ? !1 : e.insert != null ? typeof e.insert == "string" && e.insert.endsWith(` -`) : e.attributes != null ? Object.keys(e.attributes).some((n) => r.query(n, D.BLOCK) != null) : !1; +`) : e.attributes != null ? Object.keys(e.attributes).some((n) => r.query(n, M.BLOCK) != null) : !1; } -function Dd(r, t) { +function vd(r, t) { const e = t.reduce((s, i) => s + (i.delete || 0), 0); let n = t.length() - e; - return Md(r, t) && (n -= 1), n; + return yd(r, t) && (n -= 1), n; } -function si(r, t) { +function Zs(r, t) { if (!r) return r; const e = t.transformPosition(r.index), n = t.transformPosition(r.index + r.length); return { @@ -6120,34 +6095,32 @@ function si(r, t) { length: n - e }; } -class ko extends zt { +class yo extends Yt { constructor(t, e) { super(t, e), t.root.addEventListener("drop", (n) => { - var o; n.preventDefault(); let s = null; if (document.caretRangeFromPoint) s = document.caretRangeFromPoint(n.clientX, n.clientY); else if (document.caretPositionFromPoint) { - const a = document.caretPositionFromPoint(n.clientX, n.clientY); - s = document.createRange(), s.setStart(a.offsetNode, a.offset), s.setEnd(a.offsetNode, a.offset); + const o = document.caretPositionFromPoint(n.clientX, n.clientY); + s = document.createRange(), s.setStart(o.offsetNode, o.offset), s.setEnd(o.offsetNode, o.offset); } const i = s && t.selection.normalizeNative(s); if (i) { - const a = t.selection.normalizedToRange(i); - (o = n.dataTransfer) != null && o.files && this.upload(a, n.dataTransfer.files); + const o = t.selection.normalizedToRange(i); + n.dataTransfer?.files && this.upload(o, n.dataTransfer.files); } }); } upload(t, e) { const n = []; Array.from(e).forEach((s) => { - var i; - s && ((i = this.options.mimetypes) != null && i.includes(s.type)) && n.push(s); + s && this.options.mimetypes?.includes(s.type) && n.push(s); }), n.length > 0 && this.options.handler.call(this, t, n); } } -ko.DEFAULTS = { +yo.DEFAULTS = { mimetypes: ["image/png", "image/jpeg"], handler(r, t) { if (!this.quill.scroll.query("image")) @@ -6161,22 +6134,22 @@ ko.DEFAULTS = { Promise.all(e).then((n) => { const s = n.reduce((i, o) => i.insert({ image: o - }), new B().retain(r.index).delete(r.length)); + }), new k().retain(r.index).delete(r.length)); this.quill.updateContents(s, R.sources.USER), this.quill.setSelection(r.index + n.length, R.sources.SILENT); }); } }; -const jd = ["insertText", "insertReplacementText"]; -class $d extends zt { +const Ed = ["insertText", "insertReplacementText"]; +class Ad extends Yt { constructor(t, e) { super(t, e), t.root.addEventListener("beforeinput", (n) => { this.handleBeforeInput(n); - }), /Android/i.test(navigator.userAgent) || t.on(w.events.COMPOSITION_BEFORE_START, () => { + }), /Android/i.test(navigator.userAgent) || t.on(A.events.COMPOSITION_BEFORE_START, () => { this.handleCompositionStart(); }); } deleteRange(t) { - Li({ + ui({ range: t, quill: this.quill }); @@ -6186,18 +6159,18 @@ class $d extends zt { if (t.length === 0) return !1; if (e) { const n = this.quill.getFormat(t.index, 1); - this.deleteRange(t), this.quill.updateContents(new B().retain(t.index).insert(e, n), w.sources.USER); + this.deleteRange(t), this.quill.updateContents(new k().retain(t.index).insert(e, n), A.sources.USER); } else this.deleteRange(t); - return this.quill.setSelection(t.index + e.length, 0, w.sources.SILENT), !0; + return this.quill.setSelection(t.index + e.length, 0, A.sources.SILENT), !0; } handleBeforeInput(t) { - if (this.quill.composition.isComposing || t.defaultPrevented || !jd.includes(t.inputType)) + if (this.quill.composition.isComposing || t.defaultPrevented || !Ed.includes(t.inputType)) return; const e = t.getTargetRanges ? t.getTargetRanges()[0] : null; if (!e || e.collapsed === !0) return; - const n = Pd(t); + const n = wd(t); if (n == null) return; const s = this.quill.selection.normalizeNative(e), i = s ? this.quill.selection.normalizedToRange(s) : null; @@ -6208,39 +6181,37 @@ class $d extends zt { t && this.replaceText(t); } } -function Pd(r) { - var t; - return typeof r.data == "string" ? r.data : (t = r.dataTransfer) != null && t.types.includes("text/plain") ? r.dataTransfer.getData("text/plain") : null; +function wd(r) { + return typeof r.data == "string" ? r.data : r.dataTransfer?.types.includes("text/plain") ? r.dataTransfer.getData("text/plain") : null; } -const Ud = /Mac/i.test(navigator.platform), Fd = 100, Hd = (r) => !!(r.key === "ArrowLeft" || r.key === "ArrowRight" || // RTL scripts or moving from the end of the previous line -r.key === "ArrowUp" || r.key === "ArrowDown" || r.key === "Home" || Ud && r.key === "a" && r.ctrlKey === !0); -class zd extends zt { - constructor(e, n) { - super(e, n); - O(this, "isListening", !1); - O(this, "selectionChangeDeadline", 0); - this.handleArrowKeys(), this.handleNavigationShortcuts(); +const Nd = /Mac/i.test(navigator.platform), Td = 100, xd = (r) => !!(r.key === "ArrowLeft" || r.key === "ArrowRight" || // RTL scripts or moving from the end of the previous line +r.key === "ArrowUp" || r.key === "ArrowDown" || r.key === "Home" || Nd && r.key === "a" && r.ctrlKey === !0); +class Ld extends Yt { + isListening = !1; + selectionChangeDeadline = 0; + constructor(t, e) { + super(t, e), this.handleArrowKeys(), this.handleNavigationShortcuts(); } handleArrowKeys() { this.quill.keyboard.addBinding({ key: ["ArrowLeft", "ArrowRight"], offset: 0, shiftKey: null, - handler(e, n) { + handler(t, e) { let { - line: s, - event: i - } = n; - if (!(s instanceof Pt) || !s.uiNode) + line: n, + event: s + } = e; + if (!(n instanceof Pt) || !n.uiNode) return !0; - const o = getComputedStyle(s.domNode).direction === "rtl"; - return o && i.key !== "ArrowRight" || !o && i.key !== "ArrowLeft" ? !0 : (this.quill.setSelection(e.index - 1, e.length + (i.shiftKey ? 1 : 0), w.sources.USER), !1); + const i = getComputedStyle(n.domNode).direction === "rtl"; + return i && s.key !== "ArrowRight" || !i && s.key !== "ArrowLeft" ? !0 : (this.quill.setSelection(t.index - 1, t.length + (s.shiftKey ? 1 : 0), A.sources.USER), !1); } }); } handleNavigationShortcuts() { - this.quill.root.addEventListener("keydown", (e) => { - !e.defaultPrevented && Hd(e) && this.ensureListeningToSelectionChange(); + this.quill.root.addEventListener("keydown", (t) => { + !t.defaultPrevented && xd(t) && this.ensureListeningToSelectionChange(); }); } /** @@ -6250,44 +6221,44 @@ class zd extends zt { * the selection within the handler of a `selectionchange` event. */ ensureListeningToSelectionChange() { - if (this.selectionChangeDeadline = Date.now() + Fd, this.isListening) return; + if (this.selectionChangeDeadline = Date.now() + Td, this.isListening) return; this.isListening = !0; - const e = () => { + const t = () => { this.isListening = !1, Date.now() <= this.selectionChangeDeadline && this.handleSelectionChange(); }; - document.addEventListener("selectionchange", e, { + document.addEventListener("selectionchange", t, { once: !0 }); } handleSelectionChange() { - const e = document.getSelection(); - if (!e) return; - const n = e.getRangeAt(0); - if (n.collapsed !== !0 || n.startOffset !== 0) return; - const s = this.quill.scroll.find(n.startContainer); - if (!(s instanceof Pt) || !s.uiNode) return; - const i = document.createRange(); - i.setStartAfter(s.uiNode), i.setEndAfter(s.uiNode), e.removeAllRanges(), e.addRange(i); - } -} -w.register({ - "blots/block": pt, - "blots/block/embed": Ct, - "blots/break": Ht, - "blots/container": Me, - "blots/cursor": an, - "blots/embed": Ei, - "blots/inline": te, - "blots/scroll": en, - "blots/text": Ut, - "modules/clipboard": Co, - "modules/history": Ro, - "modules/keyboard": jr, - "modules/uploader": ko, - "modules/input": $d, - "modules/uiNode": zd + const t = document.getSelection(); + if (!t) return; + const e = t.getRangeAt(0); + if (e.collapsed !== !0 || e.startOffset !== 0) return; + const n = this.quill.scroll.find(e.startContainer); + if (!(n instanceof Pt) || !n.uiNode) return; + const s = document.createRange(); + s.setStartAfter(n.uiNode), s.setEndAfter(n.uiNode), t.removeAllRanges(), t.addRange(s); + } +} +A.register({ + "blots/block": gt, + "blots/block/embed": qt, + "blots/break": Ft, + "blots/container": Re, + "blots/cursor": Dt, + "blots/embed": si, + "blots/inline": At, + "blots/scroll": If, + "blots/text": $t, + "modules/clipboard": id, + "modules/history": bd, + "modules/keyboard": Cr, + "modules/uploader": yo, + "modules/input": Ad, + "modules/uiNode": Ld }); -class Kd extends Ft { +class Sd extends Ut { add(t, e) { let n = 0; if (e === "+1" || e === "-1") { @@ -6303,25 +6274,27 @@ class Kd extends Ft { return parseInt(super.value(t), 10) || void 0; } } -const Gd = new Kd("indent", "ql-indent", { - scope: D.BLOCK, +const _d = new Sd("indent", "ql-indent", { + scope: M.BLOCK, // @ts-expect-error whitelist: [1, 2, 3, 4, 5, 6, 7, 8] }); -class ii extends pt { +class Cd extends gt { + static blotName = "blockquote"; + static tagName = "blockquote"; } -O(ii, "blotName", "blockquote"), O(ii, "tagName", "blockquote"); -class li extends pt { +class qd extends gt { + static blotName = "header"; + static tagName = ["H1", "H2", "H3", "H4", "H5", "H6"]; static formats(t) { return this.tagName.indexOf(t.tagName) + 1; } } -O(li, "blotName", "header"), O(li, "tagName", ["H1", "H2", "H3", "H4", "H5", "H6"]); -class Vn extends Me { +class Dn extends Re { } -Vn.blotName = "list-container"; -Vn.tagName = "OL"; -class Wn extends pt { +Dn.blotName = "list-container"; +Dn.tagName = "OL"; +class jn extends gt { static create(t) { const e = super.create(); return e.setAttribute("data-list", t), e; @@ -6330,7 +6303,7 @@ class Wn extends pt { return t.getAttribute("data-list") || void 0; } static register() { - w.register(Vn); + A.register(Dn); } constructor(t, e) { super(t, e); @@ -6345,11 +6318,13 @@ class Wn extends pt { t === this.statics.blotName && e ? this.domNode.setAttribute("data-list", e) : super.format(t, e); } } -Wn.blotName = "list"; -Wn.tagName = "LI"; -Vn.allowedChildren = [Wn]; -Wn.requiredContainer = Vn; -class Hn extends te { +jn.blotName = "list"; +jn.tagName = "LI"; +Dn.allowedChildren = [jn]; +jn.requiredContainer = Dn; +class fi extends At { + static blotName = "bold"; + static tagName = ["STRONG", "B"]; static create() { return super.create(); } @@ -6360,11 +6335,15 @@ class Hn extends te { super.optimize(t), this.domNode.tagName !== this.statics.tagName[0] && this.replaceWith(this.statics.blotName); } } -O(Hn, "blotName", "bold"), O(Hn, "tagName", ["STRONG", "B"]); -class oi extends Hn { +class Od extends fi { + static blotName = "italic"; + static tagName = ["EM", "I"]; } -O(oi, "blotName", "italic"), O(oi, "tagName", ["EM", "I"]); -class ye extends te { +class Ar extends At { + static blotName = "link"; + static tagName = "A"; + static SANITIZED_URL = "about:blank"; + static PROTOCOL_WHITELIST = ["http", "https", "mailto", "tel", "sms"]; static create(t) { const e = super.create(t); return e.setAttribute("href", this.sanitize(t)), e.setAttribute("rel", "noopener noreferrer"), e.setAttribute("target", "_blank"), e; @@ -6373,20 +6352,21 @@ class ye extends te { return t.getAttribute("href"); } static sanitize(t) { - return Bo(t, this.PROTOCOL_WHITELIST) ? t : this.SANITIZED_URL; + return vo(t, this.PROTOCOL_WHITELIST) ? t : this.SANITIZED_URL; } format(t, e) { t !== this.statics.blotName || !e ? super.format(t, e) : this.domNode.setAttribute("href", this.constructor.sanitize(e)); } } -O(ye, "blotName", "link"), O(ye, "tagName", "A"), O(ye, "SANITIZED_URL", "about:blank"), O(ye, "PROTOCOL_WHITELIST", ["http", "https", "mailto", "tel", "sms"]); -function Bo(r, t) { +function vo(r, t) { const e = document.createElement("a"); e.href = r; const n = e.href.slice(0, e.href.indexOf(":")); return t.indexOf(n) > -1; } -class ai extends te { +class Rd extends At { + static blotName = "script"; + static tagName = ["SUB", "SUP"]; static create(t) { return t === "super" ? document.createElement("sup") : t === "sub" ? document.createElement("sub") : super.create(t); } @@ -6395,14 +6375,18 @@ class ai extends te { if (t.tagName === "SUP") return "super"; } } -O(ai, "blotName", "script"), O(ai, "tagName", ["SUB", "SUP"]); -class ci extends Hn { +class Id extends fi { + static blotName = "strike"; + static tagName = ["S", "STRIKE"]; } -O(ci, "blotName", "strike"), O(ci, "tagName", ["S", "STRIKE"]); -class ui extends te { +class kd extends At { + static blotName = "underline"; + static tagName = "U"; } -O(ui, "blotName", "underline"), O(ui, "tagName", "U"); -class Lr extends Ei { +class Bd extends si { + static blotName = "formula"; + static className = "ql-formula"; + static tagName = "SPAN"; static create(t) { if (window.katex == null) throw new Error("Formula module requires KaTeX."); @@ -6422,47 +6406,50 @@ class Lr extends Ei { return `${t}`; } } -O(Lr, "blotName", "formula"), O(Lr, "className", "ql-formula"), O(Lr, "tagName", "SPAN"); -const ql = ["alt", "height", "width"]; -class hi extends Lt { +const gl = ["alt", "height", "width"]; +class Md extends Lt { + static blotName = "image"; + static tagName = "IMG"; static create(t) { const e = super.create(t); return typeof t == "string" && e.setAttribute("src", this.sanitize(t)), e; } static formats(t) { - return ql.reduce((e, n) => (t.hasAttribute(n) && (e[n] = t.getAttribute(n)), e), {}); + return gl.reduce((e, n) => (t.hasAttribute(n) && (e[n] = t.getAttribute(n)), e), {}); } static match(t) { return /\.(jpe?g|gif|png)$/.test(t) || /^data:image\/.+;base64/.test(t); } static sanitize(t) { - return Bo(t, ["http", "https", "data"]) ? t : "//:0"; + return vo(t, ["http", "https", "data"]) ? t : "//:0"; } static value(t) { return t.getAttribute("src"); } format(t, e) { - ql.indexOf(t) > -1 ? e ? this.domNode.setAttribute(t, e) : this.domNode.removeAttribute(t) : super.format(t, e); + gl.indexOf(t) > -1 ? e ? this.domNode.setAttribute(t, e) : this.domNode.removeAttribute(t) : super.format(t, e); } } -O(hi, "blotName", "image"), O(hi, "tagName", "IMG"); -const Ol = ["height", "width"]; -class Sr extends Ct { +const pl = ["height", "width"]; +class Dd extends qt { + static blotName = "video"; + static className = "ql-video"; + static tagName = "IFRAME"; static create(t) { const e = super.create(t); return e.setAttribute("frameborder", "0"), e.setAttribute("allowfullscreen", "true"), e.setAttribute("src", this.sanitize(t)), e; } static formats(t) { - return Ol.reduce((e, n) => (t.hasAttribute(n) && (e[n] = t.getAttribute(n)), e), {}); + return pl.reduce((e, n) => (t.hasAttribute(n) && (e[n] = t.getAttribute(n)), e), {}); } static sanitize(t) { - return ye.sanitize(t); + return Ar.sanitize(t); } static value(t) { return t.getAttribute("src"); } format(t, e) { - Ol.indexOf(t) > -1 ? e ? this.domNode.setAttribute(t, e) : this.domNode.removeAttribute(t) : super.format(t, e); + pl.indexOf(t) > -1 ? e ? this.domNode.setAttribute(t, e) : this.domNode.removeAttribute(t) : super.format(t, e); } html() { const { @@ -6471,31 +6458,30 @@ class Sr extends Ct { return `
    ${t}`; } } -O(Sr, "blotName", "video"), O(Sr, "className", "ql-video"), O(Sr, "tagName", "IFRAME"); -const Rn = new Ft("code-token", "hljs", { - scope: D.INLINE +const Ln = new Ut("code-token", "hljs", { + scope: M.INLINE }); -class ae extends te { +class re extends At { static formats(t, e) { for (; t != null && t !== e.domNode; ) { - if (t.classList && t.classList.contains(At.className)) + if (t.classList && t.classList.contains(wt.className)) return super.formats(t, e); t = t.parentNode; } } constructor(t, e, n) { - super(t, e, n), Rn.add(this.domNode, n); + super(t, e, n), Ln.add(this.domNode, n); } format(t, e) { - t !== ae.blotName ? super.format(t, e) : e ? Rn.add(this.domNode, e) : (Rn.remove(this.domNode), this.domNode.classList.remove(this.statics.className)); + t !== re.blotName ? super.format(t, e) : e ? Ln.add(this.domNode, e) : (Ln.remove(this.domNode), this.domNode.classList.remove(this.statics.className)); } optimize() { - super.optimize(...arguments), Rn.value(this.domNode) || this.unwrap(); + super.optimize(...arguments), Ln.value(this.domNode) || this.unwrap(); } } -ae.blotName = "code-token"; -ae.className = "ql-token"; -class Ot extends At { +re.blotName = "code-token"; +re.className = "ql-token"; +class Ct extends wt { static create(t) { const e = super.create(t); return typeof t == "string" && e.setAttribute("data-language", t), e; @@ -6510,37 +6496,37 @@ class Ot extends At { t === this.statics.blotName && e ? this.domNode.setAttribute("data-language", e) : super.format(t, e); } replaceWith(t, e) { - return this.formatAt(0, this.length(), ae.blotName, !1), super.replaceWith(t, e); + return this.formatAt(0, this.length(), re.blotName, !1), super.replaceWith(t, e); } } -class Bn extends De { +class _n extends Ie { attach() { super.attach(), this.forceNext = !1, this.scroll.emitMount(this); } format(t, e) { - t === Ot.blotName && (this.forceNext = !0, this.children.forEach((n) => { + t === Ct.blotName && (this.forceNext = !0, this.children.forEach((n) => { n.format(t, e); })); } formatAt(t, e, n, s) { - n === Ot.blotName && (this.forceNext = !0), super.formatAt(t, e, n, s); + n === Ct.blotName && (this.forceNext = !0), super.formatAt(t, e, n, s); } highlight(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : !1; if (this.children.head == null) return; const s = `${Array.from(this.domNode.childNodes).filter((o) => o !== this.uiNode).map((o) => o.textContent).join(` `)} -`, i = Ot.formats(this.children.head.domNode); +`, i = Ct.formats(this.children.head.domNode); if (e || this.forceNext || this.cachedText !== s) { if (s.trim().length > 0 || this.cachedText == null) { - const o = this.children.reduce((u, h) => u.concat(yo(h, !1)), new B()), a = t(s, i); + const o = this.children.reduce((u, h) => u.concat(ro(h, !1)), new k()), a = t(s, i); o.diff(a).reduce((u, h) => { let { retain: p, attributes: v } = h; return p ? (v && Object.keys(v).forEach((f) => { - [Ot.blotName, ae.blotName].includes(f) && this.formatAt(u, p, f, v[f]); + [Ct.blotName, re.blotName].includes(f) && this.formatAt(u, p, f, v[f]); }), u + p) : u; }, 0); } @@ -6549,21 +6535,21 @@ class Bn extends De { } html(t, e) { const [n] = this.children.find(t); - return `
    -${Dr(this.code(t, e))}
    +    return `
    +${_r(this.code(t, e))}
     
    `; } optimize(t) { if (super.optimize(t), this.parent != null && this.children.head != null && this.uiNode != null) { - const e = Ot.formats(this.children.head.domNode); + const e = Ct.formats(this.children.head.domNode); e !== this.uiNode.value && (this.uiNode.value = e); } } } -Bn.allowedChildren = [Ot]; -Ot.requiredContainer = Bn; -Ot.allowedChildren = [ae, an, Ut, Ht]; -const Vd = (r, t, e) => { +_n.allowedChildren = [Ct]; +Ct.requiredContainer = _n; +Ct.allowedChildren = [re, Dt, $t, Ft]; +const jd = (r, t, e) => { if (typeof r.versionString == "string") { const n = r.versionString.split(".")[0]; if (parseInt(n, 10) >= 11) @@ -6573,9 +6559,9 @@ const Vd = (r, t, e) => { } return r.highlight(t, e).value; }; -class Mo extends zt { +class Eo extends Yt { static register() { - w.register(ae, !0), w.register(Ot, !0), w.register(Bn, !0); + A.register(re, !0), A.register(Ct, !0), A.register(_n, !0); } constructor(t, e) { if (super(t, e), this.options.hljs == null) @@ -6588,8 +6574,8 @@ class Mo extends zt { }, {}), this.highlightBlot = this.highlightBlot.bind(this), this.initListener(), this.initTimer(); } initListener() { - this.quill.on(w.events.SCROLL_BLOT_MOUNT, (t) => { - if (!(t instanceof Bn)) return; + this.quill.on(A.events.SCROLL_BLOT_MOUNT, (t) => { + if (!(t instanceof _n)) return; const e = this.quill.root.ownerDocument.createElement("select"); this.options.languages.forEach((n) => { let { @@ -6599,13 +6585,13 @@ class Mo extends zt { const o = e.ownerDocument.createElement("option"); o.textContent = i, o.setAttribute("value", s), e.appendChild(o); }), e.addEventListener("change", () => { - t.format(Ot.blotName, e.value), this.quill.root.focus(), this.highlight(t, !0); - }), t.uiNode == null && (t.attachUI(e), t.children.head && (e.value = Ot.formats(t.children.head.domNode))); + t.format(Ct.blotName, e.value), this.quill.root.focus(), this.highlight(t, !0); + }), t.uiNode == null && (t.attachUI(e), t.children.head && (e.value = Ct.formats(t.children.head.domNode))); }); } initTimer() { let t = null; - this.quill.on(w.events.SCROLL_OPTIMIZE, () => { + this.quill.on(A.events.SCROLL_OPTIMIZE, () => { t && clearTimeout(t), t = setTimeout(() => { this.highlight(), t = null; }, this.options.interval); @@ -6614,34 +6600,34 @@ class Mo extends zt { highlight() { let t = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : null, e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : !1; if (this.quill.selection.composing) return; - this.quill.update(w.sources.USER); + this.quill.update(A.sources.USER); const n = this.quill.getSelection(); - (t == null ? this.quill.scroll.descendants(Bn) : [t]).forEach((i) => { + (t == null ? this.quill.scroll.descendants(_n) : [t]).forEach((i) => { i.highlight(this.highlightBlot, e); - }), this.quill.update(w.sources.SILENT), n != null && this.quill.setSelection(n, w.sources.SILENT); + }), this.quill.update(A.sources.SILENT), n != null && this.quill.setSelection(n, A.sources.SILENT); } highlightBlot(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : "plain"; if (e = this.languages[e] ? e : "plain", e === "plain") - return Dr(t).split(` + return _r(t).split(` `).reduce((s, i, o) => (o !== 0 && s.insert(` `, { - [At.blotName]: e - }), s.insert(i)), new B()); + [wt.blotName]: e + }), s.insert(i)), new k()); const n = this.quill.root.ownerDocument.createElement("div"); - return n.classList.add(At.className), n.innerHTML = Vd(this.options.hljs, e, t), Si(this.quill.scroll, n, [(s, i) => { - const o = Rn.value(s); - return o ? i.compose(new B().retain(i.length(), { - [ae.blotName]: o + return n.classList.add(wt.className), n.innerHTML = jd(this.options.hljs, e, t), hi(this.quill.scroll, n, [(s, i) => { + const o = Ln.value(s); + return o ? i.compose(new k().retain(i.length(), { + [re.blotName]: o })) : i; }], [(s, i) => s.data.split(` `).reduce((o, a, u) => (u !== 0 && o.insert(` `, { - [At.blotName]: e + [wt.blotName]: e }), o.insert(a)), i)], /* @__PURE__ */ new WeakMap()); } } -Mo.DEFAULTS = { +Eo.DEFAULTS = { hljs: window.hljs, interval: 1e3, languages: [{ @@ -6688,10 +6674,12 @@ Mo.DEFAULTS = { label: "SQL" }] }; -const Dn = class Dn extends pt { +class jt extends gt { + static blotName = "table"; + static tagName = "TD"; static create(t) { const e = super.create(); - return t ? e.setAttribute("data-row", t) : e.setAttribute("data-row", _i()), e; + return t ? e.setAttribute("data-row", t) : e.setAttribute("data-row", di()), e; } static formats(t) { if (t.hasAttribute("data-row")) @@ -6701,7 +6689,7 @@ const Dn = class Dn extends pt { return this.parent ? this.parent.children.indexOf(this) : -1; } format(t, e) { - t === Dn.blotName && e ? this.domNode.setAttribute("data-row", e) : super.format(t, e); + t === jt.blotName && e ? this.domNode.setAttribute("data-row", e) : super.format(t, e); } row() { return this.parent; @@ -6712,10 +6700,10 @@ const Dn = class Dn extends pt { table() { return this.row() && this.row().table(); } -}; -O(Dn, "blotName", "table"), O(Dn, "tagName", "TD"); -let $t = Dn; -class ce extends Me { +} +class _e extends Re { + static blotName = "table-row"; + static tagName = "TR"; checkMerge() { if (super.checkMerge() && this.next.children.head != null) { const t = this.children.head.formats(), e = this.children.tail.formats(), n = this.next.children.head.formats(), s = this.next.children.tail.formats(); @@ -6740,18 +6728,20 @@ class ce extends Me { return this.parent && this.parent.parent; } } -O(ce, "blotName", "table-row"), O(ce, "tagName", "TR"); -class Qt extends Me { +class de extends Re { + static blotName = "table-body"; + static tagName = "TBODY"; } -O(Qt, "blotName", "table-body"), O(Qt, "tagName", "TBODY"); -class un extends Me { +class wr extends Re { + static blotName = "table-container"; + static tagName = "TABLE"; balanceCells() { - const t = this.descendants(ce), e = t.reduce((n, s) => Math.max(s.children.length, n), 0); + const t = this.descendants(_e), e = t.reduce((n, s) => Math.max(s.children.length, n), 0); t.forEach((n) => { new Array(e - n.children.length).fill(0).forEach(() => { let s; - n.children.head != null && (s = $t.formats(n.children.head.domNode)); - const i = this.scroll.create($t.blotName, s); + n.children.head != null && (s = jt.formats(n.children.head.domNode)); + const i = this.scroll.create(jt.blotName, s); n.appendChild(i), i.optimize(); }); }); @@ -6760,25 +6750,25 @@ class un extends Me { return this.rows().map((e) => e.children.at(t)); } deleteColumn(t) { - const [e] = this.descendant(Qt); + const [e] = this.descendant(de); e == null || e.children.head == null || e.children.forEach((n) => { const s = n.children.at(t); - s != null && s.remove(); + s?.remove(); }); } insertColumn(t) { - const [e] = this.descendant(Qt); + const [e] = this.descendant(de); e == null || e.children.head == null || e.children.forEach((n) => { - const s = n.children.at(t), i = $t.formats(n.children.head.domNode), o = this.scroll.create($t.blotName, i); + const s = n.children.at(t), i = jt.formats(n.children.head.domNode), o = this.scroll.create(jt.blotName, i); n.insertBefore(o, s); }); } insertRow(t) { - const [e] = this.descendant(Qt); + const [e] = this.descendant(de); if (e == null || e.children.head == null) return; - const n = _i(), s = this.scroll.create(ce.blotName); + const n = di(), s = this.scroll.create(_e.blotName); e.children.head.children.forEach(() => { - const o = this.scroll.create($t.blotName, n); + const o = this.scroll.create(jt.blotName, n); s.appendChild(o); }); const i = e.children.at(t); @@ -6789,47 +6779,46 @@ class un extends Me { return t == null ? [] : t.children.map((e) => e); } } -O(un, "blotName", "table-container"), O(un, "tagName", "TABLE"); -un.allowedChildren = [Qt]; -Qt.requiredContainer = un; -Qt.allowedChildren = [ce]; -ce.requiredContainer = Qt; -ce.allowedChildren = [$t]; -$t.requiredContainer = ce; -function _i() { +wr.allowedChildren = [de]; +de.requiredContainer = wr; +de.allowedChildren = [_e]; +_e.requiredContainer = de; +_e.allowedChildren = [jt]; +jt.requiredContainer = _e; +function di() { return `row-${Math.random().toString(36).slice(2, 6)}`; } -class Wd extends zt { +class Pd extends Yt { static register() { - w.register($t), w.register(ce), w.register(Qt), w.register(un); + A.register(jt), A.register(_e), A.register(de), A.register(wr); } constructor() { super(...arguments), this.listenBalanceCells(); } balanceTables() { - this.quill.scroll.descendants(un).forEach((t) => { + this.quill.scroll.descendants(wr).forEach((t) => { t.balanceCells(); }); } deleteColumn() { const [t, , e] = this.getTable(); - e != null && (t.deleteColumn(e.cellOffset()), this.quill.update(w.sources.USER)); + e != null && (t.deleteColumn(e.cellOffset()), this.quill.update(A.sources.USER)); } deleteRow() { const [, t] = this.getTable(); - t != null && (t.remove(), this.quill.update(w.sources.USER)); + t != null && (t.remove(), this.quill.update(A.sources.USER)); } deleteTable() { const [t] = this.getTable(); if (t == null) return; const e = t.offset(); - t.remove(), this.quill.update(w.sources.USER), this.quill.setSelection(e, w.sources.SILENT); + t.remove(), this.quill.update(A.sources.USER), this.quill.setSelection(e, A.sources.SILENT); } getTable() { let t = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : this.quill.getSelection(); if (t == null) return [null, null, null, -1]; const [e, n] = this.quill.getLine(t.index); - if (e == null || e.statics.blotName !== $t.blotName) + if (e == null || e.statics.blotName !== jt.blotName) return [null, null, null, -1]; const s = e.parent; return [s.parent.parent, s, e, n]; @@ -6840,9 +6829,9 @@ class Wd extends zt { const [n, s, i] = this.getTable(e); if (i == null) return; const o = i.cellOffset(); - n.insertColumn(o + t), this.quill.update(w.sources.USER); + n.insertColumn(o + t), this.quill.update(A.sources.USER); let a = s.rowOffset(); - t === 0 && (a += 1), this.quill.setSelection(e.index + a, e.length, w.sources.SILENT); + t === 0 && (a += 1), this.quill.setSelection(e.index + a, e.length, A.sources.SILENT); } insertColumnLeft() { this.insertColumn(0); @@ -6856,7 +6845,7 @@ class Wd extends zt { const [n, s, i] = this.getTable(e); if (i == null) return; const o = s.rowOffset(); - n.insertRow(o + t), this.quill.update(w.sources.USER), t > 0 ? this.quill.setSelection(e, w.sources.SILENT) : this.quill.setSelection(e.index + s.children.length, e.length, w.sources.SILENT); + n.insertRow(o + t), this.quill.update(A.sources.USER), t > 0 ? this.quill.setSelection(e, A.sources.SILENT) : this.quill.setSelection(e.index + s.children.length, e.length, A.sources.SILENT); } insertRowAbove() { this.insertRow(0); @@ -6871,40 +6860,38 @@ class Wd extends zt { const o = new Array(e).fill(` `).join(""); return i.insert(o, { - table: _i() + table: di() }); - }, new B().retain(n.index)); - this.quill.updateContents(s, w.sources.USER), this.quill.setSelection(n.index, w.sources.SILENT), this.balanceTables(); + }, new k().retain(n.index)); + this.quill.updateContents(s, A.sources.USER), this.quill.setSelection(n.index, A.sources.SILENT), this.balanceTables(); } listenBalanceCells() { - this.quill.on(w.events.SCROLL_OPTIMIZE, (t) => { - t.some((e) => ["TD", "TR", "TBODY", "TABLE"].includes(e.target.tagName) ? (this.quill.once(w.events.TEXT_CHANGE, (n, s, i) => { - i === w.sources.USER && this.balanceTables(); + this.quill.on(A.events.SCROLL_OPTIMIZE, (t) => { + t.some((e) => ["TD", "TR", "TBODY", "TABLE"].includes(e.target.tagName) ? (this.quill.once(A.events.TEXT_CHANGE, (n, s, i) => { + i === A.sources.USER && this.balanceTables(); }), !0) : !1); }); } } -const Cl = he("quill:toolbar"); -class qi extends zt { +const ml = ie("quill:toolbar"); +class gi extends Yt { constructor(t, e) { - var n, s; if (super(t, e), Array.isArray(this.options.container)) { - const i = document.createElement("div"); - i.setAttribute("role", "toolbar"), Zd(i, this.options.container), (s = (n = t.container) == null ? void 0 : n.parentNode) == null || s.insertBefore(i, t.container), this.container = i; + const n = document.createElement("div"); + n.setAttribute("role", "toolbar"), $d(n, this.options.container), t.container?.parentNode?.insertBefore(n, t.container), this.container = n; } else typeof this.options.container == "string" ? this.container = document.querySelector(this.options.container) : this.container = this.options.container; if (!(this.container instanceof HTMLElement)) { - Cl.error("Container required for toolbar", this.options); + ml.error("Container required for toolbar", this.options); return; } - this.container.classList.add("ql-toolbar"), this.controls = [], this.handlers = {}, this.options.handlers && Object.keys(this.options.handlers).forEach((i) => { - var a; - const o = (a = this.options.handlers) == null ? void 0 : a[i]; - o && this.addHandler(i, o); - }), Array.from(this.container.querySelectorAll("button, select")).forEach((i) => { - this.attach(i); - }), this.quill.on(w.events.EDITOR_CHANGE, () => { - const [i] = this.quill.selection.getRange(); - this.update(i); + this.container.classList.add("ql-toolbar"), this.controls = [], this.handlers = {}, this.options.handlers && Object.keys(this.options.handlers).forEach((n) => { + const s = this.options.handlers?.[n]; + s && this.addHandler(n, s); + }), Array.from(this.container.querySelectorAll("button, select")).forEach((n) => { + this.attach(n); + }), this.quill.on(A.events.EDITOR_CHANGE, () => { + const [n] = this.quill.selection.getRange(); + this.update(n); }); } addHandler(t, e) { @@ -6914,7 +6901,7 @@ class qi extends zt { let e = Array.from(t.classList).find((s) => s.indexOf("ql-") === 0); if (!e) return; if (e = e.slice(3), t.tagName === "BUTTON" && t.setAttribute("type", "button"), this.handlers[e] == null && this.quill.scroll.query(e) == null) { - Cl.warn("ignoring attaching to nonexistent format", e, t); + ml.warn("ignoring attaching to nonexistent format", e, t); return; } const n = t.tagName === "SELECT" ? "change" : "click"; @@ -6935,11 +6922,11 @@ class qi extends zt { this.quill.scroll.query(e).prototype instanceof Lt ) { if (i = prompt(`Enter ${e}`), !i) return; - this.quill.updateContents(new B().retain(o.index).delete(o.length).insert({ + this.quill.updateContents(new k().retain(o.index).delete(o.length).insert({ [e]: i - }), w.sources.USER); + }), A.sources.USER); } else - this.quill.format(e, i, w.sources.USER); + this.quill.format(e, i, A.sources.USER); this.update(o); }), this.controls.push([e, t]); } @@ -6970,32 +6957,32 @@ class qi extends zt { }); } } -qi.DEFAULTS = {}; -function Il(r, t, e) { +gi.DEFAULTS = {}; +function bl(r, t, e) { const n = document.createElement("button"); n.setAttribute("type", "button"), n.classList.add(`ql-${t}`), n.setAttribute("aria-pressed", "false"), e != null ? (n.value = e, n.setAttribute("aria-label", `${t}: ${e}`)) : n.setAttribute("aria-label", t), r.appendChild(n); } -function Zd(r, t) { +function $d(r, t) { Array.isArray(t[0]) || (t = [t]), t.forEach((e) => { const n = document.createElement("span"); n.classList.add("ql-formats"), e.forEach((s) => { if (typeof s == "string") - Il(n, s); + bl(n, s); else { const i = Object.keys(s)[0], o = s[i]; - Array.isArray(o) ? Yd(n, i, o) : Il(n, i, o); + Array.isArray(o) ? Ud(n, i, o) : bl(n, i, o); } }), r.appendChild(n); }); } -function Yd(r, t, e) { +function Ud(r, t, e) { const n = document.createElement("select"); n.classList.add(`ql-${t}`), e.forEach((s) => { const i = document.createElement("option"); s !== !1 ? i.setAttribute("value", String(s)) : i.setAttribute("selected", "selected"), n.appendChild(i); }), r.appendChild(n); } -qi.DEFAULTS = { +gi.DEFAULTS = { container: null, handlers: { clean() { @@ -7004,86 +6991,86 @@ qi.DEFAULTS = { if (r.length === 0) { const t = this.quill.getFormat(); Object.keys(t).forEach((e) => { - this.quill.scroll.query(e, D.INLINE) != null && this.quill.format(e, !1, w.sources.USER); + this.quill.scroll.query(e, M.INLINE) != null && this.quill.format(e, !1, A.sources.USER); }); } else - this.quill.removeFormat(r.index, r.length, w.sources.USER); + this.quill.removeFormat(r.index, r.length, A.sources.USER); }, direction(r) { const { align: t } = this.quill.getFormat(); - r === "rtl" && t == null ? this.quill.format("align", "right", w.sources.USER) : !r && t === "right" && this.quill.format("align", !1, w.sources.USER), this.quill.format("direction", r, w.sources.USER); + r === "rtl" && t == null ? this.quill.format("align", "right", A.sources.USER) : !r && t === "right" && this.quill.format("align", !1, A.sources.USER), this.quill.format("direction", r, A.sources.USER); }, indent(r) { const t = this.quill.getSelection(), e = this.quill.getFormat(t), n = parseInt(e.indent || 0, 10); if (r === "+1" || r === "-1") { let s = r === "+1" ? 1 : -1; - e.direction === "rtl" && (s *= -1), this.quill.format("indent", n + s, w.sources.USER); + e.direction === "rtl" && (s *= -1), this.quill.format("indent", n + s, A.sources.USER); } }, link(r) { - r === !0 && (r = prompt("Enter link URL:")), this.quill.format("link", r, w.sources.USER); + r === !0 && (r = prompt("Enter link URL:")), this.quill.format("link", r, A.sources.USER); }, list(r) { const t = this.quill.getSelection(), e = this.quill.getFormat(t); - r === "check" ? e.list === "checked" || e.list === "unchecked" ? this.quill.format("list", !1, w.sources.USER) : this.quill.format("list", "unchecked", w.sources.USER) : this.quill.format("list", r, w.sources.USER); + r === "check" ? e.list === "checked" || e.list === "unchecked" ? this.quill.format("list", !1, A.sources.USER) : this.quill.format("list", "unchecked", A.sources.USER) : this.quill.format("list", r, A.sources.USER); } } }; -const Xd = '', Qd = '', Jd = '', tg = '', eg = '', ng = '', rg = '', sg = '', Rl = '', ig = '', lg = '', og = '', ag = '', cg = '', ug = '', hg = '', fg = '', dg = '', gg = '', pg = '', mg = '', bg = '', yg = '', vg = '', Eg = '', Ag = '', wg = '', Ng = '', Tg = '', xg = '', Lg = '', Sg = '', _g = '', zn = { +const Fd = '', Hd = '', zd = '', Kd = '', Gd = '', Vd = '', Wd = '', Zd = '', yl = '', Yd = '', Xd = '', Qd = '', Jd = '', tg = '', eg = '', ng = '', rg = '', sg = '', ig = '', lg = '', og = '', ag = '', cg = '', ug = '', hg = '', fg = '', dg = '', gg = '', pg = '', mg = '', bg = '', yg = '', vg = '', kn = { align: { - "": Xd, - center: Qd, - right: Jd, - justify: tg + "": Fd, + center: Hd, + right: zd, + justify: Kd }, - background: eg, - blockquote: ng, - bold: rg, - clean: sg, - code: Rl, - "code-block": Rl, - color: ig, + background: Gd, + blockquote: Vd, + bold: Wd, + clean: Zd, + code: yl, + "code-block": yl, + color: Yd, direction: { - "": lg, - rtl: og + "": Xd, + rtl: Qd }, - formula: ag, + formula: Jd, header: { - 1: cg, - 2: ug, - 3: hg, - 4: fg, - 5: dg, - 6: gg + 1: tg, + 2: eg, + 3: ng, + 4: rg, + 5: sg, + 6: ig }, - italic: pg, - image: mg, + italic: lg, + image: og, indent: { - "+1": bg, - "-1": yg + "+1": ag, + "-1": cg }, - link: vg, + link: ug, list: { - bullet: Eg, - check: Ag, - ordered: wg + bullet: hg, + check: fg, + ordered: dg }, script: { - sub: Ng, - super: Tg + sub: gg, + super: pg }, - strike: xg, - table: Lg, - underline: Sg, - video: _g -}, qg = ''; -let kl = 0; -function Bl(r, t) { + strike: mg, + table: bg, + underline: yg, + video: vg +}, Eg = ''; +let vl = 0; +function El(r, t) { r.setAttribute(t, `${r.getAttribute(t) !== "true"}`); } -class $r { +class qr { constructor(t) { this.select = t, this.container = document.createElement("span"), this.buildPicker(), this.select.style.display = "none", this.select.parentNode.insertBefore(this.container, this.select), this.label.addEventListener("mousedown", () => { this.togglePicker(); @@ -7099,7 +7086,7 @@ class $r { }), this.select.addEventListener("change", this.update.bind(this)); } togglePicker() { - this.container.classList.toggle("ql-expanded"), Bl(this.label, "aria-expanded"), Bl(this.options, "aria-hidden"); + this.container.classList.toggle("ql-expanded"), El(this.label, "aria-expanded"), El(this.options, "aria-hidden"); } buildItem(t) { const e = document.createElement("span"); @@ -7120,11 +7107,11 @@ class $r { } buildLabel() { const t = document.createElement("span"); - return t.classList.add("ql-picker-label"), t.innerHTML = qg, t.tabIndex = "0", t.setAttribute("role", "button"), t.setAttribute("aria-expanded", "false"), this.container.appendChild(t), t; + return t.classList.add("ql-picker-label"), t.innerHTML = Eg, t.tabIndex = "0", t.setAttribute("role", "button"), t.setAttribute("aria-expanded", "false"), this.container.appendChild(t), t; } buildOptions() { const t = document.createElement("span"); - t.classList.add("ql-picker-options"), t.setAttribute("aria-hidden", "true"), t.tabIndex = "-1", t.id = `ql-picker-options-${kl}`, kl += 1, this.label.setAttribute("aria-controls", t.id), this.options = t, Array.from(this.select.options).forEach((e) => { + t.classList.add("ql-picker-options"), t.setAttribute("aria-hidden", "true"), t.tabIndex = "-1", t.id = `ql-picker-options-${vl}`, vl += 1, this.label.setAttribute("aria-controls", t.id), this.options = t, Array.from(this.select.options).forEach((e) => { const n = this.buildItem(e); t.appendChild(n), e.selected === !0 && this.selectItem(n); }), this.container.appendChild(t); @@ -7143,7 +7130,7 @@ class $r { selectItem(t) { let e = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : !1; const n = this.container.querySelector(".ql-selected"); - t !== n && (n != null && n.classList.remove("ql-selected"), t != null && (t.classList.add("ql-selected"), this.select.selectedIndex = Array.from(t.parentNode.children).indexOf(t), t.hasAttribute("data-value") ? this.label.setAttribute("data-value", t.getAttribute("data-value")) : this.label.removeAttribute("data-value"), t.hasAttribute("data-label") ? this.label.setAttribute("data-label", t.getAttribute("data-label")) : this.label.removeAttribute("data-label"), e && (this.select.dispatchEvent(new Event("change")), this.close()))); + t !== n && (n?.classList.remove("ql-selected"), t != null && (t.classList.add("ql-selected"), this.select.selectedIndex = Array.from(t.parentNode.children).indexOf(t), t.hasAttribute("data-value") ? this.label.setAttribute("data-value", t.getAttribute("data-value")) : this.label.removeAttribute("data-value"), t.hasAttribute("data-label") ? this.label.setAttribute("data-label", t.getAttribute("data-label")) : this.label.removeAttribute("data-label"), e && (this.select.dispatchEvent(new Event("change")), this.close()))); } update() { let t; @@ -7159,7 +7146,7 @@ class $r { this.label.classList.toggle("ql-active", e); } } -class Do extends $r { +class Ao extends qr { constructor(t, e) { super(t), this.label.innerHTML = e, this.container.classList.add("ql-color-picker"), Array.from(this.container.querySelectorAll(".ql-picker-item")).slice(0, 7).forEach((n) => { n.classList.add("ql-primary"); @@ -7175,7 +7162,7 @@ class Do extends $r { n && (n.tagName === "line" ? n.style.stroke = s : n.style.fill = s); } } -class jo extends $r { +class wo extends qr { constructor(t, e) { super(t), this.container.classList.add("ql-icon-picker"), Array.from(this.container.querySelectorAll(".ql-picker-item")).forEach((n) => { n.innerHTML = e[n.getAttribute("data-value") || ""]; @@ -7190,15 +7177,15 @@ class jo extends $r { } } } -const Og = (r) => { +const Ag = (r) => { const { overflowY: t } = getComputedStyle(r, null); return t !== "visible" && t !== "clip"; }; -class $o { +class No { constructor(t, e) { - this.quill = t, this.boundsContainer = e || document.body, this.root = t.addContainer("ql-tooltip"), this.root.innerHTML = this.constructor.TEMPLATE, Og(this.quill.root) && this.quill.root.addEventListener("scroll", () => { + this.quill = t, this.boundsContainer = e || document.body, this.root = t.addContainer("ql-tooltip"), this.root.innerHTML = this.constructor.TEMPLATE, Ag(this.quill.root) && this.quill.root.addEventListener("scroll", () => { this.root.style.marginTop = `${-1 * this.quill.root.scrollTop}px`; }), this.hide(); } @@ -7220,8 +7207,8 @@ class $o { this.root.classList.remove("ql-editing"), this.root.classList.remove("ql-hidden"); } } -const Cg = [!1, "center", "right", "justify"], Ig = ["#000000", "#e60000", "#ff9900", "#ffff00", "#008a00", "#0066cc", "#9933ff", "#ffffff", "#facccc", "#ffebcc", "#ffffcc", "#cce8cc", "#cce0f5", "#ebd6ff", "#bbbbbb", "#f06666", "#ffc266", "#ffff66", "#66b966", "#66a3e0", "#c285ff", "#888888", "#a10000", "#b26b00", "#b2b200", "#006100", "#0047b2", "#6b24b2", "#444444", "#5c0000", "#663d00", "#666600", "#003700", "#002966", "#3d1466"], Rg = [!1, "serif", "monospace"], kg = ["1", "2", "3", !1], Bg = ["small", !1, "large", "huge"]; -class Zn extends cn { +const wg = [!1, "center", "right", "justify"], Ng = ["#000000", "#e60000", "#ff9900", "#ffff00", "#008a00", "#0066cc", "#9933ff", "#ffffff", "#facccc", "#ffebcc", "#ffffcc", "#cce8cc", "#cce0f5", "#ebd6ff", "#bbbbbb", "#f06666", "#ffc266", "#ffff66", "#66b966", "#66a3e0", "#c285ff", "#888888", "#a10000", "#b26b00", "#b2b200", "#006100", "#0047b2", "#6b24b2", "#444444", "#5c0000", "#663d00", "#666600", "#003700", "#002966", "#3d1466"], Tg = [!1, "serif", "monospace"], xg = ["1", "2", "3", !1], Lg = ["small", !1, "large", "huge"]; +class Pn extends rn { constructor(t, e) { super(t, e); const n = (s) => { @@ -7258,13 +7245,13 @@ class Zn extends cn { } buildPickers(t, e) { this.pickers = Array.from(t).map((s) => { - if (s.classList.contains("ql-align") && (s.querySelector("option") == null && On(s, Cg), typeof e.align == "object")) - return new jo(s, e.align); + if (s.classList.contains("ql-align") && (s.querySelector("option") == null && Nn(s, wg), typeof e.align == "object")) + return new wo(s, e.align); if (s.classList.contains("ql-background") || s.classList.contains("ql-color")) { const i = s.classList.contains("ql-background") ? "background" : "color"; - return s.querySelector("option") == null && On(s, Ig, i === "background" ? "#ffffff" : "#000000"), new Do(s, e[i]); + return s.querySelector("option") == null && Nn(s, Ng, i === "background" ? "#ffffff" : "#000000"), new Ao(s, e[i]); } - return s.querySelector("option") == null && (s.classList.contains("ql-font") ? On(s, Rg) : s.classList.contains("ql-header") ? On(s, kg) : s.classList.contains("ql-size") && On(s, Bg)), new $r(s); + return s.querySelector("option") == null && (s.classList.contains("ql-font") ? Nn(s, Tg) : s.classList.contains("ql-header") ? Nn(s, xg) : s.classList.contains("ql-size") && Nn(s, Lg)), new qr(s); }); const n = () => { this.pickers.forEach((s) => { @@ -7274,7 +7261,7 @@ class Zn extends cn { this.quill.on(R.events.EDITOR_CHANGE, n); } } -Zn.DEFAULTS = ve({}, cn.DEFAULTS, { +Pn.DEFAULTS = fe({}, rn.DEFAULTS, { modules: { toolbar: { handlers: { @@ -7295,7 +7282,7 @@ Zn.DEFAULTS = ve({}, cn.DEFAULTS, { } } }); -class Po extends $o { +class To extends No { constructor(t, e) { super(t, e), this.textbox = this.root.querySelector('input[type="text"]'), this.listen(); } @@ -7332,7 +7319,7 @@ class Po extends $o { break; } case "video": - t = Mg(t); + t = Sg(t); // eslint-disable-next-line no-fallthrough case "formula": { if (!t) break; @@ -7353,23 +7340,24 @@ class Po extends $o { this.textbox.value = "", this.hide(); } } -function Mg(r) { +function Sg(r) { let t = r.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtube\.com\/watch.*v=([a-zA-Z0-9_-]+)/) || r.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtu\.be\/([a-zA-Z0-9_-]+)/); return t ? `${t[1] || "https"}://www.youtube.com/embed/${t[2]}?showinfo=0` : (t = r.match(/^(?:(https?):\/\/)?(?:www\.)?vimeo\.com\/(\d+)/)) ? `${t[1] || "https"}://player.vimeo.com/video/${t[2]}/` : r; } -function On(r, t) { +function Nn(r, t) { let e = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : !1; t.forEach((n) => { const s = document.createElement("option"); n === e ? s.setAttribute("selected", "selected") : s.setAttribute("value", String(n)), r.appendChild(s); }); } -const Dg = [["bold", "italic", "link"], [{ +const _g = [["bold", "italic", "link"], [{ header: 1 }, { header: 2 }, "blockquote"]]; -class Uo extends Po { +class Cg extends To { + static TEMPLATE = ['', '
    ', '', '', "
    "].join(""); constructor(t, e) { super(t, e), this.quill.on(R.events.EDITOR_CHANGE, (n, s, i, o) => { if (n === R.events.SELECTION_CHANGE) @@ -7380,7 +7368,7 @@ class Uo extends Po { const u = this.quill.getBounds(s); u != null && this.position(u); } else { - const u = a[a.length - 1], h = this.quill.getIndex(u), p = Math.min(u.length() - 1, s.index + s.length - h), v = this.quill.getBounds(new Ie(h, p)); + const u = a[a.length - 1], h = this.quill.getIndex(u), p = Math.min(u.length() - 1, s.index + s.length - h), v = this.quill.getBounds(new Se(h, p)); v != null && this.position(v); } } else document.activeElement !== this.textbox && this.quill.hasFocus() && this.hide(); @@ -7408,57 +7396,54 @@ class Uo extends Po { return n.style.marginLeft = "", e !== 0 && (n.style.marginLeft = `${-1 * e - n.offsetWidth / 2}px`), e; } } -O(Uo, "TEMPLATE", ['', '
    ', '', '', "
    "].join("")); -class Fo extends Zn { +class xo extends Pn { constructor(t, e) { - e.modules.toolbar != null && e.modules.toolbar.container == null && (e.modules.toolbar.container = Dg), super(t, e), this.quill.container.classList.add("ql-bubble"); + e.modules.toolbar != null && e.modules.toolbar.container == null && (e.modules.toolbar.container = _g), super(t, e), this.quill.container.classList.add("ql-bubble"); } extendToolbar(t) { - this.tooltip = new Uo(this.quill, this.options.bounds), t.container != null && (this.tooltip.root.appendChild(t.container), this.buildButtons(t.container.querySelectorAll("button"), zn), this.buildPickers(t.container.querySelectorAll("select"), zn)); + this.tooltip = new Cg(this.quill, this.options.bounds), t.container != null && (this.tooltip.root.appendChild(t.container), this.buildButtons(t.container.querySelectorAll("button"), kn), this.buildPickers(t.container.querySelectorAll("select"), kn)); } } -Fo.DEFAULTS = ve({}, Zn.DEFAULTS, { +xo.DEFAULTS = fe({}, Pn.DEFAULTS, { modules: { toolbar: { handlers: { link(r) { - r ? this.quill.theme.tooltip.edit() : this.quill.format("link", !1, w.sources.USER); + r ? this.quill.theme.tooltip.edit() : this.quill.format("link", !1, A.sources.USER); } } } } }); -const jg = [[{ +const qg = [[{ header: ["1", "2", "3", !1] }], ["bold", "italic", "underline", "link"], [{ list: "ordered" }, { list: "bullet" }], ["clean"]]; -class Ho extends Po { - constructor() { - super(...arguments); - O(this, "preview", this.root.querySelector("a.ql-preview")); - } +class Og extends To { + static TEMPLATE = ['', '', '', ''].join(""); + preview = this.root.querySelector("a.ql-preview"); listen() { - super.listen(), this.root.querySelector("a.ql-action").addEventListener("click", (e) => { - this.root.classList.contains("ql-editing") ? this.save() : this.edit("link", this.preview.textContent), e.preventDefault(); - }), this.root.querySelector("a.ql-remove").addEventListener("click", (e) => { + super.listen(), this.root.querySelector("a.ql-action").addEventListener("click", (t) => { + this.root.classList.contains("ql-editing") ? this.save() : this.edit("link", this.preview.textContent), t.preventDefault(); + }), this.root.querySelector("a.ql-remove").addEventListener("click", (t) => { if (this.linkRange != null) { - const n = this.linkRange; - this.restoreFocus(), this.quill.formatText(n, "link", !1, R.sources.USER), delete this.linkRange; + const e = this.linkRange; + this.restoreFocus(), this.quill.formatText(e, "link", !1, R.sources.USER), delete this.linkRange; } - e.preventDefault(), this.hide(); - }), this.quill.on(R.events.SELECTION_CHANGE, (e, n, s) => { - if (e != null) { - if (e.length === 0 && s === R.sources.USER) { - const [i, o] = this.quill.scroll.descendant(ye, e.index); - if (i != null) { - this.linkRange = new Ie(e.index - o, i.length()); - const a = ye.formats(i.domNode); - this.preview.textContent = a, this.preview.setAttribute("href", a), this.show(); - const u = this.quill.getBounds(this.linkRange); - u != null && this.position(u); + t.preventDefault(), this.hide(); + }), this.quill.on(R.events.SELECTION_CHANGE, (t, e, n) => { + if (t != null) { + if (t.length === 0 && n === R.sources.USER) { + const [s, i] = this.quill.scroll.descendant(Ar, t.index); + if (s != null) { + this.linkRange = new Se(t.index - i, s.length()); + const o = Ar.formats(s.domNode); + this.preview.textContent = o, this.preview.setAttribute("href", o), this.show(); + const a = this.quill.getBounds(this.linkRange); + a != null && this.position(a); return; } } else @@ -7471,13 +7456,12 @@ class Ho extends Po { super.show(), this.root.removeAttribute("data-mode"); } } -O(Ho, "TEMPLATE", ['', '', '', ''].join("")); -class zo extends Zn { +class Lo extends Pn { constructor(t, e) { - e.modules.toolbar != null && e.modules.toolbar.container == null && (e.modules.toolbar.container = jg), super(t, e), this.quill.container.classList.add("ql-snow"); + e.modules.toolbar != null && e.modules.toolbar.container == null && (e.modules.toolbar.container = qg), super(t, e), this.quill.container.classList.add("ql-snow"); } extendToolbar(t) { - t.container != null && (t.container.classList.add("ql-snow"), this.buildButtons(t.container.querySelectorAll("button"), zn), this.buildPickers(t.container.querySelectorAll("select"), zn), this.tooltip = new Ho(this.quill, this.options.bounds), t.container.querySelector(".ql-link") && this.quill.keyboard.addBinding({ + t.container != null && (t.container.classList.add("ql-snow"), this.buildButtons(t.container.querySelectorAll("button"), kn), this.buildPickers(t.container.querySelectorAll("select"), kn), this.tooltip = new Og(this.quill, this.options.bounds), t.container.querySelector(".ql-link") && this.quill.keyboard.addBinding({ key: "k", shortKey: !0 }, (e, n) => { @@ -7485,7 +7469,7 @@ class zo extends Zn { })); } } -zo.DEFAULTS = ve({}, Zn.DEFAULTS, { +Lo.DEFAULTS = fe({}, Pn.DEFAULTS, { modules: { toolbar: { handlers: { @@ -7500,70 +7484,64 @@ zo.DEFAULTS = ve({}, Zn.DEFAULTS, { } = this.quill.theme; n.edit("link", e); } else - this.quill.format("link", !1, w.sources.USER); + this.quill.format("link", !1, A.sources.USER); } } } } }); -w.register({ - "attributors/attribute/direction": No, - "attributors/class/align": Eo, - "attributors/class/background": nd, - "attributors/class/color": ed, - "attributors/class/direction": To, - "attributors/class/font": So, - "attributors/class/size": qo, - "attributors/style/align": Ao, - "attributors/style/background": Ni, - "attributors/style/color": wi, - "attributors/style/direction": xo, - "attributors/style/font": _o, - "attributors/style/size": Oo +A.register({ + "attributors/attribute/direction": ao, + "attributors/class/align": io, + "attributors/class/background": Mf, + "attributors/class/color": Bf, + "attributors/class/direction": co, + "attributors/class/font": fo, + "attributors/class/size": po, + "attributors/style/align": lo, + "attributors/style/background": oi, + "attributors/style/color": li, + "attributors/style/direction": uo, + "attributors/style/font": go, + "attributors/style/size": mo }, !0); -w.register({ - "formats/align": Eo, - "formats/direction": To, - "formats/indent": Gd, - "formats/background": Ni, - "formats/color": wi, - "formats/font": So, - "formats/size": qo, - "formats/blockquote": ii, - "formats/code-block": At, - "formats/header": li, - "formats/list": Wn, - "formats/bold": Hn, - "formats/code": Ti, - "formats/italic": oi, - "formats/link": ye, - "formats/script": ai, - "formats/strike": ci, - "formats/underline": ui, - "formats/formula": Lr, - "formats/image": hi, - "formats/video": Sr, - "modules/syntax": Mo, - "modules/table": Wd, - "modules/toolbar": qi, - "themes/bubble": Fo, - "themes/snow": zo, - "ui/icons": zn, - "ui/picker": $r, - "ui/icon-picker": jo, - "ui/color-picker": Do, - "ui/tooltip": $o +A.register({ + "formats/align": io, + "formats/direction": co, + "formats/indent": _d, + "formats/background": oi, + "formats/color": li, + "formats/font": fo, + "formats/size": po, + "formats/blockquote": Cd, + "formats/code-block": wt, + "formats/header": qd, + "formats/list": jn, + "formats/bold": fi, + "formats/code": ai, + "formats/italic": Od, + "formats/link": Ar, + "formats/script": Rd, + "formats/strike": Id, + "formats/underline": kd, + "formats/formula": Bd, + "formats/image": Md, + "formats/video": Dd, + "modules/syntax": Eo, + "modules/table": Pd, + "modules/toolbar": gi, + "themes/bubble": xo, + "themes/snow": Lo, + "ui/icons": kn, + "ui/picker": qr, + "ui/icon-picker": wo, + "ui/color-picker": Ao, + "ui/tooltip": No }, !0); -const Zt = window.React, $g = window.React.createRef; -class Pr extends Zt.Component { - constructor(t) { - super(t), this.editingAreaRef = $g(), this.dirtyProps = [ - "modules", - "formats", - "bounds", - "theme", - "children" - ], this.cleanProps = [ +const Te = window.React, Al = window.React.createRef; +var Or = class extends Te.Component { + constructor(r) { + super(r), this.editingAreaRef = Al(), this.containerRef = Al(), this.dirtyProps = ["modules", "formats", "bounds", "theme", "children"], this.cleanProps = [ "id", "className", "style", @@ -7575,38 +7553,42 @@ class Pr extends Zt.Component { "onBlur", "onKeyPress", "onKeyDown", - "onKeyUp" + "onKeyUp", + "useSemanticHTML" ], this.state = { generation: 0 - }, this.selection = null, this.onEditorChange = (n, s, i, o) => { - var a, u; - n === "text-change" ? (a = this.onEditorChangeText) == null || a.call(this, this.editor.root.innerHTML, s, o, this.unprivilegedEditor) : n === "selection-change" && ((u = this.onEditorChangeSelection) == null || u.call(this, s, o, this.unprivilegedEditor)); + }, this.selection = null, this.onEditorChange = (e, n, s, i) => { + e === "text-change" ? this.onEditorChangeText?.( + this.props.useSemanticHTML !== !1 ? this.editor.getSemanticHTML() : this.editor.root.innerHTML, + n, + i, + this.unprivilegedEditor + ) : e === "selection-change" && this.onEditorChangeSelection?.(n, i, this.unprivilegedEditor); }; - const e = this.isControlled() ? t.value : t.defaultValue; - this.value = e ?? ""; + const t = this.isControlled() ? r.value : r.defaultValue; + this.value = t ?? ""; } - validateProps(t) { - if (Zt.Children.count(t.children) > 1) + validateProps(r) { + if (Te.Children.count(r.children) > 1) throw new Error("The Quill editing area can only be composed of a single React element."); - if (Zt.Children.count(t.children)) { - const e = Zt.Children.only(t.children); - if ((e == null ? void 0 : e.type) === "textarea") - throw new Error("Quill does not support editing on a