From bec720c77bb6ffb08f142d4077ebab162cea655a Mon Sep 17 00:00:00 2001 From: andymedinadev <107598069+andymedinadev@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:42:40 -0300 Subject: [PATCH] feat: Add urlSync config --- index.html | 16 ++++++++ src/aside.js | 7 +--- .../codi-editor/extensions/editor-hotkeys.js | 6 +-- src/constants/initial-settings.js | 1 + src/events-controller.js | 16 +++++++- src/language/en.js | 4 ++ src/language/es.js | 5 +++ src/language/pt.js | 5 +++ src/main.js | 29 +++++++------- src/url-sync.js | 39 +++++++++++++++++++ src/utils/notification.js | 12 ++++-- src/utils/url.js | 13 +++++++ 12 files changed, 126 insertions(+), 27 deletions(-) create mode 100644 src/url-sync.js create mode 100644 src/utils/url.js diff --git a/index.html b/index.html index e6cfab4b..f1ada494 100644 --- a/index.html +++ b/index.html @@ -430,6 +430,7 @@ +
Features › Auto save: @@ -443,6 +444,21 @@
+ +
+ + Features: + URL Sync + + +

+ Keeps the URL in sync with your current code for easy sharing. Disable to + prevent flooding your browser history. +

+
diff --git a/src/aside.js b/src/aside.js index 21e488d3..3b73007b 100644 --- a/src/aside.js +++ b/src/aside.js @@ -2,7 +2,6 @@ import { eventBus, EVENTS } from './events-controller.js' import { $, $$ } from './utils/dom.js' import * as Preview from './utils/WindowPreviewer' import { BUTTON_ACTIONS } from './constants/button-actions.js' -import { copyToClipboard } from './utils/string.js' import { resetConsoleBadge } from './console.js' const $aside = $('aside') @@ -23,10 +22,8 @@ const SIMPLE_CLICK_ACTIONS = { Preview.showPreviewerWindow() }, - [BUTTON_ACTIONS.copyToClipboard]: async () => { - const url = new URL(window.location.href) - const urlToCopy = `https://codi.link${url.pathname}` - copyToClipboard(urlToCopy) + [BUTTON_ACTIONS.copyToClipboard]: () => { + eventBus.emit(EVENTS.COPY_CURRENT_CODE_URL) }, [BUTTON_ACTIONS.clearHistory]: () => { diff --git a/src/components/codi-editor/extensions/editor-hotkeys.js b/src/components/codi-editor/extensions/editor-hotkeys.js index 4a859c29..83b482f5 100644 --- a/src/components/codi-editor/extensions/editor-hotkeys.js +++ b/src/components/codi-editor/extensions/editor-hotkeys.js @@ -1,6 +1,6 @@ import * as monaco from 'monaco-editor' import { $ } from '../../../utils/dom.js' -import { copyToClipboard } from '../../../utils/string' +import { eventBus, EVENTS } from '../../../events-controller.js' export const initEditorHotKeys = (editor) => { // Shortcut: Open/Close Settings @@ -22,9 +22,7 @@ export const initEditorHotKeys = (editor) => { editor.addCommand( monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyC, () => { - const url = new URL(window.location.href) - const urlToCopy = `https://codi.link${url.pathname}` - copyToClipboard(urlToCopy) + eventBus.emit(EVENTS.COPY_CURRENT_CODE_URL) } ) } diff --git a/src/constants/initial-settings.js b/src/constants/initial-settings.js index c8a77306..c981c98a 100644 --- a/src/constants/initial-settings.js +++ b/src/constants/initial-settings.js @@ -17,6 +17,7 @@ export const DEFAULT_INITIAL_SETTINGS = { zipFileName: 'codi.link', zipInSingleFile: false, saveLocalstorage: true, + urlSync: true, layout: { gutters: DEFAULT_LAYOUT, style: DEFAULT_GRID_TEMPLATE, diff --git a/src/events-controller.js b/src/events-controller.js index 30bd0260..ca1bc872 100644 --- a/src/events-controller.js +++ b/src/events-controller.js @@ -1,5 +1,6 @@ import { decode } from 'js-base64' -import { capitalize, searchByLine } from './utils/string.js' +import { capitalize, copyToClipboard, searchByLine } from './utils/string.js' +import { getEncodedPath } from './utils/url.js' import { downloadUserCode } from './download.js' import { getState } from './state.js' import { getHistoryState } from './history.js' @@ -42,6 +43,7 @@ export const EVENTS = { DRAG_FILE: 'DRAG_FILE', OPEN_EXISTING_INSTANCE: 'OPEN_EXISTING_INSTANCE', OPEN_NEW_INSTANCE: 'OPEN_NEW_INSTANCE', + COPY_CURRENT_CODE_URL: 'COPY-CURRENT-CODE-URL', CLEAR_HISTORY: 'CLEAR_HISTORY' } @@ -122,3 +124,15 @@ eventBus.on(EVENTS.CLEAR_HISTORY, () => { const { clearHistory } = getHistoryState() clearHistory() }) + +eventBus.on(EVENTS.COPY_CURRENT_CODE_URL, async () => { + const html = htmlEditor.getValue() + const css = cssEditor.getValue() + const js = jsEditor.getValue() + + const encodedPath = getEncodedPath({ html, css, js }) + + const urlToCopy = `${window.location.origin}${encodedPath}` + + await copyToClipboard(urlToCopy) +}) diff --git a/src/language/en.js b/src/language/en.js index 5c848144..1a5a1721 100644 --- a/src/language/en.js +++ b/src/language/en.js @@ -61,6 +61,10 @@ const en = { localStorage: 'Local storage', automaticallySaveUrl: 'Automatically save URL to local storage for fast content loading', + features: 'Features', + urlSync: 'URL Sync', + urlSyncCheckbox: 'Automatically update URL', + urlSyncDescription: 'Keeps the URL in sync with your current code for easy sharing. Disable to prevent flooding your browser history.', searchDependency: 'Search and add a package...' } diff --git a/src/language/es.js b/src/language/es.js index 4cf023d6..cc185f43 100644 --- a/src/language/es.js +++ b/src/language/es.js @@ -61,6 +61,11 @@ const es = { localStorage: 'Almacenamiento local', automaticallySaveUrl: 'Guardar automáticamente la URL en el almacenamiento local para una carga rápida del contenido', + features: 'Características', + urlSync: 'URL Sync', + urlSyncCheckbox: 'Actualizar URL automáticamente', + urlSyncDescription: + 'Actualiza automáticamente la URL con tu código actual. Desactívalo para evitar saturar el historial de navegación.', searchDependency: 'Buscar y agregar un paquete...' } diff --git a/src/language/pt.js b/src/language/pt.js index 14ff2473..56ed5815 100644 --- a/src/language/pt.js +++ b/src/language/pt.js @@ -61,6 +61,11 @@ const pt = { localStorage: 'Armazenamento local', automaticallySaveUrl: 'Salvar automaticamente a URL no armazenamento local para carregamento rápido de conteúdo', + features: 'Recursos', + urlSync: 'URL Sync', + urlSyncCheckbox: 'Atualizar automaticamente a URL', + urlSyncDescription: + 'Atualiza automaticamente a URL com o código atual. Desative para evitar sobrecarregar o histórico do navegador.', searchDependency: 'Pesquisar e adicionar um pacote...' } diff --git a/src/main.js b/src/main.js index e94fe142..869de5d7 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,4 @@ -import { encode, decode } from 'js-base64' +import { decode } from 'js-base64' import { $, $$ } from './utils/dom.js' import { createEditor } from './editor.js' import debounce from './utils/debounce.js' @@ -11,6 +11,8 @@ import setTheme from './theme.js' import setLanguage from './language.js' import { configurePrettierHotkeys } from './monaco-prettier/configurePrettier' import { getHistoryState, subscribeHistory, setHistory } from './history.js' +import { getEncodedString } from './utils/url.js' +import { setUrlSync, handleUrlSyncOnType } from './url-sync.js' import './aside.js' import './skypack.js' @@ -70,15 +72,11 @@ subscribe(state => { setGridLayout(state.layout) setTheme(state.theme) setLanguage(state.language) + setUrlSync(state.urlSync, EDITORS) }) const MS_UPDATE_DEBOUNCED_TIME = 200 -const MS_UPDATE_HASH_DEBOUNCED_TIME = 1000 const debouncedUpdate = debounce(update, MS_UPDATE_DEBOUNCED_TIME) -const debouncedUpdateHash = debounce( - updateHashedCode, - MS_UPDATE_HASH_DEBOUNCED_TIME -) const { html: htmlEditor, css: cssEditor, javascript: jsEditor } = EDITORS @@ -116,8 +114,9 @@ function update ({ notReload } = {}) { Preview.updatePreview(values) + const { maxExecutionTime, urlSync } = getState() + if (!notReload) { - const { maxExecutionTime } = getState() runJs(values.js, parseInt(maxExecutionTime)) .then(() => { iframe.setAttribute('src', Preview.getPreviewUrl()) @@ -129,10 +128,17 @@ function update ({ notReload } = {}) { updateCss() - debouncedUpdateHash(values) if (saveLocalstorage) { updateHistory(values) } + + if (urlSync) { + handleUrlSyncOnType(values) + } + + // fixes url bugs when using history + setUrlSync(urlSync, EDITORS) + updateButtonAvailabilityIfContent(values) } @@ -144,14 +150,9 @@ function updateCss () { } } -function updateHashedCode ({ html, css, js }) { - const hashedCode = `${encode(html)}|${encode(css)}|${encode(js)}` - window.history.replaceState(null, null, `/${hashedCode}`) -} - function updateHistory ({ html, css, js }) { const { history } = getHistoryState() - const hashedCode = `${encode(html)}|${encode(css)}|${encode(js)}` + const hashedCode = getEncodedString({ html, css, js }) const isEmpty = !html.replace(/\n/g, '').trim() && !css.replace(/\n/g, '').trim() && !js.replace(/\n/g, '').trim() if (isEmpty && !history.current) { diff --git a/src/url-sync.js b/src/url-sync.js new file mode 100644 index 00000000..24bbf7fe --- /dev/null +++ b/src/url-sync.js @@ -0,0 +1,39 @@ +import { getCleanPath, getEncodedPath } from './utils/url.js' +import debounce from './utils/debounce.js' + +const MS_UPDATE_HASH_DEBOUNCED_TIME = 1000 + +function getCurrentCodeFromEditors (EDITORS) { + return { + html: EDITORS.html?.getValue() || '', + css: EDITORS.css?.getValue() || '', + js: EDITORS.javascript?.getValue() || '' + } +} + +function updateHashedPath ({ html, css, js }) { + window.history.replaceState(null, null, getEncodedPath({ html, css, js })) +} + +const debouncedUpdateHashedPath = debounce(updateHashedPath, MS_UPDATE_HASH_DEBOUNCED_TIME) + +export function handleUrlSyncOnType (codeValues) { + debouncedUpdateHashedPath(codeValues) +} + +export function setUrlSync (isUrlSyncEnabled, EDITORS) { + const currentPathIsEmpty = window.location.pathname === '/' + + const shouldEncodeUrl = isUrlSyncEnabled && currentPathIsEmpty + const shouldCleanUrl = !isUrlSyncEnabled && !currentPathIsEmpty + + if (shouldEncodeUrl) { + const { html, css, js } = getCurrentCodeFromEditors(EDITORS) + window.history.replaceState(null, null, getEncodedPath({ html, css, js })) + return + } + + if (shouldCleanUrl) { + window.history.replaceState(null, null, getCleanPath()) + } +} diff --git a/src/utils/notification.js b/src/utils/notification.js index 713a124f..67cfb9c4 100644 --- a/src/utils/notification.js +++ b/src/utils/notification.js @@ -7,7 +7,7 @@ const STATE_ICONS = { } const TRANSITION_DURATION = 400 // ms -const NOTIFICATION_DURATION = 300000 // ms +const NOTIFICATION_DURATION = 3000 // ms export default { /** @@ -48,17 +48,23 @@ export default { notification.setAttribute('aria-live', 'assertive') notification.setAttribute('aria-atomic', 'true') - setTimeout(() => { + const timerIdOut = setTimeout(() => { notification.classList.remove('animation-in') notification.classList.add('animation-out') }, NOTIFICATION_DURATION - TRANSITION_DURATION / 2) // Remove notification after NOTIFICATION_DURATION - setTimeout(() => { + const timerIdRemove = setTimeout(() => { notification.remove() }, NOTIFICATION_DURATION) + const clearTimers = () => { + clearTimeout(timerIdOut) + clearTimeout(timerIdRemove) + } + notification.querySelector('.icon-close').addEventListener('click', () => { + clearTimers() notification.classList.add('bounce-leave') setTimeout(() => { notification.remove() diff --git a/src/utils/url.js b/src/utils/url.js new file mode 100644 index 00000000..0dd0a645 --- /dev/null +++ b/src/utils/url.js @@ -0,0 +1,13 @@ +import { encode } from 'js-base64' + +export function getCleanPath () { + return '/' +} + +export function getEncodedPath ({ html = '', css = '', js = '' }) { + return `/${encode(html)}%7C${encode(css)}%7C${encode(js)}` +} + +export function getEncodedString ({ html = '', css = '', js = '' }) { + return `${encode(html)}|${encode(css)}|${encode(js)}` +}