Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a live edit feature to the realtime compiler #1458

Merged
merged 72 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
da1263a
Create LiveEditController.php
caendesilva Nov 13, 2023
0fc7311
Buffer page router contents
caendesilva Nov 7, 2023
3cde09a
Add configuration option for the live edit feature
caendesilva Nov 12, 2023
49bc7a5
Add enabled method
caendesilva Nov 13, 2023
766808b
Add new live edit API route to page router
caendesilva Nov 13, 2023
3feb14c
Authorize POST requests
caendesilva Nov 13, 2023
4972dae
Add expectsJson method to base controller
caendesilva Nov 13, 2023
2d6647e
Handle HttpExceptions
caendesilva Nov 13, 2023
69984c5
Merge branch 'master' into realtime-compiler-live-edit
caendesilva Nov 13, 2023
09e3620
Collect and validate required input
caendesilva Nov 13, 2023
b06340a
Get and validate the page
caendesilva Nov 13, 2023
e3923d5
Save the page Markdown
caendesilva Nov 13, 2023
7b7a1c9
Return with success response
caendesilva Nov 13, 2023
bfd3d3a
Scaffold live edit resource files
caendesilva Nov 13, 2023
083dd8d
Create accessor to get live edit resources
caendesilva Nov 13, 2023
3e4e39c
Merge branch 'master' into realtime-compiler-live-edit
caendesilva Nov 13, 2023
9166d4e
Update page router to inject live edit scripts when enabled
caendesilva Nov 13, 2023
6b42303
Add environment variable for the live edit feature
caendesilva Nov 13, 2023
54b7a4f
Create base live edit markup
caendesilva Nov 13, 2023
4e2bfaf
Simplify function signature
caendesilva Nov 13, 2023
ec4f02f
Render markdown into textarea
caendesilva Nov 13, 2023
42ae982
Create function to initialize live editor
caendesilva Nov 13, 2023
dd6ed11
Get the article element
caendesilva Nov 13, 2023
8eae7fd
Create base double click event listener function
caendesilva Nov 13, 2023
c569a19
Sketch out editor switching logic
caendesilva Nov 13, 2023
5fb295e
Flip if-else
caendesilva Nov 13, 2023
df568f6
Extract function
caendesilva Nov 13, 2023
7500b4d
Invert faulty boolean logic
caendesilva Nov 13, 2023
ca5f77d
Implement editor setup function
caendesilva Nov 13, 2023
293b2a9
Implement show editor function
caendesilva Nov 13, 2023
b86e715
Use helper function
caendesilva Nov 13, 2023
134931f
Create function to hide editor
caendesilva Nov 13, 2023
1792f8b
Add event listener to hide editor on cancel button click
caendesilva Nov 13, 2023
c90e82f
Use function name instead of closure wrapper
caendesilva Nov 13, 2023
394976e
Focus on the textarea
caendesilva Nov 14, 2023
16391d8
Add form identifier
caendesilva Nov 14, 2023
1beb4b6
Remove trailing parentheses
caendesilva Nov 14, 2023
79ea5e6
Add form submit event listener
caendesilva Nov 14, 2023
4bcb38e
Session must be started by static method
caendesilva Nov 14, 2023
7de722e
Add hidden token input
caendesilva Nov 14, 2023
cd3e45c
Add hidden path input
caendesilva Nov 14, 2023
06708d2
Change textarea identifier
caendesilva Nov 14, 2023
3f973d1
Implement form handler
caendesilva Nov 14, 2023
0bf73fa
Add todo
caendesilva Nov 14, 2023
99fdaa6
Swap declaration order
caendesilva Nov 14, 2023
35bb27a
Reassign editor after inserting node
caendesilva Nov 14, 2023
6b36a57
Apply CSS classes from article to editor
caendesilva Nov 14, 2023
d0e8a7e
Add CSS selector for page type
caendesilva Nov 14, 2023
d2f88d8
Revert "Add CSS selector for page type"
caendesilva Nov 14, 2023
1d0c6b8
Add prose class to header
caendesilva Nov 14, 2023
ba64ab4
Dynamic row count
caendesilva Nov 14, 2023
6d2d1bc
Revert "Dynamic row count"
caendesilva Nov 14, 2023
2849716
Style editor
caendesilva Nov 14, 2023
37b3645
Simplify input names
caendesilva Nov 14, 2023
576c69b
Simplify element selectors
caendesilva Nov 14, 2023
5038c12
Create live editor stylesheet
caendesilva Nov 14, 2023
0ff89ce
Add dynamic margins depending on page type
caendesilva Nov 14, 2023
f61e0f8
Change internal console context handling
caendesilva Nov 14, 2023
7bb0803
Write to console when updating page
caendesilva Nov 14, 2023
fca682a
Move function declarations to outer function scope
caendesilva Nov 14, 2023
cf9c92e
Add support for keyboard shortcuts
caendesilva Nov 14, 2023
d2595ea
Add local storage option to disable shortcuts
caendesilva Nov 14, 2023
ecaf1f5
Equalize header margins
caendesilva Nov 14, 2023
4411be9
Add markup for editor preferences button
caendesilva Nov 14, 2023
a27ee30
Send redirect response
caendesilva Nov 14, 2023
a407d2a
Replace custom form handler with browser form handling
caendesilva Nov 14, 2023
64b05e5
Extract helper method
caendesilva Nov 14, 2023
185e9ef
Formatting
caendesilva Nov 14, 2023
b065cb6
Match status message to code
caendesilva Nov 14, 2023
8ca06ba
Revert "Add markup for editor preferences button"
caendesilva Nov 14, 2023
ee6e9b7
Merge branch 'master' into realtime-compiler-live-edit
caendesilva Nov 14, 2023
c9f5321
Merge branch 'master' into realtime-compiler-live-edit
caendesilva Nov 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/hyde.php
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,9 @@
// Should preview pages be saved to the output directory?
'save_preview' => true,

// Should the live edit feature be enabled?
'live_edit' => env('SERVER_LIVE_EDIT', true),

// Configure the realtime compiler dashboard
'dashboard' => [
// Should the realtime compiler dashboard be enabled?
Expand Down
3 changes: 3 additions & 0 deletions packages/framework/config/hyde.php
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,9 @@
// Should preview pages be saved to the output directory?
'save_preview' => true,

// Should the live edit feature be enabled?
'live_edit' => env('SERVER_LIVE_EDIT', true),

// Configure the realtime compiler dashboard
'dashboard' => [
// Should the realtime compiler dashboard be enabled?
Expand Down
31 changes: 31 additions & 0 deletions packages/realtime-compiler/resources/live-edit.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div id="__realtime-compiler-live-edit-insert">
<!-- The live editor insert is not saved to your static site -->
@php
/** @var \Hyde\Pages\Concerns\BaseMarkdownPage $page */
$markdown = $page->markdown()->body();
@endphp
<style>{!! $styles !!}</style>
<template id="live-edit-template">
<section id="live-edit-container" style="margin-top: {{ $page instanceof \Hyde\Pages\DocumentationPage ? '1rem' : '-1rem'}};">
<form id="liveEditForm" action="/_hyde/live-edit" method="POST">
<header class="prose dark:prose-invert mb-3">
<h2 class="mb-0">Live Editor</h2>
<menu>
<button id="liveEditCancel" type="button">
Cancel
</button>
<button id="liveEditSubmit" type="submit">
Save
</button>
</menu>
</header>
<input type="hidden" name="_token" value="{{ $csrfToken }}">
<input type="hidden" name="page" value="{{ $page->getSourcePath() }}">
<label for="live-editor" class="sr-only">Edit page contents</label>
<textarea name="markdown" id="live-editor" cols="30" rows="20" class="rounded-lg bg-gray-200 dark:bg-gray-800">{{ $markdown }}</textarea>
</form>
</section>
</template>
<script>{!! $scripts !!}</script>
<script>initLiveEdit()</script>
</div>
63 changes: 63 additions & 0 deletions packages/realtime-compiler/resources/live-edit.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#live-editor {
width: 100%;
height: 100%;
min-height: 300px;
border: none;
outline: none;
font-family: 'Source Code Pro', monospace;
padding: 1rem;
white-space: pre-line;
}

#live-editor:focus {
outline: 2px solid transparent;
outline-offset: 2px;
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;
border-color: #2563eb;
}

#live-edit-container header {
width: 100%;
max-width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}

#live-edit-container menu button {
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
font-weight: 500;
font-size: 0.75rem;
line-height: 1.25rem;
text-transform: uppercase;
letter-spacing: 0.05em;
cursor: pointer;
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out;
}

#liveEditCancel {
background-color: #e5e7eb;
border-color: #e5e7eb;
color: #1f2937;
margin-right: 0.35rem;
}

#liveEditCancel:hover {
background-color: #d1d5db;
border-color: #d1d5db;
color: #1f2937;
}

#liveEditSubmit {
background-color: #2563eb;
border-color: #2563eb;
color: #fff;
}

#liveEditSubmit:hover {
background-color: #1d4ed8;
border-color: #1d4ed8;
color: #fff;
}
105 changes: 105 additions & 0 deletions packages/realtime-compiler/resources/live-edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
function initLiveEdit() {
function getArticle() {
let article = document.querySelector('#content > article');

if (article === null) {
// If no article element is found the user may have a custom template, so we cannot know which element to edit.
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.');
}

return article;
}

function getLiveEditor() {
return document.querySelector('#live-edit-container');
}

function showEditor() {
article.style.display = 'none';
getLiveEditor().style.display = '';
focusOnTextarea();
}

function hideEditor() {
article.style.display = '';
getLiveEditor().style.display = 'none';
}

function focusOnTextarea() {
const textarea = getLiveEditor().querySelector('textarea');

textarea.selectionStart = textarea.value.length;
textarea.focus();
}

function switchToEditor() {

function hasEditorBeenSetUp() {
return getLiveEditor() !== null;
}

function setupEditor() {
const template = document.getElementById('live-edit-template');
const article = getArticle();
let editor = document.importNode(template.content, true);
article.parentNode.insertBefore(editor, article.nextSibling);
editor = getLiveEditor();

// Apply CSS classes from article to editor to match layout
editor.classList.add(...article.classList);

showEditor();

document.getElementById('liveEditCancel').addEventListener('click', hideEditor);
}

if (hasEditorBeenSetUp()) {
showEditor();
} else {
setupEditor();
}
}

function handleShortcut(event) {
let isEditorHidden = getLiveEditor() === null || getLiveEditor().style.display === 'none';
let isEditorVisible = getLiveEditor() !== null && getLiveEditor().style.display !== 'none';

if (event.ctrlKey && event.key === 'e') {
event.preventDefault();

if (isEditorHidden) {
switchToEditor();
} else {
hideEditor();
}
}

if (event.ctrlKey && event.key === 's') {
if (isEditorVisible) {
event.preventDefault();

document.getElementById('liveEditSubmit').click();
}
}

if (event.key === 'Escape') {
if (isEditorVisible) {
event.preventDefault();

hideEditor();
}
}
}

function shortcutsEnabled() {
return localStorage.getItem('hydephp.live-edit.shortcuts') !== 'false';
}

const article = getArticle();

article.addEventListener('dblclick', switchToEditor);

if (shortcutsEnabled()) {
document.addEventListener('keydown', handleShortcut);
}
}
75 changes: 75 additions & 0 deletions packages/realtime-compiler/src/Http/LiveEditController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Hyde\RealtimeCompiler\Http;

use Hyde\Hyde;
use Hyde\Support\Models\Route;
use Hyde\Support\Models\Redirect;
use Hyde\Markdown\Models\Markdown;
use Illuminate\Support\Facades\Blade;
use Hyde\Pages\Concerns\BaseMarkdownPage;

/**
* @internal This class is not intended to be edited outside the Hyde Realtime Compiler.
*/
class LiveEditController extends BaseController
{
protected bool $withConsoleOutput = true;
protected bool $withSession = true;

public function handle(): HtmlResponse
{
$this->authorizePostRequest();

return $this->handleRequest();
}

protected function handleRequest(): HtmlResponse
{
$pagePath = $this->request->data['page'] ?? $this->abort(400, 'Must provide page path');
$content = $this->request->data['markdown'] ?? $this->abort(400, 'Must provide content');

$page = Hyde::pages()->getPage($pagePath);

if (! $page instanceof BaseMarkdownPage) {
$this->abort(400, 'Page is not a markdown page');
}

$page->markdown = new Markdown($content);
$page->save();

$this->writeToConsole("Updated file '$pagePath'", 'hyde@live-edit');

return $this->redirectToPage($page->getRoute());
}

public static function enabled(): bool
{
return config('hyde.server.live_edit', true);
}

public static function injectLiveEditScript(string $html): string
{
session_start();

return str_replace('</body>', sprintf('%s</body>', Blade::render(file_get_contents(__DIR__.'/../../resources/live-edit.blade.php'), [
'styles' => file_get_contents(__DIR__.'/../../resources/live-edit.css'),
'scripts' => file_get_contents(__DIR__.'/../../resources/live-edit.js'),
'csrfToken' => self::generateCSRFToken(),
])), $html);
}

protected function redirectToPage(Route $route): HtmlResponse
{
$redirectPage = new Redirect($this->request->path, "../$route");
Hyde::shareViewData($redirectPage);

return (new HtmlResponse(303, 'See Other', [
'body' => $redirectPage->compile(),
]))->withHeaders([
'Location' => $route,
]);
}
}
10 changes: 10 additions & 0 deletions packages/realtime-compiler/src/Routing/PageRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
use Desilva\Microserve\Request;
use Desilva\Microserve\Response;
use Hyde\Foundation\Facades\Routes;
use Hyde\Pages\Concerns\BaseMarkdownPage;
use Hyde\Framework\Actions\StaticPageBuilder;
use Hyde\RealtimeCompiler\Http\LiveEditController;
use Hyde\Framework\Features\Documentation\DocumentationSearchPage;
use Hyde\Pages\Concerns\HydePage;
use Hyde\RealtimeCompiler\Concerns\InteractsWithLaravel;
Expand Down Expand Up @@ -36,6 +38,10 @@ protected function handlePageRequest(): Response
return (new DashboardController($this->request))->handle();
}

if ($this->request->path === '/_hyde/live-edit' && LiveEditController::enabled()) {
return (new LiveEditController($this->request))->handle();
}

return new HtmlResponse(200, 'OK', [
'body' => $this->getHtml($this->getPageFromRoute()),
]);
Expand Down Expand Up @@ -70,6 +76,10 @@ protected function getHtml(HydePage $page): string
$contents = $page->compile();
}

if ($page instanceof BaseMarkdownPage && LiveEditController::enabled()) {
$contents = LiveEditController::injectLiveEditScript($contents);
}

return $contents;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/realtime-compiler/tests/RealtimeCompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

ob_start();

beforeEach(function () {
putenv('SERVER_LIVE_EDIT=false');
});

test('handle routes index page', function () {
putenv('SERVER_DASHBOARD=false');
mockRoute('');
Expand Down
Loading