-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
…2404) * handle chunks with vite * add editor context and useHeaderEditor hook * use --emitDeclarationOnly when running tsc * fix error message * rename file * move back state reducer functions * add setHeaders * specify files in package.json * add changeset * create reusable hooks for editor functionality * fix docs build * fix for loop in useKeyMap * rename useSyncValue * loosen check for detecting macOS * avoid type casting and error on closing tab * add types package as dev dependency for escape-html
- Loading branch information
1 parent
7f695b1
commit 029ddf8
Showing
13 changed files
with
561 additions
and
277 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'graphiql': minor | ||
'@graphiql/react': minor | ||
--- | ||
|
||
Add a context provider for editors and move the logic of the headers editor from the `graphiql` package into a hook `useHeaderEditor` provided by `@graphiql/react` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
let isMacOs = false; | ||
|
||
if (typeof window === 'object') { | ||
isMacOs = window.navigator.platform.toLowerCase().indexOf('mac') === 0; | ||
} | ||
|
||
export const commonKeys = { | ||
// Persistent search box in Query Editor | ||
[isMacOs ? 'Cmd-F' : 'Ctrl-F']: 'findPersistent', | ||
'Cmd-G': 'findPersistent', | ||
'Ctrl-G': 'findPersistent', | ||
|
||
// Editor improvements | ||
'Ctrl-Left': 'goSubwordLeft', | ||
'Ctrl-Right': 'goSubwordRight', | ||
'Alt-Left': 'goGroupLeft', | ||
'Alt-Right': 'goGroupRight', | ||
}; | ||
|
||
export const commonCodeMirrorAddons = [ | ||
import('codemirror/addon/hint/show-hint'), | ||
import('codemirror/addon/edit/matchbrackets'), | ||
import('codemirror/addon/edit/closebrackets'), | ||
import('codemirror/addon/fold/brace-fold'), | ||
import('codemirror/addon/fold/foldgutter'), | ||
import('codemirror/addon/lint/lint'), | ||
import('codemirror/addon/search/searchcursor'), | ||
import('codemirror/addon/search/jump-to-line'), | ||
import('codemirror/addon/dialog/dialog'), | ||
// @ts-expect-error | ||
import('codemirror/keymap/sublime'), | ||
]; | ||
|
||
/** | ||
* Dynamically import codemirror and dependencies | ||
* This works for codemirror 5, not sure if the same imports work for 6 | ||
*/ | ||
export async function importCodeMirror( | ||
addons: Promise<any>[], | ||
options?: { useCommonAddons?: boolean }, | ||
) { | ||
const CodeMirror = await import('codemirror').then(c => | ||
// Depending on bundler and settings the dynamic import either returns a | ||
// function (e.g. parcel) or an object containing a `default` property | ||
typeof c === 'function' ? c : c.default, | ||
); | ||
const allAddons = | ||
options?.useCommonAddons === false | ||
? addons | ||
: commonCodeMirrorAddons.concat(addons); | ||
await Promise.all(allAddons.map(addon => addon)); | ||
return CodeMirror; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import type { Editor, EditorChange } from 'codemirror'; | ||
|
||
import { | ||
GraphQLNonNull, | ||
GraphQLList, | ||
GraphQLType, | ||
GraphQLField, | ||
} from 'graphql'; | ||
import escapeHTML from 'escape-html'; | ||
import MD from 'markdown-it'; | ||
import { importCodeMirror } from './common'; | ||
|
||
const md = new MD(); | ||
|
||
/** | ||
* Render a custom UI for CodeMirror's hint which includes additional info | ||
* about the type and description for the selected context. | ||
*/ | ||
export default function onHasCompletion( | ||
_cm: Editor, | ||
data: EditorChange | undefined, | ||
onHintInformationRender: (el: HTMLDivElement) => void, | ||
) { | ||
importCodeMirror([], { useCommonAddons: false }).then(CodeMirror => { | ||
let information: HTMLDivElement | null; | ||
let deprecation: HTMLDivElement | null; | ||
CodeMirror.on( | ||
data, | ||
'select', | ||
// @ts-expect-error | ||
(ctx: GraphQLField<{}, {}, {}>, el: HTMLDivElement) => { | ||
// Only the first time (usually when the hint UI is first displayed) | ||
// do we create the information nodes. | ||
if (!information) { | ||
const hintsUl = el.parentNode as Node & ParentNode; | ||
|
||
// This "information" node will contain the additional info about the | ||
// highlighted typeahead option. | ||
information = document.createElement('div'); | ||
information.className = 'CodeMirror-hint-information'; | ||
hintsUl.appendChild(information); | ||
|
||
// This "deprecation" node will contain info about deprecated usage. | ||
deprecation = document.createElement('div'); | ||
deprecation.className = 'CodeMirror-hint-deprecation'; | ||
hintsUl.appendChild(deprecation); | ||
|
||
// When CodeMirror attempts to remove the hint UI, we detect that it was | ||
// removed and in turn remove the information nodes. | ||
let onRemoveFn: EventListener | null; | ||
hintsUl.addEventListener( | ||
'DOMNodeRemoved', | ||
(onRemoveFn = (event: Event) => { | ||
if (event.target === hintsUl) { | ||
hintsUl.removeEventListener('DOMNodeRemoved', onRemoveFn); | ||
information = null; | ||
deprecation = null; | ||
onRemoveFn = null; | ||
} | ||
}), | ||
); | ||
} | ||
|
||
// Now that the UI has been set up, add info to information. | ||
const description = ctx.description | ||
? md.render(ctx.description) | ||
: 'Self descriptive.'; | ||
const type = ctx.type | ||
? '<span class="infoType">' + renderType(ctx.type) + '</span>' | ||
: ''; | ||
|
||
information.innerHTML = | ||
'<div class="content">' + | ||
(description.slice(0, 3) === '<p>' | ||
? '<p>' + type + description.slice(3) | ||
: type + description) + | ||
'</div>'; | ||
|
||
if (ctx && deprecation && ctx.deprecationReason) { | ||
const reason = ctx.deprecationReason | ||
? md.render(ctx.deprecationReason) | ||
: ''; | ||
deprecation.innerHTML = | ||
'<span class="deprecation-label">Deprecated</span>' + reason; | ||
deprecation.style.display = 'block'; | ||
} else if (deprecation) { | ||
deprecation.style.display = 'none'; | ||
} | ||
|
||
// Additional rendering? | ||
if (onHintInformationRender) { | ||
onHintInformationRender(information); | ||
} | ||
}, | ||
); | ||
}); | ||
} | ||
|
||
function renderType(type: GraphQLType): string { | ||
if (type instanceof GraphQLNonNull) { | ||
return `${renderType(type.ofType)}!`; | ||
} | ||
if (type instanceof GraphQLList) { | ||
return `[${renderType(type.ofType)}]`; | ||
} | ||
return `<a class="typeName">${escapeHTML(type.name)}</a>`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import type { Editor } from 'codemirror'; | ||
import { createContext, ReactNode, useState } from 'react'; | ||
|
||
export type EditorContextType = { | ||
headerEditor: Editor | null; | ||
setHeaderEditor(newEditor: Editor): void; | ||
}; | ||
|
||
export const EditorContext = createContext<EditorContextType>({ | ||
headerEditor: null, | ||
setHeaderEditor() {}, | ||
}); | ||
|
||
export function EditorContextProvider(props: { | ||
children: ReactNode; | ||
initialValue?: string; | ||
}) { | ||
const [editor, setEditor] = useState<Editor | null>(null); | ||
return ( | ||
<EditorContext.Provider | ||
value={{ headerEditor: editor, setHeaderEditor: setEditor }}> | ||
{props.children} | ||
</EditorContext.Provider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { useContext, useEffect, useRef } from 'react'; | ||
|
||
import { commonKeys, importCodeMirror } from './common'; | ||
import { EditorContext } from './context'; | ||
import { | ||
CompletionCallback, | ||
EditCallback, | ||
EmptyCallback, | ||
useChangeHandler, | ||
useCompletion, | ||
useKeyMap, | ||
useResizeEditor, | ||
useSynchronizeValue, | ||
} from './hooks'; | ||
|
||
export type UseHeaderEditorArgs = { | ||
editorTheme?: string; | ||
onEdit?: EditCallback; | ||
onHintInformationRender?: CompletionCallback; | ||
onPrettifyQuery?: EmptyCallback; | ||
onMergeQuery?: EmptyCallback; | ||
onRunQuery?: EmptyCallback; | ||
readOnly?: boolean; | ||
value?: string; | ||
}; | ||
|
||
export function useHeaderEditor({ | ||
editorTheme = 'graphiql', | ||
onEdit, | ||
onHintInformationRender, | ||
onMergeQuery, | ||
onPrettifyQuery, | ||
onRunQuery, | ||
readOnly = false, | ||
value, | ||
}: UseHeaderEditorArgs = {}) { | ||
const context = useContext(EditorContext); | ||
const ref = useRef<HTMLDivElement>(null); | ||
|
||
if (!context) { | ||
throw new Error( | ||
'Tried to call the `useHeaderEditor` hook without the necessary context. Make sure that the `EditorContextProvider` from `@graphiql/react` is rendered higher in the tree.', | ||
); | ||
} | ||
|
||
const { headerEditor, setHeaderEditor } = context; | ||
|
||
useEffect(() => { | ||
importCodeMirror([ | ||
// @ts-expect-error | ||
import('codemirror/mode/javascript/javascript'), | ||
]).then(CodeMirror => { | ||
const container = ref.current; | ||
if (!container) { | ||
return; | ||
} | ||
|
||
const newEditor = CodeMirror(container, { | ||
lineNumbers: true, | ||
tabSize: 2, | ||
mode: { name: 'javascript', json: true }, | ||
theme: editorTheme, | ||
keyMap: 'sublime', | ||
autoCloseBrackets: true, | ||
matchBrackets: true, | ||
showCursorWhenSelecting: true, | ||
readOnly: readOnly ? 'nocursor' : false, | ||
foldGutter: true, | ||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], | ||
extraKeys: commonKeys, | ||
}); | ||
|
||
newEditor.addKeyMap({ | ||
'Cmd-Space'() { | ||
newEditor.showHint({ completeSingle: false, container }); | ||
}, | ||
'Ctrl-Space'() { | ||
newEditor.showHint({ completeSingle: false, container }); | ||
}, | ||
'Alt-Space'() { | ||
newEditor.showHint({ completeSingle: false, container }); | ||
}, | ||
'Shift-Space'() { | ||
newEditor.showHint({ completeSingle: false, container }); | ||
}, | ||
}); | ||
|
||
newEditor.on('keyup', (editorInstance, event) => { | ||
const code = event.keyCode; | ||
if ( | ||
(code >= 65 && code <= 90) || // letters | ||
(!event.shiftKey && code >= 48 && code <= 57) || // numbers | ||
(event.shiftKey && code === 189) || // underscore | ||
(event.shiftKey && code === 222) // " | ||
) { | ||
editorInstance.execCommand('autocomplete'); | ||
} | ||
}); | ||
|
||
setHeaderEditor(newEditor); | ||
}); | ||
}, [editorTheme, readOnly, setHeaderEditor]); | ||
|
||
useSynchronizeValue(headerEditor, value); | ||
|
||
useChangeHandler(headerEditor, onEdit); | ||
|
||
useCompletion(headerEditor, onHintInformationRender); | ||
|
||
useKeyMap(headerEditor, ['Cmd-Enter', 'Ctrl-Enter'], onRunQuery); | ||
useKeyMap(headerEditor, ['Shift-Ctrl-P'], onPrettifyQuery); | ||
useKeyMap(headerEditor, ['Shift-Ctrl-M'], onMergeQuery); | ||
|
||
useResizeEditor(headerEditor, ref); | ||
|
||
return ref; | ||
} |
Oops, something went wrong.