Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ Once selected this pane as a view of a file, then the pen icon allows to switch

Editing can be aborted by the cancel(X) button.

## File explorer integration

The pane returns a headless `source-pane-source-provider` and loads
`SourceEditorCard` only when Source is selected. Mount the returned pane beneath
`solid-ui-file-explorer-provider` (using `composeExplorer`) so the provider and
card receive the shared store, resource URI and editor registration contract.
The editor waits for both the subscribed explorer context and fetched content;
either may arrive after its first render. Context accessors used by Lit lifecycle
change checks must also be reactive properties.

JavaScript files (`.js`, `.mjs`, `.cjs`) offer and prefer Source even when a
server omits MIME metadata or erroneously types the file as an LDP container.

## Contribute

### Tech stack
Expand Down
847 changes: 470 additions & 377 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "source-pane",
"version": "4.0.0",
"version": "4.0.1-0",
"description": "Solid-compatible Panes: Source editor",
"main": "dist/index.cjs.js",
"exports": {
Expand Down Expand Up @@ -70,10 +70,10 @@
"@lit/context": "^1.1.6",
"@typescript-eslint/parser": "^8.63.0",
"lit": "^3.3.3",
"pane-registry": "5.0.1",
"pane-registry": "5.0.2-1",
"rdflib": "2.4.1",
"solid-logic": "6.0.0",
"solid-ui": "5.0.1",
"solid-logic": "6.0.1-4",
"solid-ui": "5.0.2-3",
"vite": "^8.0.16",
"vitest": "^4.0.18",
"vitest-axe": "^0.1.0"
Expand Down
82 changes: 66 additions & 16 deletions src/components/source-editor-card/SourceEditorCard.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { html, nothing } from 'lit'
import { consume } from '@lit/context'
import { customElement, query, state } from 'lit/decorators.js'
import { customElement, property, query, state } from 'lit/decorators.js'
import { NamedNode, parse, serialize } from 'rdflib'
import { WebComponent, type CodeEditor } from 'solid-ui'
import { fileExplorerContext, type FileExplorerContext } from 'solid-ui'
import { DEFAULT_FILE_EXPLORER_CONTEXT, fileExplorerContext } from 'solid-ui'
import { solidLogicSingleton } from 'solid-logic'
import 'solid-ui/components/button'
import { checkSyntax, happy } from '../../helpers'
import styles from './SourceEditorCard.styles.css'
import { getStatusSection } from '../../StatusSection'
import { compactable } from '../../compactableFormats'
import { sourceContext, SourceContext } from '../../primitives/context'
import { getResponseMetadata } from '../../resourceLoader'

@customElement('source-pane-source-editor-card')
export default class SourceEditorCard extends WebComponent {
Expand All @@ -19,8 +19,12 @@ export default class SourceEditorCard extends WebComponent {
private _editor?: CodeEditor
private _originalContent?: string
private _originalContentType?: string
private _loadedResourceUri?: string
private _dirtyState = false
private _initializing = false
private _syncGeneration = 0
private _syncQueue: Promise<void> = Promise.resolve()
private _editingRequested = false

@state()
accessor _editorReady = false
Expand All @@ -31,10 +35,12 @@ export default class SourceEditorCard extends WebComponent {
accessor _editorMount: HTMLDivElement | null = null

@consume({ context: sourceContext, subscribe: true })
@property({ attribute: false })
accessor sourceContext: SourceContext = undefined as unknown as SourceContext

@consume({ context: fileExplorerContext, subscribe: true })
accessor fileExplorerContext: FileExplorerContext = undefined as unknown as FileExplorerContext
@property({ attribute: false })
accessor fileExplorerContext = DEFAULT_FILE_EXPLORER_CONTEXT

private _requireSourceContext () {
if (!this.sourceContext) {
Expand Down Expand Up @@ -65,6 +71,12 @@ export default class SourceEditorCard extends WebComponent {
}

beginEditing() {
if (!this._editorReady) {
this._editingRequested = true
return
}

this._editingRequested = false
this._isEditing = true
this.updateEditingState(true)
this.setReadOnly(false)
Expand All @@ -80,21 +92,33 @@ export default class SourceEditorCard extends WebComponent {
}
}

private async _syncEditorToCurrentView() {
private async _syncEditorToCurrentView (syncGeneration?: number) {
if (!this._editor) return

const { content, contentType } = await this._getViewContent()
this.updateEditingState(false)
const sourceContext = this._requireSourceContext()
this._originalContent = sourceContext.originalContent
this._originalContentType = sourceContext.editorMetadata.contentType
const content = sourceContext.originalContent ?? ''
const contentType = this._originalContentType ?? 'text/turtle'
await this._editor.setLanguage(contentType)
if (syncGeneration !== undefined && syncGeneration !== this._syncGeneration) return
this._editor.setReadOnly(true)
this._editor.replaceContent(content)
this._editor.resetDirtyState()
this.updateDirtyState(false)
}

private queueEditorSynchronization () {
const syncGeneration = ++this._syncGeneration
this._syncQueue = this._syncQueue.then(() => this._syncEditorToCurrentView(syncGeneration))
return this._syncQueue
}

updateDirtyState(dirty: boolean) {
if (this._dirtyState === dirty) return
this._dirtyState = dirty
this.fileExplorerContext?.edit?.updateDirtyState?.(dirty)
this.fileExplorerContext.setDirty(dirty)
}

updateEditingState(editing: boolean) {
Expand All @@ -112,13 +136,14 @@ export default class SourceEditorCard extends WebComponent {

private async _initializeEditor () {
if (this._editor || this._initializing) return
this._initializing = true
const sourcePaneEditor = this._editorMount
const sourceContext = this.sourceContext
if (!sourcePaneEditor || !sourceContext || !this.fileExplorerContext?.store || !this.fileExplorerContext.subjectUri) {
this._initializing = false
return
}
const explorerContext = this.fileExplorerContext
if (!sourcePaneEditor || !sourceContext?.dataLoaded ||
!explorerContext || explorerContext === DEFAULT_FILE_EXPLORER_CONTEXT ||
!explorerContext.store || !explorerContext.subjectUri) return

this._initializing = true
try {
const { CodeEditor } = await import('solid-ui')
this._originalContent = sourceContext.originalContent
Expand All @@ -133,7 +158,9 @@ export default class SourceEditorCard extends WebComponent {
throw new Error(`Error initializing code editor: ${err instanceof Error ? err.message : String(err)}`)
}
this._editorReady = true
this._loadedResourceUri = sourceContext.resourceUri
this._editor?.setReadOnly(true)
if (this._editingRequested) this.beginEditing()
} catch (err) {
const { showError } = getStatusSection()
showError(`Error loading code editor: ${err instanceof Error ? err.message : String(err)}`)
Expand All @@ -146,6 +173,23 @@ export default class SourceEditorCard extends WebComponent {
await this._initializeEditor()
}

protected async updated (changedProperties: Map<string, unknown>) {
if (changedProperties.has('sourceContext') && !this.sourceContext?.dataLoaded) {
this._loadedResourceUri = undefined
return
}

if ((!changedProperties.has('sourceContext') && !changedProperties.has('fileExplorerContext')) ||
!this.sourceContext?.dataLoaded) return

if (!this._editor) {
await this._initializeEditor()
} else if (this._loadedResourceUri !== this.sourceContext.resourceUri) {
this._loadedResourceUri = this.sourceContext.resourceUri
await this.queueEditorSynchronization()
}
}

disconnectedCallback() {
super.disconnectedCallback()
if (this._editor) {
Expand All @@ -155,12 +199,13 @@ export default class SourceEditorCard extends WebComponent {
this._editorReady = false
this._dirtyState = false
this._initializing = false
this._loadedResourceUri = undefined
}

private async cancelHandler () {
const sourceContext = this.sourceContext
if (!sourceContext) return
await this._syncEditorToCurrentView()
await this.queueEditorSynchronization()
this._resetEditorState()
}

Expand Down Expand Up @@ -193,8 +238,11 @@ export default class SourceEditorCard extends WebComponent {
try {
const response = await fetcher.webOperation('HEAD', saveSubject.uri) // , defaultFetchHeaders())
if (!happy(response, 'HEAD')) return
const metadata = getResponseMetadata(store, saveSubject as any, response)
sourceContext.updateMetadata(metadata)
const metadata = await solidLogicSingleton.resource.fetchMetadata(saveSubject)
sourceContext.updateMetadata({
contentType: metadata.contentType,
eTag: metadata.eTag
})
this._resetEditorState()
} catch (err) {
throw err
Expand Down Expand Up @@ -259,7 +307,9 @@ export default class SourceEditorCard extends WebComponent {
}

render() {
const sectionClass = this._editorReady ? 'sourcePaneCard' : 'sourcePaneCard sourcePaneCardLoading'
const sectionClass = this._editorReady && this.sourceContext?.dataLoaded
? 'sourcePaneCard'
: 'sourcePaneCard sourcePaneCardLoading'

return html`
<section class=${sectionClass}>
Expand Down
18 changes: 0 additions & 18 deletions src/components/source-provider/SourceProvider.styles.css

This file was deleted.

Loading
Loading