|
1 | 1 | @namespace PrompterOne.Shared.Components.Editor |
| 2 | +@using Microsoft.Extensions.Localization |
| 3 | +@using PrompterOne.Shared.Localization |
| 4 | +@inject IStringLocalizer<SharedResource> Localizer |
2 | 5 |
|
3 | 6 | <div class="editor-rendered-view" data-test="@UiTestIds.Editor.RenderedView"> |
4 | 7 | <div class="editor-rendered-strip" data-test="@UiTestIds.Editor.RenderedStrip"> |
|
33 | 36 |
|
34 | 37 | @foreach (var block in segment.Blocks) |
35 | 38 | { |
36 | | - <article class="editor-rendered-block" data-test="@UiTestIds.Editor.RenderedBlock(block.SegmentIndex, block.BlockIndex)"> |
| 39 | + <article class="@GetBlockCssClass(block)" |
| 40 | + draggable="true" |
| 41 | + @ondragstart="() => OnBlockDragStart(block)" |
| 42 | + @ondragend="OnBlockDragEnd" |
| 43 | + @ondragenter="() => OnBlockDragEnter(block)" |
| 44 | + @ondragleave="() => OnBlockDragLeave(block)" |
| 45 | + @ondragover:preventDefault="true" |
| 46 | + @ondrop="() => OnBlockDropAsync(block)" |
| 47 | + @ondrop:preventDefault="true" |
| 48 | + data-test="@UiTestIds.Editor.RenderedBlock(block.SegmentIndex, block.BlockIndex)"> |
37 | 49 | <div class="editor-rendered-block-head"> |
| 50 | + <span class="editor-rendered-block-drag-handle" |
| 51 | + aria-hidden="true">⋮⋮</span> |
38 | 52 | <span class="editor-rendered-block-number">@block.Number</span> |
39 | 53 | <span class="editor-rendered-block-title">@block.Name</span> |
| 54 | + <span class="editor-rendered-block-actions"> |
| 55 | + <button type="button" |
| 56 | + class="editor-rendered-block-move" |
| 57 | + aria-label="@Text(UiTextKey.EditorRenderedMoveBlockUp)" |
| 58 | + disabled="@(!CanMoveBlock(block, -1))" |
| 59 | + @onclick="() => MoveBlockByOffsetAsync(block, -1)" |
| 60 | + data-test="@UiTestIds.Editor.RenderedBlockMoveUp(block.SegmentIndex, block.BlockIndex)">↑</button> |
| 61 | + <button type="button" |
| 62 | + class="editor-rendered-block-move" |
| 63 | + aria-label="@Text(UiTextKey.EditorRenderedMoveBlockDown)" |
| 64 | + disabled="@(!CanMoveBlock(block, 1))" |
| 65 | + @onclick="() => MoveBlockByOffsetAsync(block, 1)" |
| 66 | + data-test="@UiTestIds.Editor.RenderedBlockMoveDown(block.SegmentIndex, block.BlockIndex)">↓</button> |
| 67 | + </span> |
40 | 68 | </div> |
41 | 69 | <textarea class="editor-rendered-text" |
42 | 70 | aria-label="@block.Name" |
|
67 | 95 |
|
68 | 96 | [Parameter] public EventCallback<EditorRenderedBlockTextChange> TextChanged { get; set; } |
69 | 97 |
|
| 98 | + [Parameter] public EventCallback<EditorRenderedBlockReorderRequest> BlockReorderRequested { get; set; } |
| 99 | + |
| 100 | + private EditorRenderedBlockViewModel? _draggedBlock; |
| 101 | + |
| 102 | + private EditorRenderedBlockViewModel? _dragOverBlock; |
| 103 | + |
70 | 104 | private Task OnBlockInputAsync(EditorRenderedBlockViewModel block, ChangeEventArgs args) => |
71 | 105 | TextChanged.InvokeAsync(new EditorRenderedBlockTextChange( |
72 | 106 | block.SegmentIndex, |
|
78 | 112 | 0, |
79 | 113 | 0, |
80 | 114 | args.Value?.ToString() ?? string.Empty)); |
| 115 | + |
| 116 | + private void OnBlockDragStart(EditorRenderedBlockViewModel block) |
| 117 | + { |
| 118 | + _draggedBlock = block; |
| 119 | + _dragOverBlock = null; |
| 120 | + } |
| 121 | + |
| 122 | + private void OnBlockDragEnter(EditorRenderedBlockViewModel block) |
| 123 | + { |
| 124 | + if (_draggedBlock is not null && !IsSameBlock(_draggedBlock, block)) |
| 125 | + { |
| 126 | + _dragOverBlock = block; |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + private void OnBlockDragLeave(EditorRenderedBlockViewModel block) |
| 131 | + { |
| 132 | + if (_dragOverBlock is not null && IsSameBlock(_dragOverBlock, block)) |
| 133 | + { |
| 134 | + _dragOverBlock = null; |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + private async Task OnBlockDropAsync(EditorRenderedBlockViewModel target) |
| 139 | + { |
| 140 | + var dragged = _draggedBlock; |
| 141 | + _draggedBlock = null; |
| 142 | + _dragOverBlock = null; |
| 143 | + if (dragged is null || IsSameBlock(dragged, target)) |
| 144 | + { |
| 145 | + return; |
| 146 | + } |
| 147 | + |
| 148 | + await BlockReorderRequested.InvokeAsync(new EditorRenderedBlockReorderRequest( |
| 149 | + dragged.SegmentIndex, |
| 150 | + dragged.BlockIndex, |
| 151 | + target.SegmentIndex, |
| 152 | + target.BlockIndex, |
| 153 | + IsBefore(dragged, target))); |
| 154 | + } |
| 155 | + |
| 156 | + private void OnBlockDragEnd() |
| 157 | + { |
| 158 | + _draggedBlock = null; |
| 159 | + _dragOverBlock = null; |
| 160 | + } |
| 161 | + |
| 162 | + private async Task MoveBlockByOffsetAsync(EditorRenderedBlockViewModel block, int offset) |
| 163 | + { |
| 164 | + var blocks = FlattenBlocks(); |
| 165 | + var index = blocks.FindIndex(candidate => IsSameBlock(candidate, block)); |
| 166 | + var targetIndex = index + offset; |
| 167 | + if (index < 0 || targetIndex < 0 || targetIndex >= blocks.Count) |
| 168 | + { |
| 169 | + return; |
| 170 | + } |
| 171 | + |
| 172 | + var target = blocks[targetIndex]; |
| 173 | + await BlockReorderRequested.InvokeAsync(new EditorRenderedBlockReorderRequest( |
| 174 | + block.SegmentIndex, |
| 175 | + block.BlockIndex, |
| 176 | + target.SegmentIndex, |
| 177 | + target.BlockIndex, |
| 178 | + offset > 0)); |
| 179 | + } |
| 180 | + |
| 181 | + private bool CanMoveBlock(EditorRenderedBlockViewModel block, int offset) |
| 182 | + { |
| 183 | + var blocks = FlattenBlocks(); |
| 184 | + var index = blocks.FindIndex(candidate => IsSameBlock(candidate, block)); |
| 185 | + var targetIndex = index + offset; |
| 186 | + return index >= 0 && targetIndex >= 0 && targetIndex < blocks.Count; |
| 187 | + } |
| 188 | + |
| 189 | + private string GetBlockCssClass(EditorRenderedBlockViewModel block) |
| 190 | + { |
| 191 | + var tokens = new List<string> { "editor-rendered-block" }; |
| 192 | + if (_draggedBlock is not null && IsSameBlock(_draggedBlock, block)) |
| 193 | + { |
| 194 | + tokens.Add("editor-rendered-block--dragging"); |
| 195 | + } |
| 196 | + |
| 197 | + if (_dragOverBlock is not null && IsSameBlock(_dragOverBlock, block)) |
| 198 | + { |
| 199 | + tokens.Add("editor-rendered-block--drop-target"); |
| 200 | + } |
| 201 | + |
| 202 | + return string.Join(' ', tokens); |
| 203 | + } |
| 204 | + |
| 205 | + private List<EditorRenderedBlockViewModel> FlattenBlocks() => |
| 206 | + Segments.SelectMany(segment => segment.Blocks).ToList(); |
| 207 | + |
| 208 | + private static bool IsSameBlock(EditorRenderedBlockViewModel left, EditorRenderedBlockViewModel right) => |
| 209 | + left.SegmentIndex == right.SegmentIndex && left.BlockIndex == right.BlockIndex; |
| 210 | + |
| 211 | + private static bool IsBefore(EditorRenderedBlockViewModel source, EditorRenderedBlockViewModel target) => |
| 212 | + source.SegmentIndex < target.SegmentIndex || |
| 213 | + (source.SegmentIndex == target.SegmentIndex && source.BlockIndex < target.BlockIndex); |
| 214 | + |
| 215 | + private string Text(UiTextKey key) => Localizer[key.ToString()]; |
81 | 216 | } |
0 commit comments