diff --git a/src/BootstrapBlazor.Server/Components/Samples/Watermarks.razor b/src/BootstrapBlazor.Server/Components/Samples/Watermarks.razor new file mode 100644 index 00000000000..8c58b1ff862 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/Watermarks.razor @@ -0,0 +1,50 @@ +@page "/watermark" +@inject IStringLocalizer Localizer + +

@Localizer["WatermarkTitle"]

+ +

@Localizer["Watermarkntro"]

+ + +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+ +
+

this is a watermark demo

+
这是 watermark 演示
+
+
+
diff --git a/src/BootstrapBlazor.Server/Components/Samples/Watermarks.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Watermarks.razor.cs new file mode 100644 index 00000000000..1342c2b0021 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/Watermarks.razor.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Server.Components.Samples; + +/// +/// Watermarks 组件 +/// +public partial class Watermarks +{ + private string _text = "BootstrapBlazor"; + + private int _fontSize = 16; + + private int _gap = 40; + + private int _rotate = -40; + + private string _color = "#0000004d"; +} diff --git a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs index 82f1ccf96b7..cbfcbe4314a 100644 --- a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs +++ b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs @@ -774,6 +774,12 @@ void AddData(DemoMenuItem item) { Text = Localizer["Waterfall"], Url = "tutorials/waterfall" + }, + new() + { + IsNew = true, + Text = Localizer["Watermark"], + Url = "watermark" } }; AddBadge(item); diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 8206b4338bb..ffe1c9dd773 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -4787,7 +4787,8 @@ "Player": "Player", "RDKit": "RDKit", "SmilesDrawer": "SmilesDrawer", - "Affix": "Affix" + "Affix": "Affix", + "Watermark": "Watermark" }, "BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": { "TablesHeaderTitle": "Header grouping function", @@ -6878,5 +6879,11 @@ "AffixPositionTitle": "Position", "AffixPositionIntro": "Use the parameter Position to control whether the top or bottom is fixed", "AffixOffsetDesc": "The parameter Position controls whether the top or bottom is fixed, and the Offset value sets the offset to the top or bottom" + }, + "BootstrapBlazor.Server.Components.Samples.Watermarks": { + "WatermarkTitle": "Watermark", + "Watermarkntro": "Add specific text or patterns to the page", + "WatermarkNormalTitle": "Basic usage", + "WatermarkNormalIntro": "Use the Text property to set a string to specify the watermark text" } } diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 51a4f2d3a46..7e945f3b555 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -4787,7 +4787,8 @@ "Player": "播放器 Player", "RDKit": "分子式组件 RDKit", "SmilesDrawer": "分子式组件 SmilesDrawer", - "Affix": "固钉组件 Affix" + "Affix": "固钉组件 Affix", + "Watermark": "水印组件 Watermark" }, "BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": { "TablesHeaderTitle": "表头分组功能", @@ -6877,5 +6878,11 @@ "AffixNormalIntro": "固钉默认固定在页面顶部", "AffixPositionTitle": "位置与距离", "AffixPositionIntro": "通过参数 Position 控制固定顶端还是底端,通过 Offset 值设置到顶端或者底端距离偏移量" + }, + "BootstrapBlazor.Server.Components.Samples.Watermarks": { + "WatermarkTitle": "Watermark 水印组件", + "Watermarkntro": "在页面上添加文本或图片等水印信息", + "WatermarkNormalTitle": "基础用法", + "WatermarkNormalIntro": "使用 Text 属性设置一个字符串指定水印内容" } } diff --git a/src/BootstrapBlazor.Server/docs.json b/src/BootstrapBlazor.Server/docs.json index 93b03571024..16be555f488 100644 --- a/src/BootstrapBlazor.Server/docs.json +++ b/src/BootstrapBlazor.Server/docs.json @@ -223,7 +223,8 @@ "player": "Players", "rdkit": "Rdkits", "smiles-drawer": "SmilesDrawers", - "affix": "Affixs" + "affix": "Affixs", + "watermark": "Watermarks" }, "video": { "table": "BV1ap4y1x7Qn?p=1", diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index 3dbb728cb92..56dd19fc5c5 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@ - + - 9.2.7-beta05 + 9.2.7 diff --git a/src/BootstrapBlazor/Components/Watermark/Watermark.razor b/src/BootstrapBlazor/Components/Watermark/Watermark.razor new file mode 100644 index 00000000000..6670ace4726 --- /dev/null +++ b/src/BootstrapBlazor/Components/Watermark/Watermark.razor @@ -0,0 +1,7 @@ +@namespace BootstrapBlazor.Components +@inherits BootstrapModuleComponentBase +@attribute [BootstrapModuleAutoLoader] + +
+ @ChildContent +
diff --git a/src/BootstrapBlazor/Components/Watermark/Watermark.razor.cs b/src/BootstrapBlazor/Components/Watermark/Watermark.razor.cs new file mode 100644 index 00000000000..3a05127aa6d --- /dev/null +++ b/src/BootstrapBlazor/Components/Watermark/Watermark.razor.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Components; + +/// +/// Watermark 组件 +/// +public partial class Watermark +{ + /// + /// 获得/设置 组件内容 + /// + [Parameter] + [EditorRequired] + public RenderFragment? ChildContent { get; set; } + + /// + /// 获得/设置 水印文本 默认 BootstrapBlazor + /// + [Parameter] + public string? Text { get; set; } + + /// + /// 获得/设置 字体大小 默认 null 未设置 默认使用 16px 字体大小 单位 px + /// + [Parameter] + public int? FontSize { get; set; } + + /// + /// 获得/设置 颜色 默认 null 未设置 + /// + [Parameter] + public string? Color { get; set; } + + /// + /// 获得/设置 水印的旋转角度 默认 null 45° + /// + [Parameter] + public int? Rotate { get; set; } + + /// + /// 获得/设置 水印元素的 z-index 值 默认 null + /// + [Parameter] + public int? ZIndex { get; set; } + + /// + /// 获得/设置 水印之间的间距 值 默认 null + /// + [Parameter] + public int? Gap { get; set; } + + private string? ClassString => CssBuilder.Default("bb-watermark") + .AddClassFromAttributes(AdditionalAttributes) + .Build(); + + /// + /// + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + + Text ??= "BootstrapBlazor"; + } + + /// + /// + /// + /// + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (!firstRender) + { + await InvokeVoidAsync("update", Id, GetOptions()); + } + } + + /// + /// + /// + /// + protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, GetOptions()); + + private object GetOptions() => new + { + Text, + FontSize, + Color, + Rotate, + Gap, + ZIndex + }; +} diff --git a/src/BootstrapBlazor/Components/Watermark/Watermark.razor.js b/src/BootstrapBlazor/Components/Watermark/Watermark.razor.js new file mode 100644 index 00000000000..9c52e861524 --- /dev/null +++ b/src/BootstrapBlazor/Components/Watermark/Watermark.razor.js @@ -0,0 +1,130 @@ +import Data from "../../modules/data.js" + +export function init(id, options) { + const el = document.getElementById(id); + if (el === null) { + return; + } + const watermark = { el, options }; + createWatermark(watermark); + + const observer = ob => { + ob.observe(el, { + childList: true, + attributes: true, + subtree: true + }); + } + + const observerCallback = records => { + ob.disconnect(); + updateWatermark(records, watermark); + observer(ob); + }; + + const ob = new MutationObserver(observerCallback); + observer(ob); + watermark.ob = ob; + + Data.set(id, watermark); +} + +export function update(id, options) { + const watermark = Data.get(id); + watermark.options = options; + + createWatermark(watermark); +} + +export function dispose(id) { + const watermark = Data.get(id); + Data.remove(id); + + if (watermark) { + const { ob } = watermark; + ob.disconnect(); + + delete watermark.ob; + } +} + +const updateWatermark = (records, watermark) => { + for (const record of records) { + for (const dom of record.removedNodes) { + if (dom.classList.contains('bb-watermark-bg')) { + createWatermark(watermark); + return; + } + } + + if (record.target.classList.contains('bb-watermark-bg')) { + createWatermark(watermark); + return; + } + } +} + +const createWatermark = watermark => { + const { el, options } = watermark; + const defaults = { + gap: 40, + fontSize: 16, + text: 'BootstrapBlazor', + rotate: -40, + color: '#0000004d' + }; + + for (const key in options) { + if (options[key] === void 0 || options[key] === null) { + delete options[key]; + } + } + + const bg = getWatermark({ ...defaults, ...options }); + const div = document.createElement('div'); + const { base64, styleSize } = bg; + div.style.backgroundImage = `url(${base64})`; + div.style.backgroundSize = `${styleSize}px ${styleSize}px`; + div.style.backgroundRepeat = 'repeat'; + div.style.pointerEvents = 'none'; + div.style.opacity = '1'; + div.style.position = 'absolute'; + div.style.inset = '0'; + div.style.zIndex = '999'; + div.classList.add("bb-watermark-bg"); + + const mark = el.querySelector('.bb-watermark-bg'); + if (mark) { + mark.remove(); + } + el.appendChild(div); +} + +const getWatermark = props => { + const canvas = document.createElement('canvas'); + const devicePixelRatio = window.devicePixelRatio || 1; + + const fontSize = props.fontSize * devicePixelRatio; + const font = fontSize + 'px serif'; + const ctx = canvas.getContext('2d'); + + ctx.font = font; + const { width } = ctx.measureText(props.text); + const canvasSize = Math.max(100, width) + props.gap * devicePixelRatio; + canvas.width = canvasSize; + canvas.height = canvasSize; + ctx.translate(canvas.width / 2, canvas.height / 2); + + ctx.rotate((Math.PI / 180) * props.rotate); + ctx.fillStyle = props.color; + ctx.font = font; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + ctx.fillText(props.text, 0, 0); + return { + base64: canvas.toDataURL(), + size: canvasSize, + styleSize: canvasSize / devicePixelRatio, + }; +} diff --git a/src/BootstrapBlazor/Components/Watermark/Watermark.razor.scss b/src/BootstrapBlazor/Components/Watermark/Watermark.razor.scss new file mode 100644 index 00000000000..d2f97f6c411 --- /dev/null +++ b/src/BootstrapBlazor/Components/Watermark/Watermark.razor.scss @@ -0,0 +1,3 @@ +.bb-watermark { + position: relative; +} diff --git a/src/BootstrapBlazor/wwwroot/scss/components.scss b/src/BootstrapBlazor/wwwroot/scss/components.scss index c8574a7aab1..4f47522764a 100644 --- a/src/BootstrapBlazor/wwwroot/scss/components.scss +++ b/src/BootstrapBlazor/wwwroot/scss/components.scss @@ -108,4 +108,5 @@ @import "../../Components/TreeView/TreeView.razor.scss"; @import "../../Components/Marquee/Marquee.razor.scss"; @import "../../Components/Waterfall/Waterfall.razor.scss"; +@import "../../Components/Watermark/Watermark.razor.scss"; @import "./meilisearch.scss"; diff --git a/test/UnitTest/Components/WatermarkTest.cs b/test/UnitTest/Components/WatermarkTest.cs new file mode 100644 index 00000000000..7ed8c8effcf --- /dev/null +++ b/test/UnitTest/Components/WatermarkTest.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace UnitTest.Components; + +public class WatermarkTest : BootstrapBlazorTestBase +{ + [Fact] + public void Watermark_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.Text, null); + pb.Add(a => a.FontSize, 16); + pb.Add(a => a.Gap, 40); + pb.Add(a => a.Color, "#0000000D"); + pb.Add(a => a.Rotate, 40); + pb.Add(a => a.ZIndex, 10); + pb.Add(a => a.ChildContent, b => b.AddMarkupContent(0, "Test")); + }); + cut.MarkupMatches("
Test
"); + + cut.SetParametersAndRender(); + } +}