mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Replace ajax with fetch, improve image diff (#27267)
1. Dropzone attachment removal, pretty simple replacement 2. Image diff: The previous code fetched every image twice, once via `img[src]` and once via `$.ajax`. Now it's only fetched once and a second time only when necessary. The image diff code was partially rewritten. --------- Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
		@@ -32,6 +32,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/markup"
 | 
						"code.gitea.io/gitea/modules/markup"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	api "code.gitea.io/gitea/modules/structs"
 | 
						api "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/typesniffer"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/upload"
 | 
						"code.gitea.io/gitea/modules/upload"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
	"code.gitea.io/gitea/services/gitdiff"
 | 
						"code.gitea.io/gitea/services/gitdiff"
 | 
				
			||||||
@@ -60,6 +61,21 @@ func setCompareContext(ctx *context.Context, before, head *git.Commit, headOwner
 | 
				
			|||||||
		return blob
 | 
							return blob
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Data["GetSniffedTypeForBlob"] = func(blob *git.Blob) typesniffer.SniffedType {
 | 
				
			||||||
 | 
							st := typesniffer.SniffedType{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if blob == nil {
 | 
				
			||||||
 | 
								return st
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							st, err := blob.GuessContentType()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Error("GuessContentType failed: %v", err)
 | 
				
			||||||
 | 
								return st
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return st
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	setPathsCompareContext(ctx, before, head, headOwner, headName)
 | 
						setPathsCompareContext(ctx, before, head, headOwner, headName)
 | 
				
			||||||
	setImageCompareContext(ctx)
 | 
						setImageCompareContext(ctx)
 | 
				
			||||||
	setCsvCompareContext(ctx)
 | 
						setCsvCompareContext(ctx)
 | 
				
			||||||
@@ -87,16 +103,7 @@ func setPathsCompareContext(ctx *context.Context, base, head *git.Commit, headOw
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// setImageCompareContext sets context data that is required by image compare template
 | 
					// setImageCompareContext sets context data that is required by image compare template
 | 
				
			||||||
func setImageCompareContext(ctx *context.Context) {
 | 
					func setImageCompareContext(ctx *context.Context) {
 | 
				
			||||||
	ctx.Data["IsBlobAnImage"] = func(blob *git.Blob) bool {
 | 
						ctx.Data["IsSniffedTypeAnImage"] = func(st typesniffer.SniffedType) bool {
 | 
				
			||||||
		if blob == nil {
 | 
					 | 
				
			||||||
			return false
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		st, err := blob.GuessContentType()
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Error("GuessContentType failed: %v", err)
 | 
					 | 
				
			||||||
			return false
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage())
 | 
							return st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -97,7 +97,9 @@
 | 
				
			|||||||
					{{/*notice: the index of Diff.Files should not be used for element ID, because the index will be restarted from 0 when doing load-more for PRs with a lot of files*/}}
 | 
										{{/*notice: the index of Diff.Files should not be used for element ID, because the index will be restarted from 0 when doing load-more for PRs with a lot of files*/}}
 | 
				
			||||||
					{{$blobBase := call $.GetBlobByPathForCommit $.BeforeCommit $file.OldName}}
 | 
										{{$blobBase := call $.GetBlobByPathForCommit $.BeforeCommit $file.OldName}}
 | 
				
			||||||
					{{$blobHead := call $.GetBlobByPathForCommit $.HeadCommit $file.Name}}
 | 
										{{$blobHead := call $.GetBlobByPathForCommit $.HeadCommit $file.Name}}
 | 
				
			||||||
					{{$isImage := or (call $.IsBlobAnImage $blobBase) (call $.IsBlobAnImage $blobHead)}}
 | 
										{{$sniffedTypeBase := call $.GetSniffedTypeForBlob $blobBase}}
 | 
				
			||||||
 | 
										{{$sniffedTypeHead := call $.GetSniffedTypeForBlob $blobHead}}
 | 
				
			||||||
 | 
										{{$isImage:= or (call $.IsSniffedTypeAnImage $sniffedTypeBase) (call $.IsSniffedTypeAnImage $sniffedTypeHead)}}
 | 
				
			||||||
					{{$isCsv := (call $.IsCsvFile $file)}}
 | 
										{{$isCsv := (call $.IsCsvFile $file)}}
 | 
				
			||||||
					{{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}}
 | 
										{{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}}
 | 
				
			||||||
					{{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}}
 | 
										{{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}}
 | 
				
			||||||
@@ -198,9 +200,9 @@
 | 
				
			|||||||
								<div id="diff-rendered-{{$file.NameHash}}" class="file-body file-code {{if $.IsSplitStyle}}code-diff-split{{else}}code-diff-unified{{end}} gt-overflow-x-scroll">
 | 
													<div id="diff-rendered-{{$file.NameHash}}" class="file-body file-code {{if $.IsSplitStyle}}code-diff-split{{else}}code-diff-unified{{end}} gt-overflow-x-scroll">
 | 
				
			||||||
									<table class="chroma gt-w-100">
 | 
														<table class="chroma gt-w-100">
 | 
				
			||||||
										{{if $isImage}}
 | 
															{{if $isImage}}
 | 
				
			||||||
											{{template "repo/diff/image_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead}}
 | 
																{{template "repo/diff/image_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead "sniffedTypeBase" $sniffedTypeBase "sniffedTypeHead" $sniffedTypeHead}}
 | 
				
			||||||
										{{else}}
 | 
															{{else}}
 | 
				
			||||||
											{{template "repo/diff/csv_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead}}
 | 
																{{template "repo/diff/csv_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead "sniffedTypeBase" $sniffedTypeBase "sniffedTypeHead" $sniffedTypeHead}}
 | 
				
			||||||
										{{end}}
 | 
															{{end}}
 | 
				
			||||||
									</table>
 | 
														</table>
 | 
				
			||||||
								</div>
 | 
													</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,12 @@
 | 
				
			|||||||
{{if or .blobBase .blobHead}}
 | 
					{{if or .blobBase .blobHead}}
 | 
				
			||||||
<tr>
 | 
					<tr>
 | 
				
			||||||
	<td colspan="2">
 | 
						<td colspan="2">
 | 
				
			||||||
		<div class="image-diff" data-path-before="{{.root.BeforeRawPath}}/{{PathEscapeSegments .file.OldName}}" data-path-after="{{.root.RawPath}}/{{PathEscapeSegments .file.Name}}">
 | 
							<div class="image-diff"
 | 
				
			||||||
 | 
								data-path-before="{{.root.BeforeRawPath}}/{{PathEscapeSegments .file.OldName}}"
 | 
				
			||||||
 | 
								data-path-after="{{.root.RawPath}}/{{PathEscapeSegments .file.Name}}"
 | 
				
			||||||
 | 
								data-mime-before="{{.sniffedTypeBase.GetMimeType}}"
 | 
				
			||||||
 | 
								data-mime-after="{{.sniffedTypeHead.GetMimeType}}"
 | 
				
			||||||
 | 
							>
 | 
				
			||||||
			<div class="ui secondary pointing tabular top attached borderless menu new-menu">
 | 
								<div class="ui secondary pointing tabular top attached borderless menu new-menu">
 | 
				
			||||||
				<div class="new-menu-inner">
 | 
									<div class="new-menu-inner">
 | 
				
			||||||
					<a class="item active" data-tab="diff-side-by-side-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.side_by_side"}}</a>
 | 
										<a class="item active" data-tab="diff-side-by-side-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.side_by_side"}}</a>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ import {htmlEscape} from 'escape-goat';
 | 
				
			|||||||
import {showTemporaryTooltip} from '../modules/tippy.js';
 | 
					import {showTemporaryTooltip} from '../modules/tippy.js';
 | 
				
			||||||
import {confirmModal} from './comp/ConfirmModal.js';
 | 
					import {confirmModal} from './comp/ConfirmModal.js';
 | 
				
			||||||
import {showErrorToast} from '../modules/toast.js';
 | 
					import {showErrorToast} from '../modules/toast.js';
 | 
				
			||||||
import {request} from '../modules/fetch.js';
 | 
					import {request, POST} from '../modules/fetch.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
 | 
					const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -243,9 +243,8 @@ export function initGlobalDropzone() {
 | 
				
			|||||||
        this.on('removedfile', (file) => {
 | 
					        this.on('removedfile', (file) => {
 | 
				
			||||||
          $(`#${file.uuid}`).remove();
 | 
					          $(`#${file.uuid}`).remove();
 | 
				
			||||||
          if ($dropzone.data('remove-url')) {
 | 
					          if ($dropzone.data('remove-url')) {
 | 
				
			||||||
            $.post($dropzone.data('remove-url'), {
 | 
					            POST($dropzone.data('remove-url'), {
 | 
				
			||||||
              file: file.uuid,
 | 
					              data: new URLSearchParams({file: file.uuid}),
 | 
				
			||||||
              _csrf: csrfToken,
 | 
					 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,14 @@
 | 
				
			|||||||
import $ from 'jquery';
 | 
					import $ from 'jquery';
 | 
				
			||||||
import {hideElem} from '../utils/dom.js';
 | 
					import {GET} from '../modules/fetch.js';
 | 
				
			||||||
 | 
					import {hideElem, loadElem} from '../utils/dom.js';
 | 
				
			||||||
 | 
					import {parseDom} from '../utils.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getDefaultSvgBoundsIfUndefined(svgXml, src) {
 | 
					function getDefaultSvgBoundsIfUndefined(text, src) {
 | 
				
			||||||
  const DefaultSize = 300;
 | 
					  const DefaultSize = 300;
 | 
				
			||||||
  const MaxSize = 99999;
 | 
					  const MaxSize = 99999;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const svg = svgXml.documentElement;
 | 
					  const svgDoc = parseDom(text, 'image/svg+xml');
 | 
				
			||||||
 | 
					  const svg = svgDoc.documentElement;
 | 
				
			||||||
  const width = svg?.width?.baseVal;
 | 
					  const width = svg?.width?.baseVal;
 | 
				
			||||||
  const height = svg?.height?.baseVal;
 | 
					  const height = svg?.height?.baseVal;
 | 
				
			||||||
  if (width === undefined || height === undefined) {
 | 
					  if (width === undefined || height === undefined) {
 | 
				
			||||||
@@ -65,73 +68,53 @@ export function initImageDiff() {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $('.image-diff:not([data-image-diff-loaded])').each(function() {
 | 
					  $('.image-diff:not([data-image-diff-loaded])').each(async function() {
 | 
				
			||||||
    const $container = $(this);
 | 
					    const $container = $(this);
 | 
				
			||||||
    $container.attr('data-image-diff-loaded', 'true');
 | 
					    $container.attr('data-image-diff-loaded', 'true');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // the container may be hidden by "viewed" checkbox, so use the parent's width for reference
 | 
					    // the container may be hidden by "viewed" checkbox, so use the parent's width for reference
 | 
				
			||||||
    const diffContainerWidth = Math.max($container.closest('.diff-file-box').width() - 300, 100);
 | 
					    const diffContainerWidth = Math.max($container.closest('.diff-file-box').width() - 300, 100);
 | 
				
			||||||
    const pathAfter = $container.data('path-after');
 | 
					 | 
				
			||||||
    const pathBefore = $container.data('path-before');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const imageInfos = [{
 | 
					    const imageInfos = [{
 | 
				
			||||||
      loaded: false,
 | 
					      path: this.getAttribute('data-path-after'),
 | 
				
			||||||
      path: pathAfter,
 | 
					      mime: this.getAttribute('data-mime-after'),
 | 
				
			||||||
      $image: $container.find('img.image-after'),
 | 
					      $images: $container.find('img.image-after'), // matches 3 <img>
 | 
				
			||||||
      $boundsInfo: $container.find('.bounds-info-after')
 | 
					      $boundsInfo: $container.find('.bounds-info-after')
 | 
				
			||||||
    }, {
 | 
					    }, {
 | 
				
			||||||
      loaded: false,
 | 
					      path: this.getAttribute('data-path-before'),
 | 
				
			||||||
      path: pathBefore,
 | 
					      mime: this.getAttribute('data-mime-before'),
 | 
				
			||||||
      $image: $container.find('img.image-before'),
 | 
					      $images: $container.find('img.image-before'), // matches 3 <img>
 | 
				
			||||||
      $boundsInfo: $container.find('.bounds-info-before')
 | 
					      $boundsInfo: $container.find('.bounds-info-before')
 | 
				
			||||||
    }];
 | 
					    }];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const info of imageInfos) {
 | 
					    await Promise.all(imageInfos.map(async (info) => {
 | 
				
			||||||
      if (info.$image.length > 0) {
 | 
					      const [success] = await Promise.all(Array.from(info.$images, (img) => {
 | 
				
			||||||
        $.ajax({
 | 
					        return loadElem(img, info.path);
 | 
				
			||||||
          url: info.path,
 | 
					      }));
 | 
				
			||||||
          success: (data, _, jqXHR) => {
 | 
					      // only the first images is associated with $boundsInfo
 | 
				
			||||||
            info.$image.on('load', () => {
 | 
					      if (!success) info.$boundsInfo.text('(image error)');
 | 
				
			||||||
              info.loaded = true;
 | 
					      if (info.mime === 'image/svg+xml') {
 | 
				
			||||||
              setReadyIfLoaded();
 | 
					        const resp = await GET(info.path);
 | 
				
			||||||
            }).on('error', () => {
 | 
					        const text = await resp.text();
 | 
				
			||||||
              info.loaded = true;
 | 
					        const bounds = getDefaultSvgBoundsIfUndefined(text, info.path);
 | 
				
			||||||
              setReadyIfLoaded();
 | 
					 | 
				
			||||||
              info.$boundsInfo.text('(image error)');
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            info.$image.attr('src', info.path);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (jqXHR.getResponseHeader('Content-Type') === 'image/svg+xml') {
 | 
					 | 
				
			||||||
              const bounds = getDefaultSvgBoundsIfUndefined(data, info.path);
 | 
					 | 
				
			||||||
        if (bounds) {
 | 
					        if (bounds) {
 | 
				
			||||||
                info.$image.attr('width', bounds.width);
 | 
					          info.$images.attr('width', bounds.width);
 | 
				
			||||||
                info.$image.attr('height', bounds.height);
 | 
					          info.$images.attr('height', bounds.height);
 | 
				
			||||||
          hideElem(info.$boundsInfo);
 | 
					          hideElem(info.$boundsInfo);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
          }
 | 
					    }));
 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        info.loaded = true;
 | 
					 | 
				
			||||||
        setReadyIfLoaded();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function setReadyIfLoaded() {
 | 
					    const $imagesAfter = imageInfos[0].$images;
 | 
				
			||||||
      if (imageInfos[0].loaded && imageInfos[1].loaded) {
 | 
					    const $imagesBefore = imageInfos[1].$images;
 | 
				
			||||||
        initViews(imageInfos[0].$image, imageInfos[1].$image);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function initViews($imageAfter, $imageBefore) {
 | 
					    initSideBySide(createContext($imagesAfter[0], $imagesBefore[0]));
 | 
				
			||||||
      initSideBySide(createContext($imageAfter[0], $imageBefore[0]));
 | 
					    if ($imagesAfter.length > 0 && $imagesBefore.length > 0) {
 | 
				
			||||||
      if ($imageAfter.length > 0 && $imageBefore.length > 0) {
 | 
					      initSwipe(createContext($imagesAfter[1], $imagesBefore[1]));
 | 
				
			||||||
        initSwipe(createContext($imageAfter[1], $imageBefore[1]));
 | 
					      initOverlay(createContext($imagesAfter[2], $imagesBefore[2]));
 | 
				
			||||||
        initOverlay(createContext($imageAfter[2], $imageBefore[2]));
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $container.find('> .image-diff-tabs').removeClass('is-loading');
 | 
					    $container.find('> .image-diff-tabs').removeClass('is-loading');
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function initSideBySide(sizes) {
 | 
					    function initSideBySide(sizes) {
 | 
				
			||||||
      let factor = 1;
 | 
					      let factor = 1;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,9 +11,7 @@ const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
 | 
				
			|||||||
export function request(url, {method = 'GET', headers = {}, data, body, ...other} = {}) {
 | 
					export function request(url, {method = 'GET', headers = {}, data, body, ...other} = {}) {
 | 
				
			||||||
  let contentType;
 | 
					  let contentType;
 | 
				
			||||||
  if (!body) {
 | 
					  if (!body) {
 | 
				
			||||||
    if (data instanceof FormData) {
 | 
					    if (data instanceof FormData || data instanceof URLSearchParams) {
 | 
				
			||||||
      body = data;
 | 
					 | 
				
			||||||
    } else if (data instanceof URLSearchParams) {
 | 
					 | 
				
			||||||
      body = data;
 | 
					      body = data;
 | 
				
			||||||
    } else if (isObject(data) || Array.isArray(data)) {
 | 
					    } else if (isObject(data) || Array.isArray(data)) {
 | 
				
			||||||
      contentType = 'application/json';
 | 
					      contentType = 'application/json';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import {h} from 'vue';
 | 
					import {h} from 'vue';
 | 
				
			||||||
 | 
					import {parseDom, serializeXml} from './utils.js';
 | 
				
			||||||
import giteaDoubleChevronLeft from '../../public/assets/img/svg/gitea-double-chevron-left.svg';
 | 
					import giteaDoubleChevronLeft from '../../public/assets/img/svg/gitea-double-chevron-left.svg';
 | 
				
			||||||
import giteaDoubleChevronRight from '../../public/assets/img/svg/gitea-double-chevron-right.svg';
 | 
					import giteaDoubleChevronRight from '../../public/assets/img/svg/gitea-double-chevron-right.svg';
 | 
				
			||||||
import giteaEmptyCheckbox from '../../public/assets/img/svg/gitea-empty-checkbox.svg';
 | 
					import giteaEmptyCheckbox from '../../public/assets/img/svg/gitea-empty-checkbox.svg';
 | 
				
			||||||
@@ -145,22 +146,19 @@ const svgs = {
 | 
				
			|||||||
//  At the moment, developers must check, pick and fill the names manually,
 | 
					//  At the moment, developers must check, pick and fill the names manually,
 | 
				
			||||||
//  most of the SVG icons in assets couldn't be used directly.
 | 
					//  most of the SVG icons in assets couldn't be used directly.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const parser = new DOMParser();
 | 
					 | 
				
			||||||
const serializer = new XMLSerializer();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// retrieve an HTML string for given SVG icon name, size and additional classes
 | 
					// retrieve an HTML string for given SVG icon name, size and additional classes
 | 
				
			||||||
export function svg(name, size = 16, className = '') {
 | 
					export function svg(name, size = 16, className = '') {
 | 
				
			||||||
  if (!(name in svgs)) throw new Error(`Unknown SVG icon: ${name}`);
 | 
					  if (!(name in svgs)) throw new Error(`Unknown SVG icon: ${name}`);
 | 
				
			||||||
  if (size === 16 && !className) return svgs[name];
 | 
					  if (size === 16 && !className) return svgs[name];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const document = parser.parseFromString(svgs[name], 'image/svg+xml');
 | 
					  const document = parseDom(svgs[name], 'image/svg+xml');
 | 
				
			||||||
  const svgNode = document.firstChild;
 | 
					  const svgNode = document.firstChild;
 | 
				
			||||||
  if (size !== 16) {
 | 
					  if (size !== 16) {
 | 
				
			||||||
    svgNode.setAttribute('width', String(size));
 | 
					    svgNode.setAttribute('width', String(size));
 | 
				
			||||||
    svgNode.setAttribute('height', String(size));
 | 
					    svgNode.setAttribute('height', String(size));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (className) svgNode.classList.add(...className.split(/\s+/).filter(Boolean));
 | 
					  if (className) svgNode.classList.add(...className.split(/\s+/).filter(Boolean));
 | 
				
			||||||
  return serializer.serializeToString(svgNode);
 | 
					  return serializeXml(svgNode);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function svgParseOuterInner(name) {
 | 
					export function svgParseOuterInner(name) {
 | 
				
			||||||
@@ -176,7 +174,7 @@ export function svgParseOuterInner(name) {
 | 
				
			|||||||
  if (p1 === -1 || p2 === -1) throw new Error(`Invalid SVG icon: ${name}`);
 | 
					  if (p1 === -1 || p2 === -1) throw new Error(`Invalid SVG icon: ${name}`);
 | 
				
			||||||
  const svgInnerHtml = svgStr.slice(p1 + 1, p2);
 | 
					  const svgInnerHtml = svgStr.slice(p1 + 1, p2);
 | 
				
			||||||
  const svgOuterHtml = svgStr.slice(0, p1 + 1) + svgStr.slice(p2);
 | 
					  const svgOuterHtml = svgStr.slice(0, p1 + 1) + svgStr.slice(p2);
 | 
				
			||||||
  const svgDoc = parser.parseFromString(svgOuterHtml, 'image/svg+xml');
 | 
					  const svgDoc = parseDom(svgOuterHtml, 'image/svg+xml');
 | 
				
			||||||
  const svgOuter = svgDoc.firstChild;
 | 
					  const svgOuter = svgDoc.firstChild;
 | 
				
			||||||
  return {svgOuter, svgInnerHtml};
 | 
					  return {svgOuter, svgInnerHtml};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -128,3 +128,14 @@ export function decodeURLEncodedBase64(base64url) {
 | 
				
			|||||||
    .replace(/_/g, '/')
 | 
					    .replace(/_/g, '/')
 | 
				
			||||||
    .replace(/-/g, '+'));
 | 
					    .replace(/-/g, '+'));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const domParser = new DOMParser();
 | 
				
			||||||
 | 
					const xmlSerializer = new XMLSerializer();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function parseDom(text, contentType) {
 | 
				
			||||||
 | 
					  return domParser.parseFromString(text, contentType);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function serializeXml(node) {
 | 
				
			||||||
 | 
					  return xmlSerializer.serializeToString(node);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -183,3 +183,14 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
 | 
				
			|||||||
export function onInputDebounce(fn) {
 | 
					export function onInputDebounce(fn) {
 | 
				
			||||||
  return debounce(300, fn);
 | 
					  return debounce(300, fn);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Set the `src` attribute on an element and returns a promise that resolves once the element
 | 
				
			||||||
 | 
					// has loaded or errored. Suitable for all elements mention in:
 | 
				
			||||||
 | 
					// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/load_event
 | 
				
			||||||
 | 
					export function loadElem(el, src) {
 | 
				
			||||||
 | 
					  return new Promise((resolve) => {
 | 
				
			||||||
 | 
					    el.addEventListener('load', () => resolve(true), {once: true});
 | 
				
			||||||
 | 
					    el.addEventListener('error', () => resolve(false), {once: true});
 | 
				
			||||||
 | 
					    el.src = src;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user