Skip to content

Commit e1eba70

Browse files
authored
Merge pull request #1458 from hydephp/realtime-compiler-live-edit
Add a live edit feature to the realtime compiler
2 parents 9749340 + c9f5321 commit e1eba70

File tree

8 files changed

+294
-0
lines changed

8 files changed

+294
-0
lines changed

config/hyde.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,9 @@
418418
// Should preview pages be saved to the output directory?
419419
'save_preview' => true,
420420

421+
// Should the live edit feature be enabled?
422+
'live_edit' => env('SERVER_LIVE_EDIT', true),
423+
421424
// Configure the realtime compiler dashboard
422425
'dashboard' => [
423426
// Should the realtime compiler dashboard be enabled?

packages/framework/config/hyde.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,9 @@
418418
// Should preview pages be saved to the output directory?
419419
'save_preview' => true,
420420

421+
// Should the live edit feature be enabled?
422+
'live_edit' => env('SERVER_LIVE_EDIT', true),
423+
421424
// Configure the realtime compiler dashboard
422425
'dashboard' => [
423426
// Should the realtime compiler dashboard be enabled?
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<div id="__realtime-compiler-live-edit-insert">
2+
<!-- The live editor insert is not saved to your static site -->
3+
@php
4+
/** @var \Hyde\Pages\Concerns\BaseMarkdownPage $page */
5+
$markdown = $page->markdown()->body();
6+
@endphp
7+
<style>{!! $styles !!}</style>
8+
<template id="live-edit-template">
9+
<section id="live-edit-container" style="margin-top: {{ $page instanceof \Hyde\Pages\DocumentationPage ? '1rem' : '-1rem'}};">
10+
<form id="liveEditForm" action="/_hyde/live-edit" method="POST">
11+
<header class="prose dark:prose-invert mb-3">
12+
<h2 class="mb-0">Live Editor</h2>
13+
<menu>
14+
<button id="liveEditCancel" type="button">
15+
Cancel
16+
</button>
17+
<button id="liveEditSubmit" type="submit">
18+
Save
19+
</button>
20+
</menu>
21+
</header>
22+
<input type="hidden" name="_token" value="{{ $csrfToken }}">
23+
<input type="hidden" name="page" value="{{ $page->getSourcePath() }}">
24+
<label for="live-editor" class="sr-only">Edit page contents</label>
25+
<textarea name="markdown" id="live-editor" cols="30" rows="20" class="rounded-lg bg-gray-200 dark:bg-gray-800">{{ $markdown }}</textarea>
26+
</form>
27+
</section>
28+
</template>
29+
<script>{!! $scripts !!}</script>
30+
<script>initLiveEdit()</script>
31+
</div>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#live-editor {
2+
width: 100%;
3+
height: 100%;
4+
min-height: 300px;
5+
border: none;
6+
outline: none;
7+
font-family: 'Source Code Pro', monospace;
8+
padding: 1rem;
9+
white-space: pre-line;
10+
}
11+
12+
#live-editor:focus {
13+
outline: 2px solid transparent;
14+
outline-offset: 2px;
15+
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0), 0 0 0 calc(1px + 0px) rgba(37, 99, 235, 1), 0 0 #0000;
16+
border-color: #2563eb;
17+
}
18+
19+
#live-edit-container header {
20+
width: 100%;
21+
max-width: 100%;
22+
display: flex;
23+
flex-direction: row;
24+
align-items: center;
25+
justify-content: space-between;
26+
}
27+
28+
#live-edit-container menu button {
29+
padding: 0.25rem 0.5rem;
30+
border-radius: 0.375rem;
31+
font-weight: 500;
32+
font-size: 0.75rem;
33+
line-height: 1.25rem;
34+
text-transform: uppercase;
35+
letter-spacing: 0.05em;
36+
cursor: pointer;
37+
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out;
38+
}
39+
40+
#liveEditCancel {
41+
background-color: #e5e7eb;
42+
border-color: #e5e7eb;
43+
color: #1f2937;
44+
margin-right: 0.35rem;
45+
}
46+
47+
#liveEditCancel:hover {
48+
background-color: #d1d5db;
49+
border-color: #d1d5db;
50+
color: #1f2937;
51+
}
52+
53+
#liveEditSubmit {
54+
background-color: #2563eb;
55+
border-color: #2563eb;
56+
color: #fff;
57+
}
58+
59+
#liveEditSubmit:hover {
60+
background-color: #1d4ed8;
61+
border-color: #1d4ed8;
62+
color: #fff;
63+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
function initLiveEdit() {
2+
function getArticle() {
3+
let article = document.querySelector('#content > article');
4+
5+
if (article === null) {
6+
// If no article element is found the user may have a custom template, so we cannot know which element to edit.
7+
throw new Error('No article element found, cannot live edit. If you are using a custom template, please make sure to include an article element in the #content container.');
8+
}
9+
10+
return article;
11+
}
12+
13+
function getLiveEditor() {
14+
return document.querySelector('#live-edit-container');
15+
}
16+
17+
function showEditor() {
18+
article.style.display = 'none';
19+
getLiveEditor().style.display = '';
20+
focusOnTextarea();
21+
}
22+
23+
function hideEditor() {
24+
article.style.display = '';
25+
getLiveEditor().style.display = 'none';
26+
}
27+
28+
function focusOnTextarea() {
29+
const textarea = getLiveEditor().querySelector('textarea');
30+
31+
textarea.selectionStart = textarea.value.length;
32+
textarea.focus();
33+
}
34+
35+
function switchToEditor() {
36+
37+
function hasEditorBeenSetUp() {
38+
return getLiveEditor() !== null;
39+
}
40+
41+
function setupEditor() {
42+
const template = document.getElementById('live-edit-template');
43+
const article = getArticle();
44+
let editor = document.importNode(template.content, true);
45+
article.parentNode.insertBefore(editor, article.nextSibling);
46+
editor = getLiveEditor();
47+
48+
// Apply CSS classes from article to editor to match layout
49+
editor.classList.add(...article.classList);
50+
51+
showEditor();
52+
53+
document.getElementById('liveEditCancel').addEventListener('click', hideEditor);
54+
}
55+
56+
if (hasEditorBeenSetUp()) {
57+
showEditor();
58+
} else {
59+
setupEditor();
60+
}
61+
}
62+
63+
function handleShortcut(event) {
64+
let isEditorHidden = getLiveEditor() === null || getLiveEditor().style.display === 'none';
65+
let isEditorVisible = getLiveEditor() !== null && getLiveEditor().style.display !== 'none';
66+
67+
if (event.ctrlKey && event.key === 'e') {
68+
event.preventDefault();
69+
70+
if (isEditorHidden) {
71+
switchToEditor();
72+
} else {
73+
hideEditor();
74+
}
75+
}
76+
77+
if (event.ctrlKey && event.key === 's') {
78+
if (isEditorVisible) {
79+
event.preventDefault();
80+
81+
document.getElementById('liveEditSubmit').click();
82+
}
83+
}
84+
85+
if (event.key === 'Escape') {
86+
if (isEditorVisible) {
87+
event.preventDefault();
88+
89+
hideEditor();
90+
}
91+
}
92+
}
93+
94+
function shortcutsEnabled() {
95+
return localStorage.getItem('hydephp.live-edit.shortcuts') !== 'false';
96+
}
97+
98+
const article = getArticle();
99+
100+
article.addEventListener('dblclick', switchToEditor);
101+
102+
if (shortcutsEnabled()) {
103+
document.addEventListener('keydown', handleShortcut);
104+
}
105+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hyde\RealtimeCompiler\Http;
6+
7+
use Hyde\Hyde;
8+
use Hyde\Support\Models\Route;
9+
use Hyde\Support\Models\Redirect;
10+
use Hyde\Markdown\Models\Markdown;
11+
use Illuminate\Support\Facades\Blade;
12+
use Hyde\Pages\Concerns\BaseMarkdownPage;
13+
14+
/**
15+
* @internal This class is not intended to be edited outside the Hyde Realtime Compiler.
16+
*/
17+
class LiveEditController extends BaseController
18+
{
19+
protected bool $withConsoleOutput = true;
20+
protected bool $withSession = true;
21+
22+
public function handle(): HtmlResponse
23+
{
24+
$this->authorizePostRequest();
25+
26+
return $this->handleRequest();
27+
}
28+
29+
protected function handleRequest(): HtmlResponse
30+
{
31+
$pagePath = $this->request->data['page'] ?? $this->abort(400, 'Must provide page path');
32+
$content = $this->request->data['markdown'] ?? $this->abort(400, 'Must provide content');
33+
34+
$page = Hyde::pages()->getPage($pagePath);
35+
36+
if (! $page instanceof BaseMarkdownPage) {
37+
$this->abort(400, 'Page is not a markdown page');
38+
}
39+
40+
$page->markdown = new Markdown($content);
41+
$page->save();
42+
43+
$this->writeToConsole("Updated file '$pagePath'", 'hyde@live-edit');
44+
45+
return $this->redirectToPage($page->getRoute());
46+
}
47+
48+
public static function enabled(): bool
49+
{
50+
return config('hyde.server.live_edit', true);
51+
}
52+
53+
public static function injectLiveEditScript(string $html): string
54+
{
55+
session_start();
56+
57+
return str_replace('</body>', sprintf('%s</body>', Blade::render(file_get_contents(__DIR__.'/../../resources/live-edit.blade.php'), [
58+
'styles' => file_get_contents(__DIR__.'/../../resources/live-edit.css'),
59+
'scripts' => file_get_contents(__DIR__.'/../../resources/live-edit.js'),
60+
'csrfToken' => self::generateCSRFToken(),
61+
])), $html);
62+
}
63+
64+
protected function redirectToPage(Route $route): HtmlResponse
65+
{
66+
$redirectPage = new Redirect($this->request->path, "../$route");
67+
Hyde::shareViewData($redirectPage);
68+
69+
return (new HtmlResponse(303, 'See Other', [
70+
'body' => $redirectPage->compile(),
71+
]))->withHeaders([
72+
'Location' => $route,
73+
]);
74+
}
75+
}

packages/realtime-compiler/src/Routing/PageRouter.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
use Desilva\Microserve\Request;
66
use Desilva\Microserve\Response;
77
use Hyde\Foundation\Facades\Routes;
8+
use Hyde\Pages\Concerns\BaseMarkdownPage;
89
use Hyde\Framework\Actions\StaticPageBuilder;
10+
use Hyde\RealtimeCompiler\Http\LiveEditController;
911
use Hyde\Framework\Features\Documentation\DocumentationSearchPage;
1012
use Hyde\Pages\Concerns\HydePage;
1113
use Hyde\RealtimeCompiler\Concerns\InteractsWithLaravel;
@@ -36,6 +38,10 @@ protected function handlePageRequest(): Response
3638
return (new DashboardController($this->request))->handle();
3739
}
3840

41+
if ($this->request->path === '/_hyde/live-edit' && LiveEditController::enabled()) {
42+
return (new LiveEditController($this->request))->handle();
43+
}
44+
3945
return new HtmlResponse(200, 'OK', [
4046
'body' => $this->getHtml($this->getPageFromRoute()),
4147
]);
@@ -70,6 +76,10 @@ protected function getHtml(HydePage $page): string
7076
$contents = $page->compile();
7177
}
7278

79+
if ($page instanceof BaseMarkdownPage && LiveEditController::enabled()) {
80+
$contents = LiveEditController::injectLiveEditScript($contents);
81+
}
82+
7383
return $contents;
7484
}
7585

packages/realtime-compiler/tests/RealtimeCompilerTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717

1818
ob_start();
1919

20+
beforeEach(function () {
21+
putenv('SERVER_LIVE_EDIT=false');
22+
});
23+
2024
test('handle routes index page', function () {
2125
putenv('SERVER_DASHBOARD=false');
2226
mockRoute('');

0 commit comments

Comments
 (0)