);
}
export function BaseTextBlock(props: BlockProps & { className?: string }) {
let mountRef = useRef(null);
let actionTimeout = useRef(null);
let repRef = useRef>(null);
let headingLevel = useEntity(props.entityID, "block/heading-level");
let entity_set = useEntitySetContext();
let alignment =
useEntity(props.entityID, "block/text-alignment")?.data.value || "left";
let propsRef = useRef({ ...props, entity_set, alignment });
useEffect(() => {
propsRef.current = { ...props, entity_set, alignment };
}, [props, entity_set, alignment]);
let rep = useReplicache();
useEffect(() => {
repRef.current = rep.rep;
}, [rep?.rep]);
let selected = useUIState(
(s) => !!s.selectedBlocks.find((b) => b.value === props.entityID),
);
let focused = useUIState((s) => s.focusedEntity?.entityID === props.entityID);
let alignmentClass = {
left: "text-left",
right: "text-right",
center: "text-center",
justify: "text-justify",
}[alignment];
let value = useYJSValue(props.entityID);
let editorState = useEditorStates(
(s) => s.editorStates[props.entityID],
)?.editor;
let handlePaste = useHandlePaste(props.entityID, propsRef);
useLayoutEffect(() => {
if (!mountRef.current) return;
let km = TextBlockKeymap(propsRef, repRef, rep.undoManager);
let editor = EditorState.create({
schema: schema,
plugins: [
ySyncPlugin(value),
keymap(km),
inputrules(propsRef, repRef),
keymap(baseKeymap),
highlightSelectionPlugin,
autolink({
type: schema.marks.link,
shouldAutoLink: () => true,
defaultProtocol: "https",
}),
],
});
let unsubscribe = useEditorStates.subscribe((s) => {
let editorState = s.editorStates[props.entityID];
if (editorState?.initial) return;
if (editorState?.editor)
editorState.view?.updateState(editorState.editor);
});
let view = new EditorView(
{ mount: mountRef.current },
{
state: editor,
handlePaste,
handleClickOn: (view, _pos, node, _nodePos, _event, direct) => {
if (!direct) return;
if (node.nodeSize - 2 <= _pos) return;
let mark =
node
.nodeAt(_pos - 1)
?.marks.find((f) => f.type === schema.marks.link) ||
node
.nodeAt(Math.max(_pos - 2, 0))
?.marks.find((f) => f.type === schema.marks.link);
if (mark) {
window.open(mark.attrs.href, "_blank");
}
},
dispatchTransaction(tr) {
useEditorStates.setState((s) => {
let oldEditorState = this.state;
let newState = this.state.apply(tr);
let addToHistory = tr.getMeta("addToHistory");
let isBulkOp = tr.getMeta("bulkOp");
let docHasChanges = tr.steps.length !== 0 || tr.docChanged;
if (addToHistory !== false && docHasChanges) {
if (actionTimeout.current) {
window.clearTimeout(actionTimeout.current);
} else {
if (!isBulkOp) rep.undoManager.startGroup();
}
if (!isBulkOp)
actionTimeout.current = window.setTimeout(() => {
rep.undoManager.endGroup();
actionTimeout.current = null;
}, 200);
rep.undoManager.add({
redo: () => {
useEditorStates.setState((oldState) => {
let view = oldState.editorStates[props.entityID]?.view;
if (!view?.hasFocus() && !isBulkOp) view?.focus();
return {
editorStates: {
...oldState.editorStates,
[props.entityID]: {
...oldState.editorStates[props.entityID]!,
editor: newState,
},
},
};
});
},
undo: () => {
useEditorStates.setState((oldState) => {
let view = oldState.editorStates[props.entityID]?.view;
if (!view?.hasFocus() && !isBulkOp) view?.focus();
return {
editorStates: {
...oldState.editorStates,
[props.entityID]: {
...oldState.editorStates[props.entityID]!,
editor: oldEditorState,
},
},
};
});
},
});
}
return {
editorStates: {
...s.editorStates,
[props.entityID]: {
editor: newState,
view: this as unknown as EditorView,
initial: false,
keymap: km,
},
},
};
});
},
},
);
return () => {
unsubscribe();
view.destroy();
useEditorStates.setState((s) => ({
...s,
editorStates: {
...s.editorStates,
[props.entityID]: undefined,
},
}));
};
}, [props.entityID, props.parent, value, handlePaste, rep]);
return (
<>
{
if (
["***", "---", "___"].includes(
editorState?.doc.textContent.trim() || "",
)
) {
await rep.rep?.mutate.assertFact({
entity: props.entityID,
attribute: "block/type",
data: { type: "block-type-union", value: "horizontal-rule" },
});
}
if (actionTimeout.current) {
rep.undoManager.endGroup();
window.clearTimeout(actionTimeout.current);
actionTimeout.current = null;
}
}}
onFocus={() => {
setTimeout(() => {
useUIState.getState().setSelectedBlock(props);
useUIState.setState(() => ({
focusedEntity: {
entityType: "block",
entityID: props.entityID,
parent: props.parent,
},
}));
}, 5);
}}
id={elementId.block(props.entityID).text}
// unless we break *only* on urls, this is better than tailwind 'break-all'
// b/c break-all can cause breaks in the middle of words, but break-word still
// forces break if a single text string (e.g. a url) spans more than a full line
style={{ wordBreak: "break-word" }}
className={`
${alignmentClass}
grow resize-none align-top whitespace-pre-wrap bg-transparent
outline-hidden
${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""}
${props.className}`}
ref={mountRef}
/>
{editorState?.doc.textContent.length === 0 &&
props.previousBlock === null &&
props.nextBlock === null ? (
// if this is the only block on the page and is empty or is a canvas, show placeholder
) : editorState?.doc.textContent.length === 0 && focused ? (
// if not the only block on page but is the block is empty and selected, but NOT multiselected show add button
) : null}
{editorState?.doc.textContent.startsWith("/") && selected && (
)}
>
);
}
const BlockifyLink = (props: {
entityID: string;
editorState: EditorState | undefined;
}) => {
let [loading, setLoading] = useState(false);
let { editorState } = props;
let rep = useReplicache();
let smoker = useSmoker();
let isLocked = useEntity(props.entityID, "block/is-locked");
let focused = useUIState((s) => s.focusedEntity?.entityID === props.entityID);
let isBlueskyPost =
editorState?.doc.textContent.includes("bsky.app/") &&
editorState?.doc.textContent.includes("post");
// only if the line stats with http or https and doesn't have other content
// if its bluesky, change text to embed post
if (
!isLocked &&
focused &&
editorState &&
betterIsUrl(editorState.doc.textContent) &&
!editorState.doc.textContent.includes(" ")
) {
return (
);
} else return null;
};
const CommandOptions = (props: BlockProps & { className?: string }) => {
let rep = useReplicache();
let entity_set = useEntitySetContext();
let { data: pub } = useLeafletPublicationData();
return (