@phenix1024/editor 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +188 -2
- package/dist/ophx-editor.cjs +1 -0
- package/dist/ophx-editor.css +2 -0
- package/dist/ophx-editor.iife.js +149 -0
- package/dist/ophx-editor.iife.js.map +1 -0
- package/dist/ophx-editor.js +813 -0
- package/dist/ophx-editor.standalone.js +20537 -0
- package/dist/ophx-editor.standalone.js.map +1 -0
- package/dist/types/editor/extensions.d.ts +28 -0
- package/dist/types/editor/ophx-archive.d.ts +27 -0
- package/dist/types/editor/ophx-editor.d.ts +27 -0
- package/dist/types/editor/ophx.css.d.ts +2 -0
- package/dist/types/editor/template-fields.d.ts +7 -0
- package/dist/types/editor/toolbar.d.ts +6 -0
- package/dist/types/index.d.ts +6 -0
- package/package.json +81 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mars102
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,189 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ophx/editor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Установка
|
|
4
|
+
|
|
5
|
+
### npm + любой сборщик
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @phenix1024/editor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { OphxEditor, mountToolbar } from '@phenix1024/editor'
|
|
13
|
+
import '@phenix1024/editor/style.css'
|
|
14
|
+
|
|
15
|
+
const editor = new OphxEditor({
|
|
16
|
+
element: document.querySelector('#editor')!,
|
|
17
|
+
content: '<h1>Документ</h1><p>Текст…</p>',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
mountToolbar(document.querySelector('#toolbar')!, editor.editor)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Vue 3 + TypeScript
|
|
24
|
+
|
|
25
|
+
```vue
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import { onMounted, onBeforeUnmount } from 'vue'
|
|
28
|
+
import { OphxEditor, mountToolbar } from '@phenix1024/editor'
|
|
29
|
+
import '@phenix1024/editor/style.css'
|
|
30
|
+
|
|
31
|
+
let editor: OphxEditor | null = null
|
|
32
|
+
let unmountToolbar: (() => void) | null = null
|
|
33
|
+
|
|
34
|
+
onMounted(() => {
|
|
35
|
+
editor = new OphxEditor({ element: document.querySelector('#editor')! })
|
|
36
|
+
unmountToolbar = mountToolbar(document.querySelector('#toolbar')!, editor.editor)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
onBeforeUnmount(() => {
|
|
40
|
+
unmountToolbar?.()
|
|
41
|
+
editor?.destroy()
|
|
42
|
+
})
|
|
43
|
+
</script>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### CDN без сборщика
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<link rel="stylesheet" href="https://unpkg.com/@phenix1024/editor@0.1.0/dist/ophx-editor.css">
|
|
50
|
+
<script src="https://unpkg.com/@phenix1024/editor@0.1.0/dist/ophx-editor.iife.js"></script>
|
|
51
|
+
<script>
|
|
52
|
+
const editor = new OphxEditor.OphxEditor({ element: document.querySelector('#editor') })
|
|
53
|
+
OphxEditor.mountToolbar(document.querySelector('#toolbar'), editor.editor)
|
|
54
|
+
</script>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Аналогично для jsDelivr:
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phenix1024/editor@0.1.0/dist/ophx-editor.css">
|
|
61
|
+
<script src="https://cdn.jsdelivr.net/npm/@phenix1024/editor@0.1.0/dist/ophx-editor.iife.js"></script>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> Для CDN лучше жёстко фиксировать версию, чтобы обновления не ломали существующий код.
|
|
65
|
+
|
|
66
|
+
Переносимый WYSIWYM-редактор на Tiptap. Ядро не зависит от React, Vue, Svelte или другого UI-фреймворка: ему нужен только DOM-контейнер. Панель инструментов опциональна — можно использовать готовую `mountToolbar()` или построить собственный интерфейс поверх публичного Tiptap instance.
|
|
67
|
+
|
|
68
|
+
## Возможности
|
|
69
|
+
|
|
70
|
+
- Заголовки H1–H6, абзацы, маркированные и нумерованные списки; стандартные markdown/input rules Tiptap преобразуют `# `, `- ` и `1. ` при вводе.
|
|
71
|
+
- Жирный, курсив, подчёркивание, зачёркивание, ссылки и метки.
|
|
72
|
+
- Списки, отступы для абзацев/заголовков, изображения, таблицы и сворачивание разделов по заголовкам.
|
|
73
|
+
- Содержание с переходом к заголовку, поиск с подсветкой, Undo/Redo.
|
|
74
|
+
- Не допускается повторный пробел при обычном вводе; Enter не создаёт последовательные пустые строки. Проверка орфографии использует браузерную проверку (`spellcheck`) и язык `ru`.
|
|
75
|
+
- Обычные клавиатурные команды редактора и браузера: выделение, копирование/вставка, Home/End, Undo/Redo и др.
|
|
76
|
+
- Импорт/экспорт `.ophx` v2: плоский ZIP с HTML-файлом, stylesheet-файлами и изображениями.
|
|
77
|
+
- Поля шаблонов Ophx: сохранение `contenteditable`, редактирование полей `.ophx-field`, сбор значений и проверка обязательных полей.
|
|
78
|
+
|
|
79
|
+
Проверка орфографии зависит от браузера и установленных словарей. Для удалённых изображений, недоступных из-за CORS, сохраняется исходная внешняя ссылка; локальные/вставленные изображения упаковываются в корень архива.
|
|
80
|
+
|
|
81
|
+
## Разработка
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
npm install
|
|
85
|
+
npm run dev
|
|
86
|
+
npm run typecheck
|
|
87
|
+
npm run build
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`npm run build` собирает демо в `dist-demo/` и библиотеку/standalone-бандлы в `dist/`.
|
|
91
|
+
|
|
92
|
+
## Подключение без фреймворка
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { OphxEditor, mountToolbar } from '@ophx/editor'
|
|
96
|
+
import '@ophx/editor/style.css'
|
|
97
|
+
|
|
98
|
+
const editor = new OphxEditor({
|
|
99
|
+
element: document.querySelector('#editor')!,
|
|
100
|
+
content: '<h1>Документ</h1><p>Текст…</p>',
|
|
101
|
+
onUpdate: ({ editor }) => autosave(editor.getHTML()),
|
|
102
|
+
})
|
|
103
|
+
const unmountToolbar = mountToolbar(document.querySelector('#toolbar')!, editor.editor)
|
|
104
|
+
|
|
105
|
+
// При размонтировании host:
|
|
106
|
+
unmountToolbar()
|
|
107
|
+
editor.destroy()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Подключение в React
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { useEffect, useRef } from 'react'
|
|
114
|
+
import { OphxEditor, mountToolbar } from '@ophx/editor'
|
|
115
|
+
import '@ophx/editor/style.css'
|
|
116
|
+
|
|
117
|
+
export function DocumentEditor() {
|
|
118
|
+
const editorHost = useRef<HTMLDivElement>(null)
|
|
119
|
+
const toolbarHost = useRef<HTMLDivElement>(null)
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!editorHost.current || !toolbarHost.current) return
|
|
123
|
+
const editor = new OphxEditor({ element: editorHost.current })
|
|
124
|
+
const unmountToolbar = mountToolbar(toolbarHost.current, editor.editor)
|
|
125
|
+
return () => { unmountToolbar(); editor.destroy() }
|
|
126
|
+
}, [])
|
|
127
|
+
|
|
128
|
+
return <><div ref={toolbarHost} /><div ref={editorHost} /></>
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Для Vue/Svelte/Angular используется тот же DOM API в `onMounted`/`onMount`/lifecycle хуке. Не подключайте одновременно встроенный и собственный toolbar.
|
|
133
|
+
|
|
134
|
+
## Формат `.ophx`
|
|
135
|
+
|
|
136
|
+
`.ophx` соответствует v2 спецификации: это обычный ZIP без шифрования и ZIP64; записи создаются с DEFLATE, имена кодируются в UTF-8. Все файлы лежат плоским списком в корне. Внутри должен быть ровно один HTML-файл, его имя совпадает с именем внешнего `.ophx` без расширения. Стили и изображения тоже лежат в корне, без каталогов:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
Договор.ophx
|
|
140
|
+
├── Договор.html
|
|
141
|
+
├── default.css
|
|
142
|
+
└── image-1.png
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { exportOphx, importOphx, downloadBlob } from '@phenix1024/editor'
|
|
147
|
+
|
|
148
|
+
const archive = await exportOphx(editor.getHTML(), {
|
|
149
|
+
filename: 'Договор', // basename должен совпадать с именем скачиваемого .ophx
|
|
150
|
+
title: 'Договор поставки',
|
|
151
|
+
styles: { 'default.css': cssText },
|
|
152
|
+
bodyContentEditable: 'false', // если архив был импортирован с таким атрибутом
|
|
153
|
+
})
|
|
154
|
+
downloadBlob(archive, 'Договор.ophx')
|
|
155
|
+
|
|
156
|
+
const document = await importOphx(file)
|
|
157
|
+
editor.setContent(document.html)
|
|
158
|
+
const updatedArchive = await exportOphx(editor.getHTML(), {
|
|
159
|
+
filename: document.filename,
|
|
160
|
+
title: document.title,
|
|
161
|
+
styles: document.styles,
|
|
162
|
+
bodyContentEditable: document.bodyContentEditable,
|
|
163
|
+
})
|
|
164
|
+
downloadBlob(updatedArchive, `${document.filename}.ophx`)
|
|
165
|
+
// document.styles содержит все подключённые локальные CSS-файлы.
|
|
166
|
+
// document.css — удобный алиас первого stylesheet.
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Имена в `styles` и `assets` — простые имена файлов, без путей. Изображения с локальными именами можно передать в `assets`; вставленные в редактор `data:image` и `blob:` изображения автоматически превращаются в `image-<номер>.<ext>`. Внешние `http(s)` и `data:` ссылки сохраняются как внешние.
|
|
170
|
+
|
|
171
|
+
Импорт принимает только ZIP v2 с одним корневым `.html`; старые не-ZIP/XML варианты определяются как неподдерживаемые. Локальные ссылки на CSS и изображения проверяются по точным именам. Скриптовые элементы и event-handler атрибуты удаляются перед передачей HTML редактору. Содержимое `styles` возвращается вызывающему приложению — оно может применить CSS в изолированном контейнере или iframe.
|
|
172
|
+
|
|
173
|
+
### Поля шаблона
|
|
174
|
+
|
|
175
|
+
В документе сохраняются поля `span` (inline) и `p`/`div` (block) с `.ophx-field`, `contenteditable="true"` и уникальным `data-ophx-field`. Статические фрагменты с `contenteditable="false"` сохраняются; при `body contenteditable="false"` это состояние также переносится в editor-модель. Значения доступны через `editor.getFields()`, обязательные незаполненные поля — через `editor.getMissingRequiredFields()`.
|
|
176
|
+
|
|
177
|
+
Для подготовки шаблона helper `convertUnderscoresToFields(html)` заменяет каждую последовательность `_{2,}` в текстовых узлах отдельным пустым полем с ключами `f1`, `f2`, …; кодовые блоки и нередактируемые участки пропускаются.
|
|
178
|
+
|
|
179
|
+
## API
|
|
180
|
+
|
|
181
|
+
- `new OphxEditor({ element, content?, placeholder?, editable?, onUpdate?, extensions? })`
|
|
182
|
+
- `editor.editor` — исходный Tiptap `Editor` для команд и подписок.
|
|
183
|
+
- `getHTML()`, `getJSON()`, `setContent(content)`, `setSearchQuery(query)`, `destroy()`.
|
|
184
|
+
- `mountToolbar(host, editor, { onInsertImage? })` возвращает функцию очистки.
|
|
185
|
+
- `exportOphx(html, { filename, title?, styles?, assets?, bodyContentEditable? })`, `importOphx(file)`, `downloadBlob(blob, filename)`.
|
|
186
|
+
- `getFields()` и `getMissingRequiredFields()` возвращают валидные уникальные поля шаблона.
|
|
187
|
+
- `convertUnderscoresToFields(html, prefix?)` возвращает преобразованный HTML, число полей и их IDs.
|
|
188
|
+
|
|
189
|
+
Интеграционный пакет поставляет Tiptap extensions и JSZip как runtime-зависимости. Подключайте stylesheet явно в приложении, где сборщик не обрабатывает CSS imports автоматически.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));let c=require("@tiptap/core"),l=require("@tiptap/starter-kit");l=s(l,1);let u=require("@tiptap/extension-underline");u=s(u,1);let d=require("@tiptap/extension-link");d=s(d,1);let f=require("@tiptap/extension-image");f=s(f,1);let p=require("@tiptap/extension-placeholder");p=s(p,1);let m=require("@tiptap/extension-table"),h=require("@tiptap/extension-table-row"),g=require("@tiptap/extension-table-header"),_=require("@tiptap/extension-table-cell"),v=require("@tiptap/extension-heading");v=s(v,1);let y=require("@tiptap/pm/state"),b=require("@tiptap/pm/view"),x=require("jszip");x=s(x,1);var S=c.Mark.create({name:`label`,inclusive:!1,addAttributes(){return{label:{default:`highlight`,parseHTML:e=>e.dataset.label??`highlight`,renderHTML:e=>({"data-label":e.label})}}},parseHTML(){return[{tag:`mark[data-label]`},{tag:`mark`}]},renderHTML({HTMLAttributes:e}){return[`mark`,e,0]}});function C(){return{fieldId:{default:``,parseHTML:e=>e.getAttribute(`data-ophx-field`)??``,renderHTML:e=>({"data-ophx-field":e.fieldId??``})},fieldLabel:{default:null,parseHTML:e=>e.getAttribute(`data-ophx-label`),renderHTML:e=>e.fieldLabel?{"data-ophx-label":e.fieldLabel}:{}},required:{default:!1,parseHTML:e=>e.getAttribute(`data-ophx-required`)===`true`,renderHTML:e=>e.required?{"data-ophx-required":`true`}:{}}}}var w=/^[A-Za-z0-9._-]+$/,T=c.Node.create({name:`ophxInlineField`,priority:1e3,group:`inline`,inline:!0,content:`text*`,defining:!0,addAttributes:C,parseHTML(){return[{tag:`span.ophx-field[contenteditable="true"][data-ophx-field]`,priority:1e3}]},renderHTML({HTMLAttributes:e}){return[`span`,{...e,class:`ophx-field`,contenteditable:`true`},0]}}),E=c.Node.create({name:`ophxBlockField`,priority:1e3,group:`block`,content:`text*`,defining:!0,addAttributes(){return{...C(),tagName:{default:`div`,parseHTML:e=>e.tagName.toLowerCase(),renderHTML:()=>({})}}},parseHTML(){return[{tag:`p.ophx-field[contenteditable="true"][data-ophx-field]`,priority:1e3},{tag:`div.ophx-field[contenteditable="true"][data-ophx-field]`,priority:1e3}]},renderHTML({node:e,HTMLAttributes:t}){return[e.attrs.tagName===`p`?`p`:`div`,{...t,class:`ophx-field`,contenteditable:`true`},0]}}),D=c.Mark.create({name:`ophxReadonlyInline`,inclusive:!1,addAttributes(){return{className:{default:null,parseHTML:e=>e.getAttribute(`class`),renderHTML:e=>e.className?{class:e.className}:{}}}},parseHTML(){return[{tag:`span[contenteditable="false"]`,priority:100}]},renderHTML({HTMLAttributes:e}){return[`span`,{...e,contenteditable:`false`},0]}}),O=c.Node.create({name:`ophxReadonlyBlock`,priority:1e3,group:`block`,content:`block*`,defining:!0,isolating:!0,addAttributes(){return{id:{default:null,parseHTML:e=>e.getAttribute(`id`),renderHTML:e=>e.id?{id:e.id}:{}},className:{default:null,parseHTML:e=>e.getAttribute(`class`),renderHTML:e=>e.className?{class:e.className}:{}},title:{default:null,parseHTML:e=>e.getAttribute(`title`),renderHTML:e=>e.title?{title:e.title}:{}}}},parseHTML(){return[{tag:`div[contenteditable="false"]`,priority:1e3}]},renderHTML({HTMLAttributes:e}){return[`div`,{...e,contenteditable:`false`},0]}}),k=new y.PluginKey(`ophxHeadingCollapse`),A=v.default.extend({addAttributes(){return{...this.parent?.(),collapsed:{default:!1,parseHTML:e=>e.dataset.collapsed===`true`,renderHTML:e=>e.collapsed?{"data-collapsed":`true`}:{}}}},addProseMirrorPlugins(){return[new y.Plugin({key:k,props:{decorations(e){let t=[],n=null;return e.doc.forEach((e,r)=>{if(e.type.name===`heading`){let i=Number(e.attrs.level);n!==null&&i<=n&&(n=null),n!==null&&t.push(b.Decoration.node(r,r+e.nodeSize,{class:`ophx-collapsed-content`})),e.attrs.collapsed&&(n=i)}else n!==null&&t.push(b.Decoration.node(r,r+e.nodeSize,{class:`ophx-collapsed-content`}))}),b.DecorationSet.create(e.doc,t)}}})]}}),j=new y.PluginKey(`ophxTextSearch`),M=c.Extension.create({name:`ophxSearch`,addStorage(){return{query:``}},addCommands(){return{setSearchQuery:e=>({tr:t,dispatch:n})=>(this.storage.query=e,n?.(t.setMeta(j,e)),!0)}},addProseMirrorPlugins(){let e=this;return[new y.Plugin({key:j,state:{init:(t,n)=>N(n.doc,e.storage.query),apply:(t,n,r,i)=>{let a=t.getMeta(j);return a!==void 0&&(e.storage.query=a),!t.docChanged&&a===void 0?n:N(i.doc,e.storage.query)}},props:{decorations(e){return j.getState(e)}}})]}});function N(e,t){if(!t.trim())return b.DecorationSet.empty;let n=[],r=t.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),i=new RegExp(r,`gi`);return e.descendants((e,t)=>{if(e.isText&&e.text){i.lastIndex=0;for(let r of e.text.matchAll(i))r.index!==void 0&&n.push(b.Decoration.inline(t+r.index,t+r.index+r[0].length,{class:`ophx-search-match`}))}}),b.DecorationSet.create(e,n)}var P=c.Extension.create({name:`ophxWysiwymRules`,addProseMirrorPlugins(){return[new y.Plugin({props:{handleTextInput(e,t,n,r){return r===` `&&/\s/.test(e.state.doc.textBetween(Math.max(0,t-1),t))},handleKeyDown(e,t){if(t.key!==`Enter`||t.shiftKey)return!1;let{$from:n}=e.state.selection;if(!n.parent.isTextblock||n.parent.content.size!==0||n.depth===0)return!1;let r=n.index(n.depth-1),i=n.node(n.depth-1);return[i.maybeChild(r-1),i.maybeChild(r+1)].some(e=>e?.isTextblock&&e.content.size===0)}}})]}}),F=c.Extension.create({name:`ophxIndentation`,addGlobalAttributes(){return[{types:[`paragraph`,`heading`],attributes:{indent:{default:0,parseHTML:e=>Number(e.dataset.indent??0),renderHTML:e=>e.indent?{"data-indent":String(e.indent)}:{}}}}]}}),I=c.Extension.create({name:`ophxEditability`,addGlobalAttributes(){return[{types:[`paragraph`,`heading`,`blockquote`,`codeBlock`,`bulletList`,`orderedList`,`listItem`,`horizontalRule`,`image`,`table`,`tableRow`,`tableHeader`,`tableCell`,`bold`,`italic`,`underline`,`strike`,`link`,`label`],attributes:{contenteditable:{default:null,parseHTML:e=>{let t=e.getAttribute(`contenteditable`);return t===`true`||t===`false`?t:null},renderHTML:e=>e.contenteditable?{contenteditable:e.contenteditable}:{}}}}]}});function L(e){return w.test(e)}var R=class{editor;constructor(e){this.editor=new c.Editor({element:e.element,content:e.content??`<p></p>`,editable:e.editable??!0,extensions:[l.default.configure({heading:!1,underline:!1,link:!1}),A.configure({levels:[1,2,3,4,5,6]}),u.default,d.default.configure({openOnClick:!1,autolink:!0,linkOnPaste:!0}),f.default.configure({allowBase64:!0,inline:!1}),m.Table.configure({resizable:!0}),h.TableRow,g.TableHeader,_.TableCell,T,E,O,D,S,F,I,M,P,p.default.configure({placeholder:e.placeholder??`Начните писать…`}),...e.extensions??[]],editorProps:{attributes:{class:`ophx-editor-surface`,spellcheck:`true`,lang:`ru`,"aria-label":`Редактор документа`}},onUpdate:e.onUpdate})}getHTML(){return this.editor.getHTML()}getJSON(){return this.editor.getJSON()}setContent(e){this.editor.commands.setContent(e,{emitUpdate:!1})}setSearchQuery(e){this.editor.commands.setSearchQuery(e)}getFields(){let e=Array.from(this.editor.view.dom.querySelectorAll(`[data-ophx-field]`)).filter(e=>{let t=e.getAttribute(`data-ophx-field`)??``;return e.classList.contains(`ophx-field`)&&e.getAttribute(`contenteditable`)===`true`&&L(t)}),t=new Map;for(let n of e){let e=n.getAttribute(`data-ophx-field`);t.set(e,(t.get(e)??0)+1)}return e.filter(e=>t.get(e.getAttribute(`data-ophx-field`))===1).map(e=>({id:e.getAttribute(`data-ophx-field`),label:e.getAttribute(`data-ophx-label`),required:e.getAttribute(`data-ophx-required`)===`true`,value:e.textContent??``}))}getMissingRequiredFields(){return this.getFields().filter(e=>e.required&&!e.value.trim())}destroy(){this.editor.destroy()}},ee=[{action:`bold`,label:`B`,title:`Жирный (⌘/Ctrl+B)`},{action:`italic`,label:`I`,title:`Курсив (⌘/Ctrl+I)`},{action:`underline`,label:`U`,title:`Подчёркнутый (⌘/Ctrl+U)`},{action:`strike`,label:`S̶`,title:`Зачёркнутый`},{action:`bullet`,label:`• Список`,title:`Маркированный список`},{action:`ordered`,label:`1. Список`,title:`Нумерованный список`},{action:`outdent`,label:`← Отступ`,title:`Уменьшить отступ`},{action:`indent`,label:`Отступ →`,title:`Увеличить отступ`},{action:`link`,label:`Ссылка`,title:`Добавить ссылку`},{action:`image`,label:`Изображение`,title:`Вставить изображение`},{action:`table`,label:`Таблица`,title:`Вставить таблицу`},{action:`collapse`,label:`Свернуть`,title:`Свернуть/развернуть раздел`},{action:`label`,label:`Метка`,title:`Выделить текст меткой`},{action:`toc`,label:`Содержание`,title:`Содержание документа`},{action:`undo`,label:`↶`,title:`Отменить (⌘/Ctrl+Z)`},{action:`redo`,label:`↷`,title:`Повторить (⌘/Ctrl+Shift+Z)`}];function te(e,t,n={}){e.classList.add(`ophx-toolbar`),e.replaceChildren();let r=document.createElement(`select`);r.className=`ophx-toolbar__style`,r.setAttribute(`aria-label`,`Стиль текста`),r.innerHTML=`<option value="p">Обычный текст</option><option value="1">Заголовок 1</option><option value="2">Заголовок 2</option><option value="3">Заголовок 3</option>`,r.addEventListener(`change`,()=>{let e=Number(r.value),n=t.chain().focus();r.value===`p`?n.setParagraph().run():n.setHeading({level:e}).run()}),e.append(r);let i=new Map;for(let n of ee){let r=document.createElement(`button`);r.type=`button`,r.className=`ophx-toolbar__button`,r.dataset.action=n.action,r.textContent=n.label,r.title=n.title,r.setAttribute(`aria-label`,n.title),r.addEventListener(`mousedown`,e=>e.preventDefault()),r.addEventListener(`click`,()=>z(n.action,t,o,s)),i.set(n.action,r),e.append(r)}let a=document.createElement(`input`);a.type=`search`,a.className=`ophx-toolbar__search`,a.placeholder=`Поиск…`,a.setAttribute(`aria-label`,`Поиск по тексту`),a.addEventListener(`input`,()=>t.commands.setSearchQuery(a.value)),a.addEventListener(`keydown`,e=>{e.key===`Escape`&&(a.value=``,t.commands.setSearchQuery(``),t.commands.focus())}),e.append(a);let o=document.createElement(`input`);o.type=`file`,o.accept=`image/*`,o.hidden=!0,o.addEventListener(`change`,()=>{let e=o.files?.[0];if(e){if(n.onInsertImage)n.onInsertImage(e);else{let n=new FileReader;n.addEventListener(`load`,()=>{typeof n.result==`string`&&t.chain().focus().setImage({src:n.result,alt:e.name}).run()}),n.readAsDataURL(e)}}o.value=``}),e.append(o);let s=document.createElement(`nav`);s.className=`ophx-toc`,s.setAttribute(`aria-label`,`Содержание документа`),s.hidden=!0,e.append(s);let c=()=>{i.get(`undo`).disabled=!t.can().undo(),i.get(`redo`).disabled=!t.can().redo(),i.get(`bold`).classList.toggle(`is-active`,t.isActive(`bold`)),i.get(`italic`).classList.toggle(`is-active`,t.isActive(`italic`)),i.get(`underline`).classList.toggle(`is-active`,t.isActive(`underline`)),i.get(`strike`).classList.toggle(`is-active`,t.isActive(`strike`));let e=t.getAttributes(`heading`).level;r.value=e?String(e):(t.isActive(`paragraph`),`p`),s.replaceChildren(),t.state.doc.descendants((e,n)=>{if(e.type.name!==`heading`)return;let r=document.createElement(`button`);r.type=`button`,r.className=`ophx-toc__item`,r.style.paddingInlineStart=`${Math.max(0,Number(e.attrs.level)-1)*12}px`,r.textContent=e.textContent||`Без названия`,r.addEventListener(`mousedown`,e=>e.preventDefault()),r.addEventListener(`click`,()=>{t.commands.setTextSelection(n+1),t.commands.focus(),t.commands.scrollIntoView()}),s.append(r)})};return t.on(`transaction`,c),t.on(`selectionUpdate`,c),c(),()=>{t.off(`transaction`,c),t.off(`selectionUpdate`,c),e.replaceChildren(),e.classList.remove(`ophx-toolbar`)}}function z(e,t,n,r){let i=t.chain().focus();switch(e){case`bold`:i.toggleBold().run();break;case`italic`:i.toggleItalic().run();break;case`underline`:i.toggleUnderline().run();break;case`strike`:i.toggleStrike().run();break;case`bullet`:i.toggleBulletList().run();break;case`ordered`:i.toggleOrderedList().run();break;case`indent`:B(t,1);break;case`outdent`:B(t,-1);break;case`link`:{let e=t.getAttributes(`link`).href,n=window.prompt(`Адрес ссылки`,e??`https://`);if(n===null)return;n.trim()?i.extendMarkRange(`link`).setLink({href:n.trim()}).run():i.extendMarkRange(`link`).unsetLink().run();break}case`image`:n.click();break;case`table`:i.insertTable({rows:3,cols:3,withHeaderRow:!0}).run();break;case`collapse`:{if(!t.isActive(`heading`))return;let e=!!t.getAttributes(`heading`).collapsed;i.updateAttributes(`heading`,{collapsed:!e}).run();break}case`label`:{let e=window.prompt(`Название метки`,`важно`);e?.trim()&&i.setMark(`label`,{label:e.trim()}).run();break}case`toc`:r.hidden=!r.hidden;break;case`undo`:i.undo().run();break;case`redo`:i.redo().run()}}function B(e,t){let{$from:n}=e.state.selection;if(n.parent.type.name===`listItem`){let n=e.chain().focus();t>0?n.sinkListItem(`listItem`).run():n.liftListItem(`listItem`).run();return}let r=n.parent.type.name;if(r!==`paragraph`&&r!==`heading`)return;let i=Number(e.getAttributes(r).indent??0);e.chain().focus().updateAttributes(r,{indent:Math.max(0,Math.min(8,i+t))}).run()}var V={avif:`image/avif`,bmp:`image/bmp`,gif:`image/gif`,ico:`image/x-icon`,jpeg:`image/jpeg`,jpg:`image/jpeg`,png:`image/png`,svg:`image/svg+xml`,tif:`image/tiff`,tiff:`image/tiff`,webp:`image/webp`};async function H(e,t={}){let n=new x.default,r=new DOMParser().parseFromString(e,`text/html`);K(r);let i=q(t.filename??t.title??`Документ`),a=t.title??r.title??i,o=t.assets??{},s=new Set(Object.keys(o).map(Y)),c=0;for(let e of Array.from(r.querySelectorAll(`img[src]`))){let t=e.getAttribute(`src`)?.trim();if(!t||/^https?:\/\//i.test(t)||/^data:(?!image\/)/i.test(t))continue;if(/^data:image\//i.test(t)||/^blob:/i.test(t)){try{let r=await fetch(t);if(!r.ok)throw Error(`Не удалось прочитать изображение: ${t}`);let i=await r.blob();if(!i.type.startsWith(`image/`))throw Error(`Вложение с data/blob URL не является изображением`);let a=`image-${++c}.${Q(i.type,t)}`;for(;s.has(a);)a=`image-${++c}.${Q(i.type,t)}`;n.file(a,i),s.add(a),e.setAttribute(`src`,encodeURIComponent(a))}catch(e){throw Error(`Не удалось добавить изображение в архив ophx`,{cause:e})}continue}let r=Y(t);if(!J(r))throw Error(`Путь изображения должен быть простым именем файла: ${t}`);let i=o[r];if(!i&&!s.has(r))throw Error(`Для локального изображения «${r}» не передан файл media`);i&&n.file(r,i),e.setAttribute(`src`,encodeURIComponent(r))}let l=G(t.styles??{});for(let[e,t]of Object.entries(l))n.file(e,t);let u=Object.keys(l).map(e=>`<link rel="stylesheet" href="${$(encodeURIComponent(e))}">`).join(``),d=r.documentElement.lang||`ru`,f=t.bodyContentEditable?` contenteditable="${t.bodyContentEditable}"`:``,p=`<!doctype html>\n<html lang="${$(d)}"><head><meta charset="utf-8"><title>${$(a)}</title>${u}</head><body${f}>${r.body.innerHTML}</body></html>`;return n.file(`${i}.html`,p),n.generateAsync({type:`blob`,mimeType:`application/zip`,compression:`DEFLATE`,compressionOptions:{level:6}})}async function U(e){await Z(e);let t;try{t=await x.default.loadAsync(e)}catch(e){throw Error(`Не удалось прочитать ZIP-архив .ophx (шифрование и неподдерживаемые ZIP-режимы не поддерживаются)`,{cause:e})}if(Object.values(t.files).some(e=>e.dir||/[\\/]/.test(e.name)))throw Error(`В текущей версии .ophx все файлы должны находиться плоским списком в корне архива`);let n=Object.values(t.files).filter(e=>!e.dir),r=n.filter(e=>e.name.toLowerCase().endsWith(`.html`));if(r.length!==1)throw r.length===0&&n.some(e=>e.name.toLowerCase().endsWith(`.xml`))?Error(`Обнаружен устаревший XML-вариант ophx; поддерживается только версия v2 с HTML5 внутри ZIP`):Error(`В архиве должен быть ровно один HTML-файл в корне; найдено: ${r.length}`);let i=r[0],a=i.name.slice(0,-5),o=`name`in e&&typeof e.name==`string`?e.name:``;if(/\.ophx$/i.test(o)&&o.replace(/\.ophx$/i,``)!==a)throw Error(`Имя HTML-файла «${i.name}» должно совпадать с именем архива «${o}»`);let s=await i.async(`string`);if(!/^\s*<!doctype\s+html\b/i.test(s))throw Error(`Основной файл .ophx должен начинаться с <!doctype html>`);let c=new DOMParser().parseFromString(s,`text/html`);if(c.querySelector(`head meta[charset]`)?.getAttribute(`charset`)?.toLowerCase()!==`utf-8`)throw Error(`В <head> документа требуется <meta charset="utf-8">`);let l={};for(let e of Array.from(c.querySelectorAll(`head link[rel~="stylesheet"][href]`))){let n=e.getAttribute(`href`)?.trim();if(!n)continue;if(X(n))throw Error(`CSS должен быть встроенным плоским файлом архива: ${n}`);let r=Y(n);if(!J(r)||!r.toLowerCase().endsWith(`.css`))throw Error(`Ссылка на стиль должна указывать на плоский .css-файл: ${n}`);let i=t.file(r);if(!i)throw Error(`Файл стиля «${r}» отсутствует в архиве`);l[r]=await i.async(`string`)}for(let e of Array.from(c.querySelectorAll(`img[src]`))){let n=e.getAttribute(`src`)?.trim();if(!n||X(n))continue;let r=Y(n);if(!J(r))throw Error(`Ссылка на изображение должна быть простым именем файла: ${n}`);let i=t.file(r);if(!i)throw Error(`Файл изображения «${r}» отсутствует в архиве`);let a=await i.async(`base64`),o=r.split(`.`).pop()?.toLowerCase()??``,s=V[o]??(o?`image/${o}`:`application/octet-stream`);e.setAttribute(`src`,`data:${s};base64,${a}`)}let u=c.body.getAttribute(`contenteditable`);if(u===`false`)for(let e of Array.from(c.body.children))e.hasAttribute(`contenteditable`)||e.setAttribute(`contenteditable`,`false`);ne(c);let d=Object.entries(l);return{html:c.body.innerHTML,title:c.title||a,filename:a,styles:l,css:d[0]?.[1]??``,bodyContentEditable:u===`true`||u===`false`?u:void 0}}function W(e,t){let n=URL.createObjectURL(e),r=window.document.createElement(`a`);r.href=n,r.download=t,r.click(),window.setTimeout(()=>URL.revokeObjectURL(n),0)}function G(e){let t={};for(let[n,r]of Object.entries(e)){let e=Y(n);if(!J(e)||!e.toLowerCase().endsWith(`.css`))throw Error(`Имя файла стиля должно быть плоским и оканчиваться на .css: ${n}`);if(Object.hasOwn(t,e))throw Error(`Повторяется имя файла стиля: ${e}`);t[e]=r}return t}function K(e){let t=Array.from(e.querySelectorAll(`[data-ophx-field], .ophx-field`)),n=new Set;for(let e of t){let t=e.getAttribute(`data-ophx-field`)??``,r=e.tagName.toLowerCase();if(!e.classList.contains(`ophx-field`)||e.getAttribute(`contenteditable`)!==`true`)throw Error(`Каждое поле должно иметь класс ophx-field и contenteditable="true"`);if(![`span`,`div`,`p`].includes(r)||!/^[A-Za-z0-9._-]+$/.test(t))throw Error(`Некорректная обёртка или идентификатор поля: ${t||`(пусто)`}`);if(n.has(t))throw Error(`Идентификатор поля должен быть уникальным: ${t}`);if(e.parentElement?.closest(`[data-ophx-field]`))throw Error(`Поля не могут быть вложенными: ${t}`);n.add(t)}}function q(e){let t=e.trim().replace(/\.(?:ophx|html)$/i,``);if(!t)return`Документ`;if(!J(t)||/[\u0000-\u001f]/.test(t))throw Error(`Имя .ophx должно быть простым именем файла без каталогов`);return t}function J(e){return!!e&&e!==`.`&&e!==`..`&&!/[\\/]/.test(e)}function Y(e){try{return decodeURIComponent(e)}catch{throw Error(`Некорректное percent-экранирование в имени ресурса: ${e}`)}}function X(e){return/^(?:data:|https?:\/\/)/i.test(e)}async function Z(e){if(e.size<2)throw Error(`Файл не является ZIP-архивом .ophx (возможен устаревший формат)`);let t=new Uint8Array(await e.slice(0,4).arrayBuffer());if(t[0]!==80||t[1]!==75)throw Error(`Неподдерживаемый устаревший ophx: ожидается ZIP-архив версии v2`)}function Q(e,t){let n=Object.entries(V).find(([,t])=>t===e)?.[0];if(n)return n;let r=t.match(/\.([a-z0-9]+)(?:[?#]|$)/i)?.[1]?.toLowerCase();return r&&/^[a-z0-9]{1,6}$/.test(r)?r:`bin`}function ne(e){e.querySelectorAll(`script, iframe, object, embed, style, link`).forEach(e=>e.remove()),e.querySelectorAll(`*`).forEach(e=>{for(let t of Array.from(e.attributes))/^on/i.test(t.name)&&e.removeAttribute(t.name),/^(href|src|xlink:href)$/i.test(t.name)&&/^\s*(javascript|vbscript):/i.test(t.value)&&e.removeAttribute(t.name),/^(src|xlink:href)$/i.test(t.name)&&/^\s*data:(?!image\/)/i.test(t.value)&&e.removeAttribute(t.name)})}function $(e){return e.replace(/[&<>"']/g,e=>({"&":`&`,"<":`<`,">":`>`,'"':`"`,"'":`'`})[e])}function re(e,t=`f`){if(!/^[A-Za-z0-9._-]+$/.test(t))throw Error(`Префикс идентификаторов полей содержит недопустимые символы`);let n=new DOMParser().parseFromString(e,`text/html`),r=new Set(Array.from(n.querySelectorAll(`[data-ophx-field]`)).map(e=>e.getAttribute(`data-ophx-field`)??``).filter(Boolean)),i=n.createTreeWalker(n.body,4),a=[];for(;i.nextNode();){let e=i.currentNode,t=e.parentElement;t&&!t.closest(`code, pre, script, style, [contenteditable="false"], [data-ophx-field]`)&&/_{2,}/.test(e.data)&&a.push(e)}let o=[],s=1;for(let e of a){let i=n.createDocumentFragment(),a=0;for(let c of e.data.matchAll(/_{2,}/g)){if(c.index===void 0)continue;c.index>a&&i.append(n.createTextNode(e.data.slice(a,c.index)));let l=`${t}${s++}`;for(;r.has(l);)l=`${t}${s++}`;r.add(l),o.push(l);let u=n.createElement(`span`);u.className=`ophx-field`,u.setAttribute(`contenteditable`,`true`),u.setAttribute(`data-ophx-field`,l),i.append(u),a=c.index+c[0].length}a<e.data.length&&i.append(n.createTextNode(e.data.slice(a))),e.replaceWith(i)}return{html:n.body.innerHTML,count:o.length,ids:o}}exports.CollapsibleHeading=A,exports.Indentation=F,exports.Label=S,exports.OphxBlockField=E,exports.OphxEditability=I,exports.OphxEditor=R,exports.OphxInlineField=T,exports.OphxReadonlyBlock=O,exports.OphxReadonlyInline=D,exports.TextSearch=M,exports.WysiwymRules=P,exports.convertUnderscoresToFields=re,exports.downloadBlob=W,exports.exportOphx=H,exports.importOphx=U,exports.isValidOphxFieldId=L,exports.mountToolbar=te;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
.ophx-editor-surface{color:#24262d;overflow-wrap:anywhere;background:#fff;outline:0;min-height:480px;padding:clamp(24px,5vw,64px) clamp(22px,8vw,100px);font:16px/1.75 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.ophx-editor-surface>:first-child{margin-top:0}.ophx-editor-surface p{margin:0 0 .8em}.ophx-editor-surface h1,.ophx-editor-surface h2,.ophx-editor-surface h3,.ophx-editor-surface h4,.ophx-editor-surface h5,.ophx-editor-surface h6{color:#191b23;margin:1.4em 0 .5em;scroll-margin-top:24px;line-height:1.25}.ophx-editor-surface h1{font-size:2.15em}.ophx-editor-surface h2{font-size:1.7em}.ophx-editor-surface h3{font-size:1.35em}.ophx-editor-surface [data-indent="1"]{margin-left:2em}.ophx-editor-surface [data-indent="2"]{margin-left:4em}.ophx-editor-surface [data-indent="3"]{margin-left:6em}.ophx-editor-surface [data-indent="4"]{margin-left:8em}.ophx-editor-surface [data-indent="5"]{margin-left:10em}.ophx-editor-surface [data-indent="6"]{margin-left:12em}.ophx-editor-surface [data-indent="7"]{margin-left:14em}.ophx-editor-surface [data-indent="8"]{margin-left:16em}.ophx-editor-surface ul,.ophx-editor-surface ol{padding-left:1.6em}.ophx-editor-surface a{color:#4359d6;text-underline-offset:3px;text-decoration:underline}.ophx-editor-surface img{border-radius:6px;max-width:100%;height:auto;display:block}.ophx-editor-surface table{border-collapse:collapse;table-layout:fixed;width:100%;margin:1em 0}.ophx-editor-surface th,.ophx-editor-surface td{vertical-align:top;border:1px solid #dfe1e8;padding:8px 10px}.ophx-editor-surface th{background:#f5f6fa;font-weight:650}.ophx-editor-surface mark{background:#fff1a8;border-radius:3px;padding:0 2px}.ophx-editor-surface mark[data-label=важно]{background:#ffd9d9}.ophx-editor-surface mark[data-label=идея]{background:#d9f4e3}.ophx-editor-surface .ophx-field{border-bottom:1px dotted #888;outline:none;min-width:4em}.ophx-editor-surface .ophx-field:focus{border-bottom-style:solid}.ophx-editor-surface .ophx-field:empty:before{content:attr(data-ophx-label);color:#aaa}.ophx-editor-surface [data-collapsed=true]:after{content:" ▸";color:#8b91a2;font-size:.7em}.ophx-editor-surface .ophx-collapsed-content{display:none}.ophx-editor-surface .ophx-search-match{background:#ffe580;border-radius:2px}.ophx-editor-surface .is-empty:before{content:attr(data-placeholder);color:#a4a8b3;float:left;pointer-events:none;height:0}.ophx-toolbar{background:#fff;border-bottom:1px solid #e6e8ed;flex-wrap:wrap;align-items:center;gap:5px;padding:10px 12px;font:13px/1.4 Inter,ui-sans-serif,system-ui,sans-serif;display:flex;position:relative}.ophx-toolbar__style,.ophx-toolbar__button{color:#3a3d48;min-height:34px;font:inherit;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;padding:6px 9px}.ophx-toolbar__style{border-color:#e6e8ed;margin-right:4px}.ophx-toolbar__button:hover,.ophx-toolbar__button.is-active{background:#f1f3fa;border-color:#e1e4ee}.ophx-toolbar__button:disabled{opacity:.38;cursor:default}.ophx-toolbar__button:focus-visible,.ophx-toolbar__style:focus-visible{outline-offset:1px;outline:2px solid #5868db}.ophx-toc{z-index:3;background:#fff;border:1px solid #e4e6ed;border-radius:9px;min-width:220px;max-width:min(360px,90vw);max-height:320px;padding:8px;display:grid;position:absolute;top:calc(100% + 4px);right:12px;overflow:auto;box-shadow:0 12px 30px #1820441f}.ophx-toc[hidden]{display:none}.ophx-toc__item{text-align:left;color:#373a46;cursor:pointer;background:0 0;border:0;border-radius:5px;padding:7px 8px}.ophx-toc__item:hover{background:#f2f4fa}
|
|
2
|
+
/*$vite$:1*/
|