Skip to content

Fix paste availability across editor tabs - #1101

Merged
jfhenon merged 1 commit into
SVG-Edit:masterfrom
veselin-kutsarov:dev
Jul 29, 2026
Merged

Fix paste availability across editor tabs#1101
jfhenon merged 1 commit into
SVG-Edit:masterfrom
veselin-kutsarov:dev

Conversation

@veselin-kutsarov

@veselin-kutsarov veselin-kutsarov commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Use sessionStorage as the clipboard source of truth. Notify each editor when clipboard data changes and keep paste enabled after the temporary localStorage transport value is removed. Avoid broadcasting empty or invalid clipboard data.

PR description

Summary

This PR fixes copy and paste between SVG-Edit instances running in separate same-origin browser tabs.

Clipboard data was already transferred between tabs through a temporary localStorage value and stored in each tab's sessionStorage. However, the Paste context-menu actions were disabled again when the temporary localStorage value was removed, even though valid clipboard data remained available in sessionStorage.

Changes

  • Use sessionStorage as the source of truth for clipboard availability.
  • Add hasClipboardData() to validate that the clipboard contains pasteable elements.
  • Emit a clipboardChanged event after:
    • copying elements in the current tab;
    • receiving clipboard data from another tab.
  • Update the editor UI through the shared clipboard event instead of manipulating the context-menu DOM from SVGCanvas core.
  • Ignore removal of the temporary localStorage transport value when determining Paste availability.
  • Avoid broadcasting missing, empty, or invalid clipboard data.
  • Add a Playwright test covering copy and paste between two browser tabs.
  • Update the tracked coverage summary.

Verification

  • npm test passed.
  • 606 unit tests passed.
  • 82 Playwright tests passed.
  • Full lint passed.
  • Production build passed.
  • Cross-tab clipboard test confirms that:
    • clipboard JSON reaches the second tab;
    • the temporary localStorage value is removed;
    • Paste remains enabled;
    • the copied element can be pasted successfully.

Checklist

Note that we require UI tests to ensure that the added feature will not be
nixed by some future fix and that there is at least some test-as-documentation
to indicate how the fix or enhancement is expected to behave.

  • - Added Cypress UI tests
  • - Ran npm test, ensuring linting passes and that Cypress UI tests keep
    coverage to at least the same percent (reflected in the coverage badge
    that should be updated after the tests run)
  • - Added any user documentation. Though not required, this can be a big
    help both for future users and for the PR reviewer.

Summary by Sourcery

Ensure clipboard availability is driven by sessionStorage-backed events so paste remains enabled across same-origin editor tabs.

New Features:

  • Expose a hasClipboardData() API on SvgCanvas to report whether pasteable clipboard contents are available.
  • Emit a clipboardChanged event from SvgCanvas when clipboard contents are updated locally or received from another tab and have the editor react to it.

Bug Fixes:

  • Keep paste actions enabled when clipboard data is present in sessionStorage even after the temporary localStorage transport value is cleared.
  • Avoid flashing or broadcasting empty, missing, or invalid clipboard data when syncing the clipboard between tabs.

Enhancements:

  • Drive editor clipboard UI state from shared SvgCanvas events instead of directly manipulating context-menu DOM from core code.
  • Simplify storage event handling by removing Editor-level listeners in favor of SvgCanvas clipboardChanged bindings.

Tests:

  • Add a Playwright end-to-end test validating copy and paste between two browser tabs while ensuring paste stays enabled after localStorage is cleared.
  • Extend unit tests around selected element copying and clipboard behavior to cover clipboardChanged emission and invalid clipboard data handling.

Use sessionStorage as the clipboard source of truth.
Notify each editor when clipboard data changes and keep paste enabled
after the temporary localStorage transport value is removed.
Avoid broadcasting empty or invalid clipboard data.
@veselin-kutsarov
veselin-kutsarov marked this pull request as ready for review July 28, 2026 06:46
@sourcery-ai

sourcery-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

Centralizes clipboard availability logic in SvgCanvas using sessionStorage as the source of truth, emits a clipboardChanged event to keep the editor UI in sync across tabs, and adds tests to ensure cross-tab copy/paste keeps Paste enabled even after localStorage transport cleanup.

Sequence diagram for cross-tab clipboardChanged and paste availability

sequenceDiagram
  actor User
  participant Tab1_Editor as Tab1_Editor
  participant Tab1_SvgCanvas as Tab1_SvgCanvas
  participant SessionStorage as SessionStorage
  participant LocalStorage as LocalStorage
  participant Tab2_SvgCanvas as Tab2_SvgCanvas
  participant Tab2_Editor as Tab2_Editor

  User->>Tab1_Editor: copySelectedElements
  Tab1_Editor->>Tab1_SvgCanvas: copySelectedElements
  Tab1_SvgCanvas->>SessionStorage: setItem(CLIPBOARD_ID, data)
  Tab1_SvgCanvas->>Tab1_SvgCanvas: call(clipboardChanged)
  Tab1_SvgCanvas->>Tab1_SvgCanvas: flashStorage
  Tab1_SvgCanvas->>LocalStorage: setItem(CLIPBOARD_ID, data)

  LocalStorage-->>Tab2_SvgCanvas: storage event (key=CLIPBOARD_ID, newValue)
  Tab2_SvgCanvas->>SessionStorage: setItem(CLIPBOARD_ID, newValue)
  Tab2_SvgCanvas->>Tab2_SvgCanvas: call(clipboardChanged)

  Tab2_SvgCanvas->>Tab2_Editor: clipboardChanged handler
  Tab2_Editor->>Tab2_SvgCanvas: hasClipboardData
  Tab2_SvgCanvas-->>Tab2_Editor: boolean
  Tab2_Editor->>Tab2_Editor: enableOrDisableClipboard
  Tab2_Editor->>Tab2_Editor: canvMenu.setAttribute(enablemenuitems|disablemenuitems)
Loading

File-Level Changes

Change Details Files
Use sessionStorage-based clipboard state and event-driven updates for paste availability across tabs.
  • On storage events with svgedit_clipboard key, store incoming clipboard JSON in sessionStorage and emit a clipboardChanged event from SvgCanvas.
  • Introduce hasClipboardData() on SvgCanvas to validate clipboard JSON in sessionStorage and report whether there is at least one pasteable element.
  • Guard flashStorage() so it only mirrors clipboard data into localStorage when there is valid clipboard data in sessionStorage, avoiding broadcasting invalid or empty payloads.
  • Update Editor.enableOrDisableClipboard() to query svgCanvas.hasClipboardData() instead of reading localStorage directly and to toggle paste menu items accordingly.
  • Bind EditorStartup to the clipboardChanged event from SvgCanvas instead of a window storage listener, so the editor UI reacts to clipboard changes via SvgCanvas events.
  • Emit clipboardChanged when copying selected elements, replacing direct context-menu DOM manipulation in core code.
packages/svgcanvas/svgcanvas.js
src/editor/Editor.js
src/editor/EditorStartup.js
packages/svgcanvas/core/selected-elem.js
Harden clipboard behavior and tests to prevent invalid data and cross-test leakage, and add an e2e cross-tab clipboard test.
  • Extend unit tests for selected-elem to clear both sessionStorage and localStorage before/after each test, and to assert clipboardChanged emission and hasClipboardData() correctness when copying.
  • Add a unit test ensuring missing, empty, or invalid clipboard data does not cause flashing into localStorage and that hasClipboardData() returns false for such cases.
  • Add a Playwright e2e test that opens a second tab, accepts storage prompts, waits for clipboard JSON to arrive in sessionStorage, asserts the temporary localStorage value is cleared, verifies Paste remains enabled, and successfully pastes the copied element.
  • Update e2e clipboard test helpers to use dismissStorageDialog and selectEnglishAndSnap for second tab initialization.
  • Refresh coverage-summary.json to reflect new/changed tests and coverage numbers.
tests/unit/selected-elem.test.js
tests/e2e/clipboard.spec.js
coverage/coverage-summary.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In SvgCanvas.hasClipboardData and flashStorage, consider wrapping sessionStorage/localStorage access in a try/catch similar to the previous enableOrDisableClipboard logic so that environments with restricted storage (e.g., private mode) don’t throw and break clipboard enablement.
  • You fire clipboardChanged both when copying locally and when receiving storage events regardless of whether hasClipboardData() is true; you might want to gate or coalesce these emissions so the editor UI only reacts when pasteability actually changes, reducing redundant updates on invalid/empty payloads.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `SvgCanvas.hasClipboardData` and `flashStorage`, consider wrapping `sessionStorage`/`localStorage` access in a try/catch similar to the previous `enableOrDisableClipboard` logic so that environments with restricted storage (e.g., private mode) don’t throw and break clipboard enablement.
- You fire `clipboardChanged` both when copying locally and when receiving storage events regardless of whether `hasClipboardData()` is true; you might want to gate or coalesce these emissions so the editor UI only reacts when pasteability actually changes, reducing redundant updates on invalid/empty payloads.

## Individual Comments

### Comment 1
<location path="packages/svgcanvas/svgcanvas.js" line_range="297-301" />
<code_context>
       } else if (ev.key === CLIPBOARD_ID) {
         // Another tab sent data.
         sessionStorage.setItem(CLIPBOARD_ID, ev.newValue)
+        this.call('clipboardChanged')
       }
     }
</code_context>
<issue_to_address>
**suggestion:** Avoid storing a literal 'null' string in sessionStorage when clipboard is cleared

When `ev.newValue` is `null`, this call stores the string `'null'` under `CLIPBOARD_ID`. Since we treat clipboard absence specially, consider calling `sessionStorage.removeItem(CLIPBOARD_ID)` instead in that case to keep storage semantics clear and avoid surprises for code that reads the raw storage value.

```suggestion
      } else if (ev.key === CLIPBOARD_ID) {
        // Another tab sent data.
        if (ev.newValue === null) {
          sessionStorage.removeItem(CLIPBOARD_ID)
        } else {
          sessionStorage.setItem(CLIPBOARD_ID, ev.newValue)
        }
        this.call('clipboardChanged')
      }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread packages/svgcanvas/svgcanvas.js
@veselin-kutsarov

veselin-kutsarov commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Hey - I've found 1 issue, and left some high level feedback:

  • In SvgCanvas.hasClipboardData and flashStorage, consider wrapping sessionStorage/localStorage access in a try/catch similar to the previous enableOrDisableClipboard logic so that environments with restricted storage (e.g., private mode) don’t throw and break clipboard enablement.
  • You fire clipboardChanged both when copying locally and when receiving storage events regardless of whether hasClipboardData() is true; you might want to gate or coalesce these emissions so the editor UI only reacts when pasteability actually changes, reducing redundant updates on invalid/empty payloads.

Thanks for the suggestion. The storageChange handler returns before this
branch when ev.newValue is null. The localStorage removal is only cleanup
of the transient cross-tab transport and must not clear the clipboard
stored in sessionStorage. Removing the sessionStorage value here would
disable Paste again after every flash and reintroduce the bug fixed by
this PR.

@jfhenon
jfhenon merged commit cdfdc94 into SVG-Edit:master Jul 29, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants