Skip to content

Instantly share code, notes, and snippets.

@ljmotta
Last active October 5, 2020 20:10
Show Gist options
  • Save ljmotta/89dc701fc7f8007649a763e50b20a2ec to your computer and use it in GitHub Desktop.
Save ljmotta/89dc701fc7f8007649a763e50b20a2ec to your computer and use it in GitHub Desktop.
Base64Png Editor examples
useEffect(() => {
const ctx = canvasRef.current?.getContext("2d")!;
ctx.filter = `invert(${invert}%)`;
ctx.drawImage(imageRef.current!, 0, 0);
setEditorContent(canvasRef.current!.toDataURL().split(",")[1]);
}, [invert]);
const [originalContent, setOriginalContent] = useState("");
const [editorContent, setEditorContent] = useState("");
export class Base64PngEditorFactory implements EditorFactory {
public supports(fileExtension: string) {
return fileExtension === "base64png";
}
public createEditor(envelopeContext: KogitoEditorEnvelopeContextType, initArgs: EditorInitArgs) {
return Promise.resolve(new Base64PngEditorInterface(envelopeContext, initArgs));
}
}
const INITIAL_INVERT = "0";
interface Props {
envelopeContext: KogitoEditorEnvelopeContextType;
}
export const Base64PngEditor = React.forwardRef<EditorApi, Props>((props, forwardedRef) => {
const [editorContent, setEditorContent] = useState("");
const [originalContent, setOriginalContent] = useState("");
const stateControl = useMemo(() => new Base64PngStateControl(), [originalContent]);
const getContent = useCallback(() => {
return editorContent;
}, [editorContent]);
const getPreview = useCallback(() => {
const width = imageRef.current!.width;
const height = imageRef.current!.height;
return `
<svg version="1.1" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="${width}" height="${height}" xlink:href="data:image/png;base64,${editorContent}" />
</svg>`;
}, [editorContent]);
const setContent = useCallback((path: string, content: string) => {
setOriginalContent(content);
stateControl.clearStateControl();
updateEditorToInitialState();
}, [stateControl]);
const undo = useCallback(() => {
stateControl.undo();
updateEditorStateWithCurrentEdit(stateControl.getCurrentBase64PngEdit());
}, [stateControl]);
const redo = useCallback(() => {
stateControl.redo();
updateEditorStateWithCurrentEdit(stateControl.getCurrentBase64PngEdit());
}, [stateControl]);
const updateEditorStateWithCurrentEdit = useCallback((edit?: Base64PngEdit) => {
if (edit) {
setInvert(edit.invert);
} else {
updateEditorToInitialState();
}
}, []);
const updateEditorToInitialState = useCallback(() => {
setInvert(INITIAL_INVERT);
}, []);
useEffect(() => {
props.envelopeContext.channelApi.notify("receive_ready");
}, []);
useImperativeHandle(forwardedRef, () => {
return {
getContent: () => Promise.resolve(getContent()),
setContent: (path: string, content: string) => Promise.resolve(setContent(path, content)),
getPreview: () => Promise.resolve(getPreview()),
undo: () => Promise.resolve(undo()),
redo: () => Promise.resolve(redo()),
getElementPosition: (selector: string) => Promise.resolve(DEFAULT_RECT),
};
});
const imageRef = useRef<HTMLImageElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [disabled, setDisabled] = useState(true);
const [invert, setInvert] = useState(INITIAL_INVERT);
const tweakInvert = useCallback((value: string) => {
setInvert(value);
const command: Base64PngEdit = {
id: new Date().getTime().toString(),
filter: `invert(${value})`,
invert: value,
};
stateControl.updateCommandStack(JSON.stringify(command));
props.envelopeContext.channelApi.notify("receive_newEdit", command);
}, [invert, stateControl]);
useEffect(() => {
const invertId = props.envelopeContext.services.keyboardShortcuts.registerKeyPress(
"i",
`Edit | Invert Image`,
async () => {
if (!disabled && invert === "100") {
tweakInvert("0");
} else if (!disabled && invert === "0") {
tweakInvert("100");
}
}
);
return () => {
props.envelopeContext.services.keyboardShortcuts.deregister(invertId);
};
}, [disabled, invert]);
useEffect(() => {
const ctx = canvasRef.current?.getContext("2d")!;
ctx.filter = stateControl.getCurrentBase64PngEdit()?.filter ?? `invert(${invert}%)`;
ctx.drawImage(imageRef.current!, 0, 0);
setEditorContent(canvasRef.current!.toDataURL().split(",")[1]);
}, [invert, stateControl]);
useEffect(() => {
const ctx = canvasRef.current?.getContext("2d")!;
canvasRef.current!.width = 0;
canvasRef.current!.height = 0;
imageRef.current!.onload = () => {
canvasRef.current!.width = imageRef.current!.width;
canvasRef.current!.height = imageRef.current!.height;
ctx.drawImage(imageRef.current!, 0, 0);
setEditorContent(canvasRef.current!.toDataURL().split(",")[1]);
setDisabled(false);
};
return () => {
imageRef.current!.onload = null;
};
}, []);
return (
<div className={"base64png-editor--main"}>
<div className={"base64png-editor--viewport"}>
<img
ref={imageRef}
className={"base64png-editor--image"}
src={`data:image/png;base64,${originalContent}`}
alt={"Original"}
/>
{disabled && (
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
<Title headingLevel="h5" size="4xl">
Empty image
</Title>
</EmptyState>
)}
<canvas ref={canvasRef} className={"base64png-editor--canvas"} />
</div>
<div className={"base64png-editor--tweaks"}>
<Nav aria-label="Image tweaker">
<NavList>
<NavItem itemId={0}>
<div className={"base64png-editor--tweaks-nav-item"}>
<p>Invert</p>
<Switch
id="invert-switch"
isDisabled={disabled}
isChecked={invert === "100"}
onChange={(value) => tweakInvert(value ? "100" : "0")}
/>
</div>
</NavItem>
{stateControl.isDirty() && (
<div style={{ display: "flex", alignItems: "center", padding: "20px" }}>
<p style={{ color: "red" }}>Image was Edited!</p>
</div>
)}
</NavList>
</Nav>
</div>
</div>
);
});
interface Props {
envelopeContext: KogitoEditorEnvelopeContextType;
}
/**
* The forwardRef parameter is a RefForwardingComponent, which has its props and the forwardedRef which is
* utlized by the useImperativeHandle hook to expose the methods defined on the EditorApi type.
*/
export const Base64PngEditor = React.forwardRef<EditorApi, Props>((props, forwardedRef) => {
// All the editor code is here.
}
.base64png-editor--div-viewport {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
}
.base64png-editor--image {
display: none;
}
.base64png-editor--canvas {
max-width: 600px;
max-height: 600px;
}
return (
<div className={"base64png-editor--viewport"}>
<img
ref={imageRef}
className={"base64png-editor--image"}
src={`data:image/png;base64,${originalContent}`}
alt={"Original"}
/>
<canvas
ref={canvasRef}
className={"base64png-editor--canvas"}
/>
</div>
)
useEffect(() => {
const ctx = canvasRef.current?.getContext("2d")!;
/**
* Set the initial canvas size
*/
canvasRef.current!.width = 0;
canvasRef.current!.height = 0;
imageRef.current!.onload = () => {
/**
* Update the canvas size
*/
canvasRef.current!.width = imageRef.current!.width;
canvasRef.current!.height = imageRef.current!.height;
/**
* Draw the image on the canvas
*/
ctx.drawImage(imageRef.current!, 0, 0);
/**
* The toDataUrl() returns the base64 information in the format: "<base64 header>,<base64 data>"
* We just need the base64 data.
*/
setEditorContent(canvasRef.current!.toDataURL().split(",")[1]);
};
return () => {
imageRef.current!.onload = null;
};
}, []);
useEffect(() => {
const ctx = canvasRef.current?.getContext("2d")!;
canvasRef.current!.width = 0;
canvasRef.current!.height = 0;
imageRef.current!.onload = () => {
canvasRef.current!.width = imageRef.current!.width;
canvasRef.current!.height = imageRef.current!.height;
ctx.drawImage(imageRef.current!, 0, 0);
setEditorContent(canvasRef.current!.toDataURL().split(",")[1]);
/**
* When the image finishes to load, the tweakers can properly work
*/
setDisabled(false);
};
return () => {
imageRef.current!.onload = null;
};
}, []);
const imageRef = useRef<HTMLImageElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
interface Props {
envelopeContext: KogitoEditorEnvelopeContextType;
}
export const Base64PngEditor = React.forwardRef<EditorApi, Props>((props, forwardedRef) => {
const [originalContent, setOriginalContent] = useState("");
const [editorContent, setEditorContent] = useState("");
const imageRef = useRef<HTMLImageElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
props.envelopeContext.channelApi.notify("receive_ready");
}, []);
const getContent = useCallback(() => {
return editorContent;
}, [editorContent]);
const getPreview = useCallback(() => {
const width = imageRef.current!.width;
const height = imageRef.current!.height;
return `
<svg version="1.1" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="${width}" height="${height}" xlink:href="data:image/png;base64,${editorContent}" />
</svg>`;
}, [editorContent]);
const setContent = useCallback((path: string, content: string) => {
setOriginalContent(content);
}, []);
useImperativeHandle(forwardedRef, () => {
return {
getContent: () => Promise.resolve(getContent()),
getPreview: () => Promise.resolve(getPreview()),
setContent: (path: string, content: string) => Promise.resolve(setContent(path, content)),
undo: () => Promise.resolve(),
redo: () => Promise.resolve(),
getElementPosition: (selector: string) => Promise.resolve(DEFAULT_RECT),
};
});
useEffect(() => {
const ctx = canvasRef.current?.getContext("2d")!;
canvasRef.current!.width = 0;
canvasRef.current!.height = 0;
imageRef.current!.onload = () => {
canvasRef.current!.width = imageRef.current!.width;
canvasRef.current!.height = imageRef.current!.height;
ctx.drawImage(imageRef.current!, 0, 0);
setEditorContent(canvasRef.current!.toDataURL().split(",")[1]);
};
return () => {
imageRef.current!.onload = null;
};
}, []);
return (
<div className={"base64png-editor--viewport"}>
<img
ref={imageRef}
className={"base64png-editor--image"}
src={`data:image/png;base64,${originalContent}`}
alt={"Original"}
/>
<canvas ref={canvasRef} className={"base64png-editor--canvas"} />
</div>
);
});
export class Base64PngEditorInterface implements Editor {
/**
* The Editor ref so its possible to access its methods imperactivily
*/
private editorRef: React.RefObject<EditorApi>;
/**
* Additional information used by the Channel.
*/
public af_isReact = true;
public af_componentId: "base64png-editor";
public af_componentTitle: "Base64 PNG Editor";
constructor(
private readonly envelopeContext: KogitoEditorEnvelopeContextType,
private readonly initArgs: EditorInitArgs
) {
this.editorRef = React.createRef<EditorApi>();
}
/**
* Retrieve the Editor content.
*/
public getContent(): Promise<string> {
return this.editorRef.current?.getContent()!;
}
/**
* Retrieve the Guided Tour current position. The Editor implements a getElementPosition method, but it always return a
* the DEFAULT_RECT because the Editor dosn't support a Guided Tour.
*/
public getElementPosition(selector: string): Promise<Rect | undefined> {
return this.editorRef.current?.getElementPosition(selector)!;
}
/**
* Set the Editor content.
* @param path The file path that is being open.
* @param content The file content in a string format.
*/
public setContent(path: string, content: string): Promise<void> {
return this.editorRef.current?.setContent(path, content)!;
}
/**
* Get the content preview which is a SVG.
*/
public getPreview(): Promise<string | undefined> {
return this.editorRef.current?.getPreview()!;
}
/**
* Calls the Editor undo method.
*/
public undo(): Promise<void> {
return this.editorRef.current?.undo()!;
}
/**
* Calls the Editor redo method.
*/
public redo(): Promise<void> {
return this.editorRef.current?.redo()!;
}
/**
* Retrieve the root component of the Editor. Here the Editor is going to be initialized with its props.
*/
public af_componentRoot() {
return <Base64PngEditor ref={this.editorRef} envelopeContext={this.envelopeContext} />;
}
}
interface Props {
envelopeContext: KogitoEditorEnvelopeContextType;
}
export const Base64PngEditor = React.forwardRef<EditorApi, Props>((props, forwardedRef) => {
const [originalContent, setOriginalContent] = useState("");
const [editorContent, setEditorContent] = useState("");
const imageRef = useRef<HTMLImageElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
props.envelopeContext.channelApi.notify("receive_ready");
}, []);
const getContent = useCallback(() => {
return editorContent;
}, [editorContent]);
const getPreview = useCallback(() => {
const width = imageRef.current!.width;
const height = imageRef.current!.height;
return `
<svg version="1.1" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="${width}" height="${height}" xlink:href="data:image/png;base64,${editorContent}" />
</svg>`;
}, [editorContent]);
const setContent = useCallback((path: string, content: string) => {
setOriginalContent(content);
}, []);
useImperativeHandle(forwardedRef, () => {
return {
getContent: () => Promise.resolve(getContent()),
getPreview: () => Promise.resolve(getPreview()),
setContent: (path: string, content: string) => Promise.resolve(setContent(path, content)),
undo: () => Promise.resolve(),
redo: () => Promise.resolve(),
getElementPosition: (selector: string) => Promise.resolve(DEFAULT_RECT),
};
});
const [disabled, setDisabled] = useState(true);
const [invert, setInvert] = useState("0");
const tweakInvert = useCallback((value: string) => {
setInvert(value);
props.envelopeContext.channelApi.notify("receive_newEdit", { id: new Date().getTime().toString() });
}, [invert]);
useEffect(() => {
const ctx = canvasRef.current?.getContext("2d")!;
ctx.filter = `invert(${invert}%)`;
ctx.drawImage(imageRef.current!, 0, 0);
setEditorContent(canvasRef.current!.toDataURL().split(",")[1]);
}, [invert]);
useEffect(() => {
const ctx = canvasRef.current?.getContext("2d")!;
canvasRef.current!.width = 0;
canvasRef.current!.height = 0;
imageRef.current!.onload = () => {
canvasRef.current!.width = imageRef.current!.width;
canvasRef.current!.height = imageRef.current!.height;
ctx.drawImage(imageRef.current!, 0, 0);
setEditorContent(canvasRef.current!.toDataURL().split(",")[1]);
setDisabled(false);
};
return () => {
imageRef.current!.onload = null;
};
}, []);
return (
<Page className={"base64png-editor--page"}>
<div className={"base64png-editor--main"}>
<div className={"base64png-editor--viewport"}>
<img
ref={imageRef}
className={"base64png-editor--image"}
src={`data:image/png;base64,${originalContent}`}
alt={"Original"}
/>
{disabled && (
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
<Title headingLevel="h5" size="4xl">
Empty image
</Title>
</EmptyState>
)}
<canvas ref={canvasRef} className={"base64png-editor--canvas"} />
</div>
<div className={"base64png-editor--tweaks"}>
<Nav aria-label="Image tweaker">
<NavList>
<NavItem itemId={0}>
<div className={"base64png-editor--tweaks-nav-item"}>
<p>Invert</p>
<Switch
id="invert-switch"
isDisabled={disabled}
isChecked={invert === "100"}
onChange={(value) => tweakInvert(value ? "100" : "0")}
/>
</div>
</NavItem>
</NavList>
</Nav>
</div>
</div>
</Page>
);
});
return (
<div className={"base64png-editor--main"}>
<div className={"base64png-editor--viewport"}>
<img
ref={imageRef}
className={"base64png-editor--image"}
src={`data:image/png;base64,${originalContent}`}
alt={"Original"}
/>
{disabled && (
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
<Title headingLevel="h5" size="4xl">
Empty image
</Title>
</EmptyState>
)}
<canvas ref={canvasRef} className={"base64png-editor--canvas"} />
</div>
<div className={"base64png-editor--tweaks"}>
<Nav aria-label="Image tweaker">
<NavList>
<NavItem itemId={0}>
<div className={"base64png-editor--tweaks-nav-item"}>
<p>Invert</p>
<Switch
id="invert-switch"
isDisabled={disabled}
isChecked={invert === "100"}
onChange={(value) => tweakInvert(value ? "100" : "0")}
/>
</div>
</NavItem>
</NavList>
</Nav>
</div>
</div>
);
const [invert, setInvert] = useState("0");
const tweakInvert = useCallback((value: string) => {
setInvert(value);
props.envelopeContext.channelApi.notify("receive_newEdit", { id: new Date().getTime().toString() });
}, [invert]);
const [disabled, setDisabled] = useState(true);
useEffect(() => {
const invertId = props.envelopeContext.services.keyboardShortcuts.registerKeyPress(
"i",
`Edit | Invert Image`,
async () => {
if (!disabled && invert === "100") {
tweakInvert("0");
} else if (!disabled && invert === "0") {
tweakInvert("100");
}
}
);
return () => {
props.envelopeContext.services.keyboardShortcuts.deregister(invertId);
};
}, [disabled, invert]);
useEffect(() => {
props.envelopeContext.channelApi.notifications.receive_ready();
}, []);
const getContent = useCallback(() => {
return editorContent;
}, [editorContent]);
const getPreview = useCallback(() => {
const width = imageRef.current!.width;
const height = imageRef.current!.height;
return `
<svg version="1.1" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="${width}" height="${height}" xlink:href="data:image/png;base64,${editorContent}" />
</svg>`;
}, [editorContent]);
const setContent = useCallback((path: string, content: string) => {
setOriginalContent(content);
}, []);
import { StateControl } from "@kogito-tooling/editor/dist/embedded";
import { KogitoEdit } from "@kogito-tooling/channel-common-api";
export interface Base64PngEdit extends KogitoEdit {
id: string;
filter: string;
invert: string;
}
export class Base64PngStateControl extends StateControl {
/**
* The command stack on the Kogito State Control accepts only strings, this method parses the string into a Base64PngEdit object.
*/
getCurrentBase64PngEdit(): Base64PngEdit | undefined {
const command = super.getCurrentCommand();
if (command) {
return JSON.parse(command) as Base64PngEdit;
}
return;
}
clearStateControl() {
super.setCurrentCommand(undefined);
super.setCommandStack([]);
}
}
/**
* Added on the end of the NavList an isDirty indicator.
*/
<NavItem itemId={0}>
<div className={"base64png-editor--tweaks-nav-item"}>
<p>Invert</p>
<Switch
id="invert-switch"
isDisabled={disabled}
isChecked={invert === "100"}
onChange={(value) => tweakInvert(value ? "100" : "0")}
/>
</div>
</NavItem>
{stateControl.isDirty() && (
<div style={{ display: "flex", alignItems: "center", padding: "20px" }}>
<p style={{ color: "red" }}>Image was Edited!</p>
</div>
)}
</NavList>
/**
* Clear the state control stack, and rollback the Editor tweak states to the initial values.
*/
const setContent = useCallback((path: string, content: string) => {
setOriginalContent(content);
stateControl.clearStateControl();
updateEditorToInitialState();
}, [stateControl]);
const stateControl = useMemo(() => new Base64PngStateControl(), [originalContent]);
const undo = useCallback(() => {
stateControl.undo();
updateEditorStateWithCurrentEdit(stateControl.getCurrentBase64PngEdit());
}, [stateControl]);
const redo = useCallback(() => {
stateControl.redo();
updateEditorStateWithCurrentEdit(stateControl.getCurrentBase64PngEdit());
}, [stateControl]);
const updateEditorStateWithCurrentEdit = useCallback((edit?: Base64PngEdit) => {
if (edit) {
setInvert(edit.invert);
} else {
updateEditorToInitialState();
}
}, []);
const updateEditorToInitialState = useCallback(() => {
setInvert(INITIAL_INVERT);
}, []);
/**
* Create a Base64PngEdit and update the state control stack with it. We've updated the receive_newEdit notification with the Base64Edit in case the needs any additional information.
*/
const tweakInvert = useCallback((value: string) => {
setInvert(value);
const command: Base64PngEdit = {
id: new Date().getTime().toString(),
filter: `invert(${value})`,
invert: value,
};
stateControl.updateCommandStack(JSON.stringify(command));
props.envelopeContext.channelApi.notify("receive_newEdit", command);
}, [invert, stateControl]);
/**
* Preferable use the state control current edit values.
*/
useEffect(() => {
const ctx = canvasRef.current?.getContext("2d")!;
ctx.filter = stateControl.getCurrentBase64PngEdit()?.filter ?? `invert(${invert}%)`;
ctx.drawImage(imageRef.current!, 0, 0);
setEditorContent(canvasRef.current!.toDataURL().split(",")[1]);
}, [invert, stateControl]);
useImperativeHandle(forwardedRef, () => {
return {
/**
* We are going to pass through on each of the method here.
*/
getContent: () => Promise.resolve(getContent()),
getPreview: () => Promise.resolve(getPreview()),
setContent: (path: string, content: string) => Promise.resolve(setContent(path, content)),
/**
* We're going to implement this methods further in the tutorial
*/
undo: () => Promise.resolve(),
redo: () => Promise.resolve(),
/**
* This method is not going to be implemented on this example, so it always resolve a DEFAULT_RECT.
*/
getElementPosition: (selector: string) => Promise.resolve(DEFAULT_RECT),
};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment