code.gitea.io/gitea@v1.22.3/web_src/js/features/imagediff.js (about) 1 import $ from 'jquery'; 2 import {GET} from '../modules/fetch.js'; 3 import {hideElem, loadElem, queryElemChildren} from '../utils/dom.js'; 4 import {parseDom} from '../utils.js'; 5 6 function getDefaultSvgBoundsIfUndefined(text, src) { 7 const DefaultSize = 300; 8 const MaxSize = 99999; 9 10 const svgDoc = parseDom(text, 'image/svg+xml'); 11 const svg = svgDoc.documentElement; 12 const width = svg?.width?.baseVal; 13 const height = svg?.height?.baseVal; 14 if (width === undefined || height === undefined) { 15 return null; // in case some svg is invalid or doesn't have the width/height 16 } 17 if (width.unitType === SVGLength.SVG_LENGTHTYPE_PERCENTAGE || height.unitType === SVGLength.SVG_LENGTHTYPE_PERCENTAGE) { 18 const img = new Image(); 19 img.src = src; 20 if (img.width > 1 && img.width < MaxSize && img.height > 1 && img.height < MaxSize) { 21 return { 22 width: img.width, 23 height: img.height, 24 }; 25 } 26 if (svg.hasAttribute('viewBox')) { 27 const viewBox = svg.viewBox.baseVal; 28 return { 29 width: DefaultSize, 30 height: DefaultSize * viewBox.width / viewBox.height, 31 }; 32 } 33 return { 34 width: DefaultSize, 35 height: DefaultSize, 36 }; 37 } 38 return null; 39 } 40 41 function createContext(imageAfter, imageBefore) { 42 const sizeAfter = { 43 width: imageAfter?.width || 0, 44 height: imageAfter?.height || 0, 45 }; 46 const sizeBefore = { 47 width: imageBefore?.width || 0, 48 height: imageBefore?.height || 0, 49 }; 50 const maxSize = { 51 width: Math.max(sizeBefore.width, sizeAfter.width), 52 height: Math.max(sizeBefore.height, sizeAfter.height), 53 }; 54 55 return { 56 imageAfter, 57 imageBefore, 58 sizeAfter, 59 sizeBefore, 60 maxSize, 61 ratio: [ 62 Math.floor(maxSize.width - sizeAfter.width) / 2, 63 Math.floor(maxSize.height - sizeAfter.height) / 2, 64 Math.floor(maxSize.width - sizeBefore.width) / 2, 65 Math.floor(maxSize.height - sizeBefore.height) / 2, 66 ], 67 }; 68 } 69 70 export function initImageDiff() { 71 $('.image-diff:not([data-image-diff-loaded])').each(async function() { 72 const $container = $(this); 73 this.setAttribute('data-image-diff-loaded', 'true'); 74 75 // the container may be hidden by "viewed" checkbox, so use the parent's width for reference 76 const diffContainerWidth = Math.max($container.closest('.diff-file-box').width() - 300, 100); 77 78 const imageInfos = [{ 79 path: this.getAttribute('data-path-after'), 80 mime: this.getAttribute('data-mime-after'), 81 $images: $container.find('img.image-after'), // matches 3 <img> 82 $boundsInfo: $container.find('.bounds-info-after'), 83 }, { 84 path: this.getAttribute('data-path-before'), 85 mime: this.getAttribute('data-mime-before'), 86 $images: $container.find('img.image-before'), // matches 3 <img> 87 $boundsInfo: $container.find('.bounds-info-before'), 88 }]; 89 90 await Promise.all(imageInfos.map(async (info) => { 91 const [success] = await Promise.all(Array.from(info.$images, (img) => { 92 return loadElem(img, info.path); 93 })); 94 // only the first images is associated with $boundsInfo 95 if (!success) info.$boundsInfo.text('(image error)'); 96 if (info.mime === 'image/svg+xml') { 97 const resp = await GET(info.path); 98 const text = await resp.text(); 99 const bounds = getDefaultSvgBoundsIfUndefined(text, info.path); 100 if (bounds) { 101 info.$images.each(function() { 102 this.setAttribute('width', bounds.width); 103 this.setAttribute('height', bounds.height); 104 }); 105 hideElem(info.$boundsInfo); 106 } 107 } 108 })); 109 110 const $imagesAfter = imageInfos[0].$images; 111 const $imagesBefore = imageInfos[1].$images; 112 113 initSideBySide(this, createContext($imagesAfter[0], $imagesBefore[0])); 114 if ($imagesAfter.length > 0 && $imagesBefore.length > 0) { 115 initSwipe(createContext($imagesAfter[1], $imagesBefore[1])); 116 initOverlay(createContext($imagesAfter[2], $imagesBefore[2])); 117 } 118 119 queryElemChildren(this, '.image-diff-tabs', (el) => el.classList.remove('is-loading')); 120 121 function initSideBySide(container, sizes) { 122 let factor = 1; 123 if (sizes.maxSize.width > (diffContainerWidth - 24) / 2) { 124 factor = (diffContainerWidth - 24) / 2 / sizes.maxSize.width; 125 } 126 127 const widthChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalWidth !== sizes.imageBefore.naturalWidth; 128 const heightChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalHeight !== sizes.imageBefore.naturalHeight; 129 if (sizes.imageAfter) { 130 const boundsInfoAfterWidth = container.querySelector('.bounds-info-after .bounds-info-width'); 131 if (boundsInfoAfterWidth) { 132 boundsInfoAfterWidth.textContent = `${sizes.imageAfter.naturalWidth}px`; 133 boundsInfoAfterWidth.classList.toggle('green', widthChanged); 134 } 135 const boundsInfoAfterHeight = container.querySelector('.bounds-info-after .bounds-info-height'); 136 if (boundsInfoAfterHeight) { 137 boundsInfoAfterHeight.textContent = `${sizes.imageAfter.naturalHeight}px`; 138 boundsInfoAfterHeight.classList.toggle('green', heightChanged); 139 } 140 } 141 142 if (sizes.imageBefore) { 143 const boundsInfoBeforeWidth = container.querySelector('.bounds-info-before .bounds-info-width'); 144 if (boundsInfoBeforeWidth) { 145 boundsInfoBeforeWidth.textContent = `${sizes.imageBefore.naturalWidth}px`; 146 boundsInfoBeforeWidth.classList.toggle('red', widthChanged); 147 } 148 const boundsInfoBeforeHeight = container.querySelector('.bounds-info-before .bounds-info-height'); 149 if (boundsInfoBeforeHeight) { 150 boundsInfoBeforeHeight.textContent = `${sizes.imageBefore.naturalHeight}px`; 151 boundsInfoBeforeHeight.classList.add('red', heightChanged); 152 } 153 } 154 155 if (sizes.imageAfter) { 156 const container = sizes.imageAfter.parentNode; 157 sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`; 158 sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`; 159 container.style.margin = '10px auto'; 160 container.style.width = `${sizes.sizeAfter.width * factor + 2}px`; 161 container.style.height = `${sizes.sizeAfter.height * factor + 2}px`; 162 } 163 164 if (sizes.imageBefore) { 165 const container = sizes.imageBefore.parentNode; 166 sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`; 167 sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`; 168 container.style.margin = '10px auto'; 169 container.style.width = `${sizes.sizeBefore.width * factor + 2}px`; 170 container.style.height = `${sizes.sizeBefore.height * factor + 2}px`; 171 } 172 } 173 174 function initSwipe(sizes) { 175 let factor = 1; 176 if (sizes.maxSize.width > diffContainerWidth - 12) { 177 factor = (diffContainerWidth - 12) / sizes.maxSize.width; 178 } 179 180 if (sizes.imageAfter) { 181 const container = sizes.imageAfter.parentNode; 182 const swipeFrame = container.parentNode; 183 sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`; 184 sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`; 185 container.style.margin = `0px ${sizes.ratio[0] * factor}px`; 186 container.style.width = `${sizes.sizeAfter.width * factor + 2}px`; 187 container.style.height = `${sizes.sizeAfter.height * factor + 2}px`; 188 swipeFrame.style.padding = `${sizes.ratio[1] * factor}px 0 0 0`; 189 swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`; 190 } 191 192 if (sizes.imageBefore) { 193 const container = sizes.imageBefore.parentNode; 194 const swipeFrame = container.parentNode; 195 sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`; 196 sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`; 197 container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`; 198 container.style.width = `${sizes.sizeBefore.width * factor + 2}px`; 199 container.style.height = `${sizes.sizeBefore.height * factor + 2}px`; 200 swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`; 201 swipeFrame.style.height = `${sizes.maxSize.height * factor + 2}px`; 202 } 203 204 // extra height for inner "position: absolute" elements 205 const swipe = $container.find('.diff-swipe')[0]; 206 if (swipe) { 207 swipe.style.width = `${sizes.maxSize.width * factor + 2}px`; 208 swipe.style.height = `${sizes.maxSize.height * factor + 30}px`; 209 } 210 211 $container.find('.swipe-bar').on('mousedown', function(e) { 212 e.preventDefault(); 213 214 const $swipeBar = $(this); 215 const $swipeFrame = $swipeBar.parent(); 216 const width = $swipeFrame.width() - $swipeBar.width() - 2; 217 218 $(document).on('mousemove.diff-swipe', (e2) => { 219 e2.preventDefault(); 220 221 const value = Math.max(0, Math.min(e2.clientX - $swipeFrame.offset().left, width)); 222 $swipeBar[0].style.left = `${value}px`; 223 $container.find('.swipe-container')[0].style.width = `${$swipeFrame.width() - value}px`; 224 225 $(document).on('mouseup.diff-swipe', () => { 226 $(document).off('.diff-swipe'); 227 }); 228 }); 229 }); 230 } 231 232 function initOverlay(sizes) { 233 let factor = 1; 234 if (sizes.maxSize.width > diffContainerWidth - 12) { 235 factor = (diffContainerWidth - 12) / sizes.maxSize.width; 236 } 237 238 if (sizes.imageAfter) { 239 const container = sizes.imageAfter.parentNode; 240 sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`; 241 sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`; 242 container.style.margin = `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`; 243 container.style.width = `${sizes.sizeAfter.width * factor + 2}px`; 244 container.style.height = `${sizes.sizeAfter.height * factor + 2}px`; 245 } 246 247 if (sizes.imageBefore) { 248 const container = sizes.imageBefore.parentNode; 249 const overlayFrame = container.parentNode; 250 sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`; 251 sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`; 252 container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`; 253 container.style.width = `${sizes.sizeBefore.width * factor + 2}px`; 254 container.style.height = `${sizes.sizeBefore.height * factor + 2}px`; 255 256 // some inner elements are `position: absolute`, so the container's height must be large enough 257 overlayFrame.style.width = `${sizes.maxSize.width * factor + 2}px`; 258 overlayFrame.style.height = `${sizes.maxSize.height * factor + 2}px`; 259 } 260 261 const rangeInput = $container[0].querySelector('input[type="range"]'); 262 function updateOpacity() { 263 if (sizes.imageAfter) { 264 sizes.imageAfter.parentNode.style.opacity = `${rangeInput.value / 100}`; 265 } 266 } 267 rangeInput?.addEventListener('input', updateOpacity); 268 updateOpacity(); 269 } 270 }); 271 }