22 * URL sharing utilities for PathView
33 * Handles encoding and decoding graph data in URLs
44 */
5+ import { compressToBase64 , decompressFromBase64 } from 'lz-string' ;
6+
7+ // Maximum safe URL length for most servers (conservative estimate)
8+ const MAX_SAFE_URL_LENGTH = 4000 ;
59
610/**
7- * Encode graph data to a base64 URL parameter
11+ * Encode graph data to a compressed base64 URL parameter
812 * @param {Object } graphData - The complete graph data object
9- * @returns {string } - Base64 encoded string
13+ * @returns {string } - Compressed base64 encoded string
1014 */
1115export function encodeGraphData ( graphData ) {
12- try {
13- const jsonString = JSON . stringify ( graphData ) ;
14- // Use btoa for base64 encoding, but handle Unicode strings properly
15- const utf8Bytes = new TextEncoder ( ) . encode ( jsonString ) ;
16- const binaryString = Array . from ( utf8Bytes , byte => String . fromCharCode ( byte ) ) . join ( '' ) ;
17- return btoa ( binaryString ) ;
18- } catch ( error ) {
19- console . error ( 'Error encoding graph data:' , error ) ;
20- return null ;
21- }
22- }
23-
24- /**
25- * Decode graph data from a base64 URL parameter
26- * @param {string } encodedData - Base64 encoded graph data
16+ try {
17+ const jsonString = JSON . stringify ( graphData ) ;
18+ // Use lz-string for much better compression than manual whitespace removal
19+ return compressToBase64 ( jsonString ) ;
20+ } catch ( error ) {
21+ console . error ( 'Error encoding graph data:' , error ) ;
22+ return null ;
23+ }
24+ } /**
25+ * Decode graph data from a compressed base64 URL parameter
26+ * @param {string } encodedData - Compressed base64 encoded graph data
2727 * @returns {Object|null } - Decoded graph data object or null if error
2828 */
2929export function decodeGraphData ( encodedData ) {
3030 try {
31- // Decode base64 and handle Unicode properly
32- const binaryString = atob ( encodedData ) ;
33- const bytes = new Uint8Array ( binaryString . length ) ;
34- for ( let i = 0 ; i < binaryString . length ; i ++ ) {
35- bytes [ i ] = binaryString . charCodeAt ( i ) ;
31+ // First try lz-string decompression (new format)
32+ const jsonString = decompressFromBase64 ( encodedData ) ;
33+ if ( jsonString ) {
34+ return JSON . parse ( jsonString ) ;
35+ }
36+
37+ // Fallback for old format (manual base64 encoding)
38+ try {
39+ const binaryString = atob ( encodedData ) ;
40+ const bytes = new Uint8Array ( binaryString . length ) ;
41+ for ( let i = 0 ; i < binaryString . length ; i ++ ) {
42+ bytes [ i ] = binaryString . charCodeAt ( i ) ;
43+ }
44+ const oldJsonString = new TextDecoder ( ) . decode ( bytes ) ;
45+ return JSON . parse ( oldJsonString ) ;
46+ } catch ( oldFormatError ) {
47+ console . warn ( 'Could not decode with old format either:' , oldFormatError ) ;
3648 }
37- const jsonString = new TextDecoder ( ) . decode ( bytes ) ;
38- return JSON . parse ( jsonString ) ;
49+
50+ return null ;
3951 } catch ( error ) {
4052 console . error ( 'Error decoding graph data:' , error ) ;
4153 return null ;
@@ -45,27 +57,32 @@ export function decodeGraphData(encodedData) {
4557/**
4658 * Generate a shareable URL with the current graph data
4759 * @param {Object } graphData - The complete graph data object
48- * @returns {string } - Complete shareable URL
60+ * @returns {Object } - Object with url and metadata about the URL
4961 */
5062export function generateShareableURL ( graphData ) {
51- try {
52- const encodedData = encodeGraphData ( graphData ) ;
53- if ( ! encodedData ) {
54- throw new Error ( 'Failed to encode graph data' ) ;
55- }
56-
57- const baseURL = window . location . origin + window . location . pathname ;
58- const url = new URL ( baseURL ) ;
59- url . searchParams . set ( 'graph' , encodedData ) ;
60-
61- return url . toString ( ) ;
62- } catch ( error ) {
63- console . error ( 'Error generating shareable URL:' , error ) ;
64- return null ;
63+ try {
64+ const encodedData = encodeGraphData ( graphData ) ;
65+ if ( ! encodedData ) {
66+ throw new Error ( 'Failed to encode graph data' ) ;
6567 }
66- }
67-
68- /**
68+
69+ const baseURL = window . location . origin + window . location . pathname ;
70+ const url = new URL ( baseURL ) ;
71+ url . searchParams . set ( 'graph' , encodedData ) ;
72+
73+ const finalURL = url . toString ( ) ;
74+
75+ return {
76+ url : finalURL ,
77+ length : finalURL . length ,
78+ isSafe : finalURL . length <= MAX_SAFE_URL_LENGTH ,
79+ maxLength : MAX_SAFE_URL_LENGTH
80+ } ;
81+ } catch ( error ) {
82+ console . error ( 'Error generating shareable URL:' , error ) ;
83+ return null ;
84+ }
85+ } /**
6986 * Extract graph data from current URL parameters
7087 * @returns {Object|null } - Graph data object or null if not found/error
7188 */
@@ -91,21 +108,21 @@ export function getGraphDataFromURL() {
91108 * @param {boolean } replaceState - Whether to replace current history state (default: false)
92109 */
93110export function updateURLWithGraphData ( graphData , replaceState = false ) {
94- try {
95- const shareableURL = generateShareableURL ( graphData ) ;
96- if ( shareableURL ) {
97- if ( replaceState ) {
98- window . history . replaceState ( { } , '' , shareableURL ) ;
99- } else {
100- window . history . pushState ( { } , '' , shareableURL ) ;
101- }
102- }
103- } catch ( error ) {
104- console . error ( 'Error updating URL with graph data:' , error ) ;
111+ try {
112+ const urlResult = generateShareableURL ( graphData ) ;
113+ if ( urlResult && urlResult . isSafe ) {
114+ if ( replaceState ) {
115+ window . history . replaceState ( { } , '' , urlResult . url ) ;
116+ } else {
117+ window . history . pushState ( { } , '' , urlResult . url ) ;
118+ }
119+ } else if ( urlResult ) {
120+ console . warn ( `URL too long (${ urlResult . length } chars), not updating browser URL` ) ;
105121 }
106- }
107-
108- /**
122+ } catch ( error ) {
123+ console . error ( 'Error updating URL with graph data:' , error ) ;
124+ }
125+ } /**
109126 * Clear graph data from URL without page reload
110127 */
111128export function clearGraphDataFromURL ( ) {
@@ -116,3 +133,54 @@ export function clearGraphDataFromURL() {
116133 console . error ( 'Error clearing graph data from URL:' , error ) ;
117134 }
118135}
136+
137+ /**
138+ * Copy shareable URL to clipboard
139+ * @param {Object } graphData - The complete graph data object
140+ * @returns {Promise<Object> } - Result object with success status and metadata
141+ */
142+ export async function copyShareableURLToClipboard ( graphData ) {
143+ try {
144+ const urlResult = generateShareableURL ( graphData ) ;
145+ if ( ! urlResult ) {
146+ throw new Error ( 'Failed to generate shareable URL' ) ;
147+ }
148+
149+ await navigator . clipboard . writeText ( urlResult . url ) ;
150+ return {
151+ success : true ,
152+ isSafe : urlResult . isSafe ,
153+ length : urlResult . length ,
154+ maxLength : urlResult . maxLength ,
155+ url : urlResult . url
156+ } ;
157+ } catch ( error ) {
158+ console . error ( 'Error copying to clipboard:' , error ) ;
159+ // Fallback for older browsers
160+ try {
161+ const urlResult = generateShareableURL ( graphData ) ;
162+ const textArea = document . createElement ( 'textarea' ) ;
163+ textArea . value = urlResult . url ;
164+ document . body . appendChild ( textArea ) ;
165+ textArea . select ( ) ;
166+ document . execCommand ( 'copy' ) ;
167+ document . body . removeChild ( textArea ) ;
168+ return {
169+ success : true ,
170+ isSafe : urlResult . isSafe ,
171+ length : urlResult . length ,
172+ maxLength : urlResult . maxLength ,
173+ url : urlResult . url
174+ } ;
175+ } catch ( fallbackError ) {
176+ console . error ( 'Clipboard fallback failed:' , fallbackError ) ;
177+ return {
178+ success : false ,
179+ isSafe : false ,
180+ length : 0 ,
181+ maxLength : MAX_SAFE_URL_LENGTH ,
182+ url : null
183+ } ;
184+ }
185+ }
186+ }
0 commit comments