code.gitea.io/gitea@v1.21.7/web_src/js/features/imagediff.js (about) 1 import $ from 'jquery'; 2 import {GET} from '../modules/fetch.js'; 3 import {hideElem, loadElem} 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 export function initImageDiff() { 42 function createContext(image1, image2) { 43 const size1 = { 44 width: image1 && image1.width || 0, 45 height: image1 && image1.height || 0 46 }; 47 const size2 = { 48 width: image2 && image2.width || 0, 49 height: image2 && image2.height || 0 50 }; 51 const max = { 52 width: Math.max(size2.width, size1.width), 53 height: Math.max(size2.height, size1.height) 54 }; 55 56 return { 57 image1: $(image1), 58 image2: $(image2), 59 size1, 60 size2, 61 max, 62 ratio: [ 63 Math.floor(max.width - size1.width) / 2, 64 Math.floor(max.height - size1.height) / 2, 65 Math.floor(max.width - size2.width) / 2, 66 Math.floor(max.height - size2.height) / 2 67 ] 68 }; 69 } 70 71 $('.image-diff:not([data-image-diff-loaded])').each(async function() { 72 const $container = $(this); 73 $container.attr('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.attr('width', bounds.width); 102 info.$images.attr('height', bounds.height); 103 hideElem(info.$boundsInfo); 104 } 105 } 106 })); 107 108 const $imagesAfter = imageInfos[0].$images; 109 const $imagesBefore = imageInfos[1].$images; 110 111 initSideBySide(createContext($imagesAfter[0], $imagesBefore[0])); 112 if ($imagesAfter.length > 0 && $imagesBefore.length > 0) { 113 initSwipe(createContext($imagesAfter[1], $imagesBefore[1])); 114 initOverlay(createContext($imagesAfter[2], $imagesBefore[2])); 115 } 116 117 $container.find('> .image-diff-tabs').removeClass('is-loading'); 118 119 function initSideBySide(sizes) { 120 let factor = 1; 121 if (sizes.max.width > (diffContainerWidth - 24) / 2) { 122 factor = (diffContainerWidth - 24) / 2 / sizes.max.width; 123 } 124 125 const widthChanged = sizes.image1.length !== 0 && sizes.image2.length !== 0 && sizes.image1[0].naturalWidth !== sizes.image2[0].naturalWidth; 126 const heightChanged = sizes.image1.length !== 0 && sizes.image2.length !== 0 && sizes.image1[0].naturalHeight !== sizes.image2[0].naturalHeight; 127 if (sizes.image1.length !== 0) { 128 $container.find('.bounds-info-after .bounds-info-width').text(`${sizes.image1[0].naturalWidth}px`).addClass(widthChanged ? 'green' : ''); 129 $container.find('.bounds-info-after .bounds-info-height').text(`${sizes.image1[0].naturalHeight}px`).addClass(heightChanged ? 'green' : ''); 130 } 131 if (sizes.image2.length !== 0) { 132 $container.find('.bounds-info-before .bounds-info-width').text(`${sizes.image2[0].naturalWidth}px`).addClass(widthChanged ? 'red' : ''); 133 $container.find('.bounds-info-before .bounds-info-height').text(`${sizes.image2[0].naturalHeight}px`).addClass(heightChanged ? 'red' : ''); 134 } 135 136 sizes.image1.css({ 137 width: sizes.size1.width * factor, 138 height: sizes.size1.height * factor 139 }); 140 sizes.image1.parent().css({ 141 margin: `10px auto`, 142 width: sizes.size1.width * factor + 2, 143 height: sizes.size1.height * factor + 2 144 }); 145 sizes.image2.css({ 146 width: sizes.size2.width * factor, 147 height: sizes.size2.height * factor 148 }); 149 sizes.image2.parent().css({ 150 margin: `10px auto`, 151 width: sizes.size2.width * factor + 2, 152 height: sizes.size2.height * factor + 2 153 }); 154 } 155 156 function initSwipe(sizes) { 157 let factor = 1; 158 if (sizes.max.width > diffContainerWidth - 12) { 159 factor = (diffContainerWidth - 12) / sizes.max.width; 160 } 161 162 sizes.image1.css({ 163 width: sizes.size1.width * factor, 164 height: sizes.size1.height * factor 165 }); 166 sizes.image1.parent().css({ 167 margin: `0px ${sizes.ratio[0] * factor}px`, 168 width: sizes.size1.width * factor + 2, 169 height: sizes.size1.height * factor + 2 170 }); 171 sizes.image1.parent().parent().css({ 172 padding: `${sizes.ratio[1] * factor}px 0 0 0`, 173 width: sizes.max.width * factor + 2 174 }); 175 sizes.image2.css({ 176 width: sizes.size2.width * factor, 177 height: sizes.size2.height * factor 178 }); 179 sizes.image2.parent().css({ 180 margin: `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`, 181 width: sizes.size2.width * factor + 2, 182 height: sizes.size2.height * factor + 2 183 }); 184 sizes.image2.parent().parent().css({ 185 width: sizes.max.width * factor + 2, 186 height: sizes.max.height * factor + 2 187 }); 188 $container.find('.diff-swipe').css({ 189 width: sizes.max.width * factor + 2, 190 height: sizes.max.height * factor + 30 /* extra height for inner "position: absolute" elements */, 191 }); 192 $container.find('.swipe-bar').on('mousedown', function(e) { 193 e.preventDefault(); 194 195 const $swipeBar = $(this); 196 const $swipeFrame = $swipeBar.parent(); 197 const width = $swipeFrame.width() - $swipeBar.width() - 2; 198 199 $(document).on('mousemove.diff-swipe', (e2) => { 200 e2.preventDefault(); 201 202 const value = Math.max(0, Math.min(e2.clientX - $swipeFrame.offset().left, width)); 203 204 $swipeBar.css({ 205 left: value 206 }); 207 $container.find('.swipe-container').css({ 208 width: $swipeFrame.width() - value 209 }); 210 $(document).on('mouseup.diff-swipe', () => { 211 $(document).off('.diff-swipe'); 212 }); 213 }); 214 }); 215 } 216 217 function initOverlay(sizes) { 218 let factor = 1; 219 if (sizes.max.width > diffContainerWidth - 12) { 220 factor = (diffContainerWidth - 12) / sizes.max.width; 221 } 222 223 sizes.image1.css({ 224 width: sizes.size1.width * factor, 225 height: sizes.size1.height * factor 226 }); 227 sizes.image2.css({ 228 width: sizes.size2.width * factor, 229 height: sizes.size2.height * factor 230 }); 231 sizes.image1.parent().css({ 232 margin: `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`, 233 width: sizes.size1.width * factor + 2, 234 height: sizes.size1.height * factor + 2 235 }); 236 sizes.image2.parent().css({ 237 margin: `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`, 238 width: sizes.size2.width * factor + 2, 239 height: sizes.size2.height * factor + 2 240 }); 241 242 // some inner elements are `position: absolute`, so the container's height must be large enough 243 // the "css(width, height)" is somewhat hacky and not easy to understand, it could be improved in the future 244 sizes.image2.parent().parent().css({ 245 width: sizes.max.width * factor + 2, 246 height: sizes.max.height * factor + 2, 247 }); 248 249 const $range = $container.find("input[type='range']"); 250 const onInput = () => sizes.image1.parent().css({ 251 opacity: $range.val() / 100 252 }); 253 $range.on('input', onInput); 254 onInput(); 255 } 256 }); 257 }