forked from MapComplete/MapComplete
		
	Split out editor, add to theme editing
This commit is contained in:
		
							parent
							
								
									2c018d7af3
								
							
						
					
					
						commit
						a8313022a0
					
				
					 3 changed files with 116 additions and 72 deletions
				
			
		| 
						 | 
					@ -13,24 +13,15 @@
 | 
				
			||||||
  import SchemaBasedInput from "./SchemaBasedInput.svelte"
 | 
					  import SchemaBasedInput from "./SchemaBasedInput.svelte"
 | 
				
			||||||
  import FloatOver from "../Base/FloatOver.svelte"
 | 
					  import FloatOver from "../Base/FloatOver.svelte"
 | 
				
			||||||
  import TagRenderingInput from "./TagRenderingInput.svelte"
 | 
					  import TagRenderingInput from "./TagRenderingInput.svelte"
 | 
				
			||||||
  import FromHtml from "../Base/FromHtml.svelte"
 | 
					 | 
				
			||||||
  import AllTagsPanel from "../Popup/AllTagsPanel.svelte"
 | 
					  import AllTagsPanel from "../Popup/AllTagsPanel.svelte"
 | 
				
			||||||
  import QuestionPreview from "./QuestionPreview.svelte"
 | 
					  import QuestionPreview from "./QuestionPreview.svelte"
 | 
				
			||||||
  import ShowConversionMessages from "./ShowConversionMessages.svelte"
 | 
					  import ShowConversionMessages from "./ShowConversionMessages.svelte"
 | 
				
			||||||
  import loader from "@monaco-editor/loader"
 | 
					  import RawEditor from "./RawEditor.svelte"
 | 
				
			||||||
  import type * as Monaco from "monaco-editor/esm/vs/editor/editor.api"
 | 
					 | 
				
			||||||
  import { onMount } from "svelte"
 | 
					 | 
				
			||||||
  import layerSchemaJSON from "../../../Docs/Schemas/LayerConfigJson.schema.json"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const layerSchema: ConfigMeta[] = <any>layerSchemaRaw
 | 
					  const layerSchema: ConfigMeta[] = <any>layerSchemaRaw
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let state: EditLayerState
 | 
					  export let state: EditLayerState
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Throw error if we don't have a state
 | 
					 | 
				
			||||||
  if (!state) {
 | 
					 | 
				
			||||||
    throw new Error("No state provided")
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  export let backToStudio: () => void
 | 
					  export let backToStudio: () => void
 | 
				
			||||||
  let messages = state.messages
 | 
					  let messages = state.messages
 | 
				
			||||||
  let hasErrors = messages.mapD(
 | 
					  let hasErrors = messages.mapD(
 | 
				
			||||||
| 
						 | 
					@ -69,7 +60,7 @@
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let requiredFields = ["id", "name", "description", "source"]
 | 
					  let requiredFields = ["id", "name", "description", "source"]
 | 
				
			||||||
  let currentlyMissing = state.configuration.map((config) => {
 | 
					  let currentlyMissing = configuration.map((config) => {
 | 
				
			||||||
    if (!config) {
 | 
					    if (!config) {
 | 
				
			||||||
      return []
 | 
					      return []
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -88,61 +79,6 @@
 | 
				
			||||||
    state.delete()
 | 
					    state.delete()
 | 
				
			||||||
    backToStudio()
 | 
					    backToStudio()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  let tabbedGroup: TabbedGroup
 | 
					 | 
				
			||||||
  let openTab: UIEventSource<number> = new UIEventSource<number>(0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let monaco: typeof Monaco
 | 
					 | 
				
			||||||
  let editorContainer: HTMLDivElement
 | 
					 | 
				
			||||||
  let layerEditor: Monaco.editor.IStandaloneCodeEditor
 | 
					 | 
				
			||||||
  let model: Monaco.editor.ITextModel
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onMount(async () => {
 | 
					 | 
				
			||||||
    openTab = tabbedGroup.getTab()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const monacoEditor = await import("monaco-editor")
 | 
					 | 
				
			||||||
    loader.config({ monaco: monacoEditor.default })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    monaco = await loader.init()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Prepare the Monaco editor (language settings)
 | 
					 | 
				
			||||||
    // A.K.A. The schemas for the Monaco editor
 | 
					 | 
				
			||||||
    monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
 | 
					 | 
				
			||||||
      validate: true,
 | 
					 | 
				
			||||||
      schemas: [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          uri: "https://mapcomplete.org/schemas/layerconfig.json",
 | 
					 | 
				
			||||||
          fileMatch: ["layer.json"],
 | 
					 | 
				
			||||||
          schema: layerSchemaJSON,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    let modelUri = monaco.Uri.parse("inmemory://inmemory/layer.json")
 | 
					 | 
				
			||||||
    model = monaco.editor.createModel(
 | 
					 | 
				
			||||||
      JSON.stringify(state.configuration.data, null, "  "),
 | 
					 | 
				
			||||||
      "json",
 | 
					 | 
				
			||||||
      modelUri
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    layerEditor = monaco.editor.create(editorContainer, {
 | 
					 | 
				
			||||||
      model: model,
 | 
					 | 
				
			||||||
      automaticLayout: true,
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // When the editor is changed, update the configuration, but only if the user hasn't typed for 500ms and the JSON is valid
 | 
					 | 
				
			||||||
    let timeout: number
 | 
					 | 
				
			||||||
    layerEditor.onDidChangeModelContent(() => {
 | 
					 | 
				
			||||||
      clearTimeout(timeout)
 | 
					 | 
				
			||||||
      timeout = setTimeout(() => {
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
          const newConfig = JSON.parse(layerEditor.getValue())
 | 
					 | 
				
			||||||
          state.configuration.setData(newConfig)
 | 
					 | 
				
			||||||
        } catch (e) {
 | 
					 | 
				
			||||||
          console.error(e)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }, 500)
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="flex h-screen flex-col">
 | 
					<div class="flex h-screen flex-col">
 | 
				
			||||||
| 
						 | 
					@ -191,7 +127,7 @@
 | 
				
			||||||
    {/each}
 | 
					    {/each}
 | 
				
			||||||
  {:else}
 | 
					  {:else}
 | 
				
			||||||
    <div class="m4 h-full overflow-y-auto">
 | 
					    <div class="m4 h-full overflow-y-auto">
 | 
				
			||||||
      <TabbedGroup bind:this={tabbedGroup}>
 | 
					      <TabbedGroup>
 | 
				
			||||||
        <div slot="title0" class="flex">
 | 
					        <div slot="title0" class="flex">
 | 
				
			||||||
          General properties
 | 
					          General properties
 | 
				
			||||||
          <ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} />
 | 
					          <ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} />
 | 
				
			||||||
| 
						 | 
					@ -254,7 +190,9 @@
 | 
				
			||||||
            Below, you'll find the raw configuration file in `.json`-format. This is mostly for
 | 
					            Below, you'll find the raw configuration file in `.json`-format. This is mostly for
 | 
				
			||||||
            debugging purposes, but you can also edit the file directly if you want.
 | 
					            debugging purposes, but you can also edit the file directly if you want.
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div class="literal-code h-64 w-full" bind:this={editorContainer} />
 | 
					          <div class="literal-code h-64 w-full">
 | 
				
			||||||
 | 
					            <RawEditor {state} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <ShowConversionMessages messages={$messages} />
 | 
					          <ShowConversionMessages messages={$messages} />
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@
 | 
				
			||||||
  import TabbedGroup from "../Base/TabbedGroup.svelte"
 | 
					  import TabbedGroup from "../Base/TabbedGroup.svelte"
 | 
				
			||||||
  import ShowConversionMessages from "./ShowConversionMessages.svelte"
 | 
					  import ShowConversionMessages from "./ShowConversionMessages.svelte"
 | 
				
			||||||
  import Region from "./Region.svelte"
 | 
					  import Region from "./Region.svelte"
 | 
				
			||||||
 | 
					  import RawEditor from "./RawEditor.svelte"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let state: EditThemeState
 | 
					  export let state: EditThemeState
 | 
				
			||||||
  let schema: ConfigMeta[] = state.schema.filter((schema) => schema.path.length > 0)
 | 
					  let schema: ConfigMeta[] = state.schema.filter((schema) => schema.path.length > 0)
 | 
				
			||||||
| 
						 | 
					@ -20,7 +21,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const perRegion: Record<string, ConfigMeta[]> = {}
 | 
					  const perRegion: Record<string, ConfigMeta[]> = {}
 | 
				
			||||||
  for (const schemaElement of schema) {
 | 
					  for (const schemaElement of schema) {
 | 
				
			||||||
    if(schemaElement.path.length > 1 && schemaElement.path[0] === "layers"){
 | 
					    if (schemaElement.path.length > 1 && schemaElement.path[0] === "layers") {
 | 
				
			||||||
      continue
 | 
					      continue
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const key = schemaElement.hints.group ?? "no-group"
 | 
					    const key = schemaElement.hints.group ?? "no-group"
 | 
				
			||||||
| 
						 | 
					@ -73,9 +74,13 @@
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div slot="title4">Configuration file</div>
 | 
					      <div slot="title4">Configuration file</div>
 | 
				
			||||||
      <div slot="content4">
 | 
					      <div slot="content4" class="h-full">
 | 
				
			||||||
        <div class="literal-code">
 | 
					        <div>
 | 
				
			||||||
          {JSON.stringify($config)}
 | 
					          Below, you'll find the raw configuration file in `.json`-format. This is mostly for
 | 
				
			||||||
 | 
					          debugging purposes, but you can also edit the file directly if you want.
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="literal-code h-full w-full">
 | 
				
			||||||
 | 
					          <RawEditor {state} />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ShowConversionMessages messages={$messages} />
 | 
					        <ShowConversionMessages messages={$messages} />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										101
									
								
								src/UI/Studio/RawEditor.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/UI/Studio/RawEditor.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,101 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					  import { onDestroy, onMount } from "svelte"
 | 
				
			||||||
 | 
					  import EditLayerState, { EditThemeState } from "./EditLayerState"
 | 
				
			||||||
 | 
					  import loader from "@monaco-editor/loader"
 | 
				
			||||||
 | 
					  import type * as Monaco from "monaco-editor/esm/vs/editor/editor.api"
 | 
				
			||||||
 | 
					  import layerSchemaJSON from "../../../Docs/Schemas/LayerConfigJson.schema.json"
 | 
				
			||||||
 | 
					  import layoutSchemaJSON from "../../../Docs/Schemas/LayoutConfigJson.schema.json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  export let state: EditLayerState | EditThemeState
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let container: HTMLDivElement
 | 
				
			||||||
 | 
					  let monaco: typeof Monaco
 | 
				
			||||||
 | 
					  let editor: Monaco.editor.IStandaloneCodeEditor
 | 
				
			||||||
 | 
					  let model: Monaco.editor.ITextModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function save() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const newConfig = JSON.parse(editor.getValue())
 | 
				
			||||||
 | 
					      state.configuration.setData(newConfig)
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(e)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Catch keyboard shortcuts
 | 
				
			||||||
 | 
					  onMount(() => {
 | 
				
			||||||
 | 
					    const handler = (e: KeyboardEvent) => {
 | 
				
			||||||
 | 
					      if (e.key === "s" && (e.ctrlKey || e.metaKey)) {
 | 
				
			||||||
 | 
					        e.preventDefault()
 | 
				
			||||||
 | 
					        save()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    window.addEventListener("keydown", handler)
 | 
				
			||||||
 | 
					    return () => window.removeEventListener("keydown", handler)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMount(async () => {
 | 
				
			||||||
 | 
					    const monacoEditor = await import("monaco-editor")
 | 
				
			||||||
 | 
					    loader.config({
 | 
				
			||||||
 | 
					      monaco: monacoEditor.default,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    monaco = await loader.init()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Determine schema based on the state
 | 
				
			||||||
 | 
					    let schemaUri: string
 | 
				
			||||||
 | 
					    if (state instanceof EditLayerState) {
 | 
				
			||||||
 | 
					      schemaUri = "https://mapcomplete.org/schemas/layerconfig.json"
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      schemaUri = "https://mapcomplete.org/schemas/layoutconfig.json"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
 | 
				
			||||||
 | 
					      validate: true,
 | 
				
			||||||
 | 
					      schemas: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          uri: schemaUri,
 | 
				
			||||||
 | 
					          fileMatch: ["file.json"],
 | 
				
			||||||
 | 
					          schema:
 | 
				
			||||||
 | 
					            schemaUri === "https://mapcomplete.org/schemas/layerconfig.json"
 | 
				
			||||||
 | 
					              ? layerSchemaJSON
 | 
				
			||||||
 | 
					              : layoutSchemaJSON,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let modelUri = monaco.Uri.parse("inmemory://inmemory/file.json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Create a new model
 | 
				
			||||||
 | 
					    model = monaco.editor.createModel(
 | 
				
			||||||
 | 
					      JSON.stringify(state.configuration.data, null, "  "),
 | 
				
			||||||
 | 
					      "json",
 | 
				
			||||||
 | 
					      modelUri
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    editor = monaco.editor.create(container, {
 | 
				
			||||||
 | 
					      model,
 | 
				
			||||||
 | 
					      automaticLayout: true,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // When the editor is changed, update the configuration, but only if the user hasn't typed for 500ms and the JSON is valid
 | 
				
			||||||
 | 
					    let timeout: number
 | 
				
			||||||
 | 
					    editor.onDidChangeModelContent(() => {
 | 
				
			||||||
 | 
					      clearTimeout(timeout)
 | 
				
			||||||
 | 
					      timeout = setTimeout(() => {
 | 
				
			||||||
 | 
					        save()
 | 
				
			||||||
 | 
					      }, 500)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onDestroy(() => {
 | 
				
			||||||
 | 
					    if (editor) {
 | 
				
			||||||
 | 
					      editor.dispose()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (model) {
 | 
				
			||||||
 | 
					      model.dispose()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div bind:this={container} class="h-full w-full" />
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue