mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-03 22:12:00 +10:00
Polish multi-value editors and hosts inline editing.
Align the domain custom editor with the default multi-value layout, keep item numbering in edit mode, and restore unaffected single-value editor styling. Refine hosts inline editing with safer blur handling, explicit cancel actions, and better behavior when blank edits are abandoned.
This commit is contained in:
+29
-14
@@ -172,6 +172,16 @@
|
|||||||
margin-bottom: 0.35rem;
|
margin-bottom: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Option label + field-level badges (editable / ok / cancel) on one row */
|
||||||
|
::deep .ec-field-label-row {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.25rem 0.5rem;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
::deep .ec-field:last-child {
|
::deep .ec-field:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
@@ -206,7 +216,7 @@
|
|||||||
/* When a field is active: value + badges in one block so any click inside (checkbox, input, ok/cancel) doesn't deactivate */
|
/* When a field is active: value + badges in one block so any click inside (checkbox, input, ok/cancel) doesn't deactivate */
|
||||||
::deep .ec-field-value-when-editing {
|
::deep .ec-field-value-when-editing {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -239,11 +249,6 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Row-level ok/cancel (and revert) for multi-value: right-align on the row below the list */
|
|
||||||
::deep .ec-field .ec-field-value-multi + .status-badge-inline {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
::deep .ec-field-value-list {
|
::deep .ec-field-value-list {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 1.25rem;
|
padding-left: 1.25rem;
|
||||||
@@ -262,18 +267,24 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Multivalue display mode: editable entry row (no list number; align with list content) */
|
|
||||||
::deep .ec-field-value-list li.ec-multi-list-entry-point {
|
|
||||||
list-style: none;
|
|
||||||
margin-left: -1.25rem;
|
|
||||||
margin-top: 0.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
::deep .ec-field-value-list li.ec-multi-list-empty-hint {
|
::deep .ec-field-value-list li.ec-multi-list-empty-hint {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin-left: -1.25rem;
|
margin-left: -1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Shared with default and custom (e.g. domain) multi-value editors */
|
||||||
|
::deep .ec-field-value-list li.ec-multi-list-add-item {
|
||||||
|
list-style: none;
|
||||||
|
margin-left: -1.25rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
border-top: 1px dashed var(--bs-border-color, #dee2e6);
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .ec-field-value-list li.ec-multi-list-add-item > .ec-multi-list-row-controls > .status-badge-inline {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* Long paths/URLs wrap; allow break anywhere so nothing is clipped */
|
/* Long paths/URLs wrap; allow break anywhere so nothing is clipped */
|
||||||
::deep .ec-field-value-item {
|
::deep .ec-field-value-item {
|
||||||
display: inline;
|
display: inline;
|
||||||
@@ -300,7 +311,11 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 991.98px) {
|
@media (max-width: 575.98px) {
|
||||||
|
::deep .ec-field-value-when-editing {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
::deep .ec-field-value-when-editing > .status-badge-inline {
|
::deep .ec-field-value-when-editing > .status-badge-inline {
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|||||||
+203
-117
@@ -6,8 +6,7 @@
|
|||||||
@inject INotificationService NotificationService
|
@inject INotificationService NotificationService
|
||||||
|
|
||||||
<div class="ec-field-value ec-field-value-multi ec-domain-rule-editor" @onclick:stopPropagation="true">
|
<div class="ec-field-value ec-field-value-multi ec-domain-rule-editor" @onclick:stopPropagation="true">
|
||||||
@* ul: avoid <ol> markers (scoped CSS does not inherit .ec-multi-list-add-item list-style rules from the generic multi editor). *@
|
<ol class="ec-field-value-list ec-domain-rule-list">
|
||||||
<ul class="ec-field-value-list ec-domain-rule-list">
|
|
||||||
@if (_displayItems.Count > 0)
|
@if (_displayItems.Count > 0)
|
||||||
{
|
{
|
||||||
@for (var i = 0; i < _displayItems.Count; i++)
|
@for (var i = 0; i < _displayItems.Count; i++)
|
||||||
@@ -19,116 +18,152 @@
|
|||||||
var rowValue = item.Value;
|
var rowValue = item.Value;
|
||||||
|
|
||||||
<li class="ec-domain-rule-row @(_editingIndex == rowIndex && !itemReadonly ? "ec-domain-rule-row-editing" : "")" @key="anchorId">
|
<li class="ec-domain-rule-row @(_editingIndex == rowIndex && !itemReadonly ? "ec-domain-rule-row-editing" : "")" @key="anchorId">
|
||||||
@if (_editingIndex == rowIndex && !itemReadonly)
|
<div class="ec-domain-rule-row-body">
|
||||||
{
|
@if (_editingIndex == rowIndex && !itemReadonly)
|
||||||
<div class="ec-domain-rule-form">
|
{
|
||||||
<p class="ec-domain-rule-intro">Sets the domain suffix dnsmasq uses: DHCP domain option, which FQDNs hosts may claim, and (with <code>expand-hosts</code>) how simple names are qualified in DNS.</p>
|
<div class="ec-domain-rule-form">
|
||||||
<label class="form-label">Edit as raw line</label>
|
<div class="ec-domain-rule-toolbar">
|
||||||
<div class="form-check">
|
<div class="ec-domain-rule-toolbar__group">
|
||||||
<input type="checkbox" class="form-check-input" id="@($"{anchorId}-raw")"
|
<span class="form-label mb-0">Editor mode</span>
|
||||||
checked="@_draftUseRaw"
|
<button type="button"
|
||||||
@onchange="@(e => OnDraftUseRawChanged(e.Value is true))" />
|
class="btn btn-link btn-sm text-muted ec-domain-rule-help p-0 border-0"
|
||||||
<label class="form-check-label" for="@($"{anchorId}-raw")">Edit as single line (advanced)</label>
|
title="@DomainRuleEditorHelpText"
|
||||||
</div>
|
aria-label="@DomainRuleEditorHelpText"
|
||||||
@if (_draftUseRaw)
|
@onclick="ShowDomainRuleEditorHelp">
|
||||||
{
|
<i class="bi bi-question-circle" aria-hidden="true"></i>
|
||||||
<textarea class="form-control form-control-sm font-monospace" rows="2"
|
</button>
|
||||||
@bind="_draftRawLine" @bind:event="oninput"
|
</div>
|
||||||
placeholder="domain[,subnet|IP|interface[,local]]"></textarea>
|
<div class="ec-domain-rule-mode-toggle" role="group" aria-label="Domain rule editor mode">
|
||||||
}
|
<button type="button"
|
||||||
else
|
class="btn btn-sm ec-domain-rule-mode-btn @(!_draftUseRaw ? "ec-domain-rule-mode-btn--active" : null)"
|
||||||
{
|
aria-pressed="@(!_draftUseRaw ? "true" : "false")"
|
||||||
<div>
|
@onclick="() => OnDraftUseRawChanged(false)">
|
||||||
<label class="form-label" for="@($"{anchorId}-dom")">Domain</label>
|
Guided
|
||||||
<input id="@($"{anchorId}-dom")" class="form-control form-control-sm"
|
</button>
|
||||||
placeholder="e.g. home.arpa or #"
|
<button type="button"
|
||||||
@bind="_draftDomain" @bind:event="oninput" />
|
class="btn btn-sm ec-domain-rule-mode-btn @(_draftUseRaw ? "ec-domain-rule-mode-btn--active" : null)"
|
||||||
<div class="ec-domain-rule-hint">Use <code>#</code> to take the search domain from resolv.conf.</div>
|
aria-pressed="@(_draftUseRaw ? "true" : "false")"
|
||||||
|
@onclick="() => OnDraftUseRawChanged(true)">
|
||||||
|
Raw line
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
@if (_draftUseRaw)
|
||||||
<label class="form-label" for="@($"{anchorId}-scope")">Applies to</label>
|
{
|
||||||
<select id="@($"{anchorId}-scope")" class="form-select form-select-sm"
|
<input class="form-control form-control-sm font-monospace"
|
||||||
@bind="_draftScopeSelect" @bind:after="OnDraftScopeAfterChange">
|
@bind="_draftRawLine" @bind:event="oninput"
|
||||||
<option value="unconditional">Any client address (default)</option>
|
placeholder="domain[,subnet|IP|interface[,local]]" />
|
||||||
<option value="range">Limit to this subnet or IP</option>
|
}
|
||||||
<option value="interface">Limit to subnets on this interface</option>
|
else
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
@if (_draftScopeSelect == "range")
|
|
||||||
{
|
{
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label" for="@($"{anchorId}-mid")">Subnet or IP</label>
|
<label class="form-label" for="@($"{anchorId}-dom")">Domain</label>
|
||||||
<input id="@($"{anchorId}-mid")" class="form-control form-control-sm"
|
<input id="@($"{anchorId}-dom")" class="form-control form-control-sm"
|
||||||
placeholder="e.g. 192.168.0.0/24 or 192.168.0.1"
|
placeholder="e.g. home.arpa or #"
|
||||||
@bind="_draftMiddle" @bind:event="oninput" />
|
@bind="_draftDomain" @bind:event="oninput" />
|
||||||
<div class="ec-domain-rule-hint">Single IP, CIDR, or start,end pair per dnsmasq docs.</div>
|
<div class="ec-domain-rule-hint-row">
|
||||||
|
<div class="ec-domain-rule-hint">Use <code>#</code> to take the search domain from resolv.conf.</div>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-link btn-sm text-muted ec-domain-rule-help p-0 border-0"
|
||||||
|
title="@HashDomainHelpText"
|
||||||
|
aria-label="@HashDomainHelpText"
|
||||||
|
@onclick="ShowHashDomainHelp">
|
||||||
|
<i class="bi bi-question-circle" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="@($"{anchorId}-loc")"
|
|
||||||
@bind="_draftAddLocal" />
|
|
||||||
<label class="form-check-label" for="@($"{anchorId}-loc")">
|
|
||||||
Also add matching <code>--local</code> forward and reverse DNS
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (_draftScopeSelect == "interface")
|
|
||||||
{
|
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label" for="@($"{anchorId}-if")">Interface</label>
|
<label class="form-label" for="@($"{anchorId}-scope")">Applies to</label>
|
||||||
<input id="@($"{anchorId}-if")" class="form-control form-control-sm"
|
<select id="@($"{anchorId}-scope")" class="form-select form-select-sm"
|
||||||
placeholder="e.g. eth0 or br0"
|
@bind="_draftScopeSelect" @bind:after="OnDraftScopeAfterChange">
|
||||||
@bind="_draftMiddle" @bind:event="oninput" />
|
<option value="unconditional">Any client address (default)</option>
|
||||||
|
<option value="range">Limit to this subnet or IP</option>
|
||||||
|
<option value="interface">Limit to subnets on this interface</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@if (_draftScopeSelect == "range")
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<label class="form-label" for="@($"{anchorId}-mid")">Subnet or IP</label>
|
||||||
|
<input id="@($"{anchorId}-mid")" class="form-control form-control-sm"
|
||||||
|
placeholder="e.g. 192.168.0.0/24 or 192.168.0.1"
|
||||||
|
@bind="_draftMiddle" @bind:event="oninput" />
|
||||||
|
<div class="ec-domain-rule-hint">Single IP, CIDR, or start,end pair per dnsmasq docs.</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="@($"{anchorId}-loc")"
|
||||||
|
@bind="_draftAddLocal" />
|
||||||
|
<label class="form-check-label" for="@($"{anchorId}-loc")">
|
||||||
|
Also add matching <code>--local</code> forward and reverse DNS
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_draftScopeSelect == "interface")
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<label class="form-label" for="@($"{anchorId}-if")">Interface</label>
|
||||||
|
<input id="@($"{anchorId}-if")" class="form-control form-control-sm"
|
||||||
|
placeholder="e.g. eth0 or br0"
|
||||||
|
@bind="_draftMiddle" @bind:event="oninput" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_draftScopeSelect == "unconditional")
|
||||||
|
{
|
||||||
|
<div class="form-check ec-domain-rule-local-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="@($"{anchorId}-loc2")"
|
||||||
|
@bind="_draftAddLocal" />
|
||||||
|
<label class="form-check-label" for="@($"{anchorId}-loc2")">
|
||||||
|
Append <code>,local</code>
|
||||||
|
</label>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-link btn-sm text-muted ec-domain-rule-help p-0 border-0"
|
||||||
|
title="@LocalSuffixHelpText"
|
||||||
|
aria-label="@LocalSuffixHelpText"
|
||||||
|
@onclick="ShowLocalSuffixHelp">
|
||||||
|
<i class="bi bi-question-circle" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (_draftScopeSelect == "unconditional")
|
<div class="ec-domain-rule-actions">
|
||||||
{
|
<StatusBadge Kind="StatusBadgeKind.ActionOk" Text="save" Clickable="true"
|
||||||
<div class="form-check">
|
Title="Apply change" AriaLabel="Apply change"
|
||||||
<input type="checkbox" class="form-check-input" id="@($"{anchorId}-loc2")"
|
PreventBlurOnMouseDown="true" OnClick="@(() => SaveEdit(rowIndex))" Class="status-badge-inline" />
|
||||||
@bind="_draftAddLocal" />
|
<StatusBadge Kind="StatusBadgeKind.ActionCancel" Text="cancel" Clickable="true"
|
||||||
<label class="form-check-label" for="@($"{anchorId}-loc2")">
|
Title="Discard edit" AriaLabel="Cancel edit"
|
||||||
Append <code>,local</code> (same as a lone <code>--local</code> for this name; uncommon)
|
PreventBlurOnMouseDown="true" OnClick="CancelInline" Class="status-badge-inline" />
|
||||||
</label>
|
</div>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<div class="ec-domain-rule-actions">
|
|
||||||
<StatusBadge Kind="StatusBadgeKind.ActionOk" Text="save" Clickable="true"
|
|
||||||
Title="Apply change" AriaLabel="Apply change"
|
|
||||||
PreventBlurOnMouseDown="true" OnClick="@(() => SaveEdit(rowIndex))" Class="status-badge-inline" />
|
|
||||||
<StatusBadge Kind="StatusBadgeKind.ActionCancel" Text="cancel" Clickable="true"
|
|
||||||
Title="Discard edit" AriaLabel="Cancel edit"
|
|
||||||
PreventBlurOnMouseDown="true" OnClick="CancelInline" Class="status-badge-inline" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
else
|
||||||
else
|
{
|
||||||
{
|
<div class="ec-domain-rule-summary">
|
||||||
<div class="ec-domain-rule-summary">
|
<span class="ec-field-value-item">@DisplayLine(rowValue)</span>
|
||||||
<span class="ec-field-value-item">@DisplayLine(rowValue)</span>
|
@if (!string.Equals(DomainRuleFields.FormatSummary(rowValue), rowValue, StringComparison.Ordinal))
|
||||||
@if (!string.Equals(DomainRuleFields.FormatSummary(rowValue), rowValue, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
<code class="text-muted small">@rowValue</code>
|
|
||||||
}
|
|
||||||
<span class="status-badge-inline">
|
|
||||||
<OptionStateBadge
|
|
||||||
AnchorId="@anchorId"
|
|
||||||
Source="@item.Source"
|
|
||||||
OptionName="@OptionName"
|
|
||||||
Value="@rowValue"
|
|
||||||
IsInteractive="@IsEditMode"
|
|
||||||
ShowEditableBadge="@(!itemReadonly)"
|
|
||||||
EditableAsButton="@(!itemReadonly)"
|
|
||||||
OnEditableClickRequested="@(!itemReadonly ? EventCallback.Factory.Create(this, () => { BeginEdit(rowIndex, rowValue); return Task.CompletedTask; }) : default)" />
|
|
||||||
@if (!itemReadonly)
|
|
||||||
{
|
{
|
||||||
<StatusBadge Kind="StatusBadgeKind.ActionRevert" Text="remove" Clickable="true"
|
<code class="text-muted small">@rowValue</code>
|
||||||
Title="Remove this value" AriaLabel="Remove"
|
|
||||||
PreventBlurOnMouseDown="true"
|
|
||||||
OnClick="@(() => RemoveAt(rowIndex))" Class="status-badge-inline" />
|
|
||||||
}
|
}
|
||||||
</span>
|
<span class="status-badge-inline">
|
||||||
</div>
|
<OptionStateBadge
|
||||||
}
|
AnchorId="@anchorId"
|
||||||
|
Source="@item.Source"
|
||||||
|
OptionName="@OptionName"
|
||||||
|
Value="@rowValue"
|
||||||
|
IsInteractive="@IsEditMode"
|
||||||
|
ShowEditableBadge="@(!itemReadonly)"
|
||||||
|
EditableAsButton="@(!itemReadonly)"
|
||||||
|
OnEditableClickRequested="@(!itemReadonly ? EventCallback.Factory.Create(this, () => { BeginEdit(rowIndex, rowValue); return Task.CompletedTask; }) : default)" />
|
||||||
|
@if (!itemReadonly)
|
||||||
|
{
|
||||||
|
<StatusBadge Kind="StatusBadgeKind.ActionRevert" Text="remove" Clickable="true"
|
||||||
|
Title="Remove this value" AriaLabel="Remove"
|
||||||
|
PreventBlurOnMouseDown="true"
|
||||||
|
OnClick="@(() => RemoveAt(rowIndex))" Class="status-badge-inline" />
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,19 +180,37 @@
|
|||||||
@if (_addingNew)
|
@if (_addingNew)
|
||||||
{
|
{
|
||||||
<div class="ec-domain-rule-form">
|
<div class="ec-domain-rule-form">
|
||||||
<p class="ec-domain-rule-intro">Sets the domain suffix dnsmasq uses: DHCP domain option, which FQDNs hosts may claim, and (with <code>expand-hosts</code>) how simple names are qualified in DNS.</p>
|
<div class="ec-domain-rule-toolbar">
|
||||||
<label class="form-label">Edit as raw line</label>
|
<div class="ec-domain-rule-toolbar__group">
|
||||||
<div class="form-check">
|
<span class="form-label mb-0">Editor mode</span>
|
||||||
<input type="checkbox" class="form-check-input" id="domain-add-raw"
|
<button type="button"
|
||||||
checked="@_draftUseRaw"
|
class="btn btn-link btn-sm text-muted ec-domain-rule-help p-0 border-0"
|
||||||
@onchange="@(e => OnDraftUseRawChanged(e.Value is true))" />
|
title="@DomainRuleEditorHelpText"
|
||||||
<label class="form-check-label" for="domain-add-raw">Edit as single line (advanced)</label>
|
aria-label="@DomainRuleEditorHelpText"
|
||||||
|
@onclick="ShowDomainRuleEditorHelp">
|
||||||
|
<i class="bi bi-question-circle" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="ec-domain-rule-mode-toggle" role="group" aria-label="Domain rule editor mode">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm ec-domain-rule-mode-btn @(!_draftUseRaw ? "ec-domain-rule-mode-btn--active" : null)"
|
||||||
|
aria-pressed="@(!_draftUseRaw ? "true" : "false")"
|
||||||
|
@onclick="() => OnDraftUseRawChanged(false)">
|
||||||
|
Guided
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm ec-domain-rule-mode-btn @(_draftUseRaw ? "ec-domain-rule-mode-btn--active" : null)"
|
||||||
|
aria-pressed="@(_draftUseRaw ? "true" : "false")"
|
||||||
|
@onclick="() => OnDraftUseRawChanged(true)">
|
||||||
|
Raw line
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (_draftUseRaw)
|
@if (_draftUseRaw)
|
||||||
{
|
{
|
||||||
<textarea class="form-control form-control-sm font-monospace" rows="2"
|
<input class="form-control form-control-sm font-monospace"
|
||||||
@bind="_draftRawLine" @bind:event="oninput"
|
@bind="_draftRawLine" @bind:event="oninput"
|
||||||
placeholder="domain[,subnet|IP|interface[,local]]"></textarea>
|
placeholder="domain[,subnet|IP|interface[,local]]" />
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -166,6 +219,16 @@
|
|||||||
<input id="domain-add-dom" class="form-control form-control-sm"
|
<input id="domain-add-dom" class="form-control form-control-sm"
|
||||||
placeholder="e.g. home.arpa or #"
|
placeholder="e.g. home.arpa or #"
|
||||||
@bind="_draftDomain" @bind:event="oninput" />
|
@bind="_draftDomain" @bind:event="oninput" />
|
||||||
|
<div class="ec-domain-rule-hint-row">
|
||||||
|
<div class="ec-domain-rule-hint">Use <code>#</code> to take the search domain from resolv.conf.</div>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-link btn-sm text-muted ec-domain-rule-help p-0 border-0"
|
||||||
|
title="@HashDomainHelpText"
|
||||||
|
aria-label="@HashDomainHelpText"
|
||||||
|
@onclick="ShowHashDomainHelp">
|
||||||
|
<i class="bi bi-question-circle" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label" for="domain-add-scope">Applies to</label>
|
<label class="form-label" for="domain-add-scope">Applies to</label>
|
||||||
@@ -181,8 +244,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="form-label" for="domain-add-mid">Subnet or IP</label>
|
<label class="form-label" for="domain-add-mid">Subnet or IP</label>
|
||||||
<input id="domain-add-mid" class="form-control form-control-sm"
|
<input id="domain-add-mid" class="form-control form-control-sm"
|
||||||
placeholder="e.g. 192.168.0.0/24"
|
placeholder="e.g. 192.168.0.0/24 or 192.168.0.1"
|
||||||
@bind="_draftMiddle" @bind:event="oninput" />
|
@bind="_draftMiddle" @bind:event="oninput" />
|
||||||
|
<div class="ec-domain-rule-hint">Single IP, CIDR, or start,end pair per dnsmasq docs.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="domain-add-loc"
|
<input type="checkbox" class="form-check-input" id="domain-add-loc"
|
||||||
@@ -197,18 +261,25 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="form-label" for="domain-add-if">Interface</label>
|
<label class="form-label" for="domain-add-if">Interface</label>
|
||||||
<input id="domain-add-if" class="form-control form-control-sm"
|
<input id="domain-add-if" class="form-control form-control-sm"
|
||||||
placeholder="e.g. eth0"
|
placeholder="e.g. eth0 or br0"
|
||||||
@bind="_draftMiddle" @bind:event="oninput" />
|
@bind="_draftMiddle" @bind:event="oninput" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (_draftScopeSelect == "unconditional")
|
else if (_draftScopeSelect == "unconditional")
|
||||||
{
|
{
|
||||||
<div class="form-check">
|
<div class="form-check ec-domain-rule-local-check">
|
||||||
<input type="checkbox" class="form-check-input" id="domain-add-loc2"
|
<input type="checkbox" class="form-check-input" id="domain-add-loc2"
|
||||||
@bind="_draftAddLocal" />
|
@bind="_draftAddLocal" />
|
||||||
<label class="form-check-label" for="domain-add-loc2">
|
<label class="form-check-label" for="domain-add-loc2">
|
||||||
Append <code>,local</code> (uncommon)
|
Append <code>,local</code>
|
||||||
</label>
|
</label>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-link btn-sm text-muted ec-domain-rule-help p-0 border-0"
|
||||||
|
title="@LocalSuffixHelpText"
|
||||||
|
aria-label="@LocalSuffixHelpText"
|
||||||
|
@onclick="ShowLocalSuffixHelp">
|
||||||
|
<i class="bi bi-question-circle" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,7 +301,7 @@
|
|||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
@@ -354,6 +425,12 @@
|
|||||||
_draftUseRaw = useRaw;
|
_draftUseRaw = useRaw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShowDomainRuleEditorHelp() => NotificationService.ShowInfo(DomainRuleEditorHelpText);
|
||||||
|
|
||||||
|
private void ShowLocalSuffixHelp() => NotificationService.ShowInfo(LocalSuffixHelpText);
|
||||||
|
|
||||||
|
private void ShowHashDomainHelp() => NotificationService.ShowInfo(HashDomainHelpText);
|
||||||
|
|
||||||
private void OnDraftScopeAfterChange()
|
private void OnDraftScopeAfterChange()
|
||||||
{
|
{
|
||||||
if (_draftScopeSelect == "unconditional")
|
if (_draftScopeSelect == "unconditional")
|
||||||
@@ -490,4 +567,13 @@
|
|||||||
await OnValuesChanged.InvokeAsync(next);
|
await OnValuesChanged.InvokeAsync(next);
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const string DomainRuleEditorHelpText =
|
||||||
|
"domain= sets the suffix dnsmasq uses for DHCP and local DNS naming. Guided mode builds the rule from fields; Raw line mode lets you enter the exact dnsmasq config line directly.";
|
||||||
|
|
||||||
|
private const string LocalSuffixHelpText =
|
||||||
|
"Appending ,local behaves like a matching --local rule for this domain, making dnsmasq answer locally instead of forwarding it upstream. This is uncommon for an unconditional domain= rule.";
|
||||||
|
|
||||||
|
private const string HashDomainHelpText =
|
||||||
|
"Use # as the domain value to take the search domain from resolv.conf instead of hard-coding a suffix in dnsmasq.";
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-19
@@ -7,32 +7,22 @@
|
|||||||
background-color: var(--bs-body-bg, #fff);
|
background-color: var(--bs-body-bg, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ec-domain-rule-editor .ec-field-value-list,
|
/* Keep <ol> markers (decimal) like the default multi-value editor; do not reset list-style here. */
|
||||||
.ec-domain-rule-list {
|
.ec-domain-rule-editor .ec-field-value-list {
|
||||||
list-style: none;
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ec-domain-rule-list > li {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ec-domain-rule-intro {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--bs-secondary-color, #6c757d);
|
|
||||||
margin-bottom: 0.35rem;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ec-domain-rule-row {
|
.ec-domain-rule-row {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ec-domain-rule-row.ec-domain-rule-row-editing {
|
.ec-domain-rule-row-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-row.ec-domain-rule-row-editing .ec-domain-rule-row-body {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,13 +42,77 @@
|
|||||||
color: var(--bs-secondary-color, #6c757d);
|
color: var(--bs-secondary-color, #6c757d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-toolbar__group {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-mode-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
padding: 0.2rem;
|
||||||
|
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-mode-btn {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.2rem 0.65rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-mode-btn:hover,
|
||||||
|
.ec-domain-rule-mode-btn:focus-visible {
|
||||||
|
color: #0f172a;
|
||||||
|
background: rgba(148, 163, 184, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-mode-btn--active,
|
||||||
|
.ec-domain-rule-mode-btn--active:hover,
|
||||||
|
.ec-domain-rule-mode-btn--active:focus-visible {
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(180deg, #0d6efd 0%, #0a58ca 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-help {
|
||||||
|
line-height: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-help:hover,
|
||||||
|
.ec-domain-rule-help:focus-visible {
|
||||||
|
color: var(--bs-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ec-domain-rule-actions {
|
.ec-domain-rule-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-editor .ec-field-value-list li.ec-multi-list-add-item .ec-domain-rule-actions .status-badge-inline,
|
||||||
|
.ec-domain-rule-actions .status-badge-inline {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ec-domain-rule-summary {
|
.ec-domain-rule-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -77,3 +131,20 @@
|
|||||||
color: var(--bs-secondary-color, #6c757d);
|
color: var(--bs-secondary-color, #6c757d);
|
||||||
margin-top: -0.25rem;
|
margin-top: -0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-hint-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-local-check {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-domain-rule-local-check .form-check-label {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|||||||
+2
-34
@@ -38,42 +38,10 @@ else
|
|||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (Context.ShowEditableBadge && Context.EffectiveItems.Count > 0)
|
else
|
||||||
{
|
|
||||||
<li class="ec-multi-list-entry-point">
|
|
||||||
<OptionStateBadge
|
|
||||||
OptionName="@Context.Descriptor.OptionName"
|
|
||||||
ShowEditableBadge="true"
|
|
||||||
EditableAsButton="true"
|
|
||||||
FieldKey="@Context.FieldKey"
|
|
||||||
IsActiveEditor="false"
|
|
||||||
IsInteractive="true"
|
|
||||||
HasPendingChange="@Context.HasPendingOrDraft"
|
|
||||||
IsCapabilityDisabled="@Context.Descriptor.IsCapabilityDisabled"
|
|
||||||
CapabilityDisabledReason="@Context.Descriptor.CapabilityDisabledReason" />
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
else if (Context.EffectiveItems.Count == 0)
|
|
||||||
{
|
{
|
||||||
<li class="ec-multi-list-empty-hint">
|
<li class="ec-multi-list-empty-hint">
|
||||||
@if (Context.ShowEditableBadge)
|
<span class="text-muted">(none)</span>
|
||||||
{
|
|
||||||
<span class="text-muted">(none)</span>
|
|
||||||
<OptionStateBadge
|
|
||||||
OptionName="@Context.Descriptor.OptionName"
|
|
||||||
ShowEditableBadge="true"
|
|
||||||
EditableAsButton="true"
|
|
||||||
FieldKey="@Context.FieldKey"
|
|
||||||
IsActiveEditor="false"
|
|
||||||
IsInteractive="true"
|
|
||||||
HasPendingChange="@Context.HasPendingOrDraft"
|
|
||||||
IsCapabilityDisabled="@Context.Descriptor.IsCapabilityDisabled"
|
|
||||||
CapabilityDisabledReason="@Context.Descriptor.CapabilityDisabledReason" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span class="text-muted">(none)</span>
|
|
||||||
}
|
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
+15
-15
@@ -49,22 +49,22 @@ else if (Descriptor.IsMultiValue)
|
|||||||
{
|
{
|
||||||
@* Custom multi: row badge only while this field is active (ok/cancel/revert). Otherwise per-item / entry badges in the fragment suffice — avoids duplicating "readonly" when RowSource matches item sources. *@
|
@* Custom multi: row badge only while this field is active (ok/cancel/revert). Otherwise per-item / entry badges in the fragment suffice — avoids duplicating "readonly" when RowSource matches item sources. *@
|
||||||
var customMultiActiveEditor = IsEditMode && IsActive && !Descriptor.IsCapabilityDisabled;
|
var customMultiActiveEditor = IsEditMode && IsActive && !Descriptor.IsCapabilityDisabled;
|
||||||
<li class="ec-field @(customMultiActiveEditor ? "ec-field-editing" : "")">
|
<li class="ec-field ec-field--multi-value @(customMultiActiveEditor ? "ec-field-editing" : "")">
|
||||||
<OptionHelpLabel AnchorId="@_anchorId" HelpKey="@_optionHelpKey" OptionLabel="@Descriptor.OptionName" Title="@(_labelTooltip ?? Descriptor.GetSource()?.GetReadOnlyTooltip())"><strong>@Descriptor.OptionName:</strong></OptionHelpLabel>
|
<div class="ec-field-label-row">
|
||||||
<span class="ec-field-value">
|
<OptionHelpLabel AnchorId="@_anchorId" HelpKey="@_optionHelpKey" OptionLabel="@Descriptor.OptionName" Title="@(_labelTooltip ?? Descriptor.GetSource()?.GetReadOnlyTooltip())"><strong>@Descriptor.OptionName:</strong></OptionHelpLabel>
|
||||||
<CascadingValue Value="@customMultiActiveEditor">
|
@if (!Descriptor.IsCapabilityDisabled)
|
||||||
<CascadingValue Value="@IsEditMode">
|
{
|
||||||
@customMulti(_multiContext)
|
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@(customMultiActiveEditor ? _multiContext.RowSource : null)" OptionName="@Descriptor.OptionName" Value="@_multiContext.EffectiveItems"
|
||||||
</CascadingValue>
|
IsInteractive="@IsEditMode" ShowEditableBadge="true" HasPendingChange="@_multiContext.HasPendingOrDraft" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@customMultiActiveEditor"
|
||||||
|
IsCapabilityDisabled="@Descriptor.IsCapabilityDisabled" CapabilityDisabledReason="@Descriptor.CapabilityDisabledReason"
|
||||||
|
OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<CascadingValue Value="@customMultiActiveEditor">
|
||||||
|
<CascadingValue Value="@IsEditMode">
|
||||||
|
@customMulti(_multiContext)
|
||||||
</CascadingValue>
|
</CascadingValue>
|
||||||
</span>
|
</CascadingValue>
|
||||||
@if (customMultiActiveEditor)
|
|
||||||
{
|
|
||||||
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@_multiContext.RowSource" OptionName="@Descriptor.OptionName" Value="@_multiContext.EffectiveItems"
|
|
||||||
IsInteractive="@(IsEditMode && !Descriptor.IsCapabilityDisabled)" ShowEditableBadge="@(!Descriptor.IsCapabilityDisabled)" HasPendingChange="@_multiContext.HasPendingOrDraft" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="true"
|
|
||||||
IsCapabilityDisabled="@Descriptor.IsCapabilityDisabled" CapabilityDisabledReason="@Descriptor.CapabilityDisabledReason"
|
|
||||||
OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
|
||||||
}
|
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
else if (_multiContext is not null)
|
else if (_multiContext is not null)
|
||||||
|
|||||||
+41
-39
@@ -19,47 +19,49 @@
|
|||||||
var rowValue = item.Value;
|
var rowValue = item.Value;
|
||||||
|
|
||||||
<li class="ec-multi-list-row @(_editingIndex == rowIndex && !itemReadonly ? "ec-multi-list-row-with-controls" : "")" @key="anchorId">
|
<li class="ec-multi-list-row @(_editingIndex == rowIndex && !itemReadonly ? "ec-multi-list-row-with-controls" : "")" @key="anchorId">
|
||||||
@if (_editingIndex == rowIndex && !itemReadonly)
|
<div class="ec-multi-list-row-body">
|
||||||
{
|
@if (_editingIndex == rowIndex && !itemReadonly)
|
||||||
<div class="ec-multi-list-row-controls">
|
{
|
||||||
<div class="ec-multi-list-input-group">
|
<div class="ec-multi-list-row-controls">
|
||||||
<input class="form-control form-control-sm ec-multi-list-input"
|
<div class="ec-multi-list-input-group">
|
||||||
placeholder="@(DnsmasqOptionPlaceholders.Get(OptionName) ?? "Value")"
|
<input class="form-control form-control-sm ec-multi-list-input"
|
||||||
@bind="_draftValue" @bind:event="oninput" />
|
placeholder="@(DnsmasqOptionPlaceholders.Get(OptionName) ?? "Value")"
|
||||||
|
@bind="_draftValue" @bind:event="oninput" />
|
||||||
|
</div>
|
||||||
|
<span class="status-badge-inline">
|
||||||
|
<StatusBadge Kind="StatusBadgeKind.ActionOk" Text="save" Clickable="true"
|
||||||
|
Title="Apply change" AriaLabel="Apply change"
|
||||||
|
PreventBlurOnMouseDown="true" OnClick="@(() => SaveEdit(rowIndex))" Class="status-badge-inline" />
|
||||||
|
<StatusBadge Kind="StatusBadgeKind.ActionCancel" Text="cancel" Clickable="true"
|
||||||
|
Title="Discard edit" AriaLabel="Cancel edit"
|
||||||
|
PreventBlurOnMouseDown="true" OnClick="CancelInline" Class="status-badge-inline" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="status-badge-inline">
|
}
|
||||||
<StatusBadge Kind="StatusBadgeKind.ActionOk" Text="save" Clickable="true"
|
else
|
||||||
Title="Apply change" AriaLabel="Apply change"
|
{
|
||||||
PreventBlurOnMouseDown="true" OnClick="@(() => SaveEdit(rowIndex))" Class="status-badge-inline" />
|
<span class="ec-field-value-item">@rowValue</span>
|
||||||
<StatusBadge Kind="StatusBadgeKind.ActionCancel" Text="cancel" Clickable="true"
|
|
||||||
Title="Discard edit" AriaLabel="Cancel edit"
|
|
||||||
PreventBlurOnMouseDown="true" OnClick="CancelInline" Class="status-badge-inline" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span class="ec-field-value-item">@rowValue</span>
|
|
||||||
|
|
||||||
<span class="status-badge-inline">
|
<span class="status-badge-inline">
|
||||||
<OptionStateBadge
|
<OptionStateBadge
|
||||||
AnchorId="@anchorId"
|
AnchorId="@anchorId"
|
||||||
Source="@item.Source"
|
Source="@item.Source"
|
||||||
OptionName="@OptionName"
|
OptionName="@OptionName"
|
||||||
Value="@rowValue"
|
Value="@rowValue"
|
||||||
IsInteractive="@IsEditMode"
|
IsInteractive="@IsEditMode"
|
||||||
ShowEditableBadge="@(!itemReadonly)"
|
ShowEditableBadge="@(!itemReadonly)"
|
||||||
EditableAsButton="@(!itemReadonly)"
|
EditableAsButton="@(!itemReadonly)"
|
||||||
OnEditableClickRequested="@(!itemReadonly ? EventCallback.Factory.Create(this, () => { BeginEdit(rowIndex, rowValue); return Task.CompletedTask; }) : default)" />
|
OnEditableClickRequested="@(!itemReadonly ? EventCallback.Factory.Create(this, () => { BeginEdit(rowIndex, rowValue); return Task.CompletedTask; }) : default)" />
|
||||||
@if (!itemReadonly)
|
@if (!itemReadonly)
|
||||||
{
|
{
|
||||||
<StatusBadge Kind="StatusBadgeKind.ActionRevert" Text="remove" Clickable="true"
|
<StatusBadge Kind="StatusBadgeKind.ActionRevert" Text="remove" Clickable="true"
|
||||||
Title="Remove this value" AriaLabel="Remove"
|
Title="Remove this value" AriaLabel="Remove"
|
||||||
PreventBlurOnMouseDown="true"
|
PreventBlurOnMouseDown="true"
|
||||||
OnClick="@(() => RemoveAt(rowIndex))" Class="status-badge-inline" />
|
OnClick="@(() => RemoveAt(rowIndex))" Class="status-badge-inline" />
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-6
@@ -13,14 +13,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ec-multi-list-row {
|
.ec-multi-list-row {
|
||||||
|
min-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-multi-list-row-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
min-height: 2rem;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ec-multi-list-row.ec-multi-list-row-with-controls {
|
.ec-multi-list-row.ec-multi-list-row-with-controls .ec-multi-list-row-body {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,18 +51,18 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ec-multi-list-row .ec-field-value-item {
|
.ec-multi-list-row-body .ec-field-value-item {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ec-multi-list-row .status-badge-inline {
|
.ec-multi-list-row-body .status-badge-inline {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ec-multi-list-row .form-control.ec-multi-list-input {
|
.ec-multi-list-row-body .form-control.ec-multi-list-input {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -86,7 +90,7 @@
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ec-multi-list-add-item .status-badge-inline {
|
.ec-multi-list-add-item > .ec-multi-list-row-controls > .status-badge-inline {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-40
@@ -2,8 +2,18 @@
|
|||||||
@using DnsmasqWebUI.Models.Dnsmasq
|
@using DnsmasqWebUI.Models.Dnsmasq
|
||||||
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
||||||
|
|
||||||
<li class="ec-field">
|
<li class="ec-field ec-field--multi-value @(Context.IsActiveEditor ? "ec-field-editing" : "")">
|
||||||
<OptionHelpLabel AnchorId="@(OptionHelpAnchorId ?? "")" HelpKey="@OptionHelpKey" OptionLabel="@Context.Descriptor.OptionName" Title="@LabelTooltip"><strong>@Context.Descriptor.OptionName:</strong></OptionHelpLabel>
|
<div class="ec-field-label-row">
|
||||||
|
<OptionHelpLabel AnchorId="@(OptionHelpAnchorId ?? "")" HelpKey="@OptionHelpKey" OptionLabel="@Context.Descriptor.OptionName" Title="@LabelTooltip"><strong>@Context.Descriptor.OptionName:</strong></OptionHelpLabel>
|
||||||
|
@if (Context.IsActiveEditor || Context.ShowEditableBadge)
|
||||||
|
{
|
||||||
|
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorIdBase ?? "")" Source="@(Context.IsActiveEditor ? Context.RowSource : null)" OptionName="@Context.Descriptor.OptionName" Value="@Context.EffectiveItems"
|
||||||
|
IsInteractive="@Context.IsEditMode" ShowEditableBadge="@Context.ShowEditableBadge" HasPendingChange="@Context.HasPendingOrDraft"
|
||||||
|
EditableAsButton="true" FieldKey="@Context.FieldKey" IsActiveEditor="@Context.IsActiveEditor"
|
||||||
|
IsCapabilityDisabled="@Context.Descriptor.IsCapabilityDisabled" CapabilityDisabledReason="@Context.Descriptor.CapabilityDisabledReason"
|
||||||
|
OnConfirmRequested="@OnConfirmRequested" OnCancelRequested="@OnCancelRequested" OnRevertRequested="@OnRevertRequested" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@if (Context.IsActiveEditor)
|
@if (Context.IsActiveEditor)
|
||||||
{
|
{
|
||||||
<EffectiveConfigMultiValueListEditor
|
<EffectiveConfigMultiValueListEditor
|
||||||
@@ -16,11 +26,6 @@
|
|||||||
Behavior="@(MultiDesc?.Behavior)"
|
Behavior="@(MultiDesc?.Behavior)"
|
||||||
Validator="@(MultiDesc?.Validator)"
|
Validator="@(MultiDesc?.Validator)"
|
||||||
OnValuesChanged="@ItemsChanged" />
|
OnValuesChanged="@ItemsChanged" />
|
||||||
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorIdBase ?? "")" Source="@Context.RowSource" OptionName="@Context.Descriptor.OptionName" Value="@Context.EffectiveItems"
|
|
||||||
IsInteractive="@Context.IsEditMode" ShowEditableBadge="@Context.ShowEditableBadge" HasPendingChange="@Context.HasPendingOrDraft"
|
|
||||||
EditableAsButton="true" FieldKey="@Context.FieldKey" IsActiveEditor="@Context.IsActiveEditor"
|
|
||||||
IsCapabilityDisabled="@Context.Descriptor.IsCapabilityDisabled" CapabilityDisabledReason="@Context.Descriptor.CapabilityDisabledReason"
|
|
||||||
OnConfirmRequested="@OnConfirmRequested" OnCancelRequested="@OnCancelRequested" OnRevertRequested="@OnRevertRequested" />
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -44,42 +49,10 @@
|
|||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (Context.ShowEditableBadge && Context.EffectiveItems.Count > 0)
|
|
||||||
{
|
|
||||||
<li class="ec-multi-list-entry-point">
|
|
||||||
<OptionStateBadge
|
|
||||||
OptionName="@Context.Descriptor.OptionName"
|
|
||||||
ShowEditableBadge="true"
|
|
||||||
EditableAsButton="true"
|
|
||||||
FieldKey="@Context.FieldKey"
|
|
||||||
IsActiveEditor="false"
|
|
||||||
IsInteractive="true"
|
|
||||||
HasPendingChange="@Context.HasPendingOrDraft"
|
|
||||||
IsCapabilityDisabled="@Context.Descriptor.IsCapabilityDisabled"
|
|
||||||
CapabilityDisabledReason="@Context.Descriptor.CapabilityDisabledReason" />
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
else if (Context.EffectiveItems.Count == 0)
|
else if (Context.EffectiveItems.Count == 0)
|
||||||
{
|
{
|
||||||
<li class="ec-multi-list-empty-hint">
|
<li class="ec-multi-list-empty-hint">
|
||||||
@if (Context.ShowEditableBadge)
|
<span class="text-muted">(none)</span>
|
||||||
{
|
|
||||||
<span class="text-muted">(none)</span>
|
|
||||||
<OptionStateBadge
|
|
||||||
OptionName="@Context.Descriptor.OptionName"
|
|
||||||
ShowEditableBadge="true"
|
|
||||||
EditableAsButton="true"
|
|
||||||
FieldKey="@Context.FieldKey"
|
|
||||||
IsActiveEditor="false"
|
|
||||||
IsInteractive="true"
|
|
||||||
HasPendingChange="@Context.HasPendingOrDraft"
|
|
||||||
IsCapabilityDisabled="@Context.Descriptor.IsCapabilityDisabled"
|
|
||||||
CapabilityDisabledReason="@Context.Descriptor.CapabilityDisabledReason" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span class="text-muted">(none)</span>
|
|
||||||
}
|
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
@@ -230,6 +230,12 @@ else
|
|||||||
@onclick="OnManagedHeaderSaveAsync">
|
@onclick="OnManagedHeaderSaveAsync">
|
||||||
<i class="bi bi-check-lg" aria-hidden="true"></i><span>Save</span>
|
<i class="bi bi-check-lg" aria-hidden="true"></i><span>Save</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn hosts-managed-action-btn hosts-managed-action-btn--cancel"
|
||||||
|
title="Discard this row edit"
|
||||||
|
@onclick="OnManagedHeaderCancelAsync">
|
||||||
|
<i class="bi bi-x-lg" aria-hidden="true"></i><span>Cancel</span>
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
else if (ManagedSelectionCount == 1)
|
else if (ManagedSelectionCount == 1)
|
||||||
{
|
{
|
||||||
@@ -865,6 +871,11 @@ else
|
|||||||
await TryFinishHostEditFromKeyboardAsync();
|
await TryFinishHostEditFromKeyboardAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnManagedHeaderCancelAsync()
|
||||||
|
{
|
||||||
|
await DiscardCurrentEditAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnManagedHeaderEditAsync()
|
private async Task OnManagedHeaderEditAsync()
|
||||||
{
|
{
|
||||||
DeactivateEffectiveConfigFieldIfActive();
|
DeactivateEffectiveConfigFieldIfActive();
|
||||||
@@ -1057,6 +1068,11 @@ else
|
|||||||
|
|
||||||
private void RevealHostFieldValidation() => _showHostFieldValidation = true;
|
private void RevealHostFieldValidation() => _showHostFieldValidation = true;
|
||||||
|
|
||||||
|
private bool IsCurrentEditBlank =>
|
||||||
|
string.IsNullOrWhiteSpace(_editAddress)
|
||||||
|
&& ParseNamesList(_editNames).Count == 0
|
||||||
|
&& string.IsNullOrWhiteSpace(_editInlineComment);
|
||||||
|
|
||||||
private void DeactivateEffectiveConfigFieldIfActive()
|
private void DeactivateEffectiveConfigFieldIfActive()
|
||||||
{
|
{
|
||||||
if (Session.Ui.ActiveFieldKey != null)
|
if (Session.Ui.ActiveFieldKey != null)
|
||||||
@@ -1072,41 +1088,8 @@ else
|
|||||||
|
|
||||||
private async Task OnInvalidEditDiscardAsync()
|
private async Task OnInvalidEditDiscardAsync()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_editingRowId) || _status == null || string.IsNullOrWhiteSpace(_status.ManagedHostsFilePath))
|
|
||||||
{
|
|
||||||
_showInvalidHostEditModal = false;
|
|
||||||
CancelEdit();
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var list = CloneEntries(CurrentManagedEntries).ToList();
|
|
||||||
var entry = list.FirstOrDefault(MatchesEditingId);
|
|
||||||
var wasNew = entry != null && entry.Id.StartsWith("new:", StringComparison.Ordinal);
|
|
||||||
if (wasNew)
|
|
||||||
{
|
|
||||||
list.Remove(entry!);
|
|
||||||
Session.Draft.SetManagedHostsDraft(
|
|
||||||
CloneEntries(_baselineManagedEntries),
|
|
||||||
list,
|
|
||||||
_status.ManagedHostsFilePath);
|
|
||||||
RebuildManagedOverlay();
|
|
||||||
InvalidateVisibleGroupsCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
_showInvalidHostEditModal = false;
|
_showInvalidHostEditModal = false;
|
||||||
var prevId = _editingRowId;
|
await DiscardCurrentEditAsync();
|
||||||
CancelEdit();
|
|
||||||
if (wasNew)
|
|
||||||
_selectedManagedRowIds.Clear();
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_selectedManagedRowIds.Clear();
|
|
||||||
if (!string.IsNullOrEmpty(prevId))
|
|
||||||
_selectedManagedRowIds.Add(prevId);
|
|
||||||
}
|
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnEditFieldKeyDown(KeyboardEventArgs e)
|
private async Task OnEditFieldKeyDown(KeyboardEventArgs e)
|
||||||
@@ -1131,6 +1114,12 @@ else
|
|||||||
if (_showInvalidHostEditModal || string.IsNullOrEmpty(_editingRowId))
|
if (_showInvalidHostEditModal || string.IsNullOrEmpty(_editingRowId))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (IsCurrentEditBlank)
|
||||||
|
{
|
||||||
|
await DiscardCurrentEditAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (IsCurrentEditValid)
|
if (IsCurrentEditValid)
|
||||||
await CommitAndEndInlineEditAsync();
|
await CommitAndEndInlineEditAsync();
|
||||||
else
|
else
|
||||||
@@ -1168,6 +1157,10 @@ else
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var recentCueInteraction = await _hostsTableScrollCueModule.InvokeAsync<bool>("wasRecentCueInteraction");
|
||||||
|
if (recentCueInteraction)
|
||||||
|
return;
|
||||||
|
|
||||||
outside = await _hostsTableScrollCueModule.InvokeAsync<bool>("isFocusOutsideElement", _editingRowRef);
|
outside = await _hostsTableScrollCueModule.InvokeAsync<bool>("isFocusOutsideElement", _editingRowRef);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is JSDisconnectedException or JSException or ObjectDisposedException or TaskCanceledException)
|
catch (Exception ex) when (ex is JSDisconnectedException or JSException or ObjectDisposedException or TaskCanceledException)
|
||||||
@@ -1178,6 +1171,12 @@ else
|
|||||||
|
|
||||||
if (outside)
|
if (outside)
|
||||||
{
|
{
|
||||||
|
if (IsCurrentEditBlank)
|
||||||
|
{
|
||||||
|
await DiscardCurrentEditAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (IsCurrentEditValid)
|
if (IsCurrentEditValid)
|
||||||
await CommitAndEndInlineEditAsync();
|
await CommitAndEndInlineEditAsync();
|
||||||
else
|
else
|
||||||
@@ -1220,6 +1219,41 @@ else
|
|||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task DiscardCurrentEditAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_editingRowId) || _status == null || string.IsNullOrWhiteSpace(_status.ManagedHostsFilePath))
|
||||||
|
{
|
||||||
|
CancelEdit();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = CloneEntries(CurrentManagedEntries).ToList();
|
||||||
|
var entry = list.FirstOrDefault(MatchesEditingId);
|
||||||
|
var wasNew = entry != null && entry.Id.StartsWith("new:", StringComparison.Ordinal);
|
||||||
|
var prevId = _editingRowId;
|
||||||
|
|
||||||
|
if (wasNew)
|
||||||
|
{
|
||||||
|
list.Remove(entry!);
|
||||||
|
Session.Draft.SetManagedHostsDraft(
|
||||||
|
CloneEntries(_baselineManagedEntries),
|
||||||
|
list,
|
||||||
|
_status.ManagedHostsFilePath);
|
||||||
|
RebuildManagedOverlay();
|
||||||
|
InvalidateVisibleGroupsCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
CancelEdit();
|
||||||
|
_showInvalidHostEditModal = false;
|
||||||
|
_selectedManagedRowIds.Clear();
|
||||||
|
if (!wasNew && !string.IsNullOrEmpty(prevId))
|
||||||
|
_selectedManagedRowIds.Add(prevId);
|
||||||
|
_selectionAnchorRowId = !wasNew ? prevId : null;
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
private bool MatchesEditingId(HostEntry e)
|
private bool MatchesEditingId(HostEntry e)
|
||||||
{
|
{
|
||||||
var id = string.IsNullOrEmpty(e.Id) ? $"managed:{e.LineNumber}" : e.Id;
|
var id = string.IsNullOrEmpty(e.Id) ? $"managed:{e.LineNumber}" : e.Id;
|
||||||
|
|||||||
@@ -376,7 +376,7 @@
|
|||||||
/* Fixed column for actions avoids path/subtitle reflow when buttons show or hide. */
|
/* Fixed column for actions avoids path/subtitle reflow when buttons show or hide. */
|
||||||
.hosts-group-header__path-row--with-managed-actions {
|
.hosts-group-header__path-row--with-managed-actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) var(--hosts-managed-actions-col, 9.75rem);
|
grid-template-columns: minmax(0, 1fr) var(--hosts-managed-actions-col, 13.5rem);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -477,6 +477,19 @@
|
|||||||
border-color: rgba(8, 66, 152, 0.92);
|
border-color: rgba(8, 66, 152, 0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hosts-managed-action-btn--cancel {
|
||||||
|
color: #334155;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||||
|
border-color: rgba(148, 163, 184, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hosts-managed-action-btn--cancel:hover,
|
||||||
|
.hosts-managed-action-btn--cancel:focus-visible {
|
||||||
|
color: #0f172a;
|
||||||
|
background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
|
||||||
|
border-color: rgba(100, 116, 139, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
.hosts-managed-action-btn--edit {
|
.hosts-managed-action-btn--edit {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||||
@@ -589,7 +602,7 @@
|
|||||||
|
|
||||||
@media (max-width: 640.98px) {
|
@media (max-width: 640.98px) {
|
||||||
.hosts-group-header__path-row--with-managed-actions {
|
.hosts-group-header__path-row--with-managed-actions {
|
||||||
--hosts-managed-actions-col: 9rem;
|
--hosts-managed-actions-col: 12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hosts-group-header__managed-actions {
|
.hosts-group-header__managed-actions {
|
||||||
|
|||||||
@@ -10,10 +10,16 @@ const POLL_MS = 100;
|
|||||||
const BTN_LEFT_CLASS = 'app-table-cue-btn--left';
|
const BTN_LEFT_CLASS = 'app-table-cue-btn--left';
|
||||||
const BTN_RIGHT_CLASS = 'app-table-cue-btn--right';
|
const BTN_RIGHT_CLASS = 'app-table-cue-btn--right';
|
||||||
const CUE_BTN_CLASS = 'app-table-cue-btn';
|
const CUE_BTN_CLASS = 'app-table-cue-btn';
|
||||||
|
const RECENT_CUE_INTERACTION_MS = 300;
|
||||||
|
|
||||||
let pollId = null;
|
let pollId = null;
|
||||||
let resizeListener = null;
|
let resizeListener = null;
|
||||||
const scrollListeners = new WeakMap();
|
const scrollListeners = new WeakMap();
|
||||||
|
let lastCueInteractionAt = 0;
|
||||||
|
|
||||||
|
function markCueInteraction() {
|
||||||
|
lastCueInteractionAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
function applyScrollCue(scrollEl, hostEl) {
|
function applyScrollCue(scrollEl, hostEl) {
|
||||||
if (!scrollEl || !hostEl) return;
|
if (!scrollEl || !hostEl) return;
|
||||||
@@ -58,7 +64,9 @@ function ensureCueButtons(hostEl) {
|
|||||||
leftBtn.type = 'button';
|
leftBtn.type = 'button';
|
||||||
leftBtn.className = CUE_BTN_CLASS + ' ' + BTN_LEFT_CLASS;
|
leftBtn.className = CUE_BTN_CLASS + ' ' + BTN_LEFT_CLASS;
|
||||||
leftBtn.setAttribute('aria-label', 'Scroll table left');
|
leftBtn.setAttribute('aria-label', 'Scroll table left');
|
||||||
|
leftBtn.addEventListener('pointerdown', markCueInteraction);
|
||||||
leftBtn.addEventListener('click', function () {
|
leftBtn.addEventListener('click', function () {
|
||||||
|
markCueInteraction();
|
||||||
scrollToSide(hostEl.querySelector(WRAPPER_SELECTOR), 'left');
|
scrollToSide(hostEl.querySelector(WRAPPER_SELECTOR), 'left');
|
||||||
});
|
});
|
||||||
hostEl.appendChild(leftBtn);
|
hostEl.appendChild(leftBtn);
|
||||||
@@ -69,7 +77,9 @@ function ensureCueButtons(hostEl) {
|
|||||||
rightBtn.type = 'button';
|
rightBtn.type = 'button';
|
||||||
rightBtn.className = CUE_BTN_CLASS + ' ' + BTN_RIGHT_CLASS;
|
rightBtn.className = CUE_BTN_CLASS + ' ' + BTN_RIGHT_CLASS;
|
||||||
rightBtn.setAttribute('aria-label', 'Scroll table right');
|
rightBtn.setAttribute('aria-label', 'Scroll table right');
|
||||||
|
rightBtn.addEventListener('pointerdown', markCueInteraction);
|
||||||
rightBtn.addEventListener('click', function () {
|
rightBtn.addEventListener('click', function () {
|
||||||
|
markCueInteraction();
|
||||||
scrollToSide(hostEl.querySelector(WRAPPER_SELECTOR), 'right');
|
scrollToSide(hostEl.querySelector(WRAPPER_SELECTOR), 'right');
|
||||||
});
|
});
|
||||||
hostEl.appendChild(rightBtn);
|
hostEl.appendChild(rightBtn);
|
||||||
@@ -170,3 +180,7 @@ export function isFocusOutsideElement(container) {
|
|||||||
if (!active) return true;
|
if (!active) return true;
|
||||||
return !container.contains(active);
|
return !container.contains(active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function wasRecentCueInteraction() {
|
||||||
|
return Date.now() - lastCueInteractionAt <= RECENT_CUE_INTERACTION_MS;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user