a tool for shared writing and social publishing
at update/delete-blocks 157 lines 5.5 kB view raw
1import { 2 BundledLanguage, 3 bundledLanguagesInfo, 4 bundledThemesInfo, 5 codeToHtml, 6} from "shiki"; 7import { useEntity, useReplicache } from "src/replicache"; 8import "katex/dist/katex.min.css"; 9import { BlockLayout, BlockProps } from "./Block"; 10import { useCallback, useLayoutEffect, useMemo, useState } from "react"; 11import { useUIState } from "src/useUIState"; 12import { BaseTextareaBlock } from "./BaseTextareaBlock"; 13import { useEntitySetContext } from "components/EntitySetProvider"; 14import { flushSync } from "react-dom"; 15import { elementId } from "src/utils/elementId"; 16import { LAST_USED_CODE_LANGUAGE_KEY } from "src/utils/codeLanguageStorage"; 17import { focusBlock } from "src/utils/focusBlock"; 18 19export function CodeBlock(props: BlockProps) { 20 let { rep, rootEntity } = useReplicache(); 21 let content = useEntity(props.entityID, "block/code"); 22 let lang = 23 useEntity(props.entityID, "block/code-language")?.data.value || "plaintext"; 24 25 let theme = 26 useEntity(rootEntity, "theme/code-theme")?.data.value || "github-light"; 27 let focusedBlock = useUIState( 28 (s) => s.focusedEntity?.entityID === props.entityID, 29 ); 30 let entity_set = useEntitySetContext(); 31 let { permissions } = entity_set; 32 const [html, setHTML] = useState<string | null>(null); 33 34 useLayoutEffect(() => { 35 if (!content) return; 36 void codeToHtml(content.data.value, { 37 lang, 38 theme, 39 structure: "classic", 40 }).then((h) => { 41 setHTML(h.replaceAll("<br>", "\n")); 42 }); 43 }, [content, lang, theme]); 44 45 const onClick = useCallback((e: React.MouseEvent<HTMLElement>) => { 46 focusBlock( 47 { parent: props.parent, value: props.value, type: "code" }, 48 { type: "end" }, 49 ); 50 }, []); 51 return ( 52 <div className="codeBlock w-full flex flex-col rounded-md gap-0.5 "> 53 <BlockLayout 54 isSelected={focusedBlock} 55 hasBackground="accent" 56 borderOnHover 57 className="p-0! min-h-10 sm:min-h-12" 58 > 59 {focusedBlock && permissions.write ? ( 60 <BaseTextareaBlock 61 placeholder="write some code…" 62 data-editable-block 63 data-entityid={props.entityID} 64 id={elementId.block(props.entityID).input} 65 block={props} 66 rep={rep} 67 permissionSet={entity_set.set} 68 spellCheck={false} 69 autoCapitalize="none" 70 autoCorrect="off" 71 className="codeBlockEditor whitespace-nowrap! overflow-auto! font-mono p-2 sm:p-3" 72 value={content?.data.value} 73 onChange={async (e) => { 74 // Update the entity with the new value 75 await rep?.mutate.assertFact({ 76 attribute: "block/code", 77 entity: props.entityID, 78 data: { type: "string", value: e.target.value }, 79 }); 80 }} 81 /> 82 ) : !html ? ( 83 <pre 84 onClick={onClick} 85 onMouseDown={(e) => e.stopPropagation()} 86 className="codeBlockRendered overflow-auto! font-mono p-2 sm:p-3 w-full h-full" 87 > 88 {content?.data.value === "" || content?.data.value === undefined ? ( 89 <div className="text-tertiary italic">write some code</div> 90 ) : ( 91 content?.data.value 92 )} 93 </pre> 94 ) : ( 95 <div 96 onMouseDown={(e) => e.stopPropagation()} 97 onClick={onClick} 98 data-lang={lang} 99 className="contents" 100 dangerouslySetInnerHTML={{ __html: html || "" }} 101 /> 102 )} 103 </BlockLayout> 104 {permissions.write && ( 105 <div className="text-sm text-tertiary flex w-full justify-between"> 106 <div className="codeBlockTheme grow flex gap-1"> 107 Theme:{" "} 108 <select 109 className="codeBlockThemeSelect text-left bg-transparent pr-1 sm:max-w-none max-w-24 w-full" 110 onClick={(e) => { 111 e.preventDefault(); 112 e.stopPropagation(); 113 }} 114 value={theme} 115 onChange={async (e) => { 116 await rep?.mutate.assertFact({ 117 attribute: "theme/code-theme", 118 entity: rootEntity, 119 data: { type: "string", value: e.target.value }, 120 }); 121 }} 122 > 123 {bundledThemesInfo.map((t) => ( 124 <option key={t.id} value={t.id}> 125 {t.displayName} 126 </option> 127 ))} 128 </select> 129 </div> 130 <select 131 className="codeBlockLang grow text-right bg-transparent pr-1 sm:max-w-none max-w-24 w-full" 132 onClick={(e) => { 133 e.preventDefault(); 134 e.stopPropagation(); 135 }} 136 value={lang} 137 onChange={async (e) => { 138 localStorage.setItem(LAST_USED_CODE_LANGUAGE_KEY, e.target.value); 139 await rep?.mutate.assertFact({ 140 attribute: "block/code-language", 141 entity: props.entityID, 142 data: { type: "string", value: e.target.value }, 143 }); 144 }} 145 > 146 <option value="plaintext">Plaintext</option> 147 {bundledLanguagesInfo.map((l) => ( 148 <option key={l.id} value={l.id}> 149 {l.name} 150 </option> 151 ))} 152 </select> 153 </div> 154 )} 155 </div> 156 ); 157}