diff --git a/docs/docs/features/display-modes.mdx b/docs/docs/features/display-modes.mdx index 9f9c581bb1..a2d52e8827 100644 --- a/docs/docs/features/display-modes.mdx +++ b/docs/docs/features/display-modes.mdx @@ -62,6 +62,20 @@ export const simpleConfig = { +## `console` + +This mode is mainly useful for embedded playgrounds. +It shows a single editor together with the [console](./console.mdx) maximized (filling the output area), with the result page collapsed. +The [tools pane](./tools-pane.mdx) is fixed to show only the console; it cannot be resized or closed. +The console input is hidden, and the code can be run from the editor. +This mode is particularly useful for [WASM-based languages](../languages/index.mdx) (e.g. Python, Ruby, PHP, C, ...) whose output is mainly text-based (sent to the console). + +Demo: Python with console + + + ## `lite` Loads a light-weight, minimal code editor, with limited playground features. diff --git a/src/livecodes/config/build-config.ts b/src/livecodes/config/build-config.ts index 77eb0fa0b6..71dbcd7038 100644 --- a/src/livecodes/config/build-config.ts +++ b/src/livecodes/config/build-config.ts @@ -224,7 +224,7 @@ export const loadParamConfig = (config: Config, params: UrlQueryParams): Partial paramsConfig.languages = params.languages .split(',') .map((lang) => lang.trim()) - .map(getLanguageByAlias) + .map((lang) => getLanguageByAlias(lang)) .filter(Boolean) as Language[]; } diff --git a/src/livecodes/config/validate-config.ts b/src/livecodes/config/validate-config.ts index 44032cb65e..3355ee5f43 100644 --- a/src/livecodes/config/validate-config.ts +++ b/src/livecodes/config/validate-config.ts @@ -30,6 +30,7 @@ export const validateConfig = (config: Partial): Partial => { 'focus', 'lite', 'simple', + 'console', 'editor', 'codeblock', 'result', diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts index c6ce47fd77..a682ef2d9c 100644 --- a/src/livecodes/core.ts +++ b/src/livecodes/core.ts @@ -628,6 +628,7 @@ const showMode = (mode?: Config['mode'], view?: Config['view']) => { focus: '111', simple: '111', lite: '111', + console: '111', editor: '110', codeblock: '010', result: '001', @@ -693,14 +694,21 @@ const showMode = (mode?: Config['mode'], view?: Config['view']) => { document.body.classList.toggle('simple-mode', mode === 'simple'); document.body.classList.toggle('focus-mode', mode === 'focus'); document.body.classList.toggle('lite-mode', mode === 'lite'); + document.body.classList.toggle('console-mode', mode === 'console'); document.body.classList.toggle('result', mode === 'result'); document.body.classList.toggle('no-result', mode === 'editor' || mode === 'codeblock'); - if ((mode === 'full' || mode === 'simple') && !split) { + if ((mode === 'full' || mode === 'simple' || mode === 'console') && !split) { split = createSplitPanes(); } if (mode === 'focus') { toolsPane?.setActiveTool('console'); } + if (mode === 'console') { + toolsPane?.setActiveTool('console'); + if (!['open', 'full'].includes(toolsPane?.getStatus() || '')) { + toolsPane?.maximize(); + } + } window.dispatchEvent(new Event(customEvents.resizeEditor)); }; @@ -5105,6 +5113,17 @@ const configureSimpleMode = (config: Config) => { }); }; +const configureConsoleMode = (config: Config) => { + setConfig({ + ...config, + tools: { + enabled: Array.isArray(config.tools?.enabled) ? config.tools.enabled : ['console'], + active: config.tools?.active || 'console', + status: config.tools?.status || 'full', + }, + }); +}; + const configureModes = ({ config, isEmbed, @@ -5129,6 +5148,9 @@ const configureModes = ({ if (config.mode === 'simple') { configureSimpleMode(config); } + if (config.mode === 'console') { + configureConsoleMode(config); + } }; const importExternalContent = async (options: { @@ -5331,7 +5353,9 @@ const initializePlayground = async ( isLite || (options?.isEmbed ?? false) || initialConfig.mode === 'simple' || - params.mode === 'simple'; + params.mode === 'simple' || + initialConfig.mode === 'console' || + params.mode === 'console'; window.history.replaceState(null, '', './'); // fix URL from "/app" to "/" await initializeStores(stores, isEmbed); diff --git a/src/livecodes/languages/__tests__/languages.spec.ts b/src/livecodes/languages/__tests__/languages.spec.ts index 4896696f0c..99113f0023 100644 --- a/src/livecodes/languages/__tests__/languages.spec.ts +++ b/src/livecodes/languages/__tests__/languages.spec.ts @@ -1,4 +1,10 @@ -import { getLanguageByAlias, getLanguageEditorId, getLanguageExtension, mapLanguage } from '..'; +import { + getLanguageByAlias, + getLanguageEditorId, + getLanguageExtension, + languageIsEnabled, + mapLanguage, +} from '..'; describe('languages', () => { test('getLanguageByAlias', () => { @@ -35,4 +41,30 @@ describe('languages', () => { expect(mapLanguage('ruby')).toBe('ruby'); expect(mapLanguage('html')).toBe('html'); }); + + describe('deprecation warnings', () => { + let warn: jest.SpyInstance; + + beforeEach(() => { + (window as any).deps.loggedDeprecations = []; + warn = jest.spyOn(console, 'warn').mockImplementation(() => { + // + }); + }); + + afterEach(() => { + warn.mockRestore(); + }); + + test('warns when a deprecated language name is used', () => { + expect(getLanguageByAlias('ocaml')).toBe('ocaml'); + expect(warn).toHaveBeenCalled(); + }); + + test('does not warn while enumerating languages', () => { + languageIsEnabled('ocaml', { languages: undefined } as any); + languageIsEnabled('ocaml', { languages: ['html', 'css', 'javascript'] } as any); + expect(warn).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/livecodes/languages/utils.ts b/src/livecodes/languages/utils.ts index 684ebea1c2..1b830c6fe2 100644 --- a/src/livecodes/languages/utils.ts +++ b/src/livecodes/languages/utils.ts @@ -1,8 +1,32 @@ -import type { Compiler, Config, CustomSettings, Language, Processor } from '../models'; +import type { + Compiler, + Config, + CustomSettings, + Language, + LanguageSpecs, + Processor, +} from '../models'; import { getDocsUrl, getLanguageCustomSettings } from '../utils/utils'; import { highlightjsUrl } from '../vendors'; -export const getLanguageByAlias = (alias: string = ''): Language | undefined => { +const logDeprecation = (alias: string, language?: LanguageSpecs) => { + if (!globalThis.window?.deps) return; + window.deps.loggedDeprecations ??= []; + const deprecation = language?.deprecation?.(alias.toLowerCase() as Language); + if (!deprecation || window.deps.loggedDeprecations.includes(deprecation.old)) return; + // eslint-disable-next-line no-console + console.warn( + `Deprecation warning: The language name "${alias}" will use the compiler "${deprecation.new}" in the future.`, + `If you want to continue using the current compiler, use the language name "${deprecation.old}" instead.`, + language?.name ? `Docs: ${getDocsUrl()}languages/${language.name}` : '', + ); + window.deps.loggedDeprecations.push(deprecation.old); +}; + +export const getLanguageByAlias = ( + alias: string = '', + options: { warn?: boolean } = {}, +): Language | undefined => { if (!alias) return; const aliasLowerCase = alias?.toLowerCase(); const language = window.deps.languages.find( @@ -11,20 +35,10 @@ export const getLanguageByAlias = (alias: string = ''): Language | undefined => lang.title.toLowerCase() === aliasLowerCase || lang.extensions.map((ext) => ext.toLowerCase()).includes(aliasLowerCase), ); - if (globalThis.window?.deps) window.deps.loggedDeprecations ??= []; - const deprecation = language?.deprecation?.(aliasLowerCase as Language); - if ( - deprecation && - window.deps.loggedDeprecations && - !window.deps.loggedDeprecations.includes(deprecation.old) - ) { - // eslint-disable-next-line no-console - console.warn( - `Deprecation warning: The language name "${alias}" will use the compiler "${deprecation.new}" in the future.`, - `If you want to continue using the current compiler, use the language name "${deprecation.old}" instead.`, - language?.name ? `Docs: ${getDocsUrl()}languages/${language.name}` : '', - ); - window.deps.loggedDeprecations?.push(deprecation.old); + // only warn when the alias is actually used (e.g. from config or user selection), + // not when enumerating languages (menus, compiler list, starter templates) + if (options.warn !== false) { + logDeprecation(alias, language); } return language?.name; }; @@ -71,7 +85,7 @@ export const mapLanguage = ( 'html'; export const languageIsEnabled = (language: Language, config: Config) => { - const lang = getLanguageByAlias(language); + const lang = getLanguageByAlias(language, { warn: false }); if (!lang) return false; if (!config.languages) return true; if ( @@ -80,7 +94,10 @@ export const languageIsEnabled = (language: Language, config: Config) => { ) { return true; } - return config.languages?.map(getLanguageByAlias).filter(Boolean).includes(lang); + return config.languages + ?.map((alias) => getLanguageByAlias(alias, { warn: false })) + .filter(Boolean) + .includes(lang); }; export const processorIsEnabled = (processor: Processor, config: Config) => { diff --git a/src/livecodes/styles/app.scss b/src/livecodes/styles/app.scss index 3e706dbb73..b898ad2b20 100644 --- a/src/livecodes/styles/app.scss +++ b/src/livecodes/styles/app.scss @@ -1793,7 +1793,8 @@ a.editor-title { } } -.simple-mode { +.simple-mode, +.console-mode { .editor-title:not(.active), &.embed #toolbar .toolbar-render #result-button, &.embed #toolbar .toolbar-render #fullscreen-button, @@ -1842,6 +1843,29 @@ a.editor-title { } } +.console-mode { + #toolbar { + margin-bottom: 0; + transform: none; + width: 100%; + } + + #zoom-button, + .console-close-button, + #size-label { + display: none !important; + } + + #tools-pane-bar { + cursor: default; + pointer-events: none; + } + + #tools-pane-buttons { + pointer-events: auto; + } +} + .focus-mode { #app-menu-button-project, #app-menu-button-settings, diff --git a/src/livecodes/templates/get-starter-templates.ts b/src/livecodes/templates/get-starter-templates.ts index d1d1487548..8ee8fc79f7 100644 --- a/src/livecodes/templates/get-starter-templates.ts +++ b/src/livecodes/templates/get-starter-templates.ts @@ -28,7 +28,9 @@ const loadTemplates = async (baseUrl: string): Promise => export const getStarterTemplates = async (config: Config, baseUrl: string): Promise => (await loadTemplates(baseUrl)) .filter((template) => { - const enabledLanguages = config.languages?.map(getLanguageByAlias).filter(Boolean); + const enabledLanguages = config.languages + ?.map((language) => getLanguageByAlias(language, { warn: false })) + .filter(Boolean); if (!enabledLanguages) return true; if (template.name === 'blank') return true; @@ -38,7 +40,7 @@ export const getStarterTemplates = async (config: Config, baseUrl: string): Prom template.script?.language, ]; for (const language of templateLanguages) { - const lang = getLanguageByAlias(language); + const lang = getLanguageByAlias(language, { warn: false }); if (!lang || !enabledLanguages.includes(lang)) return false; } return true; diff --git a/src/livecodes/toolspane/tools.ts b/src/livecodes/toolspane/tools.ts index fdda7d78b3..47704d8944 100644 --- a/src/livecodes/toolspane/tools.ts +++ b/src/livecodes/toolspane/tools.ts @@ -310,6 +310,14 @@ export const createToolsPane = ( buttons.id = 'tools-pane-buttons'; buttons.classList.add('tool-buttons'); toolsPaneBar.appendChild(buttons); + // prevent starting a drag when clicking the buttons + eventsManager.addEventListener(buttons, 'mousedown', (event) => event.stopPropagation(), false); + eventsManager.addEventListener( + buttons, + 'touchstart', + (event) => event.stopPropagation(), + false, + ); const closeButton = document.createElement('button'); closeButton.classList.add('console-close-button'); diff --git a/src/sdk/models.ts b/src/sdk/models.ts index a9f49980a6..6d56b17a02 100644 --- a/src/sdk/models.ts +++ b/src/sdk/models.ts @@ -1226,7 +1226,7 @@ export interface AppConfig { * Sets the [display mode](https://livecodes.io/docs/features/display-modes). * @default "full" */ - mode: 'full' | 'focus' | 'lite' | 'simple' | 'editor' | 'codeblock' | 'result'; + mode: 'full' | 'focus' | 'lite' | 'simple' | 'console' | 'editor' | 'codeblock' | 'result'; /** * Sets enabled and active tools and status of [tools pane](https://livecodes.io/docs/features/tools-pane).