Skip to content

Commit 5f893c5

Browse files
committed
Refactor section editor scripts
1 parent a62bc88 commit 5f893c5

9 files changed

Lines changed: 117 additions & 96 deletions

File tree

website/MyWebApp.Tests/SanitizationTests.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
public class SanitizationTests
1414
{
15-
private static (ApplicationDbContext ctx, LayoutService layout, ContentProcessingService content) CreateServices()
15+
private static (ApplicationDbContext ctx, LayoutService layout, ContentProcessingService content, TokenRenderService tokens, HtmlSanitizerService sanitizer) CreateServices()
1616
{
1717
var connection = new SqliteConnection("DataSource=:memory:");
1818
connection.Open();
@@ -35,14 +35,14 @@ private static (ApplicationDbContext ctx, LayoutService layout, ContentProcessin
3535
var layout = new LayoutService(cache, tokens);
3636
var sanitizer = new HtmlSanitizerService();
3737
var content = new ContentProcessingService(sanitizer);
38-
return (ctx, layout, content);
38+
return (ctx, layout, content, tokens, sanitizer);
3939
}
4040

4141
[Fact(Skip = "Create sanitization covered by section tests")]
4242
public async Task CreatePage_SanitizesHtml()
4343
{
44-
var (ctx, layout, content) = CreateServices();
45-
var controller = new AdminContentController(ctx, layout, content);
44+
var (ctx, layout, content, tokens, sanitizer) = CreateServices();
45+
var controller = new AdminContentController(ctx, layout, content, tokens, sanitizer);
4646
var model = new Page
4747
{
4848
Slug = "test",
@@ -62,7 +62,7 @@ public async Task CreatePage_SanitizesHtml()
6262
[Fact]
6363
public async Task CreateSection_SanitizesHtml()
6464
{
65-
var (ctx, layout, content) = CreateServices();
65+
var (ctx, layout, content, _, _) = CreateServices();
6666
var controller = new AdminPageSectionController(ctx, layout, content);
6767

6868
var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "main", Html = "<div>hi</div><script>bad()</script>", Type = PageSectionType.Html };
@@ -76,8 +76,8 @@ public async Task CreateSection_SanitizesHtml()
7676
[Fact(Skip = "Edit sanitization covered by section tests")]
7777
public async Task EditPage_SanitizesHtml()
7878
{
79-
var (ctx, layout, content) = CreateServices();
80-
var controller = new AdminContentController(ctx, layout, content);
79+
var (ctx, layout, content, tokens, sanitizer) = CreateServices();
80+
var controller = new AdminContentController(ctx, layout, content, tokens, sanitizer);
8181
var createModel = new Page
8282
{
8383
Slug = "edit",
@@ -104,7 +104,7 @@ public async Task EditPage_SanitizesHtml()
104104
[Fact]
105105
public async Task CreateSection_MarkdownConverted()
106106
{
107-
var (ctx, layout, content) = CreateServices();
107+
var (ctx, layout, content, _, _) = CreateServices();
108108
var controller = new AdminPageSectionController(ctx, layout, content);
109109
var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "md", Html = "# Hello\n<script>bad()</script>", Type = PageSectionType.Markdown };
110110
var result = await controller.Create(model, null);
@@ -117,7 +117,7 @@ public async Task CreateSection_MarkdownConverted()
117117
[Fact]
118118
public async Task CreateSection_CodeEncoded()
119119
{
120-
var (ctx, layout, content) = CreateServices();
120+
var (ctx, layout, content, _, _) = CreateServices();
121121
var controller = new AdminPageSectionController(ctx, layout, content);
122122
var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "code", Html = "<b>test</b>", Type = PageSectionType.Code };
123123
var result = await controller.Create(model, null);
@@ -129,7 +129,7 @@ public async Task CreateSection_CodeEncoded()
129129
[Fact]
130130
public async Task CreateSection_ImageStoresTag()
131131
{
132-
var (ctx, layout, content) = CreateServices();
132+
var (ctx, layout, content, _, _) = CreateServices();
133133
var controller = new AdminPageSectionController(ctx, layout, content);
134134
var bytes = new byte[] { 1, 2, 3 };
135135
using var stream = new System.IO.MemoryStream(bytes);

website/MyWebApp/Controllers/AdminContentController.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ public class AdminContentController : Controller
1818
private readonly LayoutService _layout;
1919
private readonly ContentProcessingService _content;
2020
private readonly TokenRenderService _tokens;
21+
private readonly HtmlSanitizerService _sanitizer;
2122

22-
public AdminContentController(ApplicationDbContext db, LayoutService layout, ContentProcessingService content, TokenRenderService tokens)
23+
public AdminContentController(ApplicationDbContext db, LayoutService layout, ContentProcessingService content, TokenRenderService tokens, HtmlSanitizerService sanitizer)
2324
{
2425
_db = db;
2526
_layout = layout;
2627
_content = content;
2728
_tokens = tokens;
29+
_sanitizer = sanitizer;
2830
}
2931

3032
private async Task LoadTemplatesAsync()
@@ -104,15 +106,19 @@ public async Task<IActionResult> Create(Page model)
104106
[HttpPost]
105107
public async Task<IActionResult> Preview([FromBody] PreviewRequest model)
106108
{
109+
var layout = string.IsNullOrWhiteSpace(model.Layout) ? "single-column" : model.Layout;
107110
var zones = new Dictionary<string, string>();
108111
foreach (var kv in model.Zones ?? new Dictionary<string, string>())
109112
{
110-
zones[kv.Key] = await _tokens.RenderAsync(_db, kv.Value ?? string.Empty);
113+
if (!LayoutService.IsValidZone(layout, kv.Key)) continue;
114+
var clean = _sanitizer.Sanitize(kv.Value ?? string.Empty);
115+
zones[kv.Key] = await _tokens.RenderAsync(_db, clean);
111116
}
112117
ViewBag.HeaderHtml = await _layout.GetHeaderAsync(_db);
113118
ViewBag.FooterHtml = await _layout.GetFooterAsync(_db);
114-
ViewBag.PageLayout = string.IsNullOrWhiteSpace(model.Layout) ? "single-column" : model.Layout;
119+
ViewBag.PageLayout = layout;
115120
ViewBag.ZoneHtml = zones;
121+
Response.Headers["Content-Security-Policy"] = "default-src 'self'";
116122
return View("~/Views/Pages/Show.cshtml", new Page { Title = model.Title });
117123
}
118124

website/MyWebApp/Views/AdminContent/PageEditor.cshtml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
ViewBag.LayoutZones as IReadOnlyDictionary<string, string[]> ??
9191
new Dictionary<string, string[]>()));
9292
</script>
93+
<script src="~/js/section-editor.js" asp-append-version="true"></script>
9394
<script src="~/js/page-editor.js" asp-append-version="true"></script>
9495
<script src="~/js/block-library.js" asp-append-version="true"></script>
9596
<script>

website/MyWebApp/Views/AdminPageSection/Create.cshtml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@
2020

2121
<button type="submit">Save</button>
2222
</form>
23+
<script src="~/js/section-editor.js" asp-append-version="true"></script>
2324
<script src="~/js/page-section-zone.js" asp-append-version="true"></script>

website/MyWebApp/Views/AdminPageSection/Edit.cshtml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@
2121

2222
<button type="submit">Save</button>
2323
</form>
24+
<script src="~/js/section-editor.js" asp-append-version="true"></script>
2425
<script src="~/js/page-section-zone.js" asp-append-version="true"></script>
Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,33 @@
11
@model MyWebApp.Models.PageSection
22
@using MyWebApp.Models
3-
<div>
4-
<label>Type</label>
5-
<select asp-for="Type" asp-items="Html.GetEnumSelectList<PageSectionType>()" id="type-select"></select>
6-
</div>
7-
<div id="html-editor" class="type-editor">
8-
<div class="quill-editor" id="quill-editor"></div>
9-
<input type="hidden" asp-for="Html" />
10-
</div>
11-
<div id="markdown-editor" class="type-editor">
12-
<textarea asp-for="Html"></textarea>
13-
</div>
14-
<div id="code-editor" class="type-editor">
15-
<textarea asp-for="Html"></textarea>
16-
</div>
17-
<div id="file-editor" class="type-editor">
18-
<input type="file" name="file" />
3+
@{
4+
var idxObj = ViewData["Index"];
5+
var idx = idxObj?.ToString() ?? "0";
6+
var prefix = idxObj != null ? $"Sections[{idx}]." : string.Empty;
7+
}
8+
<div class="section-editor" data-index="@idx" draggable="true">
9+
<input type="hidden" name="@($"{prefix}Id")" value="@Model.Id" />
10+
<input type="hidden" name="@($"{prefix}SortOrder")" value="@Model.SortOrder" class="sort-order" />
11+
<div>
12+
<label>Zone</label>
13+
<select class="zone-select" data-index="@idx" name="@($"{prefix}Zone")" data-selected="@Model.Zone"></select>
14+
</div>
15+
<div>
16+
<label>Type</label>
17+
<select id="type-select-@idx" name="@($"{prefix}Type")" asp-items="Html.GetEnumSelectList<PageSectionType>()" value="@Model.Type"></select>
18+
</div>
19+
<div id="html-editor-@idx" class="type-editor">
20+
<div class="quill-editor" id="quill-editor-@idx"></div>
21+
<input type="hidden" id="Html-@idx" name="@($"{prefix}Html")" value="@Model.Html" />
22+
</div>
23+
<div id="markdown-editor-@idx" class="type-editor">
24+
<textarea id="Markdown-@idx" name="@($"{prefix}Html")">@Model.Html</textarea>
25+
</div>
26+
<div id="code-editor-@idx" class="type-editor">
27+
<textarea id="Code-@idx" name="@($"{prefix}Html")">@Model.Html</textarea>
28+
</div>
29+
<div id="file-editor-@idx" class="type-editor">
30+
<input type="file" name="@($"{prefix}File")" />
1931
</div>
2032
<button type="button" class="add-library">Add to Library</button>
21-
<script>
22-
const typeSelect = document.getElementById('type-select');
23-
const htmlDiv = document.getElementById('html-editor');
24-
const markdownDiv = document.getElementById('markdown-editor');
25-
const codeDiv = document.getElementById('code-editor');
26-
const fileDiv = document.getElementById('file-editor');
27-
const quill = new Quill('#quill-editor', { theme: 'snow' });
28-
quill.root.innerHTML = document.getElementById('Html').value;
29-
function updateEditors() {
30-
htmlDiv.style.display = typeSelect.value == 'Html' ? 'block' : 'none';
31-
markdownDiv.style.display = typeSelect.value == 'Markdown' ? 'block' : 'none';
32-
codeDiv.style.display = typeSelect.value == 'Code' ? 'block' : 'none';
33-
fileDiv.style.display = (typeSelect.value == 'Image' || typeSelect.value == 'Video') ? 'block' : 'none';
34-
}
35-
updateEditors();
36-
typeSelect.addEventListener('change', updateEditors);
37-
document.querySelector('form').addEventListener('submit', function () {
38-
if (typeSelect.value === 'Html') {
39-
document.getElementById('Html').value = quill.root.innerHTML;
40-
}
41-
});
42-
</script>
33+
</div>

website/MyWebApp/wwwroot/js/block-library.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ window.addEventListener('load', () => {
3636
list.addEventListener('dragstart', e => {
3737
const card = e.target.closest('.block-card');
3838
if (!card) return;
39-
window.draggedBlockId = card.dataset.id;
39+
const ev = new CustomEvent('blockdragstart', { detail: card.dataset.id });
40+
document.dispatchEvent(ev);
4041
e.dataTransfer.effectAllowed = 'copy';
4142
});
4243
});

website/MyWebApp/wwwroot/js/page-editor.js

Lines changed: 25 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ window.addEventListener('load', () => {
33
if (!container) return;
44
const templateHtml = document.getElementById('section-template').innerHTML.trim();
55
let sectionCount = container.querySelectorAll('.section-editor').length;
6-
const editors = {};
6+
const editors = new Map();
7+
document.addEventListener('section-editor:ready', e => {
8+
editors.set(e.detail.index, e.detail.quill);
9+
e.detail.quill.on('text-change', schedulePreview);
10+
});
711
let activeIndex = null;
812
const layoutSelect = document.getElementById('layout-select');
913
let currentLayout = layoutSelect ? layoutSelect.value : 'single-column';
@@ -50,7 +54,8 @@ window.addEventListener('load', () => {
5054
const idx = sec.dataset.index;
5155
const typeSel = document.getElementById(`type-select-${idx}`);
5256
if (typeSel && typeSel.value === 'Html') {
53-
return editors[idx].root.innerHTML;
57+
const q = editors.get(idx);
58+
return q ? q.root.innerHTML : '';
5459
}
5560
const inp = document.getElementById(`Html-${idx}`);
5661
return inp ? inp.value : '';
@@ -138,15 +143,15 @@ window.addEventListener('load', () => {
138143
templateSelect.addEventListener('change', () => {
139144
const id = templateSelect.value;
140145
if (!id) return;
141-
if (activeIndex === null || !editors[activeIndex]) {
146+
if (activeIndex === null || !editors.has(activeIndex)) {
142147
alert('Select a section first');
143148
templateSelect.value = '';
144149
return;
145150
}
146151
fetch(`/AdminBlockTemplate/Html/${id}`)
147152
.then(r => r.text())
148153
.then(html => {
149-
const quill = editors[activeIndex];
154+
const quill = editors.get(activeIndex);
150155
quill.root.innerHTML = html;
151156
const input = document.getElementById(`Html-${activeIndex}`);
152157
if (input) input.value = html;
@@ -161,7 +166,7 @@ window.addEventListener('load', () => {
161166
const idx = el.dataset.index;
162167
populateZones(el.querySelector('.zone-select'));
163168
placeSection(el);
164-
initSectionEditor(idx);
169+
document.dispatchEvent(new CustomEvent('section-editor:add', { detail: el }));
165170
});
166171
updatePreview();
167172
updateZoneCounts();
@@ -195,7 +200,8 @@ window.addEventListener('load', () => {
195200
const idx = section.dataset.index;
196201
const name = prompt('Block name');
197202
if (!name) return;
198-
const html = editors[idx].root.innerHTML;
203+
const q = editors.get(idx);
204+
const html = q ? q.root.innerHTML : '';
199205
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
200206
fetch('/AdminBlockTemplate/CreateFromSection', {
201207
method: 'POST',
@@ -223,9 +229,10 @@ window.addEventListener('load', () => {
223229
section.dataset.index = index;
224230
populateZones(section.querySelector('.zone-select'));
225231
placeSection(section);
226-
initSectionEditor(index);
232+
document.dispatchEvent(new CustomEvent('section-editor:add', { detail: section }));
227233
if (htmlContent) {
228-
editors[index].root.innerHTML = htmlContent;
234+
const q = editors.get(index);
235+
if (q) q.root.innerHTML = htmlContent;
229236
const input = document.getElementById(`Html-${index}`);
230237
if (input) input.value = htmlContent;
231238
}
@@ -270,11 +277,13 @@ window.addEventListener('load', () => {
270277
});
271278
populateZones(clone.querySelector('.zone-select'));
272279
placeSection(clone);
273-
initSectionEditor(index);
274-
if (editors[original.dataset.index]) {
275-
editors[index].root.innerHTML = editors[original.dataset.index].root.innerHTML;
280+
document.dispatchEvent(new CustomEvent('section-editor:add', { detail: clone }));
281+
const orig = editors.get(original.dataset.index);
282+
const copy = editors.get(index);
283+
if (orig && copy) {
284+
copy.root.innerHTML = orig.root.innerHTML;
276285
const destInput = clone.querySelector(`#Html-${index}`);
277-
if (destInput) destInput.value = editors[index].root.innerHTML;
286+
if (destInput) destInput.value = copy.root.innerHTML;
278287
}
279288
updateIndexes();
280289
updatePreview();
@@ -294,6 +303,9 @@ window.addEventListener('load', () => {
294303

295304
let dragged = null;
296305
let draggedBlockId = null;
306+
document.addEventListener('blockdragstart', e => {
307+
draggedBlockId = e.detail;
308+
});
297309
const dropIndicator = document.createElement('div');
298310
dropIndicator.className = 'drop-indicator';
299311

@@ -349,7 +361,7 @@ window.addEventListener('load', () => {
349361
const form = document.querySelector('form');
350362
if (form) {
351363
form.addEventListener('submit', () => {
352-
Object.entries(editors).forEach(([key, quill]) => {
364+
editors.forEach((quill, key) => {
353365
const typeSelect = document.getElementById(`type-select-${key}`);
354366
if (typeSelect && typeSelect.value === 'Html') {
355367
const input = document.getElementById(`Html-${key}`);
@@ -365,37 +377,6 @@ window.addEventListener('load', () => {
365377
fetch(form.action, { method: 'POST', body: fd });
366378
}
367379

368-
function initSectionEditor(index) {
369-
const typeSelect = document.getElementById(`type-select-${index}`);
370-
const htmlDiv = document.getElementById(`html-editor-${index}`);
371-
const markdownDiv = document.getElementById(`markdown-editor-${index}`);
372-
const codeDiv = document.getElementById(`code-editor-${index}`);
373-
const fileDiv = document.getElementById(`file-editor-${index}`);
374-
const quillInput = document.getElementById(`Html-${index}`);
375-
const markdown = markdownDiv ? markdownDiv.querySelector('textarea') : null;
376-
const code = codeDiv ? codeDiv.querySelector('textarea') : null;
377-
const quill = new Quill(`#quill-editor-${index}`, { theme: 'snow' });
378-
quill.root.innerHTML = quillInput.value || '';
379-
quill.root.addEventListener('click', () => { activeIndex = index; });
380-
quill.root.addEventListener('focus', () => { activeIndex = index; });
381-
editors[index] = quill;
382-
quill.on('text-change', schedulePreview);
383-
384-
function update() {
385-
const type = typeSelect.value;
386-
htmlDiv.style.display = type === 'Html' ? 'block' : 'none';
387-
markdownDiv.style.display = type === 'Markdown' ? 'block' : 'none';
388-
codeDiv.style.display = type === 'Code' ? 'block' : 'none';
389-
fileDiv.style.display = (type === 'Image' || type === 'Video') ? 'block' : 'none';
390-
if (markdown) markdown.disabled = type !== 'Markdown';
391-
if (code) code.disabled = type !== 'Code';
392-
quillInput.disabled = type !== 'Html';
393-
const fileInput = fileDiv ? fileDiv.querySelector('input[type="file"]') : null;
394-
if (fileInput) fileInput.disabled = !(type === 'Image' || type === 'Video');
395-
}
396-
update();
397-
typeSelect.addEventListener('change', update);
398-
}
399380

400381
modePreview?.addEventListener('click', () => {
401382
pageEditor?.classList.add('preview');

0 commit comments

Comments
 (0)