@shelamkoff/rector 1.0.2 → 1.0.4

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.
@@ -1,41 +1,41 @@
1
- # Рендерер Gallery
2
-
3
- Преобразует сохранённый блок `gallery` в принадлежащий рендереру DOM.
4
- Рендерер Gallery напрямую использует `@shelamkoff/expose` и экспорт пакета `exposeStylesUrl`.
5
-
6
- Синхронная точка входа `@shelamkoff/rector/renderer` включает все встроенные рендереры, поэтому до её импорта установите `@shelamkoff/carousel` и `@shelamkoff/expose`. Значение `blockTypes: []` отключает создание встроенных рендереров, но не меняет правила разрешения модулей ESM.
7
-
8
- ## Использование
9
-
10
- ```js
11
- import { createEditorRenderer } from '@shelamkoff/rector/renderer'
12
- import { createGalleryRenderer } from '@shelamkoff/rector/renderer/renderers/gallery'
13
-
14
- const renderer = createEditorRenderer({ classPrefix: 'article', blockTypes: [] })
15
- renderer.registerRenderer(createGalleryRenderer('article', {}))
16
- const rendererStyles = renderer.injectStyles()
17
- renderer.renderTo(documentData, document.querySelector('#article'))
18
-
19
- // При удалении добавленного результата:
20
- renderer.destroy()
21
- rendererStyles.destroy()
22
- ```
23
-
24
- ## Типичные данные
25
-
26
- ```json
27
- {
28
- "images": [{ "url": "https://cdn.example/a.jpg", "caption": "A" }],
29
- "layout": "auto",
30
- "styles": { "gap": "8px", "borderRadius": "8px", "height": "420px" },
31
- "options": { "loop": true, "zoom": true, "navigation": true, "captions": true, "thumbnails": true, "fullscreen": true, "autoplayInterval": 3000 }
32
- }
33
- ```
34
-
35
- Полный контракт сохранённых данных, включая раскладки, значения по умолчанию и ограничения проверки, описан в [справочнике полей плагина Gallery](../../../plugins/gallery/README.ru.md#поля-данных).
36
-
37
- Адреса изображений проходят политику URL для медиафайлов. Рендерер владеет экземплярами `Expose` и обработчиками готовности изображений; `destroy()` освобождает их. Он объявляет стили галереи и `Expose`.
38
-
39
- Если рендерер объявляет стили, показанный выше явный вызов `EditorRenderer.injectStyles()` подключает их, а возвращённый владелец освобождает.
40
-
41
- Жизненный цикл, восстановление внутристрочных виджетов, стили и границы безопасности описаны в последовательном руководстве VitePress.
1
+ # Рендерер Gallery
2
+
3
+ Преобразует сохранённый блок `gallery` в принадлежащий рендереру DOM.
4
+ Рендерер Gallery напрямую использует `@shelamkoff/expose` и экспорт пакета `exposeStylesUrl`. Для раскладки `masonry` он использует `@shelamkoff/masonry` и освобождает экземпляр библиотеки вместе с принадлежащим рендереру DOM.
5
+
6
+ Синхронная точка входа `@shelamkoff/rector/renderer` включает все встроенные рендереры, поэтому до её импорта установите `@shelamkoff/carousel`, `@shelamkoff/expose` и `@shelamkoff/masonry`. Значение `blockTypes: []` отключает создание встроенных рендереров, но не меняет правила разрешения модулей ESM.
7
+
8
+ ## Использование
9
+
10
+ ```js
11
+ import { createEditorRenderer } from '@shelamkoff/rector/renderer'
12
+ import { createGalleryRenderer } from '@shelamkoff/rector/renderer/renderers/gallery'
13
+
14
+ const renderer = createEditorRenderer({ classPrefix: 'article', blockTypes: [] })
15
+ renderer.registerRenderer(createGalleryRenderer('article', {}))
16
+ const rendererStyles = renderer.injectStyles()
17
+ renderer.renderTo(documentData, document.querySelector('#article'))
18
+
19
+ // При удалении добавленного результата:
20
+ renderer.destroy()
21
+ rendererStyles.destroy()
22
+ ```
23
+
24
+ ## Типичные данные
25
+
26
+ ```json
27
+ {
28
+ "images": [{ "url": "https://cdn.example/a.jpg", "caption": "A" }],
29
+ "layout": "auto",
30
+ "styles": { "gap": "8px", "borderRadius": "8px", "height": "420px" },
31
+ "options": { "loop": true, "zoom": true, "navigation": true, "captions": true, "thumbnails": true, "fullscreen": true, "autoplayInterval": 3000 }
32
+ }
33
+ ```
34
+
35
+ Полный контракт сохранённых данных, включая раскладки, значения по умолчанию и ограничения проверки, описан в [справочнике полей плагина Gallery](../../../plugins/gallery/README.ru.md#поля-данных).
36
+
37
+ Адреса изображений проходят политику URL для медиафайлов. Рендерер владеет экземплярами `Expose` и обработчиками готовности изображений; `destroy()` освобождает их. Он объявляет стили галереи и `Expose`.
38
+
39
+ Если рендерер объявляет стили, показанный выше явный вызов `EditorRenderer.injectStyles()` подключает их, а возвращённый владелец освобождает.
40
+
41
+ Жизненный цикл, восстановление внутристрочных виджетов, стили и границы безопасности описаны в последовательном руководстве VitePress.
@@ -9,10 +9,11 @@ import {
9
9
  createDownload,
10
10
  createFullscreen,
11
11
  exposeStylesUrl,
12
- } from '@shelamkoff/expose'
13
- import { setSafeUrlAttribute } from '../../../shared/sanitize/sanitizeUrl.js'
12
+ } from '@shelamkoff/expose'
13
+ import { setSafeUrlAttribute } from '../../../shared/sanitize/sanitizeUrl.js'
14
+ import { mountGalleryMasonry } from '../../../shared/galleryMasonry.js'
14
15
 
15
- const styles = new URL('./styles.css', import.meta.url).href
16
+ const styles = new URL('./styles.css', import.meta.url).href
16
17
  const exposeStyles = exposeStylesUrl
17
18
 
18
19
  const MAX_VISIBLE = 6
@@ -85,16 +86,18 @@ function selectAutoTemplate(count, orientations) {
85
86
  }
86
87
  }
87
88
 
88
- /**
89
- * Create the gallery renderer and own every opened Expose instance.
90
- * @param {string} classPrefix
91
- * @param {Record<string, import('../../../shared/localeTypes').LocaleValue>} locale
92
- * @returns {import('../../types').BlockRenderer<import('../../types').GalleryBlock>}
89
+ /**
90
+ * Create the gallery renderer and own every opened Expose instance.
91
+ * @param {string} classPrefix
92
+ * @param {Record<string, import('../../../shared/localeTypes').LocaleValue>} locale
93
+ * @returns {import('../../types').BlockRenderer<import('../../types').GalleryBlock>}
93
94
  */
94
95
  export function createGalleryRenderer(classPrefix, locale) {
95
- /** @type {WeakMap<HTMLElement, Set<import('@shelamkoff/expose').Expose>>} */
96
- const activeInstances = new WeakMap()
97
- return {
96
+ /** @type {WeakMap<HTMLElement, Set<import('@shelamkoff/expose').Expose>>} */
97
+ const activeInstances = new WeakMap()
98
+ /** @type {WeakMap<HTMLElement, ReturnType<typeof mountGalleryMasonry>>} */
99
+ const masonryMounts = new WeakMap()
100
+ return {
98
101
  type: 'gallery',
99
102
  styles: [styles, exposeStyles],
100
103
 
@@ -112,7 +115,7 @@ export function createGalleryRenderer(classPrefix, locale) {
112
115
  ? 'auto'
113
116
  : rawLayout
114
117
 
115
- const container = document.createElement('div')
118
+ const container = document.createElement('div')
116
119
  /** @type {Set<import('@shelamkoff/expose').Expose>} */
117
120
  const instances = new Set()
118
121
  activeInstances.set(container, instances)
@@ -121,10 +124,12 @@ export function createGalleryRenderer(classPrefix, locale) {
121
124
  const slots = getSlotsCount(layout)
122
125
  const visibleCount = Math.min(images.length, layout === 'auto' ? MAX_VISIBLE : slots)
123
126
  const visibleImages = images.slice(0, visibleCount)
124
- const overflowCount = images.length - visibleCount
127
+ const overflowCount = images.length - visibleCount
125
128
  const openLabel = typeof locale['renderer.gallery.open'] === 'string'
126
129
  ? locale['renderer.gallery.open']
127
130
  : 'Open image'
131
+ /** @type {HTMLElement[] | null} */
132
+ let masonryItems = null
128
133
 
129
134
  if (layout === 'auto') {
130
135
  // Set initial template class, refine after images load
@@ -146,42 +151,46 @@ export function createGalleryRenderer(classPrefix, locale) {
146
151
  }
147
152
 
148
153
  visibleImages.forEach((image, i) => {
149
- const item = createItem(image, classPrefix, openLabel)
150
- const img = item.querySelector('img')
151
- if (img) {
152
- let settled = false
153
- const onImgReady = () => {
154
- if (settled) return
155
- settled = true
156
- orientations[i] = img.naturalWidth ? detectOrientation(img) : 'S'
157
- loadedCount++
158
- if (loadedCount >= visibleImages.length) onAllLoaded()
159
- }
160
- img.addEventListener('load', onImgReady, { once: true })
161
- img.addEventListener('error', onImgReady, { once: true })
162
- // A cached image can become complete between assigning `src` in
163
- // createItem() and registering these listeners. Account for that
164
- // path without counting a later load/error event twice.
165
- if (img.complete) queueMicrotask(onImgReady)
166
- }
154
+ const item = createItem(image, classPrefix, openLabel)
155
+ const img = item.querySelector('img')
156
+ if (img) {
157
+ let settled = false
158
+ const onImgReady = () => {
159
+ if (settled) return
160
+ settled = true
161
+ orientations[i] = img.naturalWidth ? detectOrientation(img) : 'S'
162
+ loadedCount++
163
+ if (loadedCount >= visibleImages.length) onAllLoaded()
164
+ }
165
+ img.addEventListener('load', onImgReady, { once: true })
166
+ img.addEventListener('error', onImgReady, { once: true })
167
+ // A cached image can become complete between assigning `src` in
168
+ // createItem() and registering these listeners. Account for that
169
+ // path without counting a later load/error event twice.
170
+ if (img.complete) queueMicrotask(onImgReady)
171
+ }
167
172
  if (overflowCount > 0 && i === visibleImages.length - 1) {
168
173
  item.appendChild(createOverflow(overflowCount, classPrefix))
169
174
  }
170
175
  container.appendChild(item)
171
176
  })
172
- } else if (layout === 'masonry') {
173
- container.classList.add('eg--masonry')
174
-
175
- images.forEach((image) => {
177
+ } else if (layout === 'masonry') {
178
+ container.classList.add('eg--masonry')
179
+ masonryItems = []
180
+
181
+ images.forEach((image) => {
176
182
  const item = createItem(image, classPrefix, openLabel)
177
- container.appendChild(item)
178
- })
183
+ const img = item.querySelector('img')
184
+ if (img) img.loading = 'eager'
185
+ masonryItems?.push(item)
186
+ container.appendChild(item)
187
+ })
179
188
  } else {
180
189
  // Specific template: 1, 2, 3a-6c, triptych, poly-*
181
190
  container.classList.add(`eg--${layout}`)
182
191
 
183
192
  visibleImages.forEach((image, i) => {
184
- const item = createItem(image, classPrefix, openLabel)
193
+ const item = createItem(image, classPrefix, openLabel)
185
194
  if (overflowCount > 0 && i === visibleImages.length - 1) {
186
195
  item.appendChild(createOverflow(overflowCount, classPrefix))
187
196
  }
@@ -190,9 +199,12 @@ export function createGalleryRenderer(classPrefix, locale) {
190
199
  }
191
200
 
192
201
  // Apply gallery inline styles
193
- if (styles) {
194
- applyGalleryStyles(container, styles, classPrefix)
195
- }
202
+ if (styles) {
203
+ applyGalleryStyles(container, styles, classPrefix)
204
+ }
205
+ if (masonryItems) {
206
+ masonryMounts.set(container, mountGalleryMasonry(container, masonryItems))
207
+ }
196
208
 
197
209
  // Expose lightbox on click
198
210
  container.addEventListener('click', (e) => {
@@ -221,8 +233,8 @@ export function createGalleryRenderer(classPrefix, locale) {
221
233
  if (o.captions ?? true) plugins.push(createCaptions())
222
234
  if (o.thumbnails) plugins.push(createThumbnails())
223
235
  if (o.fullscreen ?? true) plugins.push(createFullscreen())
224
- const autoplay = o.autoplayInterval ? createAutoplay({ interval: o.autoplayInterval }) : null
225
- if (autoplay) plugins.push(autoplay)
236
+ const autoplay = o.autoplayInterval ? createAutoplay({ interval: o.autoplayInterval }) : null
237
+ if (autoplay) plugins.push(autoplay)
226
238
  plugins.push(createTransform())
227
239
  plugins.push(createDownload())
228
240
 
@@ -231,23 +243,23 @@ export function createGalleryRenderer(classPrefix, locale) {
231
243
 
232
244
  const expose = new Expose(slides, {
233
245
  startIndex: idx,
234
- animation: 'fade',
235
- loop,
236
- navigation: o.navigation ?? true,
237
- toolbar,
238
- plugins,
239
- })
240
- instances.add(expose)
241
- expose.on('close:complete', () => {
242
- expose.destroy()
243
- instances.delete(expose)
244
- })
245
- void expose.open(idx).then(() => {
246
- autoplay?.start()
247
- }).catch(() => {
248
- expose.destroy()
249
- instances.delete(expose)
250
- })
246
+ animation: 'fade',
247
+ loop,
248
+ navigation: o.navigation ?? true,
249
+ toolbar,
250
+ plugins,
251
+ })
252
+ instances.add(expose)
253
+ expose.on('close:complete', () => {
254
+ expose.destroy()
255
+ instances.delete(expose)
256
+ })
257
+ void expose.open(idx).then(() => {
258
+ autoplay?.start()
259
+ }).catch(() => {
260
+ expose.destroy()
261
+ instances.delete(expose)
262
+ })
251
263
  })
252
264
 
253
265
  return container
@@ -257,35 +269,38 @@ export function createGalleryRenderer(classPrefix, locale) {
257
269
  * Dispose lightboxes that are still open when rendered content is replaced.
258
270
  * @param {HTMLElement} element
259
271
  */
260
- destroy(element) {
261
- const instances = activeInstances.get(element)
262
- if (!instances) return
263
- for (const expose of instances) {
264
- expose.destroy()
265
- }
266
- instances.clear()
267
- activeInstances.delete(element)
268
- },
272
+ destroy(element) {
273
+ masonryMounts.get(element)?.destroy()
274
+ masonryMounts.delete(element)
275
+ const instances = activeInstances.get(element)
276
+ if (instances) {
277
+ for (const expose of instances) {
278
+ expose.destroy()
279
+ }
280
+ instances.clear()
281
+ activeInstances.delete(element)
282
+ }
283
+ },
269
284
  }
270
285
  }
271
286
 
272
287
  /**
273
- * @param {{ url: string; caption?: string }} image
274
- * @param {string} classPrefix
275
- * @param {string} openLabel
276
- * @returns {HTMLElement}
277
- */
278
- function createItem(image, classPrefix, openLabel) {
279
- const figure = document.createElement('figure')
280
- figure.className = `${classPrefix}-gallery__item`
281
- figure.tabIndex = 0
282
- figure.setAttribute('role', 'button')
283
- figure.setAttribute('aria-label', image.caption ? `${openLabel}: ${image.caption}` : openLabel)
284
- figure.addEventListener('keydown', (event) => {
285
- if (event.key !== 'Enter' && event.key !== ' ') return
286
- event.preventDefault()
287
- figure.click()
288
- })
288
+ * @param {{ url: string; caption?: string }} image
289
+ * @param {string} classPrefix
290
+ * @param {string} openLabel
291
+ * @returns {HTMLElement}
292
+ */
293
+ function createItem(image, classPrefix, openLabel) {
294
+ const figure = document.createElement('figure')
295
+ figure.className = `${classPrefix}-gallery__item`
296
+ figure.tabIndex = 0
297
+ figure.setAttribute('role', 'button')
298
+ figure.setAttribute('aria-label', image.caption ? `${openLabel}: ${image.caption}` : openLabel)
299
+ figure.addEventListener('keydown', (event) => {
300
+ if (event.key !== 'Enter' && event.key !== ' ') return
301
+ event.preventDefault()
302
+ figure.click()
303
+ })
289
304
 
290
305
  const img = document.createElement('img')
291
306
  img.className = `${classPrefix}-gallery__image`