@3rtools/tyts-embed 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/README.md +59 -0
- package/dist/tyts-embed.js +242 -0
- package/dist/tyts-embed.mjs +237 -0
- package/index.d.ts +156 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# @3rtools/tyts-embed
|
|
2
|
+
|
|
3
|
+
Host SDK for embedding the [ТИЦЬ (Tyts)](https://github.com/alexkolodko/tyts) annotated-collage
|
|
4
|
+
editor in an iframe and driving it over postMessage. Zero dependencies, one file.
|
|
5
|
+
|
|
6
|
+
Host SDK для вбудовування редактора ТИЦЬ в iframe і керування ним через postMessage.
|
|
7
|
+
Без залежностей, один файл.
|
|
8
|
+
|
|
9
|
+
## Встановлення
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i @3rtools/tyts-embed
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { TytsEmbed } from '@3rtools/tyts-embed';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Або без збирача — класичний скрипт, який ставить `window.TytsEmbed`:
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<script src="https://unpkg.com/@3rtools/tyts-embed/dist/tyts-embed.js"></script>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Сам редактор — окрема статична збірка (`npm run build:embed` у репозиторії ТИЦЬ → `build/embed/`),
|
|
26
|
+
яку ви викладаєте на свій хостинг. SDK і збірка мають однакову версію протоколу (`TytsEmbed.PROTOCOL`).
|
|
27
|
+
|
|
28
|
+
## Використання
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const editor = TytsEmbed.create({
|
|
32
|
+
container: '#tyts', // контейнер має мати висоту або min-height
|
|
33
|
+
src: 'https://app.example/tyts/embed/',
|
|
34
|
+
onChange(doc, { dirty }) { saveButton.disabled = !dirty; },
|
|
35
|
+
onError(err) { console.warn(err.code, err.message); },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await editor.init({ token: 'change-me' }); // токен задається при збірці редактора
|
|
39
|
+
await editor.loadImage(await TytsEmbed.fileToDataUrl(file));
|
|
40
|
+
|
|
41
|
+
const doc = await editor.getDocument(); // JSON для збереження у себе
|
|
42
|
+
const { dataUrl } = await editor.export('png', 2); // 'png' | 'jpeg' | 'pdf'
|
|
43
|
+
await editor.markSaved(); // далі onChange дає dirty:false
|
|
44
|
+
await editor.loadDocument(doc); // відновити
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Усі методи повертають проміси, які резолвляться відповіддю редактора або відхиляються
|
|
48
|
+
обʼєктом `{ code, message }`: `timeout`, `not_initialized`, `origin_denied`, `invalid_token`,
|
|
49
|
+
`protocol_mismatch`, `invalid_document`, `export_failed`, `destroyed`.
|
|
50
|
+
|
|
51
|
+
## Документація
|
|
52
|
+
|
|
53
|
+
- Повний довідник методів, подій, помилок, протоколу і формату документа:
|
|
54
|
+
[docs/API.md](https://github.com/alexkolodko/tyts/blob/main/docs/API.md)
|
|
55
|
+
- Встановлення й викладання збірки редактора, безпека (`VITE_TYTS_EMBED_ALLOWED_ORIGINS`,
|
|
56
|
+
`VITE_TYTS_EMBED_TOKEN`): [docs/embed-api.md](https://github.com/alexkolodko/tyts/blob/main/docs/embed-api.md)
|
|
57
|
+
- Інтерактивний довідник із живими запитами — `api.html` у збірці редактора.
|
|
58
|
+
|
|
59
|
+
Типи (`TytsDocument`, `TytsPin`, `TytsAnchor`, `TytsEmbedInstance`) — в `index.d.ts`.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/* @3rtools/tyts-embed 0.1.0 — Host SDK для вбудовування редактора ТИЦЬ (Tyts) в iframe через postMessage */
|
|
2
|
+
(function () {
|
|
3
|
+
'use strict';
|
|
4
|
+
/**
|
|
5
|
+
* Tyts embed host SDK — load the editor in an iframe and control it via postMessage.
|
|
6
|
+
*
|
|
7
|
+
* Every request resolves when the editor answers (`tyts:initialized`, `tyts:done`,
|
|
8
|
+
* `tyts:document`, `tyts:exportResult`) or rejects with `{ code, message }`.
|
|
9
|
+
* Security (allowed origins, token) is configured when the editor bundle is built,
|
|
10
|
+
* not by the host page.
|
|
11
|
+
*
|
|
12
|
+
* This file is the single source: `build.mjs` emits it as ESM (`tyts-embed.mjs`)
|
|
13
|
+
* and as a classic script that sets `window.TytsEmbed` (`tyts-embed.js`).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const editor = TytsEmbed.create({ container: '#host', src: '/tyts/embed/', onChange(doc, meta) { ... } });
|
|
17
|
+
* await editor.init({ token: 'secret' });
|
|
18
|
+
* await editor.loadImage(await TytsEmbed.fileToDataUrl(file));
|
|
19
|
+
* const png = await editor.export('png');
|
|
20
|
+
* await editor.markSaved();
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
var PROTOCOL = 1;
|
|
24
|
+
var VERSION = '0.1.0';
|
|
25
|
+
var DEFAULT_TIMEOUT_MS = 60000;
|
|
26
|
+
|
|
27
|
+
function createRequestId() {
|
|
28
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();
|
|
29
|
+
return 'req-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveOrigin(src) {
|
|
33
|
+
try {
|
|
34
|
+
return new URL(src, window.location.href).origin;
|
|
35
|
+
} catch {
|
|
36
|
+
return window.location.origin;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function TytsEmbedInstance(options) {
|
|
41
|
+
this._options = options;
|
|
42
|
+
this._iframe = null;
|
|
43
|
+
this._targetOrigin = '*';
|
|
44
|
+
this._ready = false;
|
|
45
|
+
this._initialized = false;
|
|
46
|
+
this._queuedUntilReady = [];
|
|
47
|
+
this._requests = new Map();
|
|
48
|
+
this._onChange = options.onChange || null;
|
|
49
|
+
this._onReady = options.onReady || null;
|
|
50
|
+
this._onError = options.onError || null;
|
|
51
|
+
this._messageHandler = this._handleMessage.bind(this);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
TytsEmbedInstance.prototype._handleMessage = function (event) {
|
|
55
|
+
if (!this._iframe || event.source !== this._iframe.contentWindow) return;
|
|
56
|
+
if (this._targetOrigin !== '*' && event.origin !== this._targetOrigin) return;
|
|
57
|
+
|
|
58
|
+
var data = event.data;
|
|
59
|
+
if (!data || typeof data.type !== 'string' || data.type.indexOf('tyts:') !== 0) return;
|
|
60
|
+
|
|
61
|
+
if (data.type === 'tyts:ready') {
|
|
62
|
+
if (this._ready) return;
|
|
63
|
+
this._ready = true;
|
|
64
|
+
var queued = this._queuedUntilReady;
|
|
65
|
+
this._queuedUntilReady = [];
|
|
66
|
+
for (var i = 0; i < queued.length; i++) this._post(queued[i]);
|
|
67
|
+
if (this._onReady) this._onReady();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (data.type === 'tyts:change') {
|
|
72
|
+
if (this._onChange) this._onChange(data.document, { dirty: !!data.dirty });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var pending = data.requestId ? this._requests.get(data.requestId) : null;
|
|
77
|
+
|
|
78
|
+
if (data.type === 'tyts:error') {
|
|
79
|
+
var error = { code: data.error.code, message: data.error.message };
|
|
80
|
+
if (pending) {
|
|
81
|
+
this._requests.delete(data.requestId);
|
|
82
|
+
pending.reject(error);
|
|
83
|
+
} else if (this._onError) {
|
|
84
|
+
this._onError(error);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!pending) return;
|
|
90
|
+
this._requests.delete(data.requestId);
|
|
91
|
+
|
|
92
|
+
if (data.type === 'tyts:initialized') {
|
|
93
|
+
this._initialized = true;
|
|
94
|
+
pending.resolve();
|
|
95
|
+
} else if (data.type === 'tyts:done') {
|
|
96
|
+
pending.resolve();
|
|
97
|
+
} else if (data.type === 'tyts:document') {
|
|
98
|
+
pending.resolve(data.document);
|
|
99
|
+
} else if (data.type === 'tyts:exportResult') {
|
|
100
|
+
pending.resolve({ format: data.format, mime: data.mime, dataUrl: data.dataUrl });
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
TytsEmbedInstance.prototype._post = function (message) {
|
|
105
|
+
if (!this._iframe || !this._iframe.contentWindow) return;
|
|
106
|
+
this._iframe.contentWindow.postMessage(message, this._targetOrigin);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Send a message with a fresh requestId and resolve on the matching reply. */
|
|
110
|
+
TytsEmbedInstance.prototype._request = function (message, timeoutMs) {
|
|
111
|
+
var self = this;
|
|
112
|
+
var requestId = createRequestId();
|
|
113
|
+
var payload = Object.assign({}, message, { requestId: requestId });
|
|
114
|
+
return new Promise(function (resolve, reject) {
|
|
115
|
+
var timer = setTimeout(function () {
|
|
116
|
+
self._requests.delete(requestId);
|
|
117
|
+
reject({ code: 'timeout', message: 'Request timed out: ' + message.type });
|
|
118
|
+
}, timeoutMs || DEFAULT_TIMEOUT_MS);
|
|
119
|
+
self._requests.set(requestId, {
|
|
120
|
+
resolve: function (value) {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
resolve(value);
|
|
123
|
+
},
|
|
124
|
+
reject: function (err) {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
reject(err);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
if (self._ready) self._post(payload);
|
|
130
|
+
else self._queuedUntilReady.push(payload);
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
TytsEmbedInstance.prototype.mount = function () {
|
|
135
|
+
var container = this._options.container;
|
|
136
|
+
var el = typeof container === 'string' ? window.document.querySelector(container) : container;
|
|
137
|
+
if (!el) throw new Error('TytsEmbed: container not found');
|
|
138
|
+
|
|
139
|
+
var src = this._options.src || './';
|
|
140
|
+
this._targetOrigin = this._options.origin || resolveOrigin(src);
|
|
141
|
+
|
|
142
|
+
var iframe = window.document.createElement('iframe');
|
|
143
|
+
iframe.src = src;
|
|
144
|
+
iframe.title = this._options.title || 'Tyts Editor';
|
|
145
|
+
iframe.style.border = '0';
|
|
146
|
+
iframe.style.width = '100%';
|
|
147
|
+
iframe.style.height = '100%';
|
|
148
|
+
iframe.style.display = 'block';
|
|
149
|
+
iframe.setAttribute('allow', 'clipboard-read; clipboard-write');
|
|
150
|
+
el.innerHTML = '';
|
|
151
|
+
el.appendChild(iframe);
|
|
152
|
+
this._iframe = iframe;
|
|
153
|
+
|
|
154
|
+
window.addEventListener('message', this._messageHandler);
|
|
155
|
+
return this;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Open the session. Resolves once the editor confirms (`tyts:initialized`);
|
|
160
|
+
* the optional `document` is loaded before that.
|
|
161
|
+
* @param {{ token?: string, document?: object, timeoutMs?: number }} [config]
|
|
162
|
+
*/
|
|
163
|
+
TytsEmbedInstance.prototype.init = function (config) {
|
|
164
|
+
config = config || {};
|
|
165
|
+
return this._request({
|
|
166
|
+
type: 'tyts:init',
|
|
167
|
+
protocol: PROTOCOL,
|
|
168
|
+
token: config.token,
|
|
169
|
+
document: config.document,
|
|
170
|
+
}, config.timeoutMs);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
TytsEmbedInstance.prototype.loadDocument = function (document) {
|
|
174
|
+
return this._request({ type: 'tyts:loadDocument', document: document });
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
TytsEmbedInstance.prototype.loadImage = function (dataUrl) {
|
|
178
|
+
return this._request({ type: 'tyts:loadImage', dataUrl: dataUrl });
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
TytsEmbedInstance.prototype.getDocument = function () {
|
|
182
|
+
return this._request({ type: 'tyts:getDocument' }, 30000);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
TytsEmbedInstance.prototype.export = function (format, scale) {
|
|
186
|
+
return this._request({
|
|
187
|
+
type: 'tyts:export',
|
|
188
|
+
format: format || 'png',
|
|
189
|
+
scale: scale == null ? 1 : scale,
|
|
190
|
+
}, 120000);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** Tell the editor the current state is persisted: the next `onChange` reports `dirty: false`. */
|
|
194
|
+
TytsEmbedInstance.prototype.markSaved = function () {
|
|
195
|
+
return this._request({ type: 'tyts:markSaved' });
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
TytsEmbedInstance.prototype.reset = function () {
|
|
199
|
+
return this._request({ type: 'tyts:reset' });
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
TytsEmbedInstance.prototype.destroy = function () {
|
|
203
|
+
window.removeEventListener('message', this._messageHandler);
|
|
204
|
+
this._requests.forEach(function (pending) {
|
|
205
|
+
pending.reject({ code: 'destroyed', message: 'Embed instance destroyed' });
|
|
206
|
+
});
|
|
207
|
+
this._requests.clear();
|
|
208
|
+
this._queuedUntilReady = [];
|
|
209
|
+
if (this._iframe && this._iframe.parentNode) {
|
|
210
|
+
this._iframe.parentNode.removeChild(this._iframe);
|
|
211
|
+
}
|
|
212
|
+
this._iframe = null;
|
|
213
|
+
this._ready = false;
|
|
214
|
+
this._initialized = false;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
function create(options) {
|
|
218
|
+
var instance = new TytsEmbedInstance(options || {});
|
|
219
|
+
return instance.mount();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function fileToDataUrl(file) {
|
|
223
|
+
return new Promise(function (resolve, reject) {
|
|
224
|
+
var reader = new FileReader();
|
|
225
|
+
reader.onload = function () {
|
|
226
|
+
resolve(typeof reader.result === 'string' ? reader.result : '');
|
|
227
|
+
};
|
|
228
|
+
reader.onerror = function () { reject(new Error('Failed to read file')); };
|
|
229
|
+
reader.readAsDataURL(file);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
var TytsEmbed = {
|
|
234
|
+
create: create,
|
|
235
|
+
fileToDataUrl: fileToDataUrl,
|
|
236
|
+
PROTOCOL: PROTOCOL,
|
|
237
|
+
VERSION: VERSION,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
window.TytsEmbed = TytsEmbed;
|
|
242
|
+
})();
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tyts embed host SDK — load the editor in an iframe and control it via postMessage.
|
|
3
|
+
*
|
|
4
|
+
* Every request resolves when the editor answers (`tyts:initialized`, `tyts:done`,
|
|
5
|
+
* `tyts:document`, `tyts:exportResult`) or rejects with `{ code, message }`.
|
|
6
|
+
* Security (allowed origins, token) is configured when the editor bundle is built,
|
|
7
|
+
* not by the host page.
|
|
8
|
+
*
|
|
9
|
+
* This file is the single source: `build.mjs` emits it as ESM (`tyts-embed.mjs`)
|
|
10
|
+
* and as a classic script that sets `window.TytsEmbed` (`tyts-embed.js`).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const editor = TytsEmbed.create({ container: '#host', src: '/tyts/embed/', onChange(doc, meta) { ... } });
|
|
14
|
+
* await editor.init({ token: 'secret' });
|
|
15
|
+
* await editor.loadImage(await TytsEmbed.fileToDataUrl(file));
|
|
16
|
+
* const png = await editor.export('png');
|
|
17
|
+
* await editor.markSaved();
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
var PROTOCOL = 1;
|
|
21
|
+
var VERSION = '0.1.0';
|
|
22
|
+
var DEFAULT_TIMEOUT_MS = 60000;
|
|
23
|
+
|
|
24
|
+
function createRequestId() {
|
|
25
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();
|
|
26
|
+
return 'req-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveOrigin(src) {
|
|
30
|
+
try {
|
|
31
|
+
return new URL(src, window.location.href).origin;
|
|
32
|
+
} catch {
|
|
33
|
+
return window.location.origin;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function TytsEmbedInstance(options) {
|
|
38
|
+
this._options = options;
|
|
39
|
+
this._iframe = null;
|
|
40
|
+
this._targetOrigin = '*';
|
|
41
|
+
this._ready = false;
|
|
42
|
+
this._initialized = false;
|
|
43
|
+
this._queuedUntilReady = [];
|
|
44
|
+
this._requests = new Map();
|
|
45
|
+
this._onChange = options.onChange || null;
|
|
46
|
+
this._onReady = options.onReady || null;
|
|
47
|
+
this._onError = options.onError || null;
|
|
48
|
+
this._messageHandler = this._handleMessage.bind(this);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
TytsEmbedInstance.prototype._handleMessage = function (event) {
|
|
52
|
+
if (!this._iframe || event.source !== this._iframe.contentWindow) return;
|
|
53
|
+
if (this._targetOrigin !== '*' && event.origin !== this._targetOrigin) return;
|
|
54
|
+
|
|
55
|
+
var data = event.data;
|
|
56
|
+
if (!data || typeof data.type !== 'string' || data.type.indexOf('tyts:') !== 0) return;
|
|
57
|
+
|
|
58
|
+
if (data.type === 'tyts:ready') {
|
|
59
|
+
if (this._ready) return;
|
|
60
|
+
this._ready = true;
|
|
61
|
+
var queued = this._queuedUntilReady;
|
|
62
|
+
this._queuedUntilReady = [];
|
|
63
|
+
for (var i = 0; i < queued.length; i++) this._post(queued[i]);
|
|
64
|
+
if (this._onReady) this._onReady();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (data.type === 'tyts:change') {
|
|
69
|
+
if (this._onChange) this._onChange(data.document, { dirty: !!data.dirty });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
var pending = data.requestId ? this._requests.get(data.requestId) : null;
|
|
74
|
+
|
|
75
|
+
if (data.type === 'tyts:error') {
|
|
76
|
+
var error = { code: data.error.code, message: data.error.message };
|
|
77
|
+
if (pending) {
|
|
78
|
+
this._requests.delete(data.requestId);
|
|
79
|
+
pending.reject(error);
|
|
80
|
+
} else if (this._onError) {
|
|
81
|
+
this._onError(error);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!pending) return;
|
|
87
|
+
this._requests.delete(data.requestId);
|
|
88
|
+
|
|
89
|
+
if (data.type === 'tyts:initialized') {
|
|
90
|
+
this._initialized = true;
|
|
91
|
+
pending.resolve();
|
|
92
|
+
} else if (data.type === 'tyts:done') {
|
|
93
|
+
pending.resolve();
|
|
94
|
+
} else if (data.type === 'tyts:document') {
|
|
95
|
+
pending.resolve(data.document);
|
|
96
|
+
} else if (data.type === 'tyts:exportResult') {
|
|
97
|
+
pending.resolve({ format: data.format, mime: data.mime, dataUrl: data.dataUrl });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
TytsEmbedInstance.prototype._post = function (message) {
|
|
102
|
+
if (!this._iframe || !this._iframe.contentWindow) return;
|
|
103
|
+
this._iframe.contentWindow.postMessage(message, this._targetOrigin);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** Send a message with a fresh requestId and resolve on the matching reply. */
|
|
107
|
+
TytsEmbedInstance.prototype._request = function (message, timeoutMs) {
|
|
108
|
+
var self = this;
|
|
109
|
+
var requestId = createRequestId();
|
|
110
|
+
var payload = Object.assign({}, message, { requestId: requestId });
|
|
111
|
+
return new Promise(function (resolve, reject) {
|
|
112
|
+
var timer = setTimeout(function () {
|
|
113
|
+
self._requests.delete(requestId);
|
|
114
|
+
reject({ code: 'timeout', message: 'Request timed out: ' + message.type });
|
|
115
|
+
}, timeoutMs || DEFAULT_TIMEOUT_MS);
|
|
116
|
+
self._requests.set(requestId, {
|
|
117
|
+
resolve: function (value) {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
resolve(value);
|
|
120
|
+
},
|
|
121
|
+
reject: function (err) {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
reject(err);
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
if (self._ready) self._post(payload);
|
|
127
|
+
else self._queuedUntilReady.push(payload);
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
TytsEmbedInstance.prototype.mount = function () {
|
|
132
|
+
var container = this._options.container;
|
|
133
|
+
var el = typeof container === 'string' ? window.document.querySelector(container) : container;
|
|
134
|
+
if (!el) throw new Error('TytsEmbed: container not found');
|
|
135
|
+
|
|
136
|
+
var src = this._options.src || './';
|
|
137
|
+
this._targetOrigin = this._options.origin || resolveOrigin(src);
|
|
138
|
+
|
|
139
|
+
var iframe = window.document.createElement('iframe');
|
|
140
|
+
iframe.src = src;
|
|
141
|
+
iframe.title = this._options.title || 'Tyts Editor';
|
|
142
|
+
iframe.style.border = '0';
|
|
143
|
+
iframe.style.width = '100%';
|
|
144
|
+
iframe.style.height = '100%';
|
|
145
|
+
iframe.style.display = 'block';
|
|
146
|
+
iframe.setAttribute('allow', 'clipboard-read; clipboard-write');
|
|
147
|
+
el.innerHTML = '';
|
|
148
|
+
el.appendChild(iframe);
|
|
149
|
+
this._iframe = iframe;
|
|
150
|
+
|
|
151
|
+
window.addEventListener('message', this._messageHandler);
|
|
152
|
+
return this;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Open the session. Resolves once the editor confirms (`tyts:initialized`);
|
|
157
|
+
* the optional `document` is loaded before that.
|
|
158
|
+
* @param {{ token?: string, document?: object, timeoutMs?: number }} [config]
|
|
159
|
+
*/
|
|
160
|
+
TytsEmbedInstance.prototype.init = function (config) {
|
|
161
|
+
config = config || {};
|
|
162
|
+
return this._request({
|
|
163
|
+
type: 'tyts:init',
|
|
164
|
+
protocol: PROTOCOL,
|
|
165
|
+
token: config.token,
|
|
166
|
+
document: config.document,
|
|
167
|
+
}, config.timeoutMs);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
TytsEmbedInstance.prototype.loadDocument = function (document) {
|
|
171
|
+
return this._request({ type: 'tyts:loadDocument', document: document });
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
TytsEmbedInstance.prototype.loadImage = function (dataUrl) {
|
|
175
|
+
return this._request({ type: 'tyts:loadImage', dataUrl: dataUrl });
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
TytsEmbedInstance.prototype.getDocument = function () {
|
|
179
|
+
return this._request({ type: 'tyts:getDocument' }, 30000);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
TytsEmbedInstance.prototype.export = function (format, scale) {
|
|
183
|
+
return this._request({
|
|
184
|
+
type: 'tyts:export',
|
|
185
|
+
format: format || 'png',
|
|
186
|
+
scale: scale == null ? 1 : scale,
|
|
187
|
+
}, 120000);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** Tell the editor the current state is persisted: the next `onChange` reports `dirty: false`. */
|
|
191
|
+
TytsEmbedInstance.prototype.markSaved = function () {
|
|
192
|
+
return this._request({ type: 'tyts:markSaved' });
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
TytsEmbedInstance.prototype.reset = function () {
|
|
196
|
+
return this._request({ type: 'tyts:reset' });
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
TytsEmbedInstance.prototype.destroy = function () {
|
|
200
|
+
window.removeEventListener('message', this._messageHandler);
|
|
201
|
+
this._requests.forEach(function (pending) {
|
|
202
|
+
pending.reject({ code: 'destroyed', message: 'Embed instance destroyed' });
|
|
203
|
+
});
|
|
204
|
+
this._requests.clear();
|
|
205
|
+
this._queuedUntilReady = [];
|
|
206
|
+
if (this._iframe && this._iframe.parentNode) {
|
|
207
|
+
this._iframe.parentNode.removeChild(this._iframe);
|
|
208
|
+
}
|
|
209
|
+
this._iframe = null;
|
|
210
|
+
this._ready = false;
|
|
211
|
+
this._initialized = false;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
function create(options) {
|
|
215
|
+
var instance = new TytsEmbedInstance(options || {});
|
|
216
|
+
return instance.mount();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function fileToDataUrl(file) {
|
|
220
|
+
return new Promise(function (resolve, reject) {
|
|
221
|
+
var reader = new FileReader();
|
|
222
|
+
reader.onload = function () {
|
|
223
|
+
resolve(typeof reader.result === 'string' ? reader.result : '');
|
|
224
|
+
};
|
|
225
|
+
reader.onerror = function () { reject(new Error('Failed to read file')); };
|
|
226
|
+
reader.readAsDataURL(file);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const TytsEmbed = {
|
|
231
|
+
create: create,
|
|
232
|
+
fileToDataUrl: fileToDataUrl,
|
|
233
|
+
PROTOCOL: PROTOCOL,
|
|
234
|
+
VERSION: VERSION,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export default TytsEmbed;
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Типи host SDK @3rtools/tyts-embed. Формат документа дублює src/types.ts і src/embed/document.ts
|
|
2
|
+
// редактора; при зміні схеми оновлювати обидва місця (див. docs/API.md, розділ 7).
|
|
3
|
+
|
|
4
|
+
export type TytsExportFormat = 'png' | 'jpeg' | 'pdf';
|
|
5
|
+
|
|
6
|
+
export type TytsAnchor =
|
|
7
|
+
| { type: 'point'; x: number; y: number }
|
|
8
|
+
| { type: 'numbering'; start: { x: number; y: number }; end: { x: number; y: number } }
|
|
9
|
+
| { type: 'standalone'; x: number; y: number }
|
|
10
|
+
| { type: 'polygon'; points: Array<{ x: number; y: number }>; rotation?: number }
|
|
11
|
+
| { type: 'polyline'; points: Array<{ x: number; y: number }>; rotation?: number }
|
|
12
|
+
| { type: 'arrow'; start: { x: number; y: number }; end: { x: number; y: number }; control: { x: number; y: number } }
|
|
13
|
+
| { type: 'rectangle'; x: number; y: number; width: number; height: number; rotation?: number }
|
|
14
|
+
| { type: 'ellipse'; cx: number; cy: number; rx: number; ry: number; rotation?: number }
|
|
15
|
+
| { type: 'compass'; cx: number; cy: number; radius: number; rotation?: number }
|
|
16
|
+
| { type: 'text'; x: number; y: number; width: number; height: number; point?: { x: number; y: number } }
|
|
17
|
+
| { type: 'image'; x: number; y: number; width: number; height: number; src: string; aspect: number }
|
|
18
|
+
| { type: 'redact'; x: number; y: number; width: number; height: number; mode: 'blur' | 'pixelate' | 'fill'; strength: number }
|
|
19
|
+
| { type: 'linked'; pinId: string };
|
|
20
|
+
|
|
21
|
+
export interface TytsPin {
|
|
22
|
+
id: string;
|
|
23
|
+
anchor: TytsAnchor;
|
|
24
|
+
/** CSS-колір маркера й лінії. */
|
|
25
|
+
color: string;
|
|
26
|
+
/** Номер виноски, від 1. */
|
|
27
|
+
label: number;
|
|
28
|
+
/** HTML картки (TipTap). Порожній рядок — без тексту. */
|
|
29
|
+
description: string;
|
|
30
|
+
descPos: 'above' | 'below';
|
|
31
|
+
/** data: URL фото в картці або null. */
|
|
32
|
+
image: string | null;
|
|
33
|
+
imageAspect: number | null;
|
|
34
|
+
strokeStyle?: 'solid' | 'dashed';
|
|
35
|
+
fillStyle?: 'filled' | 'none';
|
|
36
|
+
strokeWidthScale?: number;
|
|
37
|
+
leaderPoints?: Array<{ x: number; y: number }>;
|
|
38
|
+
theme?: 'dark' | 'light';
|
|
39
|
+
textBold?: boolean;
|
|
40
|
+
textItalic?: boolean;
|
|
41
|
+
textUnderline?: boolean;
|
|
42
|
+
textStrike?: boolean;
|
|
43
|
+
textFontSize?: number;
|
|
44
|
+
textColor?: string;
|
|
45
|
+
textBackgroundColor?: string | null;
|
|
46
|
+
calloutHidden?: boolean;
|
|
47
|
+
z?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TytsCardPosition {
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TytsDocument {
|
|
58
|
+
version: 1;
|
|
59
|
+
/** data: URL фонового зображення. */
|
|
60
|
+
backgroundImage: string;
|
|
61
|
+
imageSize: { w: number; h: number };
|
|
62
|
+
sourceImageSize: { w: number; h: number } | null;
|
|
63
|
+
pins: TytsPin[];
|
|
64
|
+
/** Позиції карток за pin.id; порожній обʼєкт — редактор розкладе сам. */
|
|
65
|
+
cardPositions: Record<string, TytsCardPosition>;
|
|
66
|
+
settings?: { fontId?: 'road-ui' | 'uaf-sans'; strokeWidthScale?: number };
|
|
67
|
+
meta?: { sourceUrl?: string; sourceTitle?: string; capturedAt?: number };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface TytsEmbedError {
|
|
71
|
+
code:
|
|
72
|
+
| 'timeout'
|
|
73
|
+
| 'not_initialized'
|
|
74
|
+
| 'origin_denied'
|
|
75
|
+
| 'invalid_token'
|
|
76
|
+
| 'protocol_mismatch'
|
|
77
|
+
| 'invalid_document'
|
|
78
|
+
| 'init_failed'
|
|
79
|
+
| 'export_failed'
|
|
80
|
+
| 'handler_error'
|
|
81
|
+
| 'unknown_message'
|
|
82
|
+
| 'destroyed'
|
|
83
|
+
| (string & {});
|
|
84
|
+
message: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface TytsEmbedOptions {
|
|
88
|
+
/** Селектор або елемент; вміст очищається. Контейнер має мати висоту або min-height. */
|
|
89
|
+
container: string | Element;
|
|
90
|
+
/** URL теки embed-збірки (або її index.html). За замовчуванням `./`. */
|
|
91
|
+
src?: string;
|
|
92
|
+
/** Origin бандла для перевірки відповідей; за замовчуванням береться з `src`. */
|
|
93
|
+
origin?: string;
|
|
94
|
+
/** Атрибут title iframe. */
|
|
95
|
+
title?: string;
|
|
96
|
+
/** Iframe завантажився і слухає (один раз). */
|
|
97
|
+
onReady?: () => void;
|
|
98
|
+
/** Стан змінився (після init, з дебаунсом 250 мс, повний документ). */
|
|
99
|
+
onChange?: (document: TytsDocument, meta: { dirty: boolean }) => void;
|
|
100
|
+
/** Помилка без requestId. */
|
|
101
|
+
onError?: (error: TytsEmbedError) => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TytsEmbedInitConfig {
|
|
105
|
+
/** Обовʼязковий, якщо бандл зібрано з VITE_TYTS_EMBED_TOKEN. */
|
|
106
|
+
token?: string;
|
|
107
|
+
/** Початковий документ; застосовується до відповіді. */
|
|
108
|
+
document?: TytsDocument;
|
|
109
|
+
/** Таймаут очікування, мс (типово 60000). */
|
|
110
|
+
timeoutMs?: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface TytsExportResult {
|
|
114
|
+
format: TytsExportFormat;
|
|
115
|
+
mime: string;
|
|
116
|
+
dataUrl: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface TytsEmbedInstance {
|
|
120
|
+
/** Відкрити сесію; резолвиться після tyts:initialized. */
|
|
121
|
+
init(config?: TytsEmbedInitConfig): Promise<void>;
|
|
122
|
+
/** Нове фонове зображення (data: URL); піни скидаються. */
|
|
123
|
+
loadImage(dataUrl: string): Promise<void>;
|
|
124
|
+
/** Відновити документ. Відхиляється з invalid_document, редактор при цьому не змінюється. */
|
|
125
|
+
loadDocument(document: TytsDocument): Promise<void>;
|
|
126
|
+
/** Поточний стан або null, якщо зображення немає. */
|
|
127
|
+
getDocument(): Promise<TytsDocument | null>;
|
|
128
|
+
/** Рендер колажу. scale — множник роздільної здатності (png/jpeg). */
|
|
129
|
+
export(format?: TytsExportFormat, scale?: number): Promise<TytsExportResult>;
|
|
130
|
+
/** Поточний стан вважати збереженим: наступний onChange дає dirty:false. */
|
|
131
|
+
markSaved(): Promise<void>;
|
|
132
|
+
/** Порожній редактор. */
|
|
133
|
+
reset(): Promise<void>;
|
|
134
|
+
/** Прибрати iframe і слухачі; незавершені запити відхиляються з destroyed. */
|
|
135
|
+
destroy(): void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface TytsEmbedStatic {
|
|
139
|
+
create(options: TytsEmbedOptions): TytsEmbedInstance;
|
|
140
|
+
/** File/Blob → data: URL для loadImage. */
|
|
141
|
+
fileToDataUrl(file: Blob): Promise<string>;
|
|
142
|
+
/** Версія postMessage-протоколу; має збігатися з бандлом редактора. */
|
|
143
|
+
readonly PROTOCOL: 1;
|
|
144
|
+
/** Версія пакета. */
|
|
145
|
+
readonly VERSION: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export declare const TytsEmbed: TytsEmbedStatic;
|
|
149
|
+
export default TytsEmbed;
|
|
150
|
+
|
|
151
|
+
declare global {
|
|
152
|
+
interface Window {
|
|
153
|
+
/** Доступно після підключення dist/tyts-embed.js як <script>. */
|
|
154
|
+
TytsEmbed: TytsEmbedStatic;
|
|
155
|
+
}
|
|
156
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@3rtools/tyts-embed",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Host SDK для вбудовування редактора ТИЦЬ (Tyts) в iframe через postMessage",
|
|
5
|
+
"keywords": ["tyts", "embed", "iframe", "postmessage", "annotation", "collage"],
|
|
6
|
+
"homepage": "https://github.com/alexkolodko/tyts#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/alexkolodko/tyts.git",
|
|
10
|
+
"directory": "packages/embed"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/tyts-embed.js",
|
|
14
|
+
"module": "./dist/tyts-embed.mjs",
|
|
15
|
+
"types": "./index.d.ts",
|
|
16
|
+
"unpkg": "./dist/tyts-embed.js",
|
|
17
|
+
"jsdelivr": "./dist/tyts-embed.js",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./index.d.ts",
|
|
21
|
+
"import": "./dist/tyts-embed.mjs",
|
|
22
|
+
"default": "./dist/tyts-embed.js"
|
|
23
|
+
},
|
|
24
|
+
"./tyts-embed.js": "./dist/tyts-embed.js",
|
|
25
|
+
"./tyts-embed.mjs": "./dist/tyts-embed.mjs",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": ["dist", "index.d.ts", "README.md"],
|
|
29
|
+
"sideEffects": ["./dist/tyts-embed.js"],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "node build.mjs",
|
|
32
|
+
"prepack": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
}
|