@phenix1024/editor 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,189 +1,284 @@
1
- # @ophx/editor
1
+ # @phenix1024/editor
2
2
 
3
- ## Установка
3
+ Редактор документов для браузера на базе [Tiptap](https://tiptap.dev). Работает без фреймворков (подойдут React, Vue, Svelte, Angular или чистый JS), ему нужен только DOM-элемент. Документы сохраняются в переносимый архив `.ophx` (HTML + CSS + изображения в одном ZIP).
4
+
5
+ - Заголовки, списки, отступы, таблицы, изображения с обтеканием, ссылки и цветные метки.
6
+ - Markdown-подобный ввод: `# ` даёт заголовок, `- ` маркированный список, `1. ` нумерованный.
7
+ - Готовая панель инструментов: стиль текста, поиск с подсветкой, содержание, отмена и повтор.
8
+ - Режим просмотра, который запоминается между перезагрузками страницы.
9
+ - Поля шаблонов: заполняемые места в документе, сбор значений, проверка обязательных полей.
4
10
 
5
- ### npm + любой сборщик
11
+ ## Установка
6
12
 
7
13
  ```bash
8
14
  npm i @phenix1024/editor
9
15
  ```
10
16
 
17
+ ## Быстрый старт
18
+
19
+ ```html
20
+ <div id="toolbar"></div>
21
+ <div id="editor"></div>
22
+ ```
23
+
11
24
  ```ts
12
- import { OphxEditor, mountToolbar } from '@phenix1024/editor'
25
+ import { OphxEditor } from '@phenix1024/editor'
13
26
  import '@phenix1024/editor/style.css'
14
27
 
15
28
  const editor = new OphxEditor({
16
- element: document.querySelector('#editor')!,
17
- content: '<h1>Документ</h1><p>Текст…</p>',
29
+ element: '#editor', // элемент или CSS-селектор
30
+ toolbar: '#toolbar', // необязательно: встроенная панель инструментов
31
+ content: '<h1>Документ</h1><p>Начните писать…</p>',
32
+ onUpdate: () => localStorage.setItem('draft', editor.getHTML()),
18
33
  })
34
+ ```
35
+
36
+ Готово: редактор с панелью работает. Когда редактор больше не нужен, вызовите `editor.destroy()`. Он уберёт и панель.
37
+
38
+ ## Частые задачи
39
+
40
+ ### Получить и задать содержимое
19
41
 
20
- mountToolbar(document.querySelector('#toolbar')!, editor.editor)
42
+ ```ts
43
+ const html = editor.getHTML() // для автосохранения и отправки на сервер
44
+ const json = editor.getJSON() // структура документа Tiptap
45
+ const text = editor.getText() // простой текст
46
+
47
+ editor.setContent('<p>Новый текст</p>') // onUpdate не вызывается
48
+ editor.setContent('<p>Новый текст</p>', { emitUpdate: true })
49
+ editor.clearContent()
21
50
  ```
22
51
 
23
- ### Vue 3 + TypeScript
52
+ ### Режим просмотра
24
53
 
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'
54
+ ```ts
55
+ editor.setEditable(false) // только чтение
56
+ editor.setEditable(true) // снова редактирование
57
+ editor.isEditable // текущий режим
58
+ ```
30
59
 
31
- let editor: OphxEditor | null = null
32
- let unmountToolbar: (() => void) | null = null
60
+ Режим сохраняется в `localStorage` и восстанавливается после перезагрузки. В режиме просмотра панель отключает инструменты форматирования, а поиск и содержание остаются доступными.
33
61
 
34
- onMounted(() => {
35
- editor = new OphxEditor({ element: document.querySelector('#editor')! })
36
- unmountToolbar = mountToolbar(document.querySelector('#toolbar')!, editor.editor)
37
- })
62
+ | Опция | Поведение |
63
+ | --- | --- |
64
+ | `editable: false` | Начальный режим, если сохранённого значения ещё нет. |
65
+ | `editableStorageKey: 'my-key'` | Свой ключ `localStorage`, например отдельный для каждого документа. |
66
+ | `editableStorageKey: null` | Не запоминать режим: всегда используется `editable`. |
67
+
68
+ ### Сохранить и открыть `.ophx`
38
69
 
39
- onBeforeUnmount(() => {
40
- unmountToolbar?.()
41
- editor?.destroy()
70
+ ```ts
71
+ // Скачать файл в браузере
72
+ await editor.downloadOphx({ filename: 'Договор', title: 'Договор поставки' })
73
+
74
+ // Или получить Blob, например чтобы отправить на сервер
75
+ const blob = await editor.saveOphx({ filename: 'Договор' })
76
+
77
+ // Открыть файл, выбранный пользователем
78
+ input.addEventListener('change', async () => {
79
+ const file = input.files?.[0]
80
+ if (!file) return
81
+ const doc = await editor.loadOphx(file) // { title, filename, styles, css, html, ... }
82
+ console.log(doc.title)
42
83
  })
43
- </script>
44
84
  ```
45
85
 
46
- ### CDN без сборщика
86
+ После `loadOphx()` имя, заголовок и стили архива запоминаются. Следующий `saveOphx()` или `downloadOphx()` без параметров сохранит документ с теми же метаданными. Параметры, переданные явно, имеют приоритет.
47
87
 
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
- ```
88
+ ### Свои действия поверх Tiptap
56
89
 
57
- Аналогично для jsDelivr:
90
+ `editor.editor` — это исходный экземпляр Tiptap `Editor`. Через него доступны все команды и события:
58
91
 
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>
92
+ ```ts
93
+ editor.editor.chain().focus().toggleBold().run()
94
+ editor.editor.on('selectionUpdate', () => { /* ... */ })
62
95
  ```
63
96
 
64
- > Для CDN лучше жёстко фиксировать версию, чтобы обновления не ломали существующий код.
97
+ Дополнительные расширения Tiptap передаются через `extensions: [...]`.
65
98
 
66
- Переносимый WYSIWYM-редактор на Tiptap. Ядро не зависит от React, Vue, Svelte или другого UI-фреймворка: ему нужен только DOM-контейнер. Панель инструментов опциональна — можно использовать готовую `mountToolbar()` или построить собственный интерфейс поверх публичного Tiptap instance.
99
+ ### Загрузка изображений на сервер
67
100
 
68
- ## Возможности
101
+ По умолчанию панель вставляет изображения как `data:` URL, а при сохранении они упаковываются в `.ophx`. Чтобы загружать их на свой сервер:
69
102
 
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`, сбор значений и проверка обязательных полей.
103
+ ```ts
104
+ const editor = new OphxEditor({
105
+ element: '#editor',
106
+ toolbar: '#toolbar',
107
+ toolbarOptions: {
108
+ onInsertImage: async (file) => {
109
+ const url = await uploadToServer(file)
110
+ editor.editor.chain().focus().setImage({ src: url, alt: file.name }).run()
111
+ },
112
+ },
113
+ })
114
+ ```
78
115
 
79
- Проверка орфографии зависит от браузера и установленных словарей. Для удалённых изображений, недоступных из-за CORS, сохраняется исходная внешняя ссылка; локальные/вставленные изображения упаковываются в корень архива.
116
+ ### Метки
80
117
 
81
- ## Разработка
118
+ Выделите текст и нажмите «Метка» на панели. Во всплывающем окне введите название или выберите существующее. Клик по метке показывает её название, а в режиме редактирования там же её можно изменить или удалить. Метки сохраняются как `<mark data-label="...">`. Метки «важно» и «идея» имеют свои цвета, цвета остальных задаются в CSS:
82
119
 
83
- ```sh
84
- npm install
85
- npm run dev
86
- npm run typecheck
87
- npm run build
120
+ ```css
121
+ .ophx-editor-surface mark[data-label="срочно"] { background: #ffe0b3; }
88
122
  ```
89
123
 
90
- `npm run build` собирает демо в `dist-demo/` и библиотеку/standalone-бандлы в `dist/`.
124
+ ### Поля шаблонов
91
125
 
92
- ## Подключение без фреймворка
126
+ Поле — это элемент с классом `ophx-field`, атрибутом `contenteditable="true"` и уникальным `data-ophx-field`:
93
127
 
94
- ```ts
95
- import { OphxEditor, mountToolbar } from '@ophx/editor'
96
- import '@ophx/editor/style.css'
128
+ ```html
129
+ <p>Покупатель: <span class="ophx-field" contenteditable="true"
130
+ data-ophx-field="buyer" data-ophx-label="Покупатель" data-ophx-required="true"></span></p>
131
+ ```
97
132
 
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)
133
+ ```ts
134
+ editor.getFields() // [{ id, label, required, value }]
135
+ editor.getMissingRequiredFields() // незаполненные обязательные поля
104
136
 
105
- // При размонтировании host:
106
- unmountToolbar()
107
- editor.destroy()
137
+ import { convertUnderscoresToFields } from '@phenix1024/editor'
138
+ const { html } = convertUnderscoresToFields('<p>ФИО: ______</p>') // «___» превращается в поля f1, f2, …
108
139
  ```
109
140
 
110
- ## Подключение в React
141
+ Фрагменты с `contenteditable="false"` сохраняются и остаются нередактируемыми.
142
+
143
+ ## Фреймворки
144
+
145
+ ### React
111
146
 
112
147
  ```tsx
113
148
  import { useEffect, useRef } from 'react'
114
- import { OphxEditor, mountToolbar } from '@ophx/editor'
115
- import '@ophx/editor/style.css'
149
+ import { OphxEditor } from '@phenix1024/editor'
150
+ import '@phenix1024/editor/style.css'
116
151
 
117
- export function DocumentEditor() {
118
- const editorHost = useRef<HTMLDivElement>(null)
119
- const toolbarHost = useRef<HTMLDivElement>(null)
152
+ export function DocumentEditor({ initialHtml }: { initialHtml?: string }) {
153
+ const editorRef = useRef<HTMLDivElement>(null)
154
+ const toolbarRef = useRef<HTMLDivElement>(null)
120
155
 
121
156
  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() }
157
+ const editor = new OphxEditor({
158
+ element: editorRef.current!,
159
+ toolbar: toolbarRef.current!,
160
+ content: initialHtml,
161
+ })
162
+ return () => editor.destroy()
126
163
  }, [])
127
164
 
128
- return <><div ref={toolbarHost} /><div ref={editorHost} /></>
165
+ return (
166
+ <>
167
+ <div ref={toolbarRef} />
168
+ <div ref={editorRef} />
169
+ </>
170
+ )
129
171
  }
130
172
  ```
131
173
 
132
- Для Vue/Svelte/Angular используется тот же DOM API в `onMounted`/`onMount`/lifecycle хуке. Не подключайте одновременно встроенный и собственный toolbar.
174
+ ### Vue 3
133
175
 
134
- ## Формат `.ophx`
176
+ ```vue
177
+ <script setup lang="ts">
178
+ import { onMounted, onBeforeUnmount, ref } from 'vue'
179
+ import { OphxEditor } from '@phenix1024/editor'
180
+ import '@phenix1024/editor/style.css'
135
181
 
136
- `.ophx` соответствует v2 спецификации: это обычный ZIP без шифрования и ZIP64; записи создаются с DEFLATE, имена кодируются в UTF-8. Все файлы лежат плоским списком в корне. Внутри должен быть ровно один HTML-файл, его имя совпадает с именем внешнего `.ophx` без расширения. Стили и изображения тоже лежат в корне, без каталогов:
182
+ const editorEl = ref<HTMLElement>()
183
+ const toolbarEl = ref<HTMLElement>()
184
+ let editor: OphxEditor | undefined
137
185
 
138
- ```text
139
- Договор.ophx
140
- ├── Договор.html
141
- ├── default.css
142
- └── image-1.png
186
+ onMounted(() => {
187
+ editor = new OphxEditor({ element: editorEl.value!, toolbar: toolbarEl.value! })
188
+ })
189
+ onBeforeUnmount(() => editor?.destroy())
190
+ </script>
191
+
192
+ <template>
193
+ <div ref="toolbarEl" />
194
+ <div ref="editorEl" />
195
+ </template>
143
196
  ```
144
197
 
145
- ```ts
146
- import { exportOphx, importOphx, downloadBlob } from '@phenix1024/editor'
198
+ Не храните экземпляр `OphxEditor` в `ref()` или `reactive()`: прокси Vue мешают работе Tiptap.
147
199
 
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.
200
+ ### Без сборщика (CDN)
201
+
202
+ ```html
203
+ <link rel="stylesheet" href="https://unpkg.com/@phenix1024/editor@0.2.0/dist/ophx-editor.css">
204
+ <script src="https://unpkg.com/@phenix1024/editor@0.2.0/dist/ophx-editor.iife.js"></script>
205
+
206
+ <div id="toolbar"></div>
207
+ <div id="editor"></div>
208
+ <script>
209
+ const editor = new OphxEditor.OphxEditor({ element: '#editor', toolbar: '#toolbar' })
210
+ </script>
167
211
  ```
168
212
 
169
- Имена в `styles` и `assets` — простые имена файлов, без путей. Изображения с локальными именами можно передать в `assets`; вставленные в редактор `data:image` и `blob:` изображения автоматически превращаются в `image-<номер>.<ext>`. Внешние `http(s)` и `data:` ссылки сохраняются как внешние.
213
+ Через jsDelivr: `https://cdn.jsdelivr.net/npm/@phenix1024/editor@0.2.0/dist/...`. Версию в ссылке лучше фиксировать.
170
214
 
171
- Импорт принимает только ZIP v2 с одним корневым `.html`; старые не-ZIP/XML варианты определяются как неподдерживаемые. Локальные ссылки на CSS и изображения проверяются по точным именам. Скриптовые элементы и event-handler атрибуты удаляются перед передачей HTML редактору. Содержимое `styles` возвращается вызывающему приложению — оно может применить CSS в изолированном контейнере или iframe.
215
+ ## API
172
216
 
173
- ### Поля шаблона
217
+ ### `new OphxEditor(options)`
218
+
219
+ | Опция | Тип | По умолчанию | Описание |
220
+ | --- | --- | --- | --- |
221
+ | `element` | `HTMLElement \| string` | — | Контейнер документа (обязательно). |
222
+ | `toolbar` | `HTMLElement \| string` | — | Контейнер встроенной панели инструментов. |
223
+ | `toolbarOptions` | `{ onInsertImage?(file) }` | — | Настройки панели. |
224
+ | `content` | `string \| JSON` | `'<p></p>'` | Начальное содержимое. |
225
+ | `placeholder` | `string` | `'Начните писать…'` | Подсказка в пустом документе. |
226
+ | `editable` | `boolean` | `true` | Начальный режим, если нет сохранённого. |
227
+ | `editableStorageKey` | `string \| null` | `'ophx-editor-editable'` | Ключ `localStorage` для режима; `null` отключает хранение. |
228
+ | `onUpdate` | `({ editor }) => void` | — | Вызывается при каждом изменении документа. |
229
+ | `extensions` | `Extension[]` | `[]` | Дополнительные расширения Tiptap. |
230
+
231
+ ### Методы и свойства
232
+
233
+ | | |
234
+ | --- | --- |
235
+ | `getHTML()`, `getJSON()`, `getText()` | Содержимое документа. |
236
+ | `setContent(content, { emitUpdate? })`, `clearContent()` | Замена содержимого. |
237
+ | `setEditable(bool)`, `isEditable` | Режим редактирования или просмотра. |
238
+ | `isEmpty` | Пустой ли документ. |
239
+ | `focus(pos?)`, `blur()` | Фокус: `'start'`, `'end'`, `'all'` или позиция. |
240
+ | `undo()`, `redo()`, `canUndo()`, `canRedo()` | История изменений. |
241
+ | `setSearchQuery(query)` | Подсветка найденного текста. |
242
+ | `loadOphx(file)` | Открыть `.ophx`, возвращает `OphxDocument`. |
243
+ | `saveOphx(options?)` | Создать `.ophx` и вернуть `Blob`. |
244
+ | `downloadOphx(options?)` | Скачать `.ophx`. |
245
+ | `getFields()`, `getMissingRequiredFields()` | Поля шаблона. |
246
+ | `destroy()` | Удалить редактор и панель. |
247
+ | `editor` | Исходный Tiptap `Editor`. |
248
+
249
+ Параметры сохранения (`OphxExportOptions`): `filename` (имя без `.ophx`), `title`, `styles` (`{ 'имя.css': 'css…' }`), `assets` (файлы изображений по имени), `bodyContentEditable`.
250
+
251
+ ### Другие экспорты
252
+
253
+ - `mountToolbar(host, tiptapEditor, options?)` — смонтировать панель вручную. Возвращает функцию очистки.
254
+ - `exportOphx(html, options)`, `importOphx(file)`, `downloadBlob(blob, filename)` — работа с архивами без редактора.
255
+ - `convertUnderscoresToFields(html, prefix?)` — превращает `___` в поля шаблона.
256
+ - Расширения Tiptap: `Label`, `CollapsibleHeading`, `Indentation`, `OphxInlineField`, `OphxBlockField`, `OphxReadonlyBlock`, `OphxReadonlyInline`, `OphxEditability`, `TextSearch`, `WysiwymRules`.
174
257
 
175
- В документе сохраняются поля `span` (inline) и `p`/`div` (block) с `.ophx-field`, `contenteditable="true"` и уникальным `data-ophx-field`. Статические фрагменты с `contenteditable="false"` сохраняются; при `body contenteditable="false"` это состояние также переносится в editor-модель. Значения доступны через `editor.getFields()`, обязательные незаполненные поля — через `editor.getMissingRequiredFields()`.
258
+ ## Формат `.ophx`
176
259
 
177
- Для подготовки шаблона helper `convertUnderscoresToFields(html)` заменяет каждую последовательность `_{2,}` в текстовых узлах отдельным пустым полем с ключами `f1`, `f2`, …; кодовые блоки и нередактируемые участки пропускаются.
260
+ Обычный ZIP, все файлы лежат в корне. Внутри ровно один HTML, его имя совпадает с именем архива. Рядом лежат CSS и изображения:
178
261
 
179
- ## API
262
+ ```text
263
+ Договор.ophx
264
+ ├── Договор.html
265
+ ├── default.css
266
+ └── image-1.png
267
+ ```
268
+
269
+ - Вставленные изображения (`data:`, `blob:`) упаковываются как `image-<n>.<ext>`. Внешние `http(s)` ссылки остаются ссылками.
270
+ - При открытии из HTML удаляются `<script>` и обработчики событий (`on*`).
271
+ - CSS из архива возвращается в `doc.styles` и к странице автоматически не применяется. Где его применять, решает приложение.
272
+
273
+ ## Разработка
274
+
275
+ ```sh
276
+ npm install
277
+ npm run dev # демо на http://localhost:5173
278
+ npm run typecheck
279
+ npm run build # демо в dist-demo/, библиотека в dist/
280
+ ```
180
281
 
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.
282
+ ## Лицензия
188
283
 
189
- Интеграционный пакет поставляет Tiptap extensions и JSZip как runtime-зависимости. Подключайте stylesheet явно в приложении, где сборщик не обрабатывает CSS imports автоматически.
284
+ MIT