mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 01:43:50 +01:00
Fix logos when rootPath is used (#4367)
* Fix logos when rootPath is used * Fix close buttons used in JS Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
parent
6a0e2bcad3
commit
45edd330f5
@ -1,5 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
@ -8,7 +15,7 @@ namespace BTCPayServer.Abstractions.TagHelpers;
|
||||
|
||||
// Make sure that <svg><use href=/ are correctly working if rootpath is present
|
||||
[HtmlTargetElement("use", Attributes = "href")]
|
||||
public class SVGUse : UrlResolutionTagHelper
|
||||
public class SVGUse : UrlResolutionTagHelper2
|
||||
{
|
||||
private readonly IFileVersionProvider _fileVersionProvider;
|
||||
|
||||
@ -21,5 +28,6 @@ public class SVGUse : UrlResolutionTagHelper
|
||||
var attr = output.Attributes["href"].Value.ToString();
|
||||
attr = _fileVersionProvider.AddFileVersionToPath(ViewContext.HttpContext.Request.PathBase, attr);
|
||||
output.Attributes.SetAttribute("href", attr);
|
||||
}
|
||||
base.Process(context, output);
|
||||
}
|
||||
}
|
||||
|
314
BTCPayServer.Abstractions/TagHelpers/UrlResolutionTagHelper2.cs
Normal file
314
BTCPayServer.Abstractions/TagHelpers/UrlResolutionTagHelper2.cs
Normal file
@ -0,0 +1,314 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
|
||||
|
||||
namespace BTCPayServer.Abstractions.TagHelpers
|
||||
{
|
||||
// A copy of https://github.com/dotnet/aspnetcore/blob/39f0e0b8f40b4754418f81aef0de58a9204a1fe5/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs
|
||||
// slightly modified to also work on use tag.
|
||||
public class UrlResolutionTagHelper2 : TagHelper
|
||||
{
|
||||
// Valid whitespace characters defined by the HTML5 spec.
|
||||
private static readonly char[] ValidAttributeWhitespaceChars =
|
||||
new[] { '\t', '\n', '\u000C', '\r', ' ' };
|
||||
private static readonly Dictionary<string, string[]> ElementAttributeLookups =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "use", new[] { "href" } },
|
||||
{ "a", new[] { "href" } },
|
||||
{ "applet", new[] { "archive" } },
|
||||
{ "area", new[] { "href" } },
|
||||
{ "audio", new[] { "src" } },
|
||||
{ "base", new[] { "href" } },
|
||||
{ "blockquote", new[] { "cite" } },
|
||||
{ "button", new[] { "formaction" } },
|
||||
{ "del", new[] { "cite" } },
|
||||
{ "embed", new[] { "src" } },
|
||||
{ "form", new[] { "action" } },
|
||||
{ "html", new[] { "manifest" } },
|
||||
{ "iframe", new[] { "src" } },
|
||||
{ "img", new[] { "src", "srcset" } },
|
||||
{ "input", new[] { "src", "formaction" } },
|
||||
{ "ins", new[] { "cite" } },
|
||||
{ "link", new[] { "href" } },
|
||||
{ "menuitem", new[] { "icon" } },
|
||||
{ "object", new[] { "archive", "data" } },
|
||||
{ "q", new[] { "cite" } },
|
||||
{ "script", new[] { "src" } },
|
||||
{ "source", new[] { "src", "srcset" } },
|
||||
{ "track", new[] { "src" } },
|
||||
{ "video", new[] { "poster", "src" } },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="UrlResolutionTagHelper"/>.
|
||||
/// </summary>
|
||||
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
|
||||
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
|
||||
public UrlResolutionTagHelper2(IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder)
|
||||
{
|
||||
UrlHelperFactory = urlHelperFactory;
|
||||
HtmlEncoder = htmlEncoder;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Order => -1000 - 999;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IUrlHelperFactory"/>.
|
||||
/// </summary>
|
||||
protected IUrlHelperFactory UrlHelperFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HtmlEncoder"/>.
|
||||
/// </summary>
|
||||
protected HtmlEncoder HtmlEncoder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ViewContext"/>.
|
||||
/// </summary>
|
||||
[HtmlAttributeNotBound]
|
||||
[ViewContext]
|
||||
public ViewContext ViewContext { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(output);
|
||||
|
||||
if (output.TagName == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ElementAttributeLookups.TryGetValue(output.TagName, out var attributeNames))
|
||||
{
|
||||
for (var i = 0; i < attributeNames.Length; i++)
|
||||
{
|
||||
ProcessUrlAttribute(attributeNames[i], output);
|
||||
}
|
||||
}
|
||||
|
||||
// itemid can be present on any HTML element.
|
||||
ProcessUrlAttribute("itemid", output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves and updates URL values starting with '~/' (relative to the application's 'webroot' setting) for
|
||||
/// <paramref name="output"/>'s <see cref="TagHelperOutput.Attributes"/> whose
|
||||
/// <see cref="TagHelperAttribute.Name"/> is <paramref name="attributeName"/>.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">The attribute name used to lookup values to resolve.</param>
|
||||
/// <param name="output">The <see cref="TagHelperOutput"/>.</param>
|
||||
protected void ProcessUrlAttribute(string attributeName, TagHelperOutput output)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(attributeName);
|
||||
ArgumentNullException.ThrowIfNull(output);
|
||||
|
||||
var attributes = output.Attributes;
|
||||
// Read interface .Count once rather than per iteration
|
||||
var attributesCount = attributes.Count;
|
||||
for (var i = 0; i < attributesCount; i++)
|
||||
{
|
||||
var attribute = attributes[i];
|
||||
if (!string.Equals(attribute.Name, attributeName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attribute.Value is string stringValue)
|
||||
{
|
||||
if (TryResolveUrl(stringValue, resolvedUrl: out string? resolvedUrl))
|
||||
{
|
||||
attributes[i] = new TagHelperAttribute(
|
||||
attribute.Name,
|
||||
resolvedUrl,
|
||||
attribute.ValueStyle);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (attribute.Value is IHtmlContent htmlContent)
|
||||
{
|
||||
var htmlString = htmlContent as HtmlString;
|
||||
if (htmlString != null)
|
||||
{
|
||||
// No need for a StringWriter in this case.
|
||||
stringValue = htmlString.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
using var writer = new StringWriter();
|
||||
htmlContent.WriteTo(writer, HtmlEncoder);
|
||||
stringValue = writer.ToString();
|
||||
}
|
||||
|
||||
if (TryResolveUrl(stringValue, resolvedUrl: out IHtmlContent? resolvedUrl))
|
||||
{
|
||||
attributes[i] = new TagHelperAttribute(
|
||||
attribute.Name,
|
||||
resolvedUrl,
|
||||
attribute.ValueStyle);
|
||||
}
|
||||
else if (htmlString == null)
|
||||
{
|
||||
// Not a ~/ URL. Just avoid re-encoding the attribute value later.
|
||||
attributes[i] = new TagHelperAttribute(
|
||||
attribute.Name,
|
||||
new HtmlString(stringValue),
|
||||
attribute.ValueStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to resolve.</param>
|
||||
/// <param name="resolvedUrl">Absolute URL beginning with the application's virtual root. <c>null</c> if
|
||||
/// <paramref name="url"/> could not be resolved.</param>
|
||||
/// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns>
|
||||
protected bool TryResolveUrl(string url, out string? resolvedUrl)
|
||||
{
|
||||
resolvedUrl = null;
|
||||
var start = FindRelativeStart(url);
|
||||
if (start == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimmedUrl = CreateTrimmedString(url, start);
|
||||
|
||||
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
|
||||
resolvedUrl = urlHelper.Content(trimmedUrl);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to resolve the given <paramref name="url"/> value relative to the application's 'webroot' setting.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to resolve.</param>
|
||||
/// <param name="resolvedUrl">
|
||||
/// Absolute URL beginning with the application's virtual root. <c>null</c> if <paramref name="url"/> could
|
||||
/// not be resolved.
|
||||
/// </param>
|
||||
/// <returns><c>true</c> if the <paramref name="url"/> could be resolved; <c>false</c> otherwise.</returns>
|
||||
protected bool TryResolveUrl(string url, [NotNullWhen(true)] out IHtmlContent? resolvedUrl)
|
||||
{
|
||||
resolvedUrl = null;
|
||||
var start = FindRelativeStart(url);
|
||||
if (start == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimmedUrl = CreateTrimmedString(url, start);
|
||||
|
||||
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
|
||||
var appRelativeUrl = urlHelper.Content(trimmedUrl);
|
||||
var postTildeSlashUrlValue = trimmedUrl.Substring(2);
|
||||
|
||||
if (!appRelativeUrl.EndsWith(postTildeSlashUrlValue, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
resolvedUrl = new EncodeFirstSegmentContent(
|
||||
appRelativeUrl,
|
||||
appRelativeUrl.Length - postTildeSlashUrlValue.Length,
|
||||
postTildeSlashUrlValue);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int FindRelativeStart(string url)
|
||||
{
|
||||
if (url == null || url.Length < 2)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var maxTestLength = url.Length - 2;
|
||||
|
||||
var start = 0;
|
||||
for (; start < url.Length; start++)
|
||||
{
|
||||
if (start > maxTestLength)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!IsCharWhitespace(url[start]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Before doing more work, ensure that the URL we're looking at is app-relative.
|
||||
if (url[start] != '~' || url[start + 1] != '/')
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
private static string CreateTrimmedString(string input, int start)
|
||||
{
|
||||
var end = input.Length - 1;
|
||||
for (; end >= start; end--)
|
||||
{
|
||||
if (!IsCharWhitespace(input[end]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var len = end - start + 1;
|
||||
|
||||
// Substring returns same string if start == 0 && len == Length
|
||||
return input.Substring(start, len);
|
||||
}
|
||||
|
||||
private static bool IsCharWhitespace(char ch)
|
||||
{
|
||||
return ValidAttributeWhitespaceChars.AsSpan().IndexOf(ch) != -1;
|
||||
}
|
||||
|
||||
private sealed class EncodeFirstSegmentContent : IHtmlContent
|
||||
{
|
||||
private readonly string _firstSegment;
|
||||
private readonly int _firstSegmentLength;
|
||||
private readonly string _secondSegment;
|
||||
|
||||
public EncodeFirstSegmentContent(string firstSegment, int firstSegmentLength, string secondSegment)
|
||||
{
|
||||
_firstSegment = firstSegment;
|
||||
_firstSegmentLength = firstSegmentLength;
|
||||
_secondSegment = secondSegment;
|
||||
}
|
||||
|
||||
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
||||
{
|
||||
encoder.Encode(writer, _firstSegment, 0, _firstSegmentLength);
|
||||
writer.Write(_secondSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
@model BTCPayServer.Components.Icon.IconViewModel
|
||||
|
||||
<svg role="img" class="icon icon-@Model.Symbol">
|
||||
<use href="/img/icon-sprite.svg#@Model.Symbol"></use>
|
||||
<use href="~/img/icon-sprite.svg#@Model.Symbol"></use>
|
||||
</svg>
|
||||
|
@ -14,7 +14,7 @@
|
||||
else
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" role="img" alt="BTCPay Server" class="main-logo main-logo-btcpay @Model.CssClass">
|
||||
<use href="/img/logo.svg#small" class="main-logo-btcpay--small"/>
|
||||
<use href="/img/logo.svg#large" class="main-logo-btcpay--large"/>
|
||||
<use href="~/img/logo.svg#small" class="main-logo-btcpay--small"/>
|
||||
<use href="~/img/logo.svg#large" class="main-logo-btcpay--large"/>
|
||||
</svg>
|
||||
}
|
||||
|
@ -40,8 +40,11 @@ namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage
|
||||
// Set the relative URL to the directory name if the root path is default, otherwise add root path before the directory name
|
||||
var relativeUrl = baseUri.AbsolutePath == "/" ? LocalStorageDirectoryName : $"{baseUri.AbsolutePath}/{LocalStorageDirectoryName}";
|
||||
var url = new Uri(baseUri, relativeUrl);
|
||||
return baseResult.Replace(new DirectoryInfo(_datadirs.Value.StorageDir).FullName, url.AbsoluteUri,
|
||||
var r = baseResult.Replace(new DirectoryInfo(_datadirs.Value.StorageDir).FullName, url.AbsoluteUri,
|
||||
StringComparison.InvariantCultureIgnoreCase);
|
||||
if (Path.DirectorySeparatorChar == '\\')
|
||||
r = r.Replace(Path.DirectorySeparatorChar, '/');
|
||||
return r;
|
||||
}
|
||||
|
||||
public override async Task<string> GetTemporaryFileUrl(Uri baseUri, StoredFile storedFile,
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.CustomCSSLink))
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true"/>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container p-0 l-pos-wrapper">
|
||||
<div class="l-pos-header bg-primary py-3 px-3">
|
||||
@if (!string.IsNullOrEmpty(Model.CustomLogoLink)) {
|
||||
<img src="@Model.CustomLogoLink" height="40">
|
||||
<img src="@Model.CustomLogoLink" height="40" asp-append-version="true">
|
||||
} else {
|
||||
<h1 class="mb-0">@Model.Title</h1>
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="l-pos-header bg-primary py-3 px-3">
|
||||
@if (!string.IsNullOrEmpty(Model.CustomLogoLink))
|
||||
{
|
||||
<img src="@Model.CustomLogoLink" height="40"/>
|
||||
<img src="@Model.CustomLogoLink" height="40" asp-append-version="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -49,7 +49,7 @@
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(Theme.CssUri)" rel="stylesheet" asp-append-version="true"/>
|
||||
@if (Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" asp-append-version="true" />
|
||||
|
||||
|
@ -45,7 +45,7 @@
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.CustomCSSLink))
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true"/>
|
||||
}
|
||||
|
||||
@if (Model.IsModal)
|
||||
|
@ -43,7 +43,7 @@
|
||||
|
||||
@if (Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Safe.Json(Model);
|
||||
|
@ -49,7 +49,7 @@
|
||||
|
||||
@if (Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
@Safe.Raw(Model.EmbeddedCSS)
|
||||
<style>
|
||||
|
@ -147,7 +147,7 @@
|
||||
}
|
||||
// Careful not to override the slot with innerHTML
|
||||
};if (!slots().default) {
|
||||
componentData.domProps = { innerHTML: '<svg role="img" class="icon icon-close"><use href="/img/icon-sprite.svg#close"></use></svg>' };
|
||||
componentData.domProps = { innerHTML: '<svg role="img" class="icon icon-close" viewBox="0 0 16 16"><path d="M9.38526 8.08753L15.5498 1.85558C15.9653 1.43545 15.9653 0.805252 15.5498 0.385121C15.1342 -0.0350102 14.5108 -0.0350102 14.0952 0.385121L7.93072 6.61707L1.76623 0.315098C1.35065 -0.105033 0.727273 -0.105033 0.311688 0.315098C-0.103896 0.73523 -0.103896 1.36543 0.311688 1.78556L6.47618 8.0175L0.311688 14.2495C-0.103896 14.6696 -0.103896 15.2998 0.311688 15.7199C0.519481 15.93 0.796499 16 1.07355 16C1.35061 16 1.62769 15.93 1.83548 15.7199L7.99997 9.48797L14.1645 15.7199C14.3722 15.93 14.6493 16 14.9264 16C15.2034 16 15.4805 15.93 15.6883 15.7199C16.1039 15.2998 16.1039 14.6696 15.6883 14.2495L9.38526 8.08753Z" fill="currentColor"/></svg>' };
|
||||
}
|
||||
return h('button', mergeData(data, componentData), slots().default);
|
||||
}
|
||||
|
@ -10063,7 +10063,7 @@ var dialog = renderer["a" /* default */].create('<div class="modal note-modal" a
|
||||
$node.attr({
|
||||
'aria-label': options.title
|
||||
});
|
||||
$node.html(['<div class="modal-dialog">', '<div class="modal-content">', options.title ? '<div class="modal-header">' + '<h4 class="modal-title">' + options.title + '</h4>' + '<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" aria-hidden="true"><svg role="img" class="icon icon-close"><use href="/img/icon-sprite.svg#close" /></svg></button>' + '</div>' : '', '<div class="modal-body">' + options.body + '</div>', options.footer ? '<div class="modal-footer">' + options.footer + '</div>' : '', '</div>', '</div>'].join(''));
|
||||
$node.html(['<div class="modal-dialog">', '<div class="modal-content">', options.title ? '<div class="modal-header">' + '<h4 class="modal-title">' + options.title + '</h4>' + '<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" aria-hidden="true"><svg role="img" class="icon icon-close" viewBox="0 0 16 16"><path d="M9.38526 8.08753L15.5498 1.85558C15.9653 1.43545 15.9653 0.805252 15.5498 0.385121C15.1342 -0.0350102 14.5108 -0.0350102 14.0952 0.385121L7.93072 6.61707L1.76623 0.315098C1.35065 -0.105033 0.727273 -0.105033 0.311688 0.315098C-0.103896 0.73523 -0.103896 1.36543 0.311688 1.78556L6.47618 8.0175L0.311688 14.2495C-0.103896 14.6696 -0.103896 15.2998 0.311688 15.7199C0.519481 15.93 0.796499 16 1.07355 16C1.35061 16 1.62769 15.93 1.83548 15.7199L7.99997 9.48797L14.1645 15.7199C14.3722 15.93 14.6493 16 14.9264 16C15.2034 16 15.4805 15.93 15.6883 15.7199C16.1039 15.2998 16.1039 14.6696 15.6883 14.2495L9.38526 8.08753Z" fill="currentColor"/></svg></button>' + '</div>' : '', '<div class="modal-body">' + options.body + '</div>', options.footer ? '<div class="modal-footer">' + options.footer + '</div>' : '', '</div>', '</div>'].join(''));
|
||||
});
|
||||
var popover = renderer["a" /* default */].create(['<div class="note-popover popover in">', '<div class="arrow"></div>', '<div class="popover-content note-children-container"></div>', '</div>'].join(''), function ($node, options) {
|
||||
var direction = typeof options.direction !== 'undefined' ? options.direction : 'bottom';
|
||||
|
Loading…
Reference in New Issue
Block a user