Skip to content

Commit

Permalink
Merge pull request #1458 from hydephp/realtime-compiler-live-edit
Browse files Browse the repository at this point in the history
Add a live edit feature to the realtime compiler
  • Loading branch information
caendesilva authored Nov 14, 2023
2 parents 9749340 + c9f5321 commit e1eba70
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 0 deletions.
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

0 comments on commit e1eba70

Please sign in to comment.