working with quiz editor

This commit is contained in:
2023-08-14 09:03:52 -06:00
parent 4de6122549
commit 1fe232f6a8
12 changed files with 250 additions and 58 deletions

View File

@@ -5,6 +5,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BlazorMonaco" Version="3.0.0" />
<PackageReference Include="dotenv.net" Version="3.1.2" /> <PackageReference Include="dotenv.net" Version="3.1.2" />
<PackageReference Include="Markdig" Version="0.31.0" /> <PackageReference Include="Markdig" Version="0.31.0" />
</ItemGroup> </ItemGroup>

View File

@@ -111,3 +111,6 @@
<CourseDetails /> <CourseDetails />
} }
<br> <br>
@* <MonacoEditorDemo /> *@

View File

@@ -17,6 +17,7 @@
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,600&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,600&display=swap" rel="stylesheet">
<link rel="icon" type="image/png" href="favicon.png"/> <link rel="icon" type="image/png" href="favicon.png"/>
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" /> <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head> </head>
<body data-bs-theme="dark"> <body data-bs-theme="dark">
@@ -33,6 +34,9 @@
<a class="dismiss">🗙</a> <a class="dismiss">🗙</a>
</div> </div>
<script src="_content/BlazorMonaco/jsInterop.js"></script>
<script src="_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
<script src="_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
<script src="_framework/blazor.server.js"></script> <script src="_framework/blazor.server.js"></script>
</body> </body>
</html> </html>

View File

@@ -32,6 +32,7 @@ if (canvas_url == null)
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(); builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<IWebRequestor, WebRequestor>(); builder.Services.AddScoped<IWebRequestor, WebRequestor>();
builder.Services.AddScoped<CanvasServiceUtils>(); builder.Services.AddScoped<CanvasServiceUtils>();
builder.Services.AddScoped<CanvasAssignmentService>(); builder.Services.AddScoped<CanvasAssignmentService>();

View File

@@ -15,7 +15,6 @@
if (assignmentContext.Assignment != null) if (assignmentContext.Assignment != null)
{ {
Description = assignmentContext.Assignment.Description; Description = assignmentContext.Assignment.Description;
Preview = Markdown.ToHtml(Description);
TemplateId = assignmentContext.Assignment.TemplateId; TemplateId = assignmentContext.Assignment.TemplateId;
UseTemplate = TemplateId != null && TemplateId != ""; UseTemplate = TemplateId != null && TemplateId != "";
VariableValues = assignmentContext.Assignment.TemplateVariables; VariableValues = assignmentContext.Assignment.TemplateVariables;
@@ -27,7 +26,26 @@
assignmentContext.StateHasChanged -= reload; assignmentContext.StateHasChanged -= reload;
} }
public string Description { get; set; } = default!; private string description { get; set; } = default!;
public string Description
{
get => description;
set
{
description = value;
if (description != string.Empty)
{
if(assignmentContext.Assignment != null)
{
var newAssignment = assignmentContext.Assignment with
{
Description = description
};
assignmentContext.SaveAssignment(newAssignment);
}
}
}
}
public bool? UseTemplate { get; set; } = null; public bool? UseTemplate { get; set; } = null;
public string? TemplateId { get; set; } public string? TemplateId { get; set; }
@@ -40,18 +58,9 @@
.AssignmentTemplates .AssignmentTemplates
.FirstOrDefault(t => t.Id == TemplateId); .FirstOrDefault(t => t.Id == TemplateId);
public string Preview { get; set; } = String.Empty;
private void saveDescription(ChangeEventArgs e) private void saveDescription(ChangeEventArgs e)
{ {
if(assignmentContext.Assignment != null)
{
var newAssignment = assignmentContext.Assignment with
{
Description = e.Value?.ToString() ?? ""
};
assignmentContext.SaveAssignment(newAssignment);
}
} }
private void saveTemplateId(ChangeEventArgs e) private void saveTemplateId(ChangeEventArgs e)
@@ -67,6 +76,7 @@
} }
} }
private MarkupString preview { get => (MarkupString) Markdown.ToHtml(Description); }
} }
@@ -137,7 +147,6 @@
} }
</div> </div>
</div> </div>
} }
else else
{ {
@@ -159,11 +168,11 @@
class="form-control" class="form-control"
rows=12 rows=12
@bind="Description" @bind="Description"
@oninput="saveDescription" @bind:event="oninput"
/> />
</div> </div>
<div class="col"> <div class="col" @key="Description">
@((MarkupString)Preview) @(preview)
</div> </div>
</div> </div>
} }

View File

@@ -82,6 +82,7 @@
await canvas.Assignments.Delete(courseId, assignment); await canvas.Assignments.Delete(courseId, assignment);
} }
} }
AssignmentModal?.Hide();
} }
private void handleNameChange(ChangeEventArgs e) private void handleNameChange(ChangeEventArgs e)

View File

@@ -0,0 +1,84 @@
@using BlazorMonaco
@using BlazorMonaco.Editor
<h3>Code Editor</h3>
<div>
<div style="margin:10px 0;">
New Value: <input type="text" @bind="_valueToSet" style="width: 400px;" /> <button @onclick="SetValue">Set Value</button>
</div>
<div style="margin:10px 0;">
<button @onclick="GetValue">Get Value</button>
</div>
<div style="margin:10px 0;">
See the console for results.
</div>
</div>
<div
style="height: 300px"
>
<StandaloneCodeEditor
@ref="_editor"
Id="sample-code-editor-123"
ConstructionOptions="EditorConstructionOptions"
OnDidInit="EditorOnDidInit"
/>
</div>
@code {
private StandaloneCodeEditor _editor = null!;
private string _valueToSet = "";
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
{
return new StandaloneEditorConstructionOptions
{
Language = "markdown",
Theme = "vs-dark",
TabSize = 2,
Value = "this is the default \n value",
Minimap = new EditorMinimapOptions { Enabled = false }
};
}
private async Task EditorOnDidInit()
{
await _editor.AddCommand((int)KeyMod.CtrlCmd | (int)KeyCode.KeyH, (args) =>
{
Console.WriteLine("Ctrl+H : Initial editor command is triggered.");
});
var newDecorations = new ModelDeltaDecoration[]
{
new ModelDeltaDecoration
{
Range = new BlazorMonaco.Range(3,1,3,1),
Options = new ModelDecorationOptions
{
IsWholeLine = true,
ClassName = "decorationContentClass",
GlyphMarginClassName = "decorationGlyphMarginClass"
}
}
};
decorationIds = await _editor.DeltaDecorations(null, newDecorations);
// You can now use 'decorationIds' to change or remove the decorations
}
private string[] decorationIds = new string[0];
private async Task SetValue()
{
Console.WriteLine($"setting value to: {_valueToSet}");
await _editor.SetValue(_valueToSet);
}
private async Task GetValue()
{
var val = await _editor.GetValue();
Console.WriteLine($"value is: {val}");
}
}

View File

@@ -26,6 +26,12 @@
quizContext.StateHasChanged -= reload; quizContext.StateHasChanged -= reload;
} }
private void deleteQuiz()
{
quizContext.DeleteQuiz();
modal?.Hide();
}
private void addQuestion() private void addQuestion()
{ {
if(quizContext.Quiz != null) if(quizContext.Quiz != null)
@@ -149,6 +155,8 @@
} }
</Body> </Body>
<Footer> <Footer>
<ConfirmationModal Label="Delete" Class="btn btn-danger" OnConfirm="deleteQuiz" />
<button <button
class="btn btn-primary" class="btn btn-primary"
@onclick="() => modal?.Hide()" @onclick="() => modal?.Hide()"

View File

@@ -3,6 +3,8 @@
@using Management.Web.Shared.Components.Quiz @using Management.Web.Shared.Components.Quiz
@using Management.Web.Shared.Module.Assignment @using Management.Web.Shared.Module.Assignment
@using LocalModels @using LocalModels
@using BlazorMonaco
@using BlazorMonaco.Editor
@inject CoursePlanner configurationManagement @inject CoursePlanner configurationManagement
@inject CoursePlanner planner @inject CoursePlanner planner
@@ -12,13 +14,44 @@
[Parameter, EditorRequired] [Parameter, EditorRequired]
public LocalModule Module { get; set; } = default!; public LocalModule Module { get; set; } = default!;
private bool dragging {get; set;} = false; private bool dragging {get; set;} = false;
private string _notes { get; set; } = "";
private string notes
{
get => _notes;
set
{
if(value != _notes)
{
_notes = value;
if(planner.LocalCourse != null)
{
var newModule = Module with { Notes = _notes };
var newModules = planner.LocalCourse.Modules.Select(
m => m.Name == newModule.Name
? newModule
: m
);
planner.LocalCourse = planner.LocalCourse with
{
Modules = newModules
};
}
}
}
}
protected override void OnInitialized() protected override void OnInitialized()
{ {
if(_notes == string.Empty)
{
_notes = Module.Notes;
}
planner.StateHasChanged += reload; planner.StateHasChanged += reload;
} }
private void reload() private void reload()
{ {
this.InvokeAsync(this.StateHasChanged); this.InvokeAsync(this.StateHasChanged);
} }
public void Dispose() public void Dispose()
@@ -89,9 +122,14 @@
id="@accordionId" id="@accordionId"
class="accordion-collapse collapse" class="accordion-collapse collapse"
> >
@* data-bs-parent="#modulesAccordion" include to limit expanded sections *@
<div class="accordion-body pt-1"> <div class="accordion-body pt-1">
@* <textarea
class="form-control"
@bind="notes"
@bind:event="oninput"
placeholder="notes for the module"
rows="6"
/> *@
<div class="row m-1"> <div class="row m-1">
<div class="col my-auto"> <div class="col my-auto">
<h5>Assignments</h5> <h5>Assignments</h5>

View File

@@ -1,68 +1,91 @@
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); @import url("open-iconic/font/css/open-iconic-bootstrap.min.css");
html, body { html,
font-family: 'DM Sans', sans-serif; body {
font-family: "DM Sans", sans-serif;
} }
h1:focus { h1:focus {
outline: none; outline: none;
} }
a, .btn-link { a,
color: #0071c1; .btn-link {
color: #0071c1;
} }
.btn-primary { .btn-primary {
color: #fff; color: #fff;
background-color: #1b6ec2; background-color: #1b6ec2;
border-color: #1861ac; border-color: #1861ac;
} }
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { .btn:focus,
.btn:active:focus,
.btn-link.nav-link:focus,
.form-control:focus,
.form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
} }
.content { .content {
padding-top: 1.1rem; padding-top: 1.1rem;
} }
.valid.modified:not([type=checkbox]) { .valid.modified:not([type="checkbox"]) {
outline: 1px solid #26b050; outline: 1px solid #26b050;
} }
.invalid { .invalid {
outline: 1px solid red; outline: 1px solid red;
} }
.validation-message { .validation-message {
color: red; color: red;
} }
#blazor-error-ui { #blazor-error-ui {
background: lightyellow; background: lightyellow;
bottom: 0; bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none; display: none;
left: 0; left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem; padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed; position: fixed;
width: 100%; width: 100%;
z-index: 1000; z-index: 1000;
} }
#blazor-error-ui .dismiss { #blazor-error-ui .dismiss {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
right: 0.75rem; right: 0.75rem;
top: 0.5rem; top: 0.5rem;
} }
.blazor-error-boundary { .blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=)
padding: 1rem 1rem 1rem 3.7rem; no-repeat 1rem/1.8rem,
color: white; #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
} }
.blazor-error-boundary::after { .blazor-error-boundary::after {
content: "An error has occurred." content: "An error has occurred.";
} }
.monaco-editor-container {
height: 100%;
}
/* .minimap {
display: none;
} */
/* .decorationGlyphMarginClass {
background: red;
}
.decorationContentClass {
background: lightblue;
} */

View File

@@ -26,9 +26,7 @@ public class QuizEditorContext
{ {
if (planner.LocalCourse != null) if (planner.LocalCourse != null)
{ {
var currentModule = var currentModule = getCurrentModule(newQuiz, planner.LocalCourse);
planner.LocalCourse.Modules.First(m => m.Quizzes.Select(q => q.Id).Contains(newQuiz.Id))
?? throw new Exception("could not find current module in quiz editor context");
var updatedModules = planner.LocalCourse.Modules var updatedModules = planner.LocalCourse.Modules
.Select( .Select(
@@ -48,4 +46,25 @@ public class QuizEditorContext
Quiz = newQuiz; Quiz = newQuiz;
} }
} }
public void DeleteQuiz()
{
if (planner.LocalCourse != null && Quiz != null)
{
var currentModule = getCurrentModule(Quiz, planner.LocalCourse);
var updatedModules = planner.LocalCourse.Modules
.Where(m => m.Name != currentModule.Name)
.ToArray();
planner.LocalCourse = planner.LocalCourse with { Modules = updatedModules };
Quiz = null;
}
}
private static LocalModule getCurrentModule(LocalQuiz newQuiz, LocalCourse course)
{
return course.Modules.First(m => m.Quizzes.Select(q => q.Id).Contains(newQuiz.Id))
?? throw new Exception("could not find current module in quiz editor context");
}
} }

View File

@@ -9,4 +9,5 @@ public record LocalModule
public IEnumerable<LocalQuiz> Quizzes { get; init; } = Enumerable.Empty<LocalQuiz>(); public IEnumerable<LocalQuiz> Quizzes { get; init; } = Enumerable.Empty<LocalQuiz>();
public ulong? CanvasId { get; set; } = null; public ulong? CanvasId { get; set; } = null;
public string Notes { get; set; } = string.Empty;
} }