PageSourceSearch

https://pythonmagazine.com/plugin/editor/jodit/rara_collage_view.js?v=4

js pythonmagazine.com collected 2026-10-01 14:41:27 UTC 17,262 bytes, 382 lines download raw bytes

1/*
2 * rara_collage_view.js (v2)
3 * 게시판 뷰페이지에서 콜라주 레이아웃을 복원/정리하는 스크립트.
4 *
5 * 처리 내용:
6 *  1) 정상 콜라주(2장 이상): 비율 기반 동일 높이 flex 레이아웃 복원
7 *  2) 잔여 콜라주(1장 이하): 에디터와 동일하게 "해체" - 콜라주 껍데기를 걷어내고
8 *     이미지를 개별 사진 크기(가로 100%)로 복원
9 *  3) 떠돌이 잔여물: 콜라주 밖인데 콜라주 시절 속성(data-rara-flex 등)이나
10 *     figure flex 스타일이 남은 이미지를 개별 사진 크기로 정리
11 *
12 * 사용: /www/extend/rara_collage.extend.php 가 자동으로 이 파일을 로드함.
13 *      게시판 스킨(뷰페이지)은 전혀 수정할 필요 없음.
14 */
15(function(){
16	'use strict';
17
18	var GAP = 4;
19
20	function isImg(el){
21		return el && el.tagName === 'IMG' && (el.getAttribute('src') || '');
22	}
23
24	function setImportant(el, styles){
25		if(!el || !el.style) return;
26		Object.keys(styles || {}).forEach(function(name){
27			el.style.setProperty(name, styles[name], 'important');
28		});
29	}
30
31	function getRatio(img){
32		if(!img) return 1;
33		var w = parseFloat(img.getAttribute('data-original-width') || img.getAttribute('data-width') || img.getAttribute('width') || img.naturalWidth || 0);
34		var h = parseFloat(img.getAttribute('data-original-height') || img.getAttribute('data-height') || img.getAttribute('height') || img.naturalHeight || 0);
35		if(!w || !h){
36			var rect = img.getBoundingClientRect ? img.getBoundingClientRect() : null;
37			if(rect && rect.width && rect.height){
38				w = rect.width;
39				h = rect.height;
40			}
41		}
42		if(!w || !h) return 1;
43		return Math.max(0.35, Math.min(3.5, w / h));
44	}
45
46	function collageRowSize(count, layout){
47		if(layout === 'free3' && count <= 3) return count;
48		return 2;
49	}
50
51	function measureCollageRows(col){
52		if(!col || !col.querySelectorAll) return;
53		var figs = Array.prototype.slice.call(col.children).filter(function(child){
54			return child.nodeType === 1 && child.querySelector && child.querySelector('img');
55		});
56		if(!figs.length) return;
57		var layout = col.getAttribute('data-rara-collage-layout') || 'paired';
58		var rowSize = collageRowSize(figs.length, layout);
59		var rect = col.getBoundingClientRect ? col.getBoundingClientRect() : null;
60		var width = (rect && rect.width) || col.clientWidth || (col.parentNode && col.parentNode.getBoundingClientRect && col.parentNode.getBoundingClientRect().width) || 720;
61		var gap = parseFloat(window.getComputedStyle ? window.getComputedStyle(col).gap : GAP) || GAP;
62		for(var i = 0; i < figs.length; i += rowSize){
63			var row = figs.slice(i, i + rowSize);
64			var fullSingle = layout === 'paired' && row.length === 1;
65			var ratioSum = row.reduce(function(sum, fig){
66				return sum + getRatio(fig.querySelector('img'));
67			}, 0) || row.length;
68			var available = Math.max(1, Math.floor(width - (gap * Math.max(0, row.length - 1))));
69			var rowHeight = fullSingle ? null : Math.max(80, Math.floor(available / ratioSum));
70			var used = 0;
71			row.forEach(function(fig, rowIndex){
72				var img = fig.querySelector('img');
73				if(fullSingle){
74					fig.style.removeProperty('--rara-collage-row-height');
75					fig.style.setProperty('--rara-collage-item-width', '100%');
76					fig.style.setProperty('flex', '0 0 100%', 'important');
77					fig.style.setProperty('width', '100%', 'important');
78					fig.style.setProperty('height', 'auto', 'important');
79					if(img){
80						img.style.setProperty('width', '100%', 'important');
81						img.style.setProperty('height', 'auto', 'important');
82						img.style.setProperty('object-fit', 'contain', 'important');
83					}
84				} else {
85					var ratio = getRatio(img);
86					var itemWidth = rowIndex === row.length - 1 ? Math.max(1, available - used) : Math.max(1, Math.floor(ratio * rowHeight));
87					used += itemWidth;
88					fig.style.setProperty('--rara-collage-item-width', itemWidth + 'px');
89					fig.style.setProperty('--rara-collage-row-height', rowHeight + 'px');
90					fig.style.setProperty('height', rowHeight + 'px', 'important');
91					fig.style.setProperty('width', itemWidth + 'px', 'important');
92					fig.style.setProperty('flex', '0 0 ' + itemWidth + 'px', 'important');
93					if(img){
94						img.style.setProperty('width', '100%', 'important');
95						img.style.setProperty('height', '100%', 'important');
96						img.style.setProperty('object-fit', 'contain', 'important');
97						img.style.setProperty('background', 'transparent', 'important');
98					}
99				}
100			});
101		}
102	}
103
104	function scheduleCollageMeasure(col){
105		if(!col) return;
106		measureCollageRows(col);
107		setTimeout(function(){ measureCollageRows(col); }, 50);
108		setTimeout(function(){ measureCollageRows(col); }, 250);
109		if(window.requestAnimationFrame) requestAnimationFrame(function(){ measureCollageRows(col); });
110	}
111
112	/* 이미지를 완전한 개별 사진 상태로 복원 (에디터의 resetToIndividual과 동일) */
113	function resetToIndividual(img){
114		img.removeAttribute('data-rara-ci');
115		img.removeAttribute('data-rara-flex');
116		img.removeAttribute('width');
117		img.removeAttribute('height');
118		img.removeAttribute('sizes');
119		img.style.cssText = 'display:block;width:100%;max-width:100%;height:auto;';
120	}
121
122	function isOrphanCollageImg(img){
123		if(!isImg(img)) return false;
124		if(img.closest('.jodit-wysiwyg')) return false;
125		if(img.closest('.rara-collage') || img.closest('[data-rara-collage]')) return false;
126		return img.hasAttribute('data-rara-ci') ||
127			img.hasAttribute('data-rara-collage-image') ||
128			(img.classList && (img.classList.contains('rara-collage-img') || img.classList.contains('rara-naver-collage-img')));
129	}
130
131	function rebuildOrphanCollages(){
132		document.querySelectorAll('#bo_v_con, .rara-view-content, .member-view-content, .member-notice-content').forEach(function(root){
133			var run = [];
134
135			function blockImage(block){
136				if(!block || block.nodeType !== 1) return null;
137				var imgs = Array.prototype.slice.call(block.querySelectorAll ? block.querySelectorAll('img') : []).filter(isOrphanCollageImg);
138				if(!imgs.length && block.tagName === 'IMG' && isOrphanCollageImg(block)) imgs = [block];
139				if(imgs.length !== 1) return null;
140				var text = (block.textContent || '').replace(/\s+/g, '');
141				var cap = block.querySelector ? block.querySelector('figcaption,.rara-collage-caption') : null;
142				if(text && (!cap || text !== (cap.textContent || '').replace(/\s+/g, ''))) return null;
143				return imgs[0];
144			}
145
146			function flush(){
147				if(run.length < 2){
148					run = [];
149					return;
150				}
151				var col = document.createElement('div');
152				col.className = 'rara-collage rara-naver-collage rara-collage-' + run.length + ' rara-naver-collage-count-' + run.length;
153				col.setAttribute('data-rara-collage', String(run.length));
154				root.insertBefore(col, run[0].block);
155				run.forEach(function(item, i){
156					var fig = document.createElement('figure');
157					fig.className = 'rara-naver-collage-item';
158					item.img.setAttribute('data-rara-ci', String(i));
159					item.img.setAttribute('data-rara-collage-image', '1');
160					item.img.removeAttribute('data-rara-paste-image');
161					if(item.img.classList){
162						item.img.classList.remove('rara-paste-img');
163						item.img.classList.add('rara-collage-img');
164						item.img.classList.add('rara-naver-collage-img');
165					}
166					fig.appendChild(item.img);
167					if(item.caption) fig.appendChild(item.caption);
168					col.appendChild(fig);
169					if(item.block !== item.img && item.block.parentNode && !item.block.querySelector('img')) item.block.remove();
170				});
171				restoreCollage(col);
172				run = [];
173			}
174
175			Array.prototype.slice.call(root.childNodes).forEach(function(node){
176				if(node.nodeType === 3 && !(node.textContent || '').trim()) return;
177				if(node.nodeType !== 1){
178					flush();
179					return;
180				}
181				var img = blockImage(node);
182				if(img){
183					run.push({
184						block: node,
185						img: img,
186						caption: node.querySelector ? node.querySelector('figcaption,.rara-collage-caption') : null
187					});
188				} else {
189					flush();
190				}
191			});
192			flush();
193		});
194	}
195
196	/* 콜라주 껍데기를 걷어내고 이미지들을 개별 사진으로 복원 */
197	function dissolveOnView(col, imgs){
198		imgs.forEach(function(img){
199			var fig = img.closest ? img.closest('figure') : null;
200			var caption = fig && fig.querySelector ? fig.querySelector('figcaption,.rara-collage-caption') : null;
201			var p = document.createElement('p');
202			resetToIndividual(img);
203			p.style.cssText = 'margin:12px 0;width:100%;max-width:100%;box-sizing:border-box;';
204			p.appendChild(img);
205			if(caption) p.appendChild(caption);
206			if(col.parentNode) col.parentNode.insertBefore(p, col);
207		});
208		col.remove();
209	}
210
211	function restoreCollage(col){
212		/* 에디터 안에서는 에디터 스크립트가 처리하므로 건너뜀 */
213		if(col.closest && col.closest('.jodit-wysiwyg')) return;
214
215		var imgs = Array.prototype.slice.call(col.querySelectorAll('img')).filter(isImg);
216
217		/* ★ 1장 이하 = 잔여 콜라주 → 뷰에서도 해체하여 개별 사진 크기로 복원
218		   (이 처리가 없으면 발행 시 콜라주 시절 크기 그대로 보이는 버그 발생) */
219		if(imgs.length < 2){
220			dissolveOnView(col, imgs);
221			return;
222		}
223
224		/* figure 구조 확인/복구 */
225		var figs = [];
226		imgs.forEach(function(img){
227			var fig = img.closest('figure');
228			if(!fig || fig.parentNode !== col){
229				var nf = document.createElement('figure');
230				/* 이미지의 현재 위치에 figure를 만들어 감싼다 */
231				img.parentNode.insertBefore(nf, img);
232				nf.appendChild(img);
233				/* figure가 콜라주 바로 아래에 있도록 이동 */
234				if(nf.parentNode !== col) col.appendChild(nf);
235				fig = nf;
236			}
237			figs.push(fig);
238		});
239
240		/* figure 밖에 남은 잔여 노드(빈 figure 등) 정리 */
241		Array.prototype.slice.call(col.children).forEach(function(c){
242			if(c.tagName === 'FIGURE' && !c.querySelector('img')) c.remove();
243		});
244
245		var count = imgs.length;
246		var layout = col.getAttribute('data-rara-collage-layout') || (col.classList.contains('rara-collage-free3') ? 'free3' : 'paired');
247		if(layout !== 'free3' || count > 3) layout = 'paired';
248		col.setAttribute('data-rara-collage', String(count));
249		col.setAttribute('data-rara-collage-layout', layout);
250		col.classList.remove('rara-collage-paired', 'rara-collage-free3');
251		col.classList.add(layout === 'free3' ? 'rara-collage-free3' : 'rara-collage-paired');
252
253		/* 컨테이너 스타일 */
254		if(count === 4){
255			col.style.cssText = 'display:flex;flex-wrap:wrap;gap:' + GAP + 'px;width:100%;max-width:100%;margin:12px 0;padding:0;background:transparent;box-sizing:border-box;align-items:flex-start;clear:both;';
256			setImportant(col, {display:'flex', 'flex-wrap':'wrap', gap:GAP + 'px', width:'100%', 'max-width':'100%', margin:'12px 0', padding:'0', background:'transparent', 'box-sizing':'border-box', 'align-items':'flex-start'});
257		} else {
258			col.style.cssText = 'display:flex;flex-wrap:wrap;gap:' + GAP + 'px;width:100%;max-width:100%;margin:12px 0;padding:0;background:transparent;box-sizing:border-box;align-items:flex-start;clear:both;';
259			setImportant(col, {display:'flex', 'flex-wrap':'wrap', gap:GAP + 'px', width:'100%', 'max-width':'100%', margin:'12px 0', padding:'0', background:'transparent', 'box-sizing':'border-box', 'align-items':'flex-start'});
260		}
261
262		figs.forEach(function(fig, i){
263			fig.classList.add('rara-collage-item', 'rara-naver-collage-item');
264			fig.classList.toggle('rara-collage-full', layout === 'paired' && count % 2 === 1 && i === count - 1);
265			var base = 'display:flex;align-items:stretch;justify-content:center;margin:0;padding:0;overflow:visible;background:transparent;border-radius:0;min-width:0;max-width:100%;box-sizing:border-box;';
266			if(count === 4){
267				var savedFour = fig.getAttribute('data-rara-flex') ||
268					(imgs[i] ? imgs[i].getAttribute('data-rara-flex') : '') || '1';
269				fig.style.cssText = base + 'flex:' + savedFour + ' 1 0;--rara-collage-flex:' + savedFour + ';';
270				setImportant(fig, {display:'flex', 'align-items':'stretch', 'justify-content':'center', margin:'0', padding:'0', overflow:'visible', background:'transparent', 'border-radius':'0', 'min-width':'0', 'max-width':'100%', 'box-sizing':'border-box', flex:String(savedFour) + ' 1 0'});
271			} else {
272				var saved = fig.getAttribute('data-rara-flex') ||
273					(imgs[i] ? imgs[i].getAttribute('data-rara-flex') : '') || '1';
274				fig.style.cssText = base + 'flex:' + saved + ' 1 0;--rara-collage-flex:' + saved + ';';
275				setImportant(fig, {display:'flex', 'align-items':'stretch', 'justify-content':'center', margin:'0', padding:'0', overflow:'visible', background:'transparent', 'border-radius':'0', 'min-width':'0', 'max-width':'100%', 'box-sizing':'border-box', flex:String(saved) + ' 1 0'});
276			}
277		});
278
279		imgs.forEach(function(img, i){
280			img.style.cssText = 'display:block;width:100%;max-width:100%;height:100%;object-fit:contain;background:transparent;box-sizing:border-box;';
281			setImportant(img, {display:'block', width:'100%', 'max-width':'100%', height:'100%', 'object-fit':'contain', background:'transparent', 'box-sizing':'border-box'});
282			img.setAttribute('data-rara-collage-image', '1');
283			img.removeAttribute('data-rara-paste-image');
284			if(img.classList){
285				img.classList.remove('rara-paste-img');
286				img.classList.add('rara-collage-img');
287			}
288
289			/* 저장된 비율이 없으면 이미지 로드 후 계산 */
290			if(!figs[i].getAttribute('data-rara-flex') && !img.getAttribute('data-rara-flex')){
291				(function(fig){
292					function adjust(){
293						if(img.naturalWidth && img.naturalHeight){
294							var flexValue = String(Math.max(img.naturalWidth / img.naturalHeight, 0.3).toFixed(4));
295							fig.style.setProperty('--rara-collage-flex', flexValue);
296							fig.style.setProperty('flex', flexValue + ' 1 0', 'important');
297							fig.setAttribute('data-rara-flex', flexValue);
298							img.setAttribute('data-rara-flex', flexValue);
299							scheduleCollageMeasure(col);
300						}
301					}
302					if(img.complete && img.naturalWidth) adjust();
303					else img.addEventListener('load', adjust);
304				})(figs[i]);
305			}
306		});
307
308		Array.prototype.slice.call(col.querySelectorAll('figcaption,.rara-collage-caption')).forEach(function(cap){
309			cap.style.cssText = 'display:block;margin:6px 0 0;padding:0 2px;color:#666;font:13px/1.45 var(--rara-font-global);text-align:center;word-break:keep-all;overflow-wrap:anywhere;box-sizing:border-box;';
310			setImportant(cap, {display:'block', margin:'6px 0 0', padding:'0 2px', color:'#666', 'font-size':'13px', 'line-height':'1.45', 'text-align':'center', 'word-break':'keep-all', 'overflow-wrap':'anywhere', 'box-sizing':'border-box'});
311		});
312		scheduleCollageMeasure(col);
313	}
314
315	/* ★ 떠돌이 잔여물 정리: 콜라주 밖인데 콜라주 시절 흔적이 남은 이미지들.
316	   해체된 이미지가 data-rara-flex 속성이나 flex 스타일이 걸린 figure에
317	   싸인 채 발행되면 콜라주 크기로 보이므로, 개별 사진 크기로 복원한다. */
318	function cleanupOrphans(){
319		document.querySelectorAll('#bo_v_con img, .member-view-content img, .member-notice-content img').forEach(function(img){
320			if(img.closest('.jodit-wysiwyg')) return;
321			if(img.closest('.rara-collage') || img.closest('[data-rara-collage]')) return;
322			img.removeAttribute('width');
323			img.removeAttribute('height');
324			img.removeAttribute('sizes');
325			img.style.width = '100%';
326			img.style.maxWidth = '100%';
327			img.style.height = 'auto';
328		});
329
330		/* 1) 콜라주 밖인데 콜라주 속성이 남은 이미지 */
331		document.querySelectorAll('img[data-rara-flex], img[data-rara-ci]').forEach(function(img){
332			if(img.closest('.jodit-wysiwyg')) return;
333			if(img.closest('.rara-collage') || img.closest('[data-rara-collage]')) return;
334			resetToIndividual(img);
335			neutralizeWrapper(img);
336		});
337
338		/* 2) 콜라주 밖인데 flex 스타일이 걸린 figure 안의 이미지 */
339		document.querySelectorAll('figure[data-rara-flex], figure[style*="flex"]').forEach(function(fig){
340			if(fig.closest('.jodit-wysiwyg')) return;
341			if(fig.closest('.rara-collage') || fig.closest('[data-rara-collage]')) return;
342			var img = fig.querySelector('img');
343			if(img){
344				resetToIndividual(img);
345			}
346			fig.removeAttribute('data-rara-flex');
347			fig.style.cssText = 'margin:12px 0;width:100%;max-width:100%;box-sizing:border-box;';
348		});
349	}
350
351	/* 이미지를 감싼 figure/래퍼의 콜라주 잔여 스타일 무력화 */
352	function neutralizeWrapper(img){
353		var fig = img.closest('figure');
354		if(fig && !fig.closest('.rara-collage')){
355			fig.removeAttribute('data-rara-flex');
356			fig.style.cssText = 'margin:12px 0;width:100%;max-width:100%;box-sizing:border-box;';
357		}
358	}
359
360	function run(){
361		rebuildOrphanCollages();
362		document.querySelectorAll('.rara-collage, [data-rara-collage]').forEach(restoreCollage);
363		cleanupOrphans();
364	}
365
366	if(document.readyState === 'loading'){
367		document.addEventListener('DOMContentLoaded', run);
368	} else {
369		run();
370	}
371	/* 지연 로드되는 콘텐츠(댓글 내 이미지 등) 대응 */
372	[50, 250, 800, 1500, 3000].forEach(function(ms){
373		setTimeout(run, ms);
374	});
375	try {
376		var timer = null;
377		new MutationObserver(function(){
378			clearTimeout(timer);
379			timer = setTimeout(run, 80);
380		}).observe(document.body, {childList:true, subtree:true});
381	} catch(e){}
382})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.