9
0
mirror of https://github.com/donlon/cloudflare-error-page.git synced 2025-12-23 08:49:25 +00:00

Merge pull request #8 from donlon/save-as-dialog

Add Save As dialog to provide language examples
This commit is contained in:
Anthony Donlon
2025-12-22 21:34:48 +08:00
committed by GitHub
9 changed files with 324 additions and 35 deletions

View File

@@ -143,6 +143,80 @@
min-height: 80px; min-height: 80px;
resize: vertical; resize: vertical;
} }
.popover {
--bs-popover-body-padding-x: 0.7rem !important;
--bs-popover-body-padding-y: 0.5rem !important;
}
.save-as-dialog__container {
display: grid;
grid-template-areas:
"selector"
"code"
"buttons";
height: fit-content;
grid-template-columns: auto;
gap: 1em;
}
.save-as-dialog__selector {
grid-area: selector;
min-width: 10em;
}
.save-as-dialog__code {
grid-area: code;
width: 100%;
height: 100%;
min-height: 700px !important;
font-family: monospace;
font-size: 0.8em !important;
}
.save-as-dialog__buttons {
grid-area: buttons;
flex-direction: row;
}
.save-as-dialog__buttons>* {
flex: 1 1 auto;
}
@media (min-width: 576px) {
.save-as-dialog__container {
grid-template-areas:
"selector code"
"buttons code";
grid-template-columns: auto 2fr;
height: 100%;
}
.save-as-dialog__selector {
text-align: right;
}
.save-as-dialog__code {
min-height: 0 !important;
}
.save-as-dialog__buttons {
grid-area: buttons;
flex-direction: column;
justify-content: flex-end !important;
}
.save-as-dialog__buttons>* {
flex: none;
}
}
@media (min-width: 992px) {
.save-as-dialog__selector {
grid-area: selector;
min-width: 13em;
}
}
</style> </style>
</head> </head>
@@ -396,15 +470,19 @@
<div class="d-flex gap-2 mt-2 mb-2"> <div class="d-flex gap-2 mt-2 mb-2">
<!-- <button id="btnRender" class="btn btn-sm btn-primary">Render</button> --> <!-- <button id="btnRender" class="btn btn-sm btn-primary">Render</button> -->
<button id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button> <button type="button" id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button>
<button id="btnExport" class="btn btn-sm btn-primary">Export JSON</button> <!-- Button trigger modal -->
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#saveAsDialog">
Save as...
</button>
</div> </div>
<button id="btnShare" class="btn btn-sm btn-primary">Create shareable link</button> <button type="button" id="btnShare" class="btn btn-sm btn-secondary">Create shareable link</button>
<div class="mt-2"> <div class="mt-2">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input id="shareLink" class="form-control" readonly /> <input id="shareLink" class="form-control" readonly />
<button id="btnCopyLink" class="btn btn-outline-secondary" type="button">Copy</button> <button type="button" id="btnCopyLink" class="btn btn-outline-secondary" data-bs-toggle="popover"
data-bs-placement="top" data-bs-trigger="manual" data-bs-content="Copied">Copy</button>
</div> </div>
</div> </div>
@@ -430,9 +508,48 @@
<!-- TODO: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. --> <!-- TODO: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. -->
<iframe id="previewFrame" class="preview-frame" sandbox="allow-scripts allow-same-origin"></iframe> <iframe id="previewFrame" class="preview-frame" sandbox="allow-scripts allow-same-origin"></iframe>
</div> </div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="saveAsDialog" tabindex="-1" aria-labelledby="saveAsDialogLabel">
<div class="modal-dialog modal-xl modal-fullscreen-lg-down modal-dialog-scrollable">
<div class="modal-content h-100">
<div class="modal-header">
<h5 class="modal-title" id="saveAsDialogLabel">Save As ...</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mx-2 save-as-dialog__container">
<div id="saveAsDialogTypes" class="list-group save-as-dialog__selector">
<button type="button" data-type="json" class="list-group-item list-group-item-action active">
JSON
</button>
<button type="button" data-type="python" class="list-group-item list-group-item-action">
Python Example
</button>
<button type="button" data-type="js" class="list-group-item list-group-item-action">
NodeJS Example
</button>
</div>
<div class="d-flex gap-1 save-as-dialog__buttons">
<button type="button" class="btn btn-success" id="saveAsDialogCopyBtn" data-bs-toggle="popover"
data-bs-placement="right" data-bs-trigger="manual" data-bs-content="Copied">
Copy
</button>
<button type="button" class="btn btn-primary" id="saveAsDialogSaveBtn">
Save
</button>
</div>
<textarea id="saveAsDialogCode" class="form-control save-as-dialog__code" spellcheck="false"
readonly></textarea>
</div> </div>
</div> </div>
</div>
</div>
</div>
</body> </body>
</html> </html>

View File

@@ -10,15 +10,18 @@
"preview": "npm run build && vite preview" "preview": "npm run build && vite preview"
}, },
"devDependencies": { "devDependencies": {
"@types/bootstrap": "^5.2.10",
"@types/ejs": "^3.1.5",
"@types/html-minifier-terser": "^7.0.2",
"@types/node": "^24.10.2", "@types/node": "^24.10.2",
"html-minifier-terser": "^7.2.0", "html-minifier-terser": "^7.2.0",
"prettier": "3.7.4", "prettier": "3.7.4",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.2.6" "vite": "^7.2.6",
"vite-plugin-static-copy": "^3.1.4"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"ejs": "^3.1.10", "ejs": "^3.1.10"
"vite-plugin-static-copy": "^3.1.4"
} }
} }

4
editor/web/src/assets.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module '*?raw' {
const content: string;
export default content;
}

View File

@@ -0,0 +1,26 @@
import ejs from 'ejs';
import jsTemplate from './js.ejs?raw';
import jsonTemplate from './json.ejs?raw';
import pythonTemplate from './python.ejs?raw';
interface CodeGen {
name: string;
generate(params: any): string;
}
class EjsCodeGen implements CodeGen {
name: string;
private template: ejs.TemplateFunction;
constructor(name: string, templateContent: any) {
this.name = name;
this.template = ejs.compile(templateContent);
}
generate(params: any): string {
return this.template({ params });
}
}
export const jsCodeGen = new EjsCodeGen('NodeJS Example', jsTemplate);
export const jsonCodeGen = new EjsCodeGen('JSON', jsonTemplate);
export const pythonCodeGen = new EjsCodeGen('Python Example', pythonTemplate);

View File

@@ -0,0 +1,16 @@
import express from 'express';
import { render as render_cf_error_page } from 'cloudflare-error-page';
const app = express();
const port = 3000;
// Define a route for GET requests to the root URL
<%# TODO: format to JS-style object (key w/o parens) _%>
app.get('/', (req, res) => {
res.status(500).send(render_cf_error_page(<%-JSON.stringify(params, null, 2).replaceAll('\n', '\n ')%>));
});
// Start the server and listen on the specified port
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

View File

@@ -0,0 +1 @@
<%-JSON.stringify(params, null, 4)%>

View File

@@ -0,0 +1,29 @@
<%
// Covert the parameters to Python format object
const randomKey = Math.random() + ''
const paramsArg = JSON.stringify(params, (key, value) => {
if (typeof value === 'boolean') {
return randomKey + value.toString()
} else if (value === null) {
return randomKey + 'null'
} else {
return value
}
}, 4)
.replace(`"${randomKey}true"`, 'True')
.replace(`"${randomKey}false"`, 'False')
.replace(`"${randomKey}null"`, 'None')
_%>
from flask import Flask
from cloudflare_error_page import render as render_cf_error_page
app = Flask(__name__)
# Define a route for GET requests to the root URL
@app.route('/')
def index():
# Render the error page
return render_cf_error_page(<%=paramsArg.replaceAll('\n', '\n ')%>), 500
if __name__ == '__main__':
app.run(debug=True, port=5000)

View File

@@ -7,8 +7,12 @@
*/ */
import ejs from 'ejs'; import ejs from 'ejs';
import templateContent from './template.ejs?raw'; import templateContent from './template.ejs?raw';
import 'bootstrap/js/src/modal.js';
import Popover from 'bootstrap/js/src/popover.js';
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import { jsCodeGen, jsonCodeGen, pythonCodeGen } from './codegen';
let template = ejs.compile(templateContent); let template = ejs.compile(templateContent);
@@ -332,22 +336,6 @@ function createShareableLink() {
$('shareLink').value = result.url; $('shareLink').value = result.url;
}); });
} }
function exportJSON() {
let content = JSON.stringify(lastCfg, null, 4);
const file = new File([content], 'cloudflare-error-page-params.json', {
type: 'text/plain',
});
const url = URL.createObjectURL(file);
const link = document.createElement('a');
link.href = url;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
function resizePreviewFrame() { function resizePreviewFrame() {
const iframe = $('previewFrame'); const iframe = $('previewFrame');
const height = iframe.contentWindow.document.body.scrollHeight + 2; const height = iframe.contentWindow.document.body.scrollHeight + 2;
@@ -387,24 +375,20 @@ $('presetSelect').addEventListener('change', (e) => {
// Render / Open button handlers // Render / Open button handlers
// $('btnRender').addEventListener('click', e => { e.preventDefault(); render(); }); // $('btnRender').addEventListener('click', e => { e.preventDefault(); render(); });
$('btnOpen').addEventListener('click', (e) => { $('btnOpen').addEventListener('click', (e) => {
e.preventDefault();
openInNewTab(); openInNewTab();
}); });
$('btnShare').addEventListener('click', (e) => { $('btnShare').addEventListener('click', (e) => {
e.preventDefault();
createShareableLink(); createShareableLink();
}); });
$('btnExport').addEventListener('click', (e) => { const shareLinkPopover = new Popover($('btnCopyLink'));
e.preventDefault();
exportJSON();
});
$('btnCopyLink').addEventListener('click', () => { $('btnCopyLink').addEventListener('click', () => {
const field = $('shareLink'); const field = $('shareLink');
field.select(); field.select();
field.setSelectionRange(0, field.value.length);
navigator.clipboard.writeText(field.value).then(() => { navigator.clipboard.writeText(field.value).then(() => {
// No notification required unless you want one shareLinkPopover.show();
setTimeout(() => {
shareLinkPopover.hide();
}, 2000);
}); });
}); });
@@ -413,8 +397,7 @@ const inputs = document.querySelectorAll('#editorForm input, #editorForm textare
inputs.forEach((inp) => { inputs.forEach((inp) => {
inp.addEventListener('input', () => render()); inp.addEventListener('input', () => render());
// for radio change events (error_source) // for radio change events (error_source)
if (inp.type === 'radio') if (inp.type === 'radio') inp.addEventListener('change', () => render());
inp.addEventListener('change', () => render());
}); });
// Automatically update frame height // Automatically update frame height
@@ -423,3 +406,91 @@ const iframe = $('previewFrame');
observer.observe(iframe.contentWindow.document.body); observer.observe(iframe.contentWindow.document.body);
// resizePreviewFrame() // resizePreviewFrame()
setInterval(resizePreviewFrame, 1000); // TODO... setInterval(resizePreviewFrame, 1000); // TODO...
function saveFile(content, saveName) {
const file = new File([content], saveName, {
type: 'text/plain',
});
const url = URL.createObjectURL(file);
const link = document.createElement('a');
link.href = url;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
let saveAsType;
let saveAsContent;
function updateSaveAsDialog(e) {
if (e) {
const target = e.target;
saveAsType = target.dataset.type;
} else {
saveAsType = 'json';
}
let codegen;
switch (saveAsType) {
case 'js':
codegen = jsCodeGen;
break;
case 'json':
codegen = jsonCodeGen;
break;
case 'python':
codegen = pythonCodeGen;
break;
}
const params = { ...lastCfg };
delete params.time;
$('saveAsDialogCode').innerHTML = saveAsContent = codegen.generate(params);
$('saveAsDialogCode').scrollTop = 0;
document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => {
const isCurrent = element.dataset.type == saveAsType;
if (isCurrent) {
element.classList.add('active');
} else {
element.classList.remove('active');
}
element.ariaCurrent = isCurrent;
});
}
$('saveAsDialog').addEventListener('show.bs.modal', (e) => {
updateSaveAsDialog();
});
document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => {
element.addEventListener('click', updateSaveAsDialog);
});
const saveAsDialogCopyPopover = new Popover($('saveAsDialogCopyBtn'));
$('saveAsDialogCopyBtn').addEventListener('click', (e) => {
const field = $('saveAsDialogCode');
field.select();
// field.setSelectionRange(0, field.value.length);
navigator.clipboard.writeText(field.value).then(() => {
saveAsDialogCopyPopover.show();
setTimeout(() => {
saveAsDialogCopyPopover.hide();
}, 2000);
});
});
$('saveAsDialogSaveBtn').addEventListener('click', (e) => {
let saveName = '';
switch (saveAsType) {
case 'js':
saveName = 'cf-error-page-example.js';
break;
case 'json':
saveName = 'cf-error-page-params.json';
break;
case 'python':
saveName = 'cf_error_page_example.py';
break;
}
saveFile(saveAsContent, saveName);
});

View File

@@ -166,6 +166,11 @@
"@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@jridgewell/sourcemap-codec" "^1.4.14"
"@popperjs/core@^2.9.2":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@rollup/rollup-android-arm-eabi@4.53.3": "@rollup/rollup-android-arm-eabi@4.53.3":
version "4.53.3" version "4.53.3"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz#7e478b66180c5330429dd161bf84dad66b59c8eb" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz#7e478b66180c5330429dd161bf84dad66b59c8eb"
@@ -276,11 +281,28 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz#38ae84f4c04226c1d56a3b17296ef1e0460ecdfe" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz#38ae84f4c04226c1d56a3b17296ef1e0460ecdfe"
integrity sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ== integrity sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==
"@types/bootstrap@^5.2.10":
version "5.2.10"
resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.10.tgz#58506463bccc6602bc051487ad8d3a6458f94c6c"
integrity sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==
dependencies:
"@popperjs/core" "^2.9.2"
"@types/ejs@^3.1.5":
version "3.1.5"
resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.5.tgz#49d738257cc73bafe45c13cb8ff240683b4d5117"
integrity sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==
"@types/estree@1.0.8": "@types/estree@1.0.8":
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/html-minifier-terser@^7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-7.0.2.tgz#2290fa13e6e49b6cc0ab0afa2d6cf6a66feedb48"
integrity sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==
"@types/node@^24.10.2": "@types/node@^24.10.2":
version "24.10.2" version "24.10.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.2.tgz#82a57476a19647d8f2c7750d0924788245e39b26" resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.2.tgz#82a57476a19647d8f2c7750d0924788245e39b26"