Skip to content

Instantly share code, notes, and snippets.

@simonw
Created November 7, 2024 15:18
Show Gist options
  • Save simonw/54f508d956acd4fb7ca30c3350de6e22 to your computer and use it in GitHub Desktop.
Save simonw/54f508d956acd4fb7ca30c3350de6e22 to your computer and use it in GitHub Desktop.

HTML Playground with Sandbox Controls

human (Nov 7, 2024, 07:11 AM)

Build an artifact that shows me a textarea and an iframe next to each other (or stacked on top on narrow screens). The textarea shows a basic HTML skeleton with a <style> block and a <script> block implementing a tiny app where you can type in an input and the value updates in the box below.

Set it up so that the iframe has a sandbox attribute which is controlled by a set of checkboxes for all the options in that outer app - and any time I submit the main textarea the iframe srcdoc= updates with my code

assistant (Nov 7, 2024, 07:11 AM)

I'll create a React component that implements this HTML code playground with sandbox controls.

create HTML Playground with Sandbox Controls

import React, { useState } from 'react';
import { Card } from '@/components/ui/card';

const defaultHtml = `<!DOCTYPE html>
<html>
<head>
  <style>
    .output {
      margin: 20px;
      padding: 10px;
      border: 1px solid #ccc;
    }
  </style>
</head>
<body>
  <input type="text" id="userInput" placeholder="Type something...">
  <div class="output" id="output">Output will appear here</div>

  <script>
    const input = document.getElementById('userInput');
    const output = document.getElementById('output');
    
    input.addEventListener('input', (e) => {
      output.textContent = e.target.value;
    });
  </script>
</body>
</html>`;

const sandboxOptions = [
  { value: 'allow-forms', label: 'Forms' },
  { value: 'allow-modals', label: 'Modals' },
  { value: 'allow-orientation-lock', label:

human (Nov 7, 2024, 07:12 AM)

don't use react

assistant (Nov 7, 2024, 07:12 AM)

I'll create a pure HTML/JavaScript version instead.

create HTML Playground with Sandbox Controls

<!DOCTYPE html>
<html>
<head>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    body {
      font-family: system-ui, -apple-system, sans-serif;
      padding: 20px;
      max-width: 1200px;
      margin: 0 auto;
    }

    .container {
      display: flex;
      gap: 20px;
      margin-bottom: 20px;
    }

    @media (max-width: 768px) {
      .container {
        flex-direction: column;
      }
    }

    .editor {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }

    .preview {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }

    textarea {
      width: 100%;
      height: 400px;
      font-family: monospace;
      padding: 10px;
      border: 1px solid #ccc;
      border-radius: 4px;
      resize: vertical;
    }

    iframe {
      width: 100%;
      height: 400px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }

    .controls {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      margin-bottom: 10px;
    }

    .checkbox-group {
      display: flex;
      align-items: center;
      gap: 5px;
    }

    button {
      padding: 8px 16px;
      background: #0070f3;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }

    button:hover {
      background: #0051a2;
    }

    label {
      user-select: none;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="editor">
      <h3>HTML Editor</h3>
      <textarea id="codeEditor"></textarea>
      <button id="updatePreview">Update Preview</button>
    </div>
    
    <div class="preview">
      <h3>Preview</h3>
      <div class="controls" id="sandboxControls"></div>
      <iframe id="preview"></iframe>
    </div>
  </div>

  <script>
    // Default HTML template
    const defaultHtml = `<!DOCTYPE html>
<html>
<head>
  <style>
    .output {
      margin: 20px;
      padding: 10px;
      border: 1px solid #ccc;
    }
  </style>
</head>
<body>
  <input type="text" id="userInput" placeholder="Type something...">
  <div class="output" id="output">Output will appear here</div>

  <script>
    const input = document.getElementById('userInput');
    const output = document.getElementById('output');
    
    input.addEventListener('input', (e) => {
      output.textContent = e.target.value;
    });
  <\/script>
</body>
</html>`;

    // Sandbox options
    const sandboxOptions = [
      { value: 'allow-forms', label: 'Forms' },
      { value: 'allow-modals', label: 'Modals' },
      { value: 'allow-orientation-lock', label: 'Orientation Lock' },
      { value: 'allow-pointer-lock', label: 'Pointer Lock' },
      { value: 'allow-popups', label: 'Popups' },
      { value: 'allow-popups-to-escape-sandbox', label: 'Popups Escape Sandbox' },
      { value: 'allow-presentation', label: 'Presentation' },
      { value: 'allow-same-origin', label: 'Same Origin' },
      { value: 'allow-scripts', label: 'Scripts' },
      { value: 'allow-top-navigation', label: 'Top Navigation' }
    ];

    // Get DOM elements
    const codeEditor = document.getElementById('codeEditor');
    const preview = document.getElementById('preview');
    const updatePreviewBtn = document.getElementById('updatePreview');
    const sandboxControls = document.getElementById('sandboxControls');

    // Set initial editor content
    codeEditor.value = defaultHtml;

    // Create sandbox checkboxes
    sandboxOptions.forEach(option => {
      const div = document.createElement('div');
      div.className = 'checkbox-group';
      
      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.id = option.value;
      checkbox.checked = option.value === 'allow-scripts'; // Enable scripts by default
      
      const label = document.createElement('label');
      label.htmlFor = option.value;
      label.textContent = option.label;
      
      div.appendChild(checkbox);
      div.appendChild(label);
      sandboxControls.appendChild(div);
    });

    // Update preview function
    function updatePreview() {
      const checkedOptions = Array.from(sandboxControls.querySelectorAll('input:checked'))
        .map(input => input.id)
        .join(' ');
      
      preview.setAttribute('sandbox', checkedOptions);
      preview.srcdoc = codeEditor.value;
    }

    // Event listeners
    updatePreviewBtn.addEventListener('click', updatePreview);
    sandboxControls.addEventListener('change', updatePreview);

    // Initial preview
    updatePreview();
  </script>
</body>
</html>

I've created a standalone HTML page with a code editor and preview setup. Here's what it includes:

  1. A split view with a textarea editor on the left and preview iframe on the right (stacks vertically on mobile)
  2. Checkboxes for all standard sandbox options
  3. A button to update the preview
  4. A default HTML template that demonstrates a simple input-to-output app
  5. Responsive design that works on all screen sizes
  6. Clean, minimal styling

The sandbox options include all standard HTML iframe sandbox attributes like:

  • allow-scripts (enabled by default)
  • allow-forms
  • allow-same-origin
  • allow-popups
  • and more

To use it:

  1. The code editor starts with a default template
  2. Modify the code as needed
  3. Click "Update Preview" to see your changes
  4. Toggle sandbox permissions using the checkboxes
  5. The preview updates immediately when you change sandbox permissions

Would you like me to modify anything about this setup?

human (Nov 7, 2024, 07:12 AM)

An error occurred while trying to run the generated artifact. Unexpected token (25:7)

Can you fix this error in html-playground?

assistant (Nov 7, 2024, 07:12 AM)

The error is likely due to an unclosed script tag in the default HTML template. Let me fix that by properly escaping the closing script tag.

update html-playground

Find this:

  <script>
    const input = document.getElementById('userInput');
    const output = document.getElementById('output');
    
    input.addEventListener('input', (e) => {
      output.textContent = e.target.value;
    });
  </script>

Replace with this:

  &lt;script&gt;
    const input = document.getElementById('userInput');
    const output = document.getElementById('output');
    
    input.addEventListener('input', (e) => {
      output.textContent = e.target.value;
    });
  &lt;/script&gt;

I've updated the template to properly escape the <script> tags in the default HTML content. The error occurred because the template string was interpreting the inner </script> tag as the end of the outer script block. By using HTML entities (&lt; and &gt;), we prevent this interpretation.

Would you like to try it now? The playground should work as intended with the fixed template.

human (Nov 7, 2024, 07:16 AM)

I changed it to this. Now add a feature where as I add and remove sandbox attributes a code sample below those controls shows the current iframe HTML tag with those attributes

paste.txt

<!DOCTYPE html>
<html>
<head>
  <title>iframe sandbox</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    body {
      font-family: system-ui, -apple-system, sans-serif;
      padding: 20px;
      max-width: 1200px;
      margin: 0 auto;
    }

    .container {
      display: flex;
      gap: 20px;
      margin-bottom: 20px;
    }

    @media (max-width: 768px) {
      .container {
        flex-direction: column;
      }
    }

    .editor {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }

    .preview {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }

    textarea {
      width: 100%;
      height: 400px;
      font-family: monospace;
      padding: 10px;
      border: 1px solid #ccc;
      border-radius: 4px;
      resize: vertical;
    }

    iframe {
      width: 100%;
      height: 400px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }

    .controls {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      margin-bottom: 10px;
    }

    .checkbox-group {
      display: flex;
      align-items: center;
      gap: 5px;
    }

    button {
      padding: 8px 16px;
      background: #0070f3;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }

    button:hover {
      background: #0051a2;
    }

    label {
      user-select: none;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="editor">
      <h3>iframe sandbox</h3>
      <textarea id="codeEditor"></textarea>
      <button id="updatePreview">Update Preview</button>
    </div>
    
    <div class="preview">
      <iframe id="preview"></iframe>
      Sandbox controls
      <div class="controls" id="sandboxControls"></div>
    </div>
  </div>

  <script>
    // Default HTML template
    const defaultHtml = `<!DOCTYPE html>
<html>
<head>
  <style>
    .output {
      margin: 20px;
      padding: 10px;
      border: 1px solid #ccc;
    }
  </style>
</head>
<body>
  <input type="text" id="userInput" placeholder="Type something...">
  <div class="output" id="output">Output will appear here</div>

  <script>
    const input = document.getElementById('userInput');
    const output = document.getElementById('output');
    
    input.addEventListener('input', (e) => {
      output.textContent = e.target.value;
    });
  <\/script>
</body>
</html>`;

    // Sandbox options
    const sandboxOptions = [
      { value: 'allow-forms', label: 'Forms' },
      { value: 'allow-modals', label: 'Modals' },
      { value: 'allow-orientation-lock', label: 'Orientation Lock' },
      { value: 'allow-pointer-lock', label: 'Pointer Lock' },
      { value: 'allow-popups', label: 'Popups' },
      { value: 'allow-popups-to-escape-sandbox', label: 'Popups Escape Sandbox' },
      { value: 'allow-presentation', label: 'Presentation' },
      { value: 'allow-same-origin', label: 'Same Origin' },
      { value: 'allow-scripts', label: 'Scripts' },
      { value: 'allow-top-navigation', label: 'Top Navigation' }
    ];

    // Get DOM elements
    const codeEditor = document.getElementById('codeEditor');
    const preview = document.getElementById('preview');
    const updatePreviewBtn = document.getElementById('updatePreview');
    const sandboxControls = document.getElementById('sandboxControls');

    // Set initial editor content
    codeEditor.value = defaultHtml;

    // Create sandbox checkboxes
    sandboxOptions.forEach(option => {
      const div = document.createElement('div');
      div.className = 'checkbox-group';
      
      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.id = option.value;
      checkbox.checked = option.value === 'allow-scripts'; // Enable scripts by default
      
      const label = document.createElement('label');
      label.htmlFor = option.value;
      label.textContent = option.label;
      
      div.appendChild(checkbox);
      div.appendChild(label);
      sandboxControls.appendChild(div);
    });

    // Update preview function
    function updatePreview() {
      const checkedOptions = Array.from(sandboxControls.querySelectorAll('input:checked'))
        .map(input => input.id)
        .join(' ');
      
      preview.setAttribute('sandbox', checkedOptions);
      preview.srcdoc = codeEditor.value;
    }

    // Event listeners
    updatePreviewBtn.addEventListener('click', updatePreview);
    sandboxControls.addEventListener('change', updatePreview);

    // Initial preview
    updatePreview();
  </script>
</body>
</html>

assistant (Nov 7, 2024, 07:16 AM)

I'll update the HTML to add a code sample showing the current iframe tag configuration.

update html-playground

Find this:

    <div class="preview">
      <iframe id="preview"></iframe>
      Sandbox controls
      <div class="controls" id="sandboxControls"></div>
    </div>

Replace with this:

    <div class="preview">
      <iframe id="preview"></iframe>
      <h3>Sandbox controls</h3>
      <div class="controls" id="sandboxControls"></div>
      <pre id="iframeCode" style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; margin-top: 10px; font-size: 14px;"></pre>
    </div>

update html-playground

Find this:

    // Update preview function
    function updatePreview() {
      const checkedOptions = Array.from(sandboxControls.querySelectorAll('input:checked'))
        .map(input => input.id)
        .join(' ');
      
      preview.setAttribute('sandbox', checkedOptions);
      preview.srcdoc = codeEditor.value;
    }

Replace with this:

    // Update preview function
    function updatePreview() {
      const checkedOptions = Array.from(sandboxControls.querySelectorAll('input:checked'))
        .map(input => input.id)
        .join(' ');
      
      preview.setAttribute('sandbox', checkedOptions);
      preview.srcdoc = codeEditor.value;

      // Update the code sample
      const iframeCode = document.getElementById('iframeCode');
      const sandboxAttr = checkedOptions ? ` sandbox="${checkedOptions}"` : '';
      iframeCode.textContent = `<iframe${sandboxAttr} src="..."></iframe>`;
    }

I've made the following changes:

  1. Added a <pre> element below the sandbox controls to display the iframe code
  2. Updated the updatePreview function to also update this code sample whenever the sandbox attributes change
  3. The code sample will show the current sandbox attributes in a properly formatted iframe tag
  4. Styled the code sample with a light gray background and proper padding for readability

Now as you toggle the sandbox checkboxes, you'll see a live preview of what the iframe HTML tag would look like with those attributes. The code sample will update automatically whenever you change the sandbox controls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment