├── packages └── editor │ ├── typings.d.ts │ ├── src │ ├── components │ │ ├── index.ts │ │ ├── text-editor │ │ │ ├── interfaces.ts │ │ │ ├── core │ │ │ │ ├── helper │ │ │ │ │ ├── createDocument.ts │ │ │ │ │ ├── elementFromString.ts │ │ │ │ │ ├── isEmptyContent.ts │ │ │ │ │ ├── createNodeFromContent.ts │ │ │ │ │ ├── getSchemaType.ts │ │ │ │ │ ├── object.ts │ │ │ │ │ ├── getMarkType.ts │ │ │ │ │ ├── getNodeType.ts │ │ │ │ │ ├── setContent.ts │ │ │ │ │ ├── isActive.ts │ │ │ │ │ └── getMarkAttributes.ts │ │ │ │ ├── command │ │ │ │ │ ├── selectAll.ts │ │ │ │ │ ├── underline.ts │ │ │ │ │ ├── selectNode.ts │ │ │ │ │ ├── selectText.ts │ │ │ │ │ ├── italic.ts │ │ │ │ │ ├── bold.ts │ │ │ │ │ ├── toggleMark.ts │ │ │ │ │ ├── textColor.ts │ │ │ │ │ ├── unsetMark.ts │ │ │ │ │ ├── setFontFamily.ts │ │ │ │ │ └── setLineHeight.ts │ │ │ │ ├── schema │ │ │ │ │ └── spec │ │ │ │ │ │ ├── italicSpec.ts │ │ │ │ │ │ ├── listItemSpec.ts │ │ │ │ │ │ ├── bulletListSpec.ts │ │ │ │ │ │ ├── underlineSpec.ts │ │ │ │ │ │ ├── boldSpec.ts │ │ │ │ │ │ └── textColorSpec.ts │ │ │ │ └── plugins │ │ │ │ │ └── events.ts │ │ │ └── EditorContent.tsx │ │ ├── editor │ │ │ └── index.ts │ │ ├── button │ │ │ ├── OutlineButton.tsx │ │ │ └── Button.tsx │ │ ├── PageRender.tsx │ │ ├── popover │ │ │ └── Popover.tsx │ │ ├── carousel │ │ │ └── HorizontalCarousel.css │ │ └── dialog │ │ │ └── QuickBoxDialog.tsx │ ├── index.ts │ ├── utils │ │ ├── event │ │ │ ├── index.ts │ │ │ └── event.ts │ │ ├── network │ │ │ └── index.ts │ │ ├── keyboard │ │ │ ├── index.ts │ │ │ └── keyboard.ts │ │ ├── object │ │ │ ├── index.ts │ │ │ ├── randomItems.ts │ │ │ └── merge.ts │ │ ├── index.ts │ │ ├── browser.ts │ │ ├── settings │ │ │ ├── index.ts │ │ │ ├── components │ │ │ │ ├── SettingDivider.tsx │ │ │ │ └── FontSearchBox.tsx │ │ │ ├── SettingButton.tsx │ │ │ ├── sidebar │ │ │ │ ├── FontStyle.tsx │ │ │ │ └── Sidebar.tsx │ │ │ └── PageGridView.tsx │ │ ├── slugify.ts │ │ ├── 2d │ │ │ ├── angleBetwwenPoints.ts │ │ │ ├── horizontalAndVerticalChange.ts │ │ │ ├── distanceBetweenPoints.ts │ │ │ ├── cornersToLines.ts │ │ │ ├── isLineIntersection.ts │ │ │ ├── getSizeWithRatio.ts │ │ │ ├── boundingRect.ts │ │ │ ├── getPositionChangesBetweenTwoCorners.ts │ │ │ ├── visualCorners.tsx │ │ │ ├── rectangleInsideAnother.ts │ │ │ └── positionOfObjectInsideAnother.ts │ │ ├── applyToPoint.ts │ │ ├── layer │ │ │ ├── getPositionWhenLayerCenter.ts │ │ │ ├── page.ts │ │ │ └── resolveComponent.ts │ │ ├── dom │ │ │ ├── isElementInViewport.ts │ │ │ └── getVirtualDomHeight.ts │ │ ├── download.ts │ │ ├── resolvers.ts │ │ ├── identityGenerator.ts │ │ ├── menu │ │ │ └── actions │ │ │ │ ├── copy.ts │ │ │ │ ├── paste.ts │ │ │ │ └── duplicate.ts │ │ └── deserialize.tsx │ ├── layers │ │ ├── shape │ │ │ ├── index.ts │ │ │ ├── normalize.ts │ │ │ └── scalePath.ts │ │ ├── text │ │ │ └── index.ts │ │ ├── background │ │ │ ├── index.ts │ │ │ └── getGradientBackground.ts │ │ ├── common │ │ │ ├── index.ts │ │ │ ├── getPageSize.ts │ │ │ ├── getTransformStyle.ts │ │ │ ├── getUsedFonts.ts │ │ │ └── FontStyle.tsx │ │ ├── core │ │ │ ├── PageElement.tsx │ │ │ ├── PageContext.tsx │ │ │ ├── LayerContext.tsx │ │ │ ├── LayerElement.tsx │ │ │ └── TransformLayer.tsx │ │ ├── index.ts │ │ ├── GroupLayer.tsx │ │ ├── content │ │ │ └── TextContent.tsx │ │ ├── RootLayer.tsx │ │ └── ShapeLayer.tsx │ ├── tooltip │ │ ├── components │ │ │ ├── Tooltip │ │ │ │ ├── index.ts │ │ │ │ ├── core-styles.module.css │ │ │ │ └── styles.module.css │ │ │ ├── TooltipContent │ │ │ │ ├── index.ts │ │ │ │ ├── TooltipContentTypes.d.ts │ │ │ │ └── TooltipContent.tsx │ │ │ ├── TooltipController │ │ │ │ └── index.ts │ │ │ └── TooltipProvider │ │ │ │ ├── index.ts │ │ │ │ └── TooltipProviderTypes.d.ts │ │ └── utils │ │ │ ├── use-isomorphic-layout-effect.ts │ │ │ ├── compute-positions-types.d.ts │ │ │ ├── get-scroll-parent.ts │ │ │ └── debounce.ts │ ├── search-autocomplete │ │ ├── index.ts │ │ └── utils │ │ │ ├── index.ts │ │ │ └── config.ts │ ├── hooks │ │ ├── index.ts │ │ ├── useForwardedRef.ts │ │ ├── useDebouncedEffect.tsx │ │ ├── useEditor.ts │ │ ├── useLinkedRef.ts │ │ ├── useTrackingShiftKey.ts │ │ ├── useUsedFont.ts │ │ ├── useSelectedLayers.ts │ │ ├── useMobileDetect.ts │ │ └── useClickOutside.tsx │ ├── types │ │ ├── index.ts │ │ ├── drag.ts │ │ ├── resize.ts │ │ ├── page.ts │ │ └── common.ts │ ├── color-picker │ │ ├── index.ts │ │ ├── utils │ │ │ ├── clamp.ts │ │ │ ├── parser │ │ │ │ ├── hsv2hwb.ts │ │ │ │ ├── hex2hsv.ts │ │ │ │ ├── isHSL.ts │ │ │ │ ├── isHSV.ts │ │ │ │ ├── isRGB.ts │ │ │ │ ├── hsl2rgb.ts │ │ │ │ ├── rgbString2rgb.ts │ │ │ │ ├── hsv2hex.ts │ │ │ │ ├── hwb2hsv.ts │ │ │ │ ├── hsl2hsv.ts │ │ │ │ ├── rgb2hex.ts │ │ │ │ ├── types.ts │ │ │ │ ├── hsv2hsl.ts │ │ │ │ ├── rgb2hsv.ts │ │ │ │ ├── rgb2hsl.ts │ │ │ │ ├── hsv2rgb.ts │ │ │ │ ├── hex2rgb.ts │ │ │ │ └── helper.ts │ │ │ ├── index.ts │ │ │ └── equalColorObjects.ts │ │ ├── components │ │ │ ├── ColorPicker.tsx │ │ │ ├── HuePointer.tsx │ │ │ └── Pointer.tsx │ │ └── types.ts │ ├── drag-and-drop │ │ ├── SortableContainer │ │ │ ├── defaultGetHelperDimensions.ts │ │ │ └── defaultShouldCancelStart.ts │ │ ├── index.ts │ │ ├── SortableHandle.tsx │ │ └── Manager.tsx │ ├── useEventCallback.ts │ ├── icons │ │ ├── PlayArrowIcon.tsx │ │ ├── CheckIcon.tsx │ │ ├── ArrowBackIcon.tsx │ │ ├── ArrowRightIcon.tsx │ │ ├── ArrowForwardIcon.tsx │ │ ├── SearchIcon.tsx │ │ ├── TextAUnderlineIcon.tsx │ │ ├── PlusIcon.tsx │ │ ├── LongDashIcon.tsx │ │ ├── ArrowLeftIcon.tsx │ │ ├── TabCollapseIcon.tsx │ │ ├── UngroupIcon.tsx │ │ ├── FormatItalicIcon.tsx │ │ ├── CloseIcon.tsx │ │ ├── MoreHorizIcon.tsx │ │ ├── BackIcon.tsx │ │ ├── DotsIcon.tsx │ │ ├── TwoDashesIcon.tsx │ │ ├── MoreVertIcon.tsx │ │ ├── NextIcon.tsx │ │ ├── ThreeDashesIcon.tsx │ │ ├── SquareBoldIcon.tsx │ │ ├── DownloadIcon.tsx │ │ ├── VideoIcon.tsx │ │ ├── ArrowUpIcon.tsx │ │ ├── HamburgerIcon.tsx │ │ ├── ArrowDownIcon.tsx │ │ ├── TextIcon.tsx │ │ ├── CopyIcon.tsx │ │ ├── ElementsIcon.tsx │ │ ├── TrashIcon.tsx │ │ ├── TextAlignRightIcon.tsx │ │ ├── TextAlignCenterIcon.tsx │ │ ├── TrendingIcon.tsx │ │ ├── MinusIcon.tsx │ │ ├── RotateIcon.tsx │ │ ├── ListBulletsIcon.tsx │ │ ├── BringForwardIcon.tsx │ │ ├── SendBackwardIcon.tsx │ │ ├── NotAllowedIcon.tsx │ │ ├── UploadIcon.tsx │ │ ├── AlignLeftIcon.tsx │ │ ├── AlignTopIcon.tsx │ │ ├── ColorizeIcon.tsx │ │ ├── AddNewPageIcon.tsx │ │ ├── AlignRightIcon.tsx │ │ ├── LayoutIcon.tsx │ │ ├── AlignBottomIcon.tsx │ │ ├── HelpIcon.tsx │ │ ├── FormatUnderlineIcon.tsx │ │ ├── ExportIcon.tsx │ │ ├── LayersIcon.tsx │ │ ├── AlignCenterVerticalIcon.tsx │ │ ├── AlignCenterHorizontalIcon.tsx │ │ ├── ShapeSettingsIcon.tsx │ │ ├── GithubIcon.tsx │ │ ├── GridViewIcon.tsx │ │ ├── SendToBackIcon.tsx │ │ ├── SyncedIcon.tsx │ │ ├── FormatBoldIcon.tsx │ │ ├── TextAlignLeftIcon.tsx │ │ ├── BringToFontIcon.tsx │ │ ├── BackgroundSelectionIcon.tsx │ │ ├── FacebookIcon.tsx │ │ ├── TextAlignJustifyIcon.tsx │ │ ├── GroupingIcon.tsx │ │ ├── ResizeIcon.tsx │ │ ├── DocumentIcon.tsx │ │ ├── ImageIcon.tsx │ │ ├── DuplicateIcon.tsx │ │ ├── OfflineIcon.tsx │ │ ├── FormatUppercaseIcon.tsx │ │ ├── FrameIcon.tsx │ │ ├── ListNumbersIcon.tsx │ │ ├── SyncingIcon.tsx │ │ ├── NotesIcon.tsx │ │ ├── ConfigurationIcon.tsx │ │ ├── LineSpacingIcon.tsx │ │ ├── LockOpenIcon.tsx │ │ └── ClipboardIcon.tsx │ └── layout │ │ ├── pages │ │ └── EditorContent.tsx │ │ ├── AppLayerSettings.tsx │ │ └── sidebar │ │ ├── components │ │ └── PreviewModal.tsx │ │ └── CloseButton.tsx │ ├── Screenshot.png │ ├── .gitignore │ ├── .eslintrc.mjs │ ├── types.d.ts │ └── LICENSE ├── Screenshot.png ├── .gitignore ├── public └── sitemap.xml └── api └── package.json /packages/editor/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css'; 2 | -------------------------------------------------------------------------------- /packages/editor/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Frame'; 2 | -------------------------------------------------------------------------------- /packages/editor/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/editor/index' -------------------------------------------------------------------------------- /packages/editor/src/utils/event/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event'; 2 | -------------------------------------------------------------------------------- /packages/editor/src/utils/network/index.ts: -------------------------------------------------------------------------------- 1 | export * from './network'; 2 | -------------------------------------------------------------------------------- /packages/editor/src/layers/shape/index.ts: -------------------------------------------------------------------------------- 1 | export * from './scalePath'; 2 | -------------------------------------------------------------------------------- /packages/editor/src/layers/text/index.ts: -------------------------------------------------------------------------------- 1 | export * from './textEffect'; 2 | -------------------------------------------------------------------------------- /packages/editor/src/utils/keyboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keyboard'; 2 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenvinlu/canva-editor/HEAD/Screenshot.png -------------------------------------------------------------------------------- /packages/editor/src/layers/background/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getGradientBackground'; 2 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/components/Tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tooltip } from './Tooltip' 2 | -------------------------------------------------------------------------------- /packages/editor/src/utils/object/index.ts: -------------------------------------------------------------------------------- 1 | export * from './merge'; 2 | export * from './randomItems'; 3 | -------------------------------------------------------------------------------- /packages/editor/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenvinlu/canva-editor/HEAD/packages/editor/Screenshot.png -------------------------------------------------------------------------------- /packages/editor/src/search-autocomplete/index.ts: -------------------------------------------------------------------------------- 1 | import SearchBox from './SearchBox.js'; 2 | export { SearchBox }; 3 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/components/TooltipContent/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TooltipContent } from './TooltipContent' 2 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/components/TooltipController/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TooltipController } from './TooltipController' 2 | -------------------------------------------------------------------------------- /packages/editor/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useEditor'; 2 | export * from './useLayer'; 3 | export * from './useSelectedLayers'; 4 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/components/TooltipContent/TooltipContentTypes.d.ts: -------------------------------------------------------------------------------- 1 | export interface ITooltipContent { 2 | content: string 3 | } 4 | -------------------------------------------------------------------------------- /packages/editor/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './layer'; 3 | export * from './page'; 4 | export * from './editor'; 5 | -------------------------------------------------------------------------------- /packages/editor/src/types/drag.ts: -------------------------------------------------------------------------------- 1 | import { Delta } from "."; 2 | 3 | export type DragCallback = (e: MouseEvent | TouchEvent, position: Delta) => void; 4 | -------------------------------------------------------------------------------- /packages/editor/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event'; 2 | export * from './keyboard'; 3 | export * from './network'; 4 | export * from './object'; 5 | -------------------------------------------------------------------------------- /packages/editor/src/utils/browser.ts: -------------------------------------------------------------------------------- 1 | export const isSafari = 2 | typeof window !== 'undefined' 3 | ? navigator.userAgent.indexOf('Safari') !== -1 4 | : false; 5 | -------------------------------------------------------------------------------- /packages/editor/src/utils/settings/index.ts: -------------------------------------------------------------------------------- 1 | import LayerSettings from './LayerSettings'; 2 | import PageControl from './PageControl'; 3 | export { LayerSettings, PageControl }; 4 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/index.ts: -------------------------------------------------------------------------------- 1 | import ColorPicker from './components/ColorPicker'; 2 | import ColorIcon from './components/ColorIcon'; 3 | 4 | export { ColorPicker, ColorIcon }; 5 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/clamp.ts: -------------------------------------------------------------------------------- 1 | export const clamp = (number: number, min = 0, max = 1): number => { 2 | return number > max ? max : number < min ? min : number; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/editor/src/layers/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getTransformStyle'; 2 | export * from './getUsedFonts'; 3 | export * from './GlobalStyle'; 4 | export * from './getPageSize'; 5 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/components/TooltipProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TooltipProvider, useTooltip } from './TooltipProvider' 2 | export { default as TooltipWrapper } from './TooltipWrapper' 3 | -------------------------------------------------------------------------------- /packages/editor/src/utils/slugify.ts: -------------------------------------------------------------------------------- 1 | export const slugify = (name: string) => { 2 | if (!name) return ''; 3 | return name 4 | .toLowerCase() 5 | .replace(/[^a-zA-Z0-9 ]/g, '') 6 | .replace(/ +/g, '-'); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/hsv2hwb.ts: -------------------------------------------------------------------------------- 1 | export const hsv2hwb = ({ h, s, v }: { h: number; s: number; v: number }) => { 2 | return { 3 | h, 4 | w: ((100 - s) * v) / 100, 5 | b: 100 - v, 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { EditorView } from 'prosemirror-view'; 2 | import { EventEmitter } from './core/helper/EventEmitter'; 3 | 4 | export type TextEditor = EditorView & { 5 | events: EventEmitter; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/hex2hsv.ts: -------------------------------------------------------------------------------- 1 | import { hex2rgb } from './hex2rgb'; 2 | import { rgb2hsv } from './rgb2hsv'; 3 | 4 | export const hex2hsv = (hex: string) => { 5 | const rgb = hex2rgb(hex); 6 | return rgb2hsv(rgb); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/editor/src/drag-and-drop/SortableContainer/defaultGetHelperDimensions.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | export default function defaultGetHelperDimensions({node}) { 3 | return { 4 | height: node.offsetHeight, 5 | width: node.offsetWidth, 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/isHSL.ts: -------------------------------------------------------------------------------- 1 | import { HSLAColor } from './types'; 2 | 3 | export const isHSL = (color: Record): color is HSLAColor => { 4 | return Object.keys(color).every((k) => ['h', 's', 'l', 'a'].includes(k)); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/isHSV.ts: -------------------------------------------------------------------------------- 1 | import { HSVAColor } from './types'; 2 | 3 | export const isHSV = (color: Record): color is HSVAColor => { 4 | return Object.keys(color).every((k) => ['h', 's', 'v', 'a'].includes(k)); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/editor/src/layers/core/PageElement.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import LayerElement from './LayerElement'; 3 | 4 | export const PageElement: FC = () => { 5 | return ; 6 | }; 7 | 8 | export default PageElement; 9 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/utils/use-isomorphic-layout-effect.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useEffect } from 'react' 2 | 3 | const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect 4 | 5 | export default useIsomorphicLayoutEffect 6 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/isRGB.ts: -------------------------------------------------------------------------------- 1 | import { RGBAColor } from './types'; 2 | 3 | export const isRGB = (color: Record | string): color is RGBAColor => { 4 | return Object.keys(color).every((k) => ['r', 'g', 'b', 'a'].includes(k)); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/editor/src/drag-and-drop/index.ts: -------------------------------------------------------------------------------- 1 | export {default as SortableContainer} from './SortableContainer'; 2 | export {default as SortableElement} from './SortableElement'; 3 | export {default as SortableHandle} from './SortableHandle'; 4 | 5 | export {arrayMove} from './DDUtils'; 6 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/hsl2rgb.ts: -------------------------------------------------------------------------------- 1 | import { hsl2hsv } from './hsl2hsv'; 2 | import { hsv2rgb } from './hsv2rgb'; 3 | 4 | export const hsl2rgb = (hsl: { h: number; s: number; l: number; a: number }) => { 5 | const hsv = hsl2hsv(hsl); 6 | return hsv2rgb(hsv); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/rgbString2rgb.ts: -------------------------------------------------------------------------------- 1 | export const rgb2rgbString = ({ r, g, b, a }: { r: number; g: number; b: number; a: number }) => { 2 | if (a === 1) { 3 | return `rgb(${r}, ${g}, ${b})`; 4 | } else { 5 | return `rgba(${r}, ${g}, ${b}, ${a})`; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/components/TooltipContent/TooltipContent.tsx: -------------------------------------------------------------------------------- 1 | import type { ITooltipContent } from './TooltipContentTypes' 2 | 3 | const TooltipContent = ({ content }: ITooltipContent) => { 4 | return 5 | } 6 | 7 | export default TooltipContent 8 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/hsv2hex.ts: -------------------------------------------------------------------------------- 1 | import { hsv2rgb } from './hsv2rgb'; 2 | import { rgb2hex } from './rgb2hex'; 3 | 4 | export const hsv2hex = ({ h, s, v, a }: { h: number; s: number; v: number; a: number }) => { 5 | const rgb = hsv2rgb({ h, s, v, a }); 6 | return rgb2hex(rgb); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/editor/src/utils/2d/angleBetwwenPoints.ts: -------------------------------------------------------------------------------- 1 | import { CursorPosition } from "canva-editor/types"; 2 | 3 | export const angleBetweenPoints = (oldPos: CursorPosition, newPos: CursorPosition) => { 4 | return (Math.atan2(newPos.clientY - oldPos.clientY, newPos.clientX - oldPos.clientX) * 180) / Math.PI; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/hwb2hsv.ts: -------------------------------------------------------------------------------- 1 | export const hwb2hsv = ({ h, w, b }: { h: number; w: number; b: number }, a: number) => { 2 | return { 3 | h, 4 | s: Math.max(0, Math.min(100, b === 100 ? 0 : 100 - (w / (100 - b)) * 100)), 5 | v: 100 - b, 6 | a, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/helper/createDocument.ts: -------------------------------------------------------------------------------- 1 | import { createNodeFromContent } from './createNodeFromContent.js'; 2 | import { Schema } from 'prosemirror-model'; 3 | 4 | export function createDocument(content: string, schema: Schema) { 5 | return createNodeFromContent(content, schema); 6 | } 7 | -------------------------------------------------------------------------------- /packages/editor/src/layers/common/getPageSize.ts: -------------------------------------------------------------------------------- 1 | import { BoxSize, SerializedPage } from '../../types'; 2 | 3 | export const getPageSize = (data: SerializedPage[]) => { 4 | if (data.length === 0) { 5 | throw new Error('Incorrect data'); 6 | } 7 | return data[0].layers.ROOT.props.boxSize as BoxSize; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/editor/src/utils/applyToPoint.ts: -------------------------------------------------------------------------------- 1 | import { Delta } from "canva-editor/types"; 2 | 3 | // https://www.npmjs.com/package/transformation-matrix 4 | export const applyToPoint = (matrix: WebKitCSSMatrix, point: Delta) => ({ 5 | x: matrix.a * point.x + matrix.c * point.y, 6 | y: matrix.b * point.x + matrix.d * point.y, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/editor/src/components/editor/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get full sourcecode here: https://kenvinlu.gumroad.com/l/canva-editor 3 | */ 4 | import DesignFrame from './DesignFrame'; 5 | import CanvaEditor from './CanvaEditor'; 6 | import Preview from './Preview'; 7 | export * from './EditorContext'; 8 | export { DesignFrame, CanvaEditor, Preview }; 9 | -------------------------------------------------------------------------------- /packages/editor/src/types/resize.ts: -------------------------------------------------------------------------------- 1 | export type CornerDirection = 'topRight' | 'bottomRight' | 'bottomLeft' | 'topLeft'; 2 | export type EdgeDirection = 'top' | 'right' | 'bottom' | 'left'; 3 | export type Direction = EdgeDirection | CornerDirection; 4 | 5 | export type ResizeCallback = (e: MouseEvent | TouchEvent, direction: Direction) => void; 6 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/hsl2hsv.ts: -------------------------------------------------------------------------------- 1 | export const hsl2hsv = ({ h, s, l, a }: { h: number; s: number; l: number; a: number }) => { 2 | s *= (l < 50 ? l : 100 - l) / 100; 3 | 4 | return { 5 | h: h, 6 | s: s > 0 ? ((2 * s) / (l + s)) * 100 : 0, 7 | v: l + s, 8 | a, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/command/selectAll.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'prosemirror-state'; 2 | import { selectText } from './selectText'; 3 | 4 | export const selectAll: Command = (state, dispatch, ...rest) => { 5 | selectText({ from: 0, to: state.doc.content.size })(state, dispatch, ...rest); 6 | return true; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/schema/spec/italicSpec.ts: -------------------------------------------------------------------------------- 1 | import { MarkSpec } from 'prosemirror-model'; 2 | 3 | const ItalicSpec: MarkSpec = { 4 | parseDOM: [{ tag: 'i' }, { tag: 'em' }, { style: 'font-style=italic' }], 5 | toDOM() { 6 | return ['em', 0]; 7 | }, 8 | }; 9 | 10 | export default ItalicSpec; 11 | -------------------------------------------------------------------------------- /packages/editor/src/layers/shape/normalize.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fix clip path not work correctly on browser 3 | * @param number 4 | */ 5 | export const normalizeNumber = (number: number) => { 6 | if (number % 1 > 0 && number % 1 < 0.5) { 7 | return Math.round(number); 8 | } 9 | return Math.round(number * 1000) / 1000; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/editor/src/utils/layer/getPositionWhenLayerCenter.ts: -------------------------------------------------------------------------------- 1 | import { BoxSize } from 'canva-editor/types'; 2 | 3 | export const getPositionWhenLayerCenter = (editorSize: BoxSize, layerSize: BoxSize) => { 4 | return { 5 | x: (editorSize.width - layerSize.width) / 2, 6 | y: (editorSize.height - layerSize.height) / 2, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/helper/elementFromString.ts: -------------------------------------------------------------------------------- 1 | export function elementFromString(value: string): HTMLElement { 2 | // add a wrapper to preserve leading and trailing whitespace 3 | const wrappedValue = `${value}`; 4 | 5 | return new window.DOMParser().parseFromString(wrappedValue, 'text/html').body; 6 | } 7 | -------------------------------------------------------------------------------- /packages/editor/src/utils/dom/isElementInViewport.ts: -------------------------------------------------------------------------------- 1 | export const isElementInViewport = (viewPort: HTMLElement, element: HTMLElement) => { 2 | const distanceToTop = element.offsetTop; 3 | return ( 4 | viewPort.scrollTop + viewPort.offsetHeight >= distanceToTop && 5 | viewPort.scrollTop < element.offsetHeight + distanceToTop 6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/editor/src/layers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './shape'; 2 | export * from './text'; 3 | export * from './common'; 4 | export * from './background'; 5 | export * from './content/TextContent'; 6 | export * from './content/ImageContent'; 7 | export * from './content/ShapeContent'; 8 | export * from './content/FrameContent'; 9 | export * from './content/RootContent'; 10 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/helper/isEmptyContent.ts: -------------------------------------------------------------------------------- 1 | import { EditorState } from 'prosemirror-state'; 2 | 3 | export const isEmptyContent = (state: EditorState) => { 4 | const defaultContent = state.doc.type.createAndFill()?.toJSON(); 5 | const content = state.doc.toJSON(); 6 | return JSON.stringify(defaultContent) === JSON.stringify(content); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/editor/src/utils/2d/horizontalAndVerticalChange.ts: -------------------------------------------------------------------------------- 1 | export const horizontalAndVerticalChange = (oldRotation: number, newRotation: number, distance: number) => { 2 | const angleRadians = ((newRotation - oldRotation) * Math.PI) / 180; 3 | return { 4 | width: distance * Math.cos(angleRadians), 5 | height: distance * Math.sin(angleRadians), 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/editor/src/layers/shape/scalePath.ts: -------------------------------------------------------------------------------- 1 | export const scalePath = (path: string, scale: number) => { 2 | const arr = path.split(' '); 3 | return arr 4 | .map((v) => { 5 | if (v.match(/^[+-]?\d+(\.\d+)?$/)) { 6 | return parseFloat(v) * scale; 7 | } 8 | return v; 9 | }) 10 | .join(' '); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/editor/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/helper/createNodeFromContent.ts: -------------------------------------------------------------------------------- 1 | import { Schema, DOMParser } from 'prosemirror-model'; 2 | import { elementFromString } from './elementFromString'; 3 | 4 | export function createNodeFromContent(content: string, schema: Schema) { 5 | const parser = DOMParser.fromSchema(schema); 6 | 7 | return parser.parse(elementFromString(content)); 8 | } 9 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/helper/getSchemaType.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from 'prosemirror-model'; 2 | 3 | export function getSchemaType(name: string, schema: Schema): 'node' | 'mark' | null { 4 | if (schema.nodes[name]) { 5 | return 'node'; 6 | } 7 | 8 | if (schema.marks[name]) { 9 | return 'mark'; 10 | } 11 | 12 | return null; 13 | } 14 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/helper/object.ts: -------------------------------------------------------------------------------- 1 | export function objectIncludes(object1: Record, object2: Record): boolean { 2 | const keys = Object.keys(object2); 3 | if (!keys.length) { 4 | return true; 5 | } 6 | 7 | return keys.every((key) => { 8 | return object2[key] === object1[key]; 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /packages/editor/src/utils/2d/distanceBetweenPoints.ts: -------------------------------------------------------------------------------- 1 | import { CursorPosition } from "canva-editor/types"; 2 | 3 | export const distanceBetweenPoints = (oldPos: CursorPosition, newPos: CursorPosition, scale?: number) => { 4 | const xDiff = newPos.clientX - oldPos.clientX; 5 | const yDiff = newPos.clientY - oldPos.clientY; 6 | 7 | return Math.sqrt(xDiff * xDiff + yDiff * yDiff) / (scale || 1); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/editor/src/utils/2d/cornersToLines.ts: -------------------------------------------------------------------------------- 1 | import { Delta } from "canva-editor/types"; 2 | 3 | export const cornersToLines = ({ 4 | nw, 5 | ne, 6 | se, 7 | sw, 8 | }: { 9 | nw: Delta; 10 | ne: Delta; 11 | se: Delta; 12 | sw: Delta; 13 | }): [Delta, Delta][] => { 14 | return [ 15 | [nw, ne], 16 | [ne, se], 17 | [se, sw], 18 | [sw, nw], 19 | ]; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/editor/src/utils/settings/components/SettingDivider.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | background?: string; 5 | } 6 | const SettingDivider: FC = ({ background = 'rgba(57,76,96,.15)' }) => { 7 | return ( 8 |
15 | ); 16 | }; 17 | 18 | export default SettingDivider; 19 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/rgb2hex.ts: -------------------------------------------------------------------------------- 1 | const convert = (num: number) => { 2 | const hex = num.toString(16); 3 | return hex.length === 1 ? '0' + hex : hex; 4 | }; 5 | 6 | export const rgb2hex = ({ r, g, b, a }: { r: number; g: number; b: number; a: number }) => { 7 | const alphaHex = a < 1 ? convert(Math.round(a * 255)) : ''; 8 | return '#' + [convert(r), convert(g), convert(b), alphaHex].join(''); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/types.ts: -------------------------------------------------------------------------------- 1 | export type RGBAColor = { 2 | r: number; 3 | g: number; 4 | b: number; 5 | a: number; 6 | }; 7 | 8 | export type HSLAColor = { 9 | h: number; 10 | s: number; 11 | l: number; 12 | a: number; 13 | }; 14 | 15 | export type HSVColor = { 16 | h: number; 17 | s: number; 18 | v: number; 19 | }; 20 | 21 | export type HSVAColor = HSVColor & { 22 | a: number; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/editor/src/types/page.ts: -------------------------------------------------------------------------------- 1 | import { Layers, SerializedLayers } from './layer'; 2 | 3 | export type PageSize = { 4 | width: number; 5 | height: number; 6 | }; 7 | 8 | export type SerializedPage = { 9 | layers: SerializedLayers; 10 | name: string; 11 | notes: string; 12 | locked?: boolean; 13 | }; 14 | 15 | export type Page = { 16 | layers: Layers; 17 | name: string; 18 | notes: string; 19 | locked?: boolean; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/editor/src/utils/layer/page.ts: -------------------------------------------------------------------------------- 1 | import { serializeLayers } from './layers'; 2 | import { Page, SerializedPage } from '../../types'; 3 | 4 | export const serialize = (pages: Page[]): SerializedPage[] => { 5 | return pages.map((page) => { 6 | return { 7 | name: page.name, 8 | notes: page.notes, 9 | locked: page.locked, 10 | layers: serializeLayers(page.layers, 'ROOT'), 11 | }; 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | https://www.canvaclone.com/ 9 | 2024-09-28T02:39:02+00:00 10 | 11 | -------------------------------------------------------------------------------- /packages/editor/src/useEventCallback.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | // Saves incoming handler to the ref in order to avoid "useCallback hell" 4 | export function useEventCallback(handler?: (value: T) => void): (value: T) => void { 5 | const callbackRef = useRef(handler); 6 | const fn = useRef((value: T) => { 7 | callbackRef.current && callbackRef.current(value); 8 | }); 9 | callbackRef.current = handler; 10 | 11 | return fn.current; 12 | } 13 | -------------------------------------------------------------------------------- /packages/editor/src/hooks/useForwardedRef.ts: -------------------------------------------------------------------------------- 1 | import { ForwardedRef, useEffect, useRef } from 'react'; 2 | 3 | export const useForwardedRef = (ref: ForwardedRef) => { 4 | const innerRef = useRef(null); 5 | 6 | useEffect(() => { 7 | if (!ref) return; 8 | if (typeof ref === 'function') { 9 | ref(innerRef.current); 10 | } else { 11 | ref.current = innerRef.current; 12 | } 13 | }); 14 | 15 | return innerRef; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/helper/getMarkType.ts: -------------------------------------------------------------------------------- 1 | import { MarkType, Schema } from 'prosemirror-model'; 2 | 3 | export function getMarkType(nameOrType: string | MarkType, schema: Schema): MarkType { 4 | if (typeof nameOrType === 'string') { 5 | if (!schema.marks[nameOrType]) { 6 | throw Error(`There is no mark type named '${nameOrType}'!`); 7 | } 8 | 9 | return schema.marks[nameOrType]; 10 | } 11 | 12 | return nameOrType; 13 | } 14 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/helper/getNodeType.ts: -------------------------------------------------------------------------------- 1 | import { NodeType, Schema } from 'prosemirror-model'; 2 | 3 | export function getNodeType(nameOrType: string | NodeType, schema: Schema): NodeType { 4 | if (typeof nameOrType === 'string') { 5 | if (!schema.nodes[nameOrType]) { 6 | throw Error(`There is no node type named '${nameOrType}'!`); 7 | } 8 | 9 | return schema.nodes[nameOrType]; 10 | } 11 | 12 | return nameOrType; 13 | } 14 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "canva-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "nodemon app.js", 9 | "start": "node app.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "cors": "^2.8.5", 16 | "express": "^4.18.2", 17 | "lodash": "^4.17.21", 18 | "nodemon": "^3.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/schema/spec/listItemSpec.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec } from 'prosemirror-model'; 2 | 3 | const ListItemSpec: NodeSpec = { 4 | content: 'paragraph block*', 5 | parseDOM: [{ tag: 'li' }], 6 | attrs: { 7 | align: { default: null }, 8 | style: { default: 'display: list-item; ' }, 9 | }, 10 | 11 | toDOM(node) { 12 | return ['li', node.attrs, 0]; 13 | }, 14 | defining: true, 15 | }; 16 | 17 | export default ListItemSpec; 18 | -------------------------------------------------------------------------------- /packages/editor/src/hooks/useDebouncedEffect.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, DependencyList } from 'react'; 2 | 3 | function useDebouncedEffect(callback: { (): void; (): void; }, delay: number | undefined, dependencies: DependencyList | undefined) { 4 | useEffect(() => { 5 | const handler = setTimeout(() => { 6 | callback(); 7 | }, delay); 8 | 9 | return () => { 10 | clearTimeout(handler); 11 | }; 12 | }, dependencies); 13 | } 14 | 15 | export default useDebouncedEffect; 16 | -------------------------------------------------------------------------------- /packages/editor/src/layers/core/PageContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, FC, PropsWithChildren } from 'react'; 2 | export const PageContext = createContext<{ pageIndex: number }>({} as { pageIndex: number }); 3 | 4 | type PageProviderProps = { 5 | pageIndex: number; 6 | }; 7 | const PageProvider: FC> = ({ pageIndex, children }) => { 8 | return {children}; 9 | }; 10 | export default PageProvider; 11 | -------------------------------------------------------------------------------- /packages/editor/.eslintrc.mjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.mjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/schema/spec/bulletListSpec.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec } from 'prosemirror-model'; 2 | 3 | const BulletListSpec: NodeSpec = { 4 | content: 'listItem+', 5 | group: 'block', 6 | attrs: { 7 | align: { default: null }, 8 | style: { default: 'list-style-type: disc;padding-left: 1.7em;margin:0;' }, 9 | }, 10 | 11 | parseDOM: [{ tag: 'ul' }], 12 | toDOM(node) { 13 | return ['ul', node.attrs, 0]; 14 | }, 15 | }; 16 | 17 | export default BulletListSpec; 18 | -------------------------------------------------------------------------------- /packages/editor/src/icons/PlayArrowIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const PlayArrowIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default PlayArrowIcon; 19 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./parser/hsl2hsv"; 2 | export * from "./parser/hex2hsv"; 3 | export * from "./parser/hsl2rgb"; 4 | export * from "./parser/hsv2hex"; 5 | export * from "./parser/hsv2hsl"; 6 | export * from "./parser/hsv2rgb"; 7 | export * from "./parser/rgb2hex"; 8 | export * from "./parser/rgb2hsl"; 9 | export * from "./parser/rgb2hsv"; 10 | export * from "./parser/hex2rgb"; 11 | export * from "./parser/rgbString2rgb"; 12 | 13 | import ColorParser from "./parser"; 14 | export { ColorParser }; 15 | -------------------------------------------------------------------------------- /packages/editor/src/layers/core/LayerContext.tsx: -------------------------------------------------------------------------------- 1 | import { LayerId } from 'canva-editor/types'; 2 | import React, { createContext, FC, PropsWithChildren } from 'react'; 3 | 4 | export const LayerContext = createContext<{ id: LayerId }>({} as { id: LayerId }); 5 | 6 | type LayerProviderProps = { 7 | id: LayerId; 8 | }; 9 | const LayerProvider: FC> = ({ id, children }) => { 10 | return {children}; 11 | }; 12 | export default LayerProvider; 13 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/helper/setContent.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'prosemirror-state'; 2 | import { createDocument } from './createDocument'; 3 | 4 | export const setContent: (content: string) => Command = (content) => (state, dispatch) => { 5 | const tr = state.tr; 6 | const { doc } = tr; 7 | const document = createDocument(content, state.schema); 8 | if (dispatch) { 9 | tr.replaceWith(0, doc.content.size, document); 10 | dispatch(tr); 11 | return true; 12 | } 13 | return false; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/editor/src/layers/core/LayerElement.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, PropsWithChildren } from 'react'; 2 | import LayerProvider from './LayerContext'; 3 | import RenderLayer from './RenderLayer'; 4 | import { LayerId } from 'canva-editor/types'; 5 | 6 | type LayerElementProps = { 7 | id: LayerId; 8 | }; 9 | const LayerElement: FC> = ({ id }) => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default LayerElement; 18 | -------------------------------------------------------------------------------- /packages/editor/src/utils/download.ts: -------------------------------------------------------------------------------- 1 | export const downloadObjectAsJson = (exportName: string, data: unknown) => { 2 | const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(data)); 3 | const downloadAnchorNode = document.createElement('a'); 4 | downloadAnchorNode.setAttribute('href', dataStr); 5 | downloadAnchorNode.setAttribute('download', exportName + '.json'); 6 | document.body.appendChild(downloadAnchorNode); // required for firefox 7 | downloadAnchorNode.click(); 8 | downloadAnchorNode.remove(); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/editor/src/utils/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { ElementType } from 'react'; 2 | import RootLayer from '../layers/RootLayer'; 3 | import TextLayer from '../layers/TextLayer'; 4 | import ImageLayer from '../layers/ImageLayer'; 5 | import ShapeLayer from '../layers/ShapeLayer'; 6 | import FrameLayer from '../layers/FrameLayer'; 7 | import GroupLayer from '../layers/GroupLayer'; 8 | 9 | export const resolvers: Record = { 10 | RootLayer, 11 | TextLayer, 12 | ImageLayer, 13 | ShapeLayer, 14 | FrameLayer, 15 | GroupLayer, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/editor/src/search-autocomplete/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function debounce(func: Function, wait: number, immediate?: boolean) { 2 | let timeout: NodeJS.Timeout | null; 3 | 4 | return function (this: any) { 5 | const context = this; 6 | const args = arguments; 7 | 8 | const later = function () { 9 | timeout = null; 10 | if (!immediate) func.apply(context, args); 11 | }; 12 | 13 | if (immediate && !timeout) func.apply(context, args); 14 | 15 | timeout && clearTimeout(timeout); 16 | timeout = setTimeout(later, wait); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/editor/src/layers/common/getTransformStyle.ts: -------------------------------------------------------------------------------- 1 | import { LayerComponentProps } from "canva-editor/types"; 2 | 3 | export const getTransformStyle = (props: Partial) => { 4 | const res: string[] = []; 5 | if (props.position) { 6 | res.push(`translate(${props.position.x}px, ${props.position.y}px)`); 7 | } 8 | if (props.scale) { 9 | res.push(`scale(${props.scale})`); 10 | } 11 | if (props.rotate) { 12 | res.push(`rotate(${props.rotate}deg)`); 13 | } 14 | return res.join(' '); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/editor/src/icons/CheckIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const CheckIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default CheckIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/command/underline.ts: -------------------------------------------------------------------------------- 1 | import { unsetMark } from './unsetMark'; 2 | import { toggleMark } from './toggleMark'; 3 | import { setMark } from './setMark'; 4 | import { Command } from 'prosemirror-state'; 5 | 6 | export const toggleUnderline: Command = (...params) => { 7 | return toggleMark('underline')(...params); 8 | }; 9 | export const unsetUnderline: Command = (...params) => { 10 | return unsetMark('underline')(...params); 11 | }; 12 | export const setUnderline: Command = (...params) => { 13 | return setMark('underline')(...params); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/hsv2hsl.ts: -------------------------------------------------------------------------------- 1 | export const hsv2hsl = ({ h, s, v, a }: { h: number; s: number; v: number; a: number }) => { 2 | const hh = ((200 - s) * v) / 100; 3 | 4 | return { 5 | h: h, 6 | s: hh > 0 && hh < 200 ? ((s * v) / 100 / (hh <= 100 ? hh : 200 - hh)) * 100 : 0, 7 | l: hh / 2, 8 | a, 9 | }; 10 | }; 11 | 12 | export const hsv2hslString = ({ h, s, v, a }: { h: number; s: number; v: number; a: number }) => { 13 | const hsl = hsv2hsl({ h, s, v, a }); 14 | return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${hsl.a})`; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ArrowBackIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ArrowBackIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default ArrowBackIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ArrowRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ArrowRightIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default ArrowRightIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ArrowForwardIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ArrowForwardIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default ArrowForwardIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/utils/dom/getVirtualDomHeight.ts: -------------------------------------------------------------------------------- 1 | export const getVirtualDomHeight = (element: Element, width: number, scale: number) => { 2 | const box = document.createElement('div'); 3 | box.style.width = `${width / scale}px`; 4 | box.style.visibility = 'hidden'; 5 | box.style.top = '-9999px'; 6 | box.style.transform = `scale(${scale})`; 7 | box.style.position = 'fixed'; 8 | box.appendChild(element); 9 | document.body.appendChild(box); 10 | const clientHeight = element.clientHeight * scale; 11 | document.body.removeChild(box); 12 | return { 13 | clientHeight, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/command/selectNode.ts: -------------------------------------------------------------------------------- 1 | import { Command, NodeSelection } from 'prosemirror-state'; 2 | 3 | export const selectNode: (position: number) => Command = (position) => { 4 | return (state, dispatch) => { 5 | const tr = state.tr; 6 | const { doc } = tr; 7 | const pos = Math.max(0, Math.min(doc.content.size, position)); 8 | const selection = NodeSelection.create(doc, pos); 9 | 10 | tr.setSelection(selection); 11 | if (dispatch) { 12 | dispatch(tr); 13 | return true; 14 | } 15 | return false; 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/editor/src/utils/object/randomItems.ts: -------------------------------------------------------------------------------- 1 | export function getRandomItems(array: any, count = 10) { 2 | const shuffledArray = [...array].slice(); 3 | let currentIndex = shuffledArray.length; 4 | let randomIndex, temporaryValue; 5 | 6 | // Implement random array elements 7 | while (currentIndex !== 0) { 8 | randomIndex = Math.floor(Math.random() * currentIndex); 9 | currentIndex--; 10 | 11 | temporaryValue = shuffledArray[currentIndex]; 12 | shuffledArray[currentIndex] = shuffledArray[randomIndex]; 13 | shuffledArray[randomIndex] = temporaryValue; 14 | } 15 | 16 | return shuffledArray.slice(0, count); 17 | } 18 | -------------------------------------------------------------------------------- /packages/editor/src/hooks/useEditor.ts: -------------------------------------------------------------------------------- 1 | import { EditorContext } from 'canva-editor/components/editor/EditorContext'; 2 | import { EditorQuery, EditorState } from '../types'; 3 | import { useContext } from 'react'; 4 | 5 | export const useEditor = (collector?: (s: EditorState, query: EditorQuery) => C) => { 6 | const store = useContext(EditorContext); 7 | const { actions, getState, query, config } = store; 8 | const collected = collector ? collector(store.getState(), query) : ({} as C); 9 | return { 10 | ...collected, 11 | actions, 12 | query, 13 | state: getState(), 14 | config 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/editor/src/layers/common/getUsedFonts.ts: -------------------------------------------------------------------------------- 1 | import { SerializedPage, FontData } from 'canva-editor/types'; 2 | import { isEqual, uniqWith } from 'lodash'; 3 | import { TextContentProps } from '..'; 4 | 5 | export const getUsedFonts = (data: SerializedPage[]) => { 6 | const fontList: FontData[] = []; 7 | data.forEach((page) => { 8 | Object.entries(page.layers).forEach(([, layer]) => { 9 | if (layer.type.resolvedName === 'TextLayer') { 10 | fontList.push(...(layer.props as unknown as TextContentProps).fonts); 11 | } 12 | }); 13 | }); 14 | return uniqWith(fontList, isEqual); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/editor/src/icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const SearchIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default SearchIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/icons/TextAUnderlineIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const TextAUnderlineIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default TextAUnderlineIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/utils/settings/components/FontSearchBox.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { SearchBox } from 'canva-editor/search-autocomplete'; 3 | import SearchIcon from 'canva-editor/icons/SearchIcon'; 4 | 5 | interface Props { 6 | onSearch: (keyword: string) => void; 7 | } 8 | const FontSearchBox: FC = ({ onSearch }) => { 9 | return ( 10 | } 13 | onSearch={onSearch} 14 | autoFocus 15 | showNoResults={false} 16 | styling={{ zIndex: 2 }} 17 | placeholder='Search fonts' 18 | /> 19 | ); 20 | }; 21 | 22 | export default FontSearchBox; 23 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/components/Tooltip/core-styles.module.css: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | pointer-events: none; 6 | opacity: 0; 7 | will-change: opacity; 8 | } 9 | 10 | .fixed { 11 | position: fixed; 12 | } 13 | 14 | .arrow { 15 | position: absolute; 16 | background: inherit; 17 | } 18 | 19 | .noArrow { 20 | display: none; 21 | } 22 | 23 | .clickable { 24 | pointer-events: auto; 25 | } 26 | 27 | .show { 28 | opacity: 0.9; 29 | transition: opacity 0.15s ease-out; 30 | } 31 | 32 | .closing { 33 | opacity: 0; 34 | transition: opacity 0.15s ease-in; 35 | } 36 | 37 | /** end - core styles **/ 38 | -------------------------------------------------------------------------------- /packages/editor/src/utils/2d/isLineIntersection.ts: -------------------------------------------------------------------------------- 1 | import { Delta } from "canva-editor/types"; 2 | 3 | export const isLineIntersection = (line1: [Delta, Delta], line2: [Delta, Delta]) => { 4 | const a_dx = line1[1].x - line1[0].x; 5 | const a_dy = line1[1].y - line1[0].y; 6 | const b_dx = line2[1].x - line2[0].x; 7 | const b_dy = line2[1].y - line2[0].y; 8 | const s = (-a_dy * (line1[0].x - line2[0].x) + a_dx * (line1[0].y - line2[0].y)) / (-b_dx * a_dy + a_dx * b_dy); 9 | const t = (+b_dx * (line1[0].y - line2[0].y) - b_dy * (line1[0].x - line2[0].x)) / (-b_dx * a_dy + a_dx * b_dy); 10 | return s >= 0 && s <= 1 && t >= 0 && t <= 1; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/components/ColorPicker.tsx: -------------------------------------------------------------------------------- 1 | import { ColorModel, ColorPickerBaseProps } from '../types'; 2 | import { equalHex } from '../utils/equalColorObjects'; 3 | import { BaseColorPicker } from './BaseColorPicker'; 4 | import { hex2hsv, hsv2hex } from '../utils'; 5 | 6 | const colorModel: ColorModel = { 7 | defaultColor: '000', 8 | toHsva: hex2hsv, 9 | fromHsva: ({ h, s, v, a }) => hsv2hex({ h, s, v, a }), 10 | equal: equalHex, 11 | }; 12 | 13 | const ColorPicker = (props: Partial>) => ( 14 | 15 | ); 16 | 17 | export default ColorPicker; 18 | -------------------------------------------------------------------------------- /packages/editor/src/icons/PlusIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const PlusIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default PlusIcon; -------------------------------------------------------------------------------- /packages/editor/src/utils/2d/getSizeWithRatio.ts: -------------------------------------------------------------------------------- 1 | export const getSizeWithRatio = ( 2 | size: { x: number; y: number; width: number; height: number }, 3 | ratio: number, 4 | lockRatio: boolean, 5 | ) => { 6 | if (!lockRatio) { 7 | return size; 8 | } 9 | const newRatio = size.width / size.height; 10 | if (newRatio > ratio) { 11 | return { 12 | ...size, 13 | height: size.width / ratio, 14 | }; 15 | } else if (newRatio < ratio) { 16 | return { 17 | ...size, 18 | width: size.height * ratio, 19 | }; 20 | } else { 21 | return size; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /packages/editor/src/utils/identityGenerator.ts: -------------------------------------------------------------------------------- 1 | const prefix = 'ca_'; // Prefix 2 | const generateRandomID = (length = 10) => { 3 | const characters = 4 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 5 | let result = prefix; 6 | 7 | for (let i = prefix.length; i < length; i++) { 8 | const randomIndex = Math.floor(Math.random() * characters.length); 9 | result += characters.charAt(randomIndex); 10 | } 11 | 12 | return result; 13 | }; 14 | 15 | const isEditorID = (candidate: string, length = 10) => { 16 | return candidate.startsWith(prefix) && candidate.length === length; 17 | }; 18 | 19 | export { generateRandomID, isEditorID }; 20 | -------------------------------------------------------------------------------- /packages/editor/src/icons/LongDashIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const LongDashIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 22 | 23 | ); 24 | }; 25 | 26 | export default LongDashIcon; 27 | -------------------------------------------------------------------------------- /packages/editor/types.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | interface ColorSelectionOptions { 3 | signal?: AbortSignal; 4 | } 5 | 6 | interface ColorSelectionResult { 7 | sRGBHex: string; 8 | } 9 | 10 | interface EyeDropper { 11 | open: (options?: ColorSelectionOptions) => Promise; 12 | } 13 | 14 | interface EyeDropperConstructor { 15 | new (): EyeDropper; 16 | } 17 | declare global { 18 | interface Window { 19 | EyeDropper?: EyeDropperConstructor; 20 | } 21 | interface Array { 22 | findLastIndex( 23 | predicate: (value: T, index: number, obj: T[]) => unknown, 24 | thisArg?: any 25 | ): number 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ArrowLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ArrowLeftIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default ArrowLeftIcon; -------------------------------------------------------------------------------- /packages/editor/src/icons/TabCollapseIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const TabCollapseIcon: React.FC = ({ 5 | className = '', 6 | fill = '#252627' 7 | }: IconProps) => { 8 | return ( 9 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default TabCollapseIcon; 23 | -------------------------------------------------------------------------------- /packages/editor/src/icons/UngroupIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const UngroupIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default UngroupIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/icons/FormatItalicIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const FormatItalicIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 16 | 21 | 22 | ); 23 | }; 24 | 25 | export default FormatItalicIcon; 26 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/schema/spec/underlineSpec.ts: -------------------------------------------------------------------------------- 1 | import { MarkSpec } from 'prosemirror-model'; 2 | 3 | const TextUnderlineSpec: MarkSpec = { 4 | parseDOM: [ 5 | { tag: 'u' }, 6 | { 7 | style: 'text-decoration-line', 8 | getAttrs: (value) => { 9 | return value === 'underline' && null; 10 | }, 11 | }, 12 | { 13 | style: 'text-decoration', 14 | getAttrs: (value) => { 15 | return value === 'underline' && null; 16 | }, 17 | }, 18 | ], 19 | toDOM() { 20 | return ['u', 0]; 21 | }, 22 | }; 23 | 24 | export default TextUnderlineSpec; 25 | -------------------------------------------------------------------------------- /packages/editor/src/icons/CloseIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const CloseIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default CloseIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/icons/MoreHorizIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const MoreHorizIcon: React.FC = ({ className = '', style = {} }: IconProps) => { 5 | return ( 6 | 14 | 19 | 20 | ); 21 | }; 22 | 23 | export default MoreHorizIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/icons/BackIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const BackIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default BackIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/utils/menu/actions/copy.ts: -------------------------------------------------------------------------------- 1 | import { EditorState } from 'canva-editor/types/editor'; 2 | import { serializeLayers } from '../../layer/layers'; 3 | import { LayerId, SerializedLayerTree } from 'canva-editor/types'; 4 | 5 | export const copy = async (state: EditorState, { pageIndex, layerIds }: { pageIndex: number; layerIds: LayerId[] }) => { 6 | if (typeof window === 'undefined') return; 7 | const data: SerializedLayerTree[] = []; 8 | layerIds.map((layerId) => { 9 | data.push({ 10 | rootId: layerId, 11 | layers: serializeLayers(state.pages[pageIndex].layers, layerId), 12 | }); 13 | }); 14 | await navigator.clipboard.writeText(JSON.stringify(data)); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/editor/src/icons/DotsIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const DotsIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 24 | 25 | ); 26 | }; 27 | 28 | export default DotsIcon; 29 | -------------------------------------------------------------------------------- /packages/editor/src/icons/TwoDashesIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const TwoDashesIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 24 | 25 | ); 26 | }; 27 | 28 | export default TwoDashesIcon; 29 | -------------------------------------------------------------------------------- /packages/editor/src/icons/MoreVertIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const MoreVertIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default MoreVertIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/utils/event/event.ts: -------------------------------------------------------------------------------- 1 | export const isMouseEvent = (event: MouseEvent | TouchEvent): event is MouseEvent => { 2 | return Boolean( 3 | ((event as MouseEvent).clientX || (event as MouseEvent).clientX === 0) && 4 | ((event as MouseEvent).clientY || (event as MouseEvent).clientY === 0), 5 | ); 6 | }; 7 | export const isTouchEvent = (event: MouseEvent | TouchEvent): event is TouchEvent => { 8 | return Boolean((event as TouchEvent).touches && (event as TouchEvent).touches.length); 9 | }; 10 | 11 | export const getPosition = (event: MouseEvent | TouchEvent): { clientX: number; clientY: number } => { 12 | if (isTouchEvent(event)) { 13 | return event.touches[0]; 14 | } 15 | return event; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/rgb2hsv.ts: -------------------------------------------------------------------------------- 1 | export const rgb2hsv = ({ r, g, b, a }: { r: number; g: number; b: number; a: number }) => { 2 | let h, 3 | s = 0, 4 | v = 0; 5 | const max = Math.max(r, g, b); 6 | const min = Math.min(r, g, b); 7 | const delta = max - min; 8 | 9 | if (delta === 0) { 10 | h = 0; 11 | } else if (r === max) { 12 | h = ((g - b) / delta) % 6; 13 | } else if (g === max) { 14 | h = (b - r) / delta + 2; 15 | } else { 16 | h = (r - g) / delta + 4; 17 | } 18 | 19 | h = h * 60; 20 | if (h < 0) h += 360; 21 | 22 | s = (max === 0 ? 0 : delta / max) * 100; 23 | 24 | v = (max / 255) * 100; 25 | 26 | return { h, s, v, a }; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/editor/src/drag-and-drop/SortableContainer/defaultShouldCancelStart.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import {NodeType, closest} from '../DDUtils'; 3 | 4 | export default function defaultShouldCancelStart(event) { 5 | // Cancel sorting if the event target is an `input`, `textarea`, `select` or `option` 6 | const interactiveElements = [ 7 | NodeType.Input, 8 | NodeType.Textarea, 9 | NodeType.Select, 10 | NodeType.Option, 11 | NodeType.Button, 12 | ]; 13 | 14 | if (interactiveElements.indexOf(event.target.tagName) !== -1) { 15 | // Return true to cancel sorting 16 | return true; 17 | } 18 | 19 | if (closest(event.target, (el) => el.contentEditable === 'true')) { 20 | return true; 21 | } 22 | 23 | return false; 24 | } 25 | -------------------------------------------------------------------------------- /packages/editor/src/hooks/useLinkedRef.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useCallback, useRef } from 'react'; 2 | export function useLinkedRef(): [MutableRefObject, () => T | undefined, (data: T) => void]; 3 | export function useLinkedRef(initial: T | null): [MutableRefObject, () => T, (data: T) => void]; 4 | 5 | export function useLinkedRef( 6 | initialValue?: T, 7 | ): [MutableRefObject, () => T | undefined, (data: T) => void] { 8 | const ref = typeof initialValue === 'undefined' ? useRef() : useRef(initialValue); 9 | const getRef = useCallback(() => ref.current, []); 10 | const setRef = useCallback((data: T) => { 11 | ref.current = data; 12 | }, []); 13 | return [ref, getRef, setRef]; 14 | } 15 | -------------------------------------------------------------------------------- /packages/editor/src/icons/NextIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const NextIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default NextIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/schema/spec/boldSpec.ts: -------------------------------------------------------------------------------- 1 | import { MarkSpec } from 'prosemirror-model'; 2 | 3 | const BoldSpec: MarkSpec = { 4 | parseDOM: [ 5 | { tag: 'strong' }, 6 | // This works around a Google Docs misbehavior where 7 | // pasted content will be inexplicably wrapped in `` 8 | // tags with a font-weight normal. 9 | { tag: 'b', getAttrs: (node) => (node as HTMLElement).style.fontWeight != 'normal' && null }, 10 | { 11 | style: 'font-weight', 12 | getAttrs: (value) => /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null, 13 | }, 14 | ], 15 | toDOM() { 16 | return ['strong', 0]; 17 | }, 18 | }; 19 | 20 | export default BoldSpec; 21 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ThreeDashesIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ThreeDashesIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 16 | 26 | 27 | ); 28 | }; 29 | 30 | export default ThreeDashesIcon; 31 | -------------------------------------------------------------------------------- /packages/editor/src/utils/object/merge.ts: -------------------------------------------------------------------------------- 1 | import { isArray, mergeWith } from 'lodash'; 2 | 3 | export const mergeWithoutArray = ( 4 | obj: TObject, 5 | source: TSource, 6 | customizer?: (objVal: unknown, srcVal: unknown) => unknown, 7 | ): TObject & TSource => { 8 | if (isArray(obj) || isArray(source)) { 9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 10 | // @ts-ignore 11 | return source; 12 | } 13 | if (!customizer) { 14 | customizer = (objValue, srcValue) => { 15 | if (isArray(objValue)) { 16 | return srcValue; 17 | } 18 | return undefined; 19 | }; 20 | } 21 | return mergeWith(obj, source, customizer); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/editor/src/icons/SquareBoldIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const SquareBoldIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 14 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default SquareBoldIcon; 31 | -------------------------------------------------------------------------------- /packages/editor/src/icons/DownloadIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const DownloadIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 18 | 19 | ); 20 | }; 21 | 22 | export default DownloadIcon; 23 | -------------------------------------------------------------------------------- /packages/editor/src/icons/VideoIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const VideoIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 21 | 22 | ); 23 | }; 24 | 25 | export default VideoIcon; -------------------------------------------------------------------------------- /packages/editor/src/icons/ArrowUpIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ArrowUpIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default ArrowUpIcon; 25 | -------------------------------------------------------------------------------- /packages/editor/src/icons/HamburgerIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const HamburgerIcon: React.FC = ({ 5 | className = '', 6 | fill, 7 | }: IconProps) => { 8 | return ( 9 | 16 | 21 | 22 | ); 23 | }; 24 | 25 | export default HamburgerIcon; 26 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/utils/compute-positions-types.d.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | import type { Middleware } from '../components/Tooltip/TooltipTypes' 3 | 4 | export interface IComputePositions { 5 | elementReference?: Element | HTMLElement | null 6 | tooltipReference?: Element | HTMLElement | null 7 | tooltipArrowReference?: Element | HTMLElement | null 8 | place?: 9 | | 'top' 10 | | 'top-start' 11 | | 'top-end' 12 | | 'right' 13 | | 'right-start' 14 | | 'right-end' 15 | | 'bottom' 16 | | 'bottom-start' 17 | | 'bottom-end' 18 | | 'left' 19 | | 'left-start' 20 | | 'left-end' 21 | offset?: number 22 | strategy?: 'absolute' | 'fixed' 23 | middlewares?: Middleware[] 24 | border?: CSSProperties['border'] 25 | } 26 | -------------------------------------------------------------------------------- /packages/editor/src/hooks/useTrackingShiftKey.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export const useTrackingShiftKey = () => { 4 | const shiftKeyRef = useRef(false); 5 | useEffect(() => { 6 | const trackingShiftKey = (e: KeyboardEvent) => { 7 | shiftKeyRef.current = e.shiftKey; 8 | }; 9 | window.addEventListener('keydown', trackingShiftKey, { capture: true }); // pass editor event 10 | window.addEventListener('keyup', trackingShiftKey, { capture: true }); 11 | return () => { 12 | window.removeEventListener('keydown', trackingShiftKey, { capture: true }); 13 | window.removeEventListener('keyup', trackingShiftKey, { capture: true }); 14 | }; 15 | }, []); 16 | return shiftKeyRef; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ArrowDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ArrowDownIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default ArrowDownIcon; 25 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/schema/spec/textColorSpec.ts: -------------------------------------------------------------------------------- 1 | import { Mark, MarkSpec } from 'prosemirror-model'; 2 | 3 | const TextColorSpec: MarkSpec = { 4 | attrs: { 5 | color: { default: '' }, 6 | }, 7 | inline: true, 8 | group: 'inline', 9 | parseDOM: [ 10 | { 11 | style: 'color', 12 | getAttrs: (color) => { 13 | return { 14 | color, 15 | }; 16 | }, 17 | }, 18 | ], 19 | toDOM(mark: Mark) { 20 | const { color } = mark.attrs; 21 | let style = ''; 22 | if (color) { 23 | style += `color: ${color};`; 24 | } 25 | return ['span', { style }, 0]; 26 | }, 27 | }; 28 | 29 | export default TextColorSpec; 30 | -------------------------------------------------------------------------------- /packages/editor/src/icons/TextIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const TextIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 20 | 21 | ); 22 | }; 23 | 24 | export default TextIcon; 25 | -------------------------------------------------------------------------------- /packages/editor/src/icons/CopyIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const CopyIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default CopyIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/rgb2hsl.ts: -------------------------------------------------------------------------------- 1 | import { rgb2hsv } from './rgb2hsv'; 2 | import { hsv2hsl } from './hsv2hsl'; 3 | 4 | export const rgb2hsl = ({ r, g, b, a }: { r: number; g: number; b: number; a: number }) => { 5 | return hsv2hsl(rgb2hsv({ r, g, b, a })); 6 | }; 7 | 8 | export const rgbString2hsl = (rgb: string) => { 9 | const colors = ['r', 'g', 'b', 'a']; 10 | const colorArr = rgb 11 | .slice(rgb.indexOf('(') + 1, rgb.indexOf(')')) 12 | .split(',') 13 | .map((c) => c.trim()); 14 | const obj: { r: number; g: number; b: number; a: number } = { r: 0, g: 0, b: 0, a: 1 }; 15 | colorArr.forEach((k, i) => { 16 | const key = colors[i] as keyof typeof obj; 17 | obj[key] = parseInt(k, 10); 18 | }); 19 | return hsv2hsl(rgb2hsv(obj)); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ElementsIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ElementsIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 22 | 23 | ); 24 | }; 25 | 26 | export default ElementsIcon; -------------------------------------------------------------------------------- /packages/editor/src/icons/TrashIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const TrashIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default TrashIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/icons/TextAlignRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const TextAlignRightIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 20 | 21 | ); 22 | }; 23 | 24 | export default TextAlignRightIcon; 25 | -------------------------------------------------------------------------------- /packages/editor/src/icons/TextAlignCenterIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const TextAlignCenterIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 20 | 21 | ); 22 | }; 23 | 24 | export default TextAlignCenterIcon; 25 | -------------------------------------------------------------------------------- /packages/editor/src/hooks/useUsedFont.ts: -------------------------------------------------------------------------------- 1 | import { FontData } from 'canva-editor/types'; 2 | import { isTextLayer } from 'canva-editor/utils/layer/layers'; 3 | import { uniqBy } from 'lodash'; 4 | import { useEditor } from '.'; 5 | 6 | export const useUsedFont = () => { 7 | const { fontFamilyList } = useEditor((state) => { 8 | const fontFamilyList: FontData[] = []; 9 | state.pages.forEach((page) => { 10 | Object.entries(page.layers).forEach(([, layer]) => { 11 | if (isTextLayer(layer)) { 12 | fontFamilyList.push(...layer.data.props.fonts); 13 | } 14 | }); 15 | }); 16 | return { 17 | fontFamilyList: uniqBy(fontFamilyList, 'name'), 18 | }; 19 | }); 20 | 21 | return { usedFonts: fontFamilyList }; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/editor/src/icons/TrendingIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const TrendingIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default TrendingIcon; 25 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/helper/isActive.ts: -------------------------------------------------------------------------------- 1 | import { EditorState } from 'prosemirror-state'; 2 | import { isNodeActive } from './isNodeActive'; 3 | import { isMarkActive } from './isMarkActive'; 4 | import { getSchemaType } from './getSchemaType'; 5 | 6 | export function isActive(state: EditorState, name: string | null, attributes: Record = {}): boolean { 7 | if (!name) { 8 | return isNodeActive(state, null, attributes) || isMarkActive(state, null, attributes); 9 | } 10 | 11 | const schemaType = getSchemaType(name, state.schema); 12 | 13 | if (schemaType === 'node') { 14 | return isNodeActive(state, name, attributes); 15 | } 16 | 17 | if (schemaType === 'mark') { 18 | return isMarkActive(state, name, attributes); 19 | } 20 | 21 | return false; 22 | } 23 | -------------------------------------------------------------------------------- /packages/editor/src/icons/MinusIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const MinusIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 14 | Layer 1 15 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default MinusIcon; 29 | -------------------------------------------------------------------------------- /packages/editor/src/components/button/OutlineButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ForwardedRef } from 'react'; 2 | import BaseButton, { ButtonProps } from './Button'; 3 | 4 | type OutlineButtonProps = ButtonProps & {}; 5 | 6 | const OutlineButton = React.forwardRef( 7 | ( 8 | { icon, onClick, children, ...rest }: OutlineButtonProps, 9 | ref: ForwardedRef 10 | ) => { 11 | return ( 12 | 25 | {children} 26 | 27 | ); 28 | } 29 | ); 30 | 31 | export default OutlineButton; 32 | -------------------------------------------------------------------------------- /packages/editor/src/icons/RotateIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const RotateIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default RotateIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/utils/get-scroll-parent.ts: -------------------------------------------------------------------------------- 1 | const isScrollable = (node: Element) => { 2 | if (!(node instanceof HTMLElement || node instanceof SVGElement)) { 3 | return false 4 | } 5 | const style = getComputedStyle(node) 6 | return ['overflow', 'overflow-x', 'overflow-y'].some((propertyName) => { 7 | const value = style.getPropertyValue(propertyName) 8 | return value === 'auto' || value === 'scroll' 9 | }) 10 | } 11 | 12 | export const getScrollParent = (node: Element | null) => { 13 | if (!node) { 14 | return null 15 | } 16 | let currentParent = node.parentElement 17 | while (currentParent) { 18 | if (isScrollable(currentParent)) { 19 | return currentParent 20 | } 21 | currentParent = currentParent.parentElement 22 | } 23 | return document.scrollingElement || document.documentElement 24 | } 25 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ListBulletsIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ListBulletsIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 20 | 21 | ); 22 | }; 23 | 24 | export default ListBulletsIcon; 25 | -------------------------------------------------------------------------------- /packages/editor/src/hooks/useSelectedLayers.ts: -------------------------------------------------------------------------------- 1 | import { useEditor } from './useEditor'; 2 | import { useContext } from 'react'; 3 | import { PageContext } from '../layers/core/PageContext'; 4 | 5 | export const useSelectedLayers = () => { 6 | const { pageIndex } = useContext(PageContext); 7 | const { selectedLayerIds, selectedLayers } = useEditor((state) => { 8 | const pI = typeof pageIndex === 'undefined' ? state.activePage : pageIndex; 9 | const layerIds = (state.selectedLayers[pI] || []).filter((layerId) => { 10 | return state.pages[pI] && state.pages[pI].layers[layerId]; 11 | }); 12 | return { 13 | selectedLayerIds: layerIds, 14 | selectedLayers: layerIds.map((layerId) => state.pages[pI].layers[layerId]), 15 | }; 16 | }); 17 | 18 | return { selectedLayerIds, selectedLayers }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/editor/src/icons/BringForwardIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const BringForwardIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default BringForwardIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/icons/SendBackwardIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const SendBackwardIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default SendBackwardIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/icons/NotAllowedIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const NotAllowedIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default NotAllowedIcon; 25 | -------------------------------------------------------------------------------- /packages/editor/src/layers/GroupLayer.tsx: -------------------------------------------------------------------------------- 1 | import { LayerComponentProps, LayerComponent } from 'canva-editor/types'; 2 | import React, { PropsWithChildren } from 'react'; 3 | 4 | export interface GroupLayerProps extends LayerComponentProps { 5 | scale: number; 6 | } 7 | 8 | const GroupLayer: LayerComponent> = ({ boxSize, scale, children }) => { 9 | return ( 10 |
20 | {children} 21 |
22 | ); 23 | }; 24 | 25 | GroupLayer.info = { 26 | name: 'Group', 27 | type: 'Group', 28 | }; 29 | 30 | export default GroupLayer; 31 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/command/selectText.ts: -------------------------------------------------------------------------------- 1 | import { Command, TextSelection } from 'prosemirror-state'; 2 | 3 | export const selectText: (position: { from: number; to: number }) => Command = (position) => { 4 | return (state, dispatch) => { 5 | const tr = state.tr; 6 | const { doc } = tr; 7 | const { from, to } = position; 8 | const minPos = TextSelection.atStart(doc).from; 9 | const maxPos = TextSelection.atEnd(doc).to; 10 | const resolvedFrom = Math.min(Math.max(from, minPos), maxPos); 11 | const resolvedEnd = Math.min(Math.max(to, minPos), maxPos); 12 | const selection = TextSelection.create(doc, resolvedFrom, resolvedEnd); 13 | 14 | tr.setSelection(selection); 15 | if (dispatch) { 16 | dispatch(tr); 17 | return true; 18 | } 19 | return false; 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/editor/src/icons/UploadIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const UploadIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default UploadIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/icons/AlignLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const AlignLeftIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default AlignLeftIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/icons/AlignTopIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const AlignTopIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default AlignTopIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ColorizeIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ColorizeIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default ColorizeIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/utils/2d/boundingRect.ts: -------------------------------------------------------------------------------- 1 | // ref: https://stackoverflow.com/questions/69147768/how-to-calculate-a-bounding-box-for-a-rectangle-rotated-around-its-center 2 | import { BoxSize, Delta } from "canva-editor/types"; 3 | 4 | export const boundingRect = (boxSize: BoxSize, position: Delta, rotate: number) => { 5 | const radians = (rotate * Math.PI) / 180; 6 | const cos = Math.cos(radians); 7 | const sin = Math.sin(radians); 8 | const width = boxSize.width * Math.abs(cos) + boxSize.height * Math.abs(sin); 9 | const height = boxSize.width * Math.abs(sin) + boxSize.height * Math.abs(cos); 10 | const centerX = position.x + boxSize.width / 2; 11 | const centerY = position.y + boxSize.height / 2; 12 | return { 13 | width, 14 | height, 15 | centerX, 16 | centerY, 17 | x: centerX - width / 2, 18 | y: centerY - height / 2, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/editor/src/icons/AddNewPageIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const AddNewPageIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 18 | 22 | 23 | ); 24 | }; 25 | 26 | export default AddNewPageIcon; 27 | -------------------------------------------------------------------------------- /packages/editor/src/icons/AlignRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const AlignRightIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default AlignRightIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/utils/layer/resolveComponent.ts: -------------------------------------------------------------------------------- 1 | import { LayerComponentProps } from 'canva-editor/types'; 2 | import { JSXElementConstructor } from 'react'; 3 | import { resolvers } from '../resolvers'; 4 | 5 | export const resolveComponent = (comp: string | JSXElementConstructor): string => { 6 | const componentName = typeof comp === 'string' ? 'string' : comp.name; 7 | 8 | const getNameInResolver = (): string => { 9 | if (resolvers[componentName]) { 10 | return componentName; 11 | } 12 | 13 | for (let i = 0; i < Object.keys(resolvers).length; i++) { 14 | const name = Object.keys(resolvers)[i]; 15 | const fn = resolvers[name]; 16 | 17 | if (fn === comp) { 18 | return name; 19 | } 20 | } 21 | 22 | return comp as string; 23 | }; 24 | 25 | return getNameInResolver(); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/editor/src/icons/LayoutIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const LayoutIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 21 | 22 | ); 23 | }; 24 | 25 | export default LayoutIcon; 26 | -------------------------------------------------------------------------------- /packages/editor/src/icons/AlignBottomIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const AlignBottomIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default AlignBottomIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/utils/settings/SettingButton.tsx: -------------------------------------------------------------------------------- 1 | import EditorButton from 'canva-editor/components/EditorButton'; 2 | import { 3 | forwardRef, 4 | ForwardRefRenderFunction, 5 | HTMLProps, 6 | PropsWithChildren, 7 | } from 'react'; 8 | interface SettingButtonProps extends HTMLProps { 9 | isActive?: boolean; 10 | tooltip?: string; 11 | } 12 | const SettingButton: ForwardRefRenderFunction< 13 | HTMLDivElement, 14 | PropsWithChildren 15 | > = ({ children, tooltip, disabled, onClick, ...props }, ref) => { 16 | return ( 17 | !disabled && onClick && onClick(e)} 20 | disabled={disabled} 21 | tooltip={tooltip} 22 | {...props} 23 | > 24 | {children} 25 | 26 | ); 27 | }; 28 | export default forwardRef< 29 | HTMLDivElement, 30 | PropsWithChildren 31 | >(SettingButton); 32 | -------------------------------------------------------------------------------- /packages/editor/src/icons/HelpIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const HelpIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default HelpIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/icons/FormatUnderlineIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const FormatUnderlineIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 16 | 20 | 21 | ); 22 | }; 23 | 24 | export default FormatUnderlineIcon; 25 | -------------------------------------------------------------------------------- /packages/editor/src/utils/menu/actions/paste.ts: -------------------------------------------------------------------------------- 1 | import { EditorActions } from 'canva-editor/types/editor'; 2 | import { LayerComponentProps, SerializedLayerTree } from 'canva-editor/types'; 3 | 4 | export const paste = async ({ actions }: { actions: EditorActions }) => { 5 | if (typeof window === 'undefined') return; 6 | const data = await navigator.clipboard.readText(); 7 | try { 8 | const serializedData: SerializedLayerTree[] = JSON.parse(data); 9 | //TODO VALIDATE data 10 | serializedData.forEach((serializedLayers) => { 11 | Object.entries(serializedLayers.layers).forEach(([layerId]) => { 12 | (serializedLayers.layers[layerId].props as LayerComponentProps).position.x += 10; 13 | (serializedLayers.layers[layerId].props as LayerComponentProps).position.y += 10; 14 | }); 15 | actions.addLayerTree(serializedLayers); 16 | }); 17 | } catch (e) {} 18 | }; 19 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ExportIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ExportIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 19 | 20 | ); 21 | }; 22 | 23 | export default ExportIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/icons/LayersIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const LayersIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default LayersIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/utils/settings/sidebar/FontStyle.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { css, Global } from '@emotion/react'; 3 | import { useEditor } from 'canva-editor/hooks'; 4 | 5 | const FontStyle = () => { 6 | const { fontList } = useEditor((state) => ({ fontList: state.fontList })); 7 | const fontFaceString = useMemo(() => { 8 | const fontFaceCss: string[] = []; 9 | fontList.forEach((font) => { 10 | fontFaceCss.push(` 11 | @font-face { 12 | font-family: '${font.name}'; 13 | src: url(${font.url}) format('woff2'); 14 | font-display: block; 15 | } 16 | `); 17 | }); 18 | return fontFaceCss.join('\n'); 19 | }, [fontList]); 20 | return ( 21 | 26 | ); 27 | }; 28 | 29 | export default React.memo(FontStyle); 30 | -------------------------------------------------------------------------------- /packages/editor/src/icons/AlignCenterVerticalIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const AlignCenterVerticalIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default AlignCenterVerticalIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/command/italic.ts: -------------------------------------------------------------------------------- 1 | import { unsetMark } from './unsetMark'; 2 | import { toggleMark } from './toggleMark'; 3 | import { setMark } from './setMark'; 4 | import { Command } from 'prosemirror-state'; 5 | 6 | export const toggleItalic: Command = (...params) => { 7 | return toggleMark('italic')(...params); 8 | }; 9 | export const unsetItalic: Command = (...params) => { 10 | return unsetMark('italic')(...params); 11 | }; 12 | export const setItalic: Command = (...params) => { 13 | return setMark('italic')(...params); 14 | }; 15 | export const unsetItalicOfBlock: Command = (state, dispatch) => { 16 | const { $from, $to } = state.selection; 17 | const nodeRange = $from.blockRange($to); 18 | if (nodeRange && dispatch) { 19 | const mark = state.schema.mark('bold'); 20 | dispatch(state.tr.removeMark(nodeRange.start, nodeRange.end, mark)); 21 | return true; 22 | } 23 | return false; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/editor/src/icons/AlignCenterHorizontalIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const AlignCenterHorizontalIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default AlignCenterHorizontalIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/layers/background/getGradientBackground.ts: -------------------------------------------------------------------------------- 1 | import { GradientStyle } from "canva-editor/types"; 2 | 3 | export const getGradientBackground = (colors: string[], style: GradientStyle) => { 4 | const percent = 100 / (colors.length - 1); 5 | const colorList = colors.map((color, i) => { 6 | return `${color} ${i * percent}%`; 7 | }); 8 | switch (style) { 9 | case 'leftToRight': 10 | return `linear-gradient(90deg, ${colorList.join(', ')})`; 11 | case 'topToBottom': 12 | return `linear-gradient(${colorList.join(', ')})`; 13 | case 'topLeftToBottomRight': 14 | return `linear-gradient(135deg, ${colorList.join(', ')})`; 15 | case 'circleCenter': 16 | return `radial-gradient(circle at 50% 50%, ${colorList.join(', ')})`; 17 | case 'circleTopLeft': 18 | default: 19 | return `radial-gradient(circle at 0% 0%, ${colorList.join(', ')})`; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/components/HuePointer.tsx: -------------------------------------------------------------------------------- 1 | import { CSSObject } from "@emotion/react"; 2 | 3 | interface Props { 4 | style: CSSObject; 5 | top?: number; 6 | left: number; 7 | } 8 | 9 | export const HuePointer = ({ style, left, top = 0.5 }: Props) => { 10 | const addedCss = { 11 | top: `${top * 100}%`, 12 | left: `${left * 100}%`, 13 | }; 14 | 15 | return ( 16 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/command/bold.ts: -------------------------------------------------------------------------------- 1 | import { unsetMark } from './unsetMark'; 2 | import { toggleMark } from './toggleMark'; 3 | import { setMark } from './setMark'; 4 | import { Command } from 'prosemirror-state'; 5 | 6 | export const toggleBold: Command = (...params) => { 7 | return toggleMark('bold')(...params); 8 | }; 9 | export const unsetBold: Command = (...params) => { 10 | return unsetMark('bold')(...params); 11 | }; 12 | export const setBold: Command = (...params) => { 13 | return setMark('bold')(...params); 14 | }; 15 | 16 | export const unsetBoldOfBlock: Command = (state, dispatch) => { 17 | const tr = state.tr.setSelection(state.selection); 18 | const { $from, $to } = tr.selection; 19 | const nodeRange = $from.blockRange($to); 20 | if (nodeRange && dispatch) { 21 | dispatch(state.tr.removeMark(nodeRange.start, nodeRange.end, state.schema.mark('bold'))); 22 | return true; 23 | } 24 | return false; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/editor/src/hooks/useMobileDetect.ts: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash/debounce'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | import { singletonHook } from 'react-singleton-hook'; 4 | 5 | const initState = false; 6 | const useMobileDetect = singletonHook(initState, () => { 7 | const [isMobile, setIsMobile] = useState(initState); 8 | const width = 900; 9 | 10 | const windowListener = useCallback( 11 | debounce(() => { 12 | if (window) { 13 | setIsMobile(window.innerWidth < width); 14 | } 15 | }, 250), 16 | [] 17 | ); 18 | 19 | useEffect(() => { 20 | if (window) { 21 | setIsMobile(window.innerWidth < width); 22 | window.addEventListener('resize', windowListener); 23 | } 24 | return () => { 25 | windowListener.cancel(); 26 | window && window.removeEventListener('resize', windowListener); 27 | }; 28 | }, []); 29 | 30 | return isMobile; 31 | }); 32 | 33 | export default useMobileDetect; 34 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ShapeSettingsIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ShapeSettingsIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 14 | 22 | 30 | 38 | 39 | ); 40 | }; 41 | 42 | export default ShapeSettingsIcon; 43 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/command/toggleMark.ts: -------------------------------------------------------------------------------- 1 | import { MarkType } from 'prosemirror-model'; 2 | import { Command } from 'prosemirror-state'; 3 | import { unsetMark } from './unsetMark'; 4 | import { getMarkType } from '../helper/getMarkType'; 5 | import { isMarkActive } from '../helper/isMarkActive'; 6 | import { setMark } from './setMark'; 7 | 8 | export const toggleMark: ( 9 | typeOrName: string | MarkType, 10 | attributes?: Record, 11 | options?: { 12 | extendEmptyMarkRange?: boolean; 13 | }, 14 | ) => Command = 15 | (typeOrName, attributes = {}, options = {}) => 16 | (state, dispatch, ...rest) => { 17 | const type = getMarkType(typeOrName, state.schema); 18 | const isActive = isMarkActive(state, type); 19 | if (isActive) { 20 | return unsetMark(type, options)(state, dispatch, ...rest); 21 | } 22 | 23 | return setMark(type, attributes)(state, dispatch, ...rest); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/editor/src/icons/GithubIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const GithubIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default GithubIcon; 18 | -------------------------------------------------------------------------------- /packages/editor/src/icons/GridViewIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const GridViewIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default GridViewIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/icons/SendToBackIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const SendToBackIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default SendToBackIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/icons/SyncedIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const SyncedIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default SyncedIcon; 25 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/hsv2rgb.ts: -------------------------------------------------------------------------------- 1 | export const hsv2rgb = ({ h, s, v, a }: { h: number; s: number; v: number; a: number }) => { 2 | s = s / 100; 3 | v = v / 100; 4 | 5 | let rgb: any = []; 6 | const c = v * s; 7 | const hh = h / 60; 8 | const x = c * (1 - Math.abs((hh % 2) - 1)); 9 | const m = v - c; 10 | 11 | if (hh >= 0 && hh < 1) { 12 | rgb = [c, x, 0]; 13 | } else if (hh >= 1 && hh < 2) { 14 | rgb = [x, c, 0]; 15 | } else if (hh >= 2 && hh < 3) { 16 | rgb = [0, c, x]; 17 | } else if (h >= 3 && hh < 4) { 18 | rgb = [0, x, c]; 19 | } else if (h >= 4 && hh < 5) { 20 | rgb = [x, 0, c]; 21 | } else if (h >= 5 && hh <= 6) { 22 | rgb = [c, 0, x]; 23 | } else { 24 | rgb = [0, 0, 0]; 25 | } 26 | 27 | return { 28 | r: Math.round(255 * (rgb[0] + m)), 29 | g: Math.round(255 * (rgb[1] + m)), 30 | b: Math.round(255 * (rgb[2] + m)), 31 | a, 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/editor/src/icons/FormatBoldIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const FormatBoldIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 19 | 20 | ); 21 | }; 22 | 23 | export default FormatBoldIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/helper/getMarkAttributes.ts: -------------------------------------------------------------------------------- 1 | import { EditorState } from 'prosemirror-state'; 2 | import { Mark, MarkType } from 'prosemirror-model'; 3 | import { getMarkType } from './getMarkType'; 4 | 5 | export function getMarkAttributes(state: EditorState, typeOrName: string | MarkType): Record { 6 | const type = getMarkType(typeOrName, state.schema); 7 | const { from, to, empty } = state.selection; 8 | const marks: Mark[] = []; 9 | 10 | if (empty) { 11 | if (state.storedMarks) { 12 | marks.push(...state.storedMarks); 13 | } 14 | 15 | marks.push(...state.selection.$head.marks()); 16 | } else { 17 | state.doc.nodesBetween(from, to, (node) => { 18 | marks.push(...node.marks); 19 | }); 20 | } 21 | 22 | const mark = marks.find((markItem) => markItem.type.name === type.name); 23 | 24 | if (!mark) { 25 | return {}; 26 | } 27 | 28 | return { ...mark.attrs }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/editor/src/drag-and-drop/SortableHandle.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import * as React from 'react'; 3 | import {findDOMNode} from 'react-dom'; 4 | 5 | import {provideDisplayName} from './DDUtils'; 6 | 7 | export default function sortableHandle( 8 | WrappedComponent, 9 | config = {withRef: false}, 10 | ) { 11 | return class WithSortableHandle extends React.Component { 12 | static displayName = provideDisplayName('sortableHandle', WrappedComponent); 13 | 14 | componentDidMount() { 15 | const node = findDOMNode(this); 16 | node.sortableHandle = true; 17 | } 18 | 19 | getWrappedInstance() { 20 | return this.wrappedInstance.current; 21 | } 22 | 23 | wrappedInstance = React.createRef(); 24 | 25 | render() { 26 | const ref = config.withRef ? this.wrappedInstance : null; 27 | 28 | return ; 29 | } 30 | }; 31 | } 32 | 33 | export function isSortableHandle(node) { 34 | return node.sortableHandle != null; 35 | } 36 | -------------------------------------------------------------------------------- /packages/editor/src/icons/TextAlignLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const TextAlignLeftIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 16 | 17 | 21 | 22 | 27 | 28 | ); 29 | }; 30 | 31 | export default TextAlignLeftIcon; 32 | -------------------------------------------------------------------------------- /packages/editor/src/search-autocomplete/utils/config.ts: -------------------------------------------------------------------------------- 1 | export interface DefaultTheme { 2 | height?: string 3 | border?: string 4 | borderRadius?: string 5 | backgroundColor?: string 6 | boxShadow?: string 7 | hoverBackgroundColor?: string 8 | color?: string 9 | fontSize?: string 10 | fontFamily?: string 11 | iconColor?: string 12 | lineColor?: string 13 | placeholderColor?: string 14 | zIndex?: number 15 | clearIconMargin?: string 16 | searchIconMargin?: string 17 | } 18 | 19 | const defaultTheme: DefaultTheme = { 20 | height: '44px', 21 | border: '1px solid #dfe1e5', 22 | borderRadius: '4px', 23 | backgroundColor: 'white', 24 | boxShadow: 'none', 25 | hoverBackgroundColor: '#eee', 26 | color: '#212121', 27 | fontSize: '16px', 28 | fontFamily: 'Arial', 29 | iconColor: '#fff', 30 | lineColor: 'rgb(232, 234, 237)', 31 | placeholderColor: '#fff', 32 | zIndex: 0, 33 | clearIconMargin: '3px 10px 0 0', 34 | searchIconMargin: '0 0 0 8px' 35 | } 36 | 37 | export { defaultTheme } -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/hex2rgb.ts: -------------------------------------------------------------------------------- 1 | export const hex2rgb = (hex: string) => { 2 | if (hex[0] === '#') hex = hex.substring(1); 3 | 4 | if (hex.length < 6) { 5 | return { 6 | r: parseInt(hex[0] + hex[0], 16), 7 | g: parseInt(hex[1] + hex[1], 16), 8 | b: parseInt(hex[2] + hex[2], 16), 9 | a: hex.length === 4 ? Math.round((parseInt(hex[3] + hex[3], 16) / 255) * 100) / 100 : 1, 10 | }; 11 | } 12 | 13 | return { 14 | r: parseInt(hex.substring(0, 2), 16), 15 | g: parseInt(hex.substring(2, 4), 16), 16 | b: parseInt(hex.substring(4, 6), 16), 17 | a: hex.length === 8 ? Math.round((parseInt(hex.substring(6, 8), 16) / 255) * 100) / 100 : 1, 18 | }; 19 | }; 20 | 21 | export const hex2rgbString = (hex: string) => { 22 | const { r, g, b, a } = hex2rgb(hex); 23 | if (a === 1) { 24 | return `rgb(${r}, ${g}, ${b})`; 25 | } else { 26 | return `rgba(${r}, ${g}, ${b}, ${a})`; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /packages/editor/src/icons/BringToFontIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const BringToFontIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default BringToFontIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/layout/pages/EditorContent.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { DesignFrame } from '../../components/editor'; 3 | const defaultData = [ 4 | { 5 | name: '', 6 | notes: '', 7 | layers: { 8 | ROOT: { 9 | type: { 10 | resolvedName: 'RootLayer', 11 | }, 12 | props: { 13 | boxSize: { 14 | width: 1640, 15 | height: 924, 16 | }, 17 | position: { 18 | x: 0, 19 | y: 0, 20 | }, 21 | rotate: 0, 22 | color: 'rgb(255, 255, 255)', 23 | image: null, 24 | }, 25 | locked: false, 26 | child: [], 27 | parent: null, 28 | }, 29 | }, 30 | }, 31 | ]; 32 | 33 | interface Props { 34 | data?: any; 35 | onChanges?: (changes: any) => void; 36 | } 37 | const EditorContent: FC = ({ data = defaultData, ...props }) => { 38 | return ; 39 | }; 40 | 41 | export default EditorContent; 42 | -------------------------------------------------------------------------------- /packages/editor/src/icons/BackgroundSelectionIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const BackgroundSelectionIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default BackgroundSelectionIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/icons/FacebookIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const FacebookIcon: React.FC = () => { 5 | return ( 6 | 14 | 15 | 19 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default FacebookIcon; 29 | -------------------------------------------------------------------------------- /packages/editor/src/layers/common/FontStyle.tsx: -------------------------------------------------------------------------------- 1 | import { css, Global } from '@emotion/react'; 2 | import React, { FC, useMemo } from 'react'; 3 | import { FontData } from '../../types'; 4 | import { handleFontStyle } from 'canva-editor/utils/fontHelper'; 5 | 6 | export interface FontStyleProps { 7 | font: FontData; 8 | } 9 | 10 | const FontStyle: FC = ({ font }) => { 11 | const fontFaceString = useMemo(() => { 12 | const fontFaceCss: string[] = []; 13 | fontFaceCss.push(` 14 | @font-face { 15 | font-family: '${font.name}'; 16 | ${handleFontStyle(font.style)} 17 | src: url(${font.url}) format('woff2'); 18 | font-display: block; 19 | } 20 | `); 21 | return fontFaceCss.join('\n'); 22 | }, [font]); 23 | 24 | return ( 25 | 30 | ); 31 | }; 32 | 33 | export default React.memo(FontStyle); 34 | -------------------------------------------------------------------------------- /packages/editor/src/icons/TextAlignJustifyIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const TextAlignJustifyIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 16 | 17 | 21 | 22 | 27 | 28 | ); 29 | }; 30 | 31 | export default TextAlignJustifyIcon; 32 | -------------------------------------------------------------------------------- /packages/editor/src/icons/GroupingIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const GroupingIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | }; 20 | 21 | export default GroupingIcon; 22 | -------------------------------------------------------------------------------- /packages/editor/src/hooks/useClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import { RefObject, useCallback, useEffect, useRef } from 'react'; 2 | 3 | type Handler = (event: MouseEvent) => void; 4 | 5 | export const useClickOutside = ( 6 | ref: RefObject, 7 | handler: Handler, 8 | mouseEvent: 'mousedown' | 'mouseup' = 'mousedown', 9 | options?: AddEventListenerOptions, 10 | ): void => { 11 | const savedHandler = useRef(handler); 12 | const handleFunc = useCallback( 13 | (event: MouseEvent) => { 14 | const el = ref?.current; 15 | if (!el || el.contains(event.target as Node)) { 16 | return; 17 | } 18 | 19 | savedHandler.current(event); 20 | }, 21 | [ref, savedHandler], 22 | ); 23 | useEffect(() => { 24 | window.addEventListener(mouseEvent, handleFunc, options); 25 | return () => { 26 | window.removeEventListener(mouseEvent, handleFunc, options); 27 | }; 28 | }, [handleFunc]); 29 | }; 30 | 31 | export default useClickOutside; 32 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ResizeIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ResizeIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 18 | 19 | ); 20 | }; 21 | 22 | export default ResizeIcon; 23 | -------------------------------------------------------------------------------- /packages/editor/src/components/PageRender.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, ForwardRefRenderFunction, PropsWithChildren } from 'react'; 2 | import { BoxSize } from '../types'; 3 | import { getTransformStyle } from '../layers'; 4 | 5 | const PageRender: ForwardRefRenderFunction> = ( 6 | { boxSize, scale, children }, 7 | ref, 8 | ) => { 9 | return ( 10 |
25 | {children} 26 |
27 | ); 28 | }; 29 | export default forwardRef>(PageRender); 30 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/components/Tooltip/styles.module.css: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | padding: 4px 8px; 3 | border-radius: 3px; 4 | font-size: 90%; 5 | width: max-content; 6 | } 7 | 8 | .arrow { 9 | width: 8px; 10 | height: 8px; 11 | } 12 | 13 | [class*='react-tooltip__place-top'] > .arrow { 14 | transform: rotate(45deg); 15 | } 16 | 17 | [class*='react-tooltip__place-right'] > .arrow { 18 | transform: rotate(135deg); 19 | } 20 | 21 | [class*='react-tooltip__place-bottom'] > .arrow { 22 | transform: rotate(225deg); 23 | } 24 | 25 | [class*='react-tooltip__place-left'] > .arrow { 26 | transform: rotate(315deg); 27 | } 28 | 29 | /** Types variant **/ 30 | .dark { 31 | background: #222; 32 | color: #fff; 33 | } 34 | 35 | .light { 36 | background-color: #fff; 37 | color: #222; 38 | } 39 | 40 | .success { 41 | background-color: #8dc572; 42 | color: #fff; 43 | } 44 | 45 | .warning { 46 | background-color: #f0ad4e; 47 | color: #fff; 48 | } 49 | 50 | .error { 51 | background-color: #be6464; 52 | color: #fff; 53 | } 54 | 55 | .info { 56 | background-color: #337ab7; 57 | color: #fff; 58 | } 59 | -------------------------------------------------------------------------------- /packages/editor/src/icons/DocumentIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const DocumentIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default DocumentIcon; 25 | -------------------------------------------------------------------------------- /packages/editor/src/layout/AppLayerSettings.tsx: -------------------------------------------------------------------------------- 1 | import { useSelectedLayers } from 'canva-editor/hooks'; 2 | import { LayerSettings } from 'canva-editor/utils/settings'; 3 | 4 | const AppLayerSettings = () => { 5 | const { selectedLayerIds } = useSelectedLayers(); 6 | return ( 7 |
0 ? 'flex' : 'none', 21 | justifyContent: 'center', 22 | zIndex: 11, 23 | height: 65, 24 | }, 25 | }} 26 | > 27 | 28 |
29 | ); 30 | }; 31 | 32 | export default AppLayerSettings; 33 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ImageIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ImageIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 21 | 22 | ); 23 | }; 24 | 25 | export default ImageIcon; -------------------------------------------------------------------------------- /packages/editor/src/components/popover/Popover.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, ForwardRefRenderFunction, PropsWithChildren, useEffect, useState } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PopoverWrapper, { PopoverProps } from './PopoverWrapper'; 4 | 5 | const Popover: ForwardRefRenderFunction< 6 | HTMLDivElement, 7 | PropsWithChildren 8 | > = ({ open, children, element, ...props }, ref) => { 9 | const [container] = useState(window.document.createElement('div')); 10 | useEffect(() => { 11 | !element && window.document.body.appendChild(container); 12 | return () => { 13 | !element && window.document.body.removeChild(container); 14 | }; 15 | }, [element, container, open]); 16 | const child = ( 17 | 18 | {children} 19 | 20 | ); 21 | return open ? ReactDOM.createPortal(child, element || container) : null; 22 | }; 23 | 24 | export default forwardRef>( 25 | Popover, 26 | ); 27 | -------------------------------------------------------------------------------- /packages/editor/src/icons/DuplicateIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const DuplicateIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 20 | 26 | 27 | ); 28 | }; 29 | 30 | export default DuplicateIcon; 31 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This function debounce the received function 3 | * @param { function } func Function to be debounced 4 | * @param { number } wait Time to wait before execut the function 5 | * @param { boolean } immediate Param to define if the function will be executed immediately 6 | */ 7 | const debounce = ( 8 | func: (...args: any[]) => void, 9 | wait?: number, 10 | immediate?: boolean 11 | ) => { 12 | let timeout: any; 13 | 14 | return function debounced(this: typeof func, ...args: any[]) { 15 | const later = () => { 16 | timeout = null; 17 | if (!immediate) { 18 | func.apply(this, args); 19 | } 20 | }; 21 | 22 | if (immediate && !timeout) { 23 | /** 24 | * there's not need to clear the timeout 25 | * since we expect it to resolve and set `timeout = null` 26 | */ 27 | func.apply(this, args); 28 | timeout = setTimeout(later, wait); 29 | } 30 | 31 | if (!immediate) { 32 | if (timeout) { 33 | clearTimeout(timeout); 34 | } 35 | timeout = setTimeout(later, wait); 36 | } 37 | }; 38 | }; 39 | 40 | export default debounce; 41 | -------------------------------------------------------------------------------- /packages/editor/src/components/carousel/HorizontalCarousel.css: -------------------------------------------------------------------------------- 1 | /* HorizontalCarousel.css */ 2 | .horizontal-carousel { 3 | position: relative; 4 | overflow: hidden; 5 | } 6 | 7 | .carousel-container { 8 | display: flex; 9 | overflow-x: auto; 10 | scroll-behavior: smooth; 11 | width: 100%; 12 | scrollbar-width: thin; 13 | /* Firefox */ 14 | scrollbar-color: transparent transparent; 15 | /* Firefox */ 16 | overflow-y: hidden; 17 | word-break: normal; 18 | } 19 | 20 | .carousel-container::-webkit-scrollbar { 21 | width: 6px; 22 | } 23 | 24 | .carousel-container::-webkit-scrollbar-thumb { 25 | background-color: transparent; 26 | } 27 | 28 | .carousel-item { 29 | box-sizing: border-box; 30 | flex-shrink: 0; 31 | margin-right: 10px; 32 | } 33 | 34 | .arrow { 35 | position: absolute; 36 | top: 0; 37 | bottom: 0; 38 | font-size: 24px; 39 | border: none; 40 | cursor: pointer; 41 | } 42 | 43 | .arrow.left { 44 | left: 0; 45 | background: linear-gradient(to right, #fff, rgb(255 255 255 / 50%)); 46 | } 47 | 48 | .arrow.right { 49 | right: 0; 50 | background: linear-gradient(to right, rgb(255 255 255 / 70%), #fff); 51 | } -------------------------------------------------------------------------------- /packages/editor/src/utils/2d/getPositionChangesBetweenTwoCorners.ts: -------------------------------------------------------------------------------- 1 | import { Direction } from 'canva-editor/types/resize'; 2 | import { visualCorners } from './visualCorners'; 3 | 4 | type Corner = ReturnType; 5 | export const getPositionChangesBetweenTwoCorners = (oldCorners: Corner, newCorners: Corner, direction: Direction) => { 6 | let changeX: number, changeY: number; 7 | switch (direction) { 8 | case 'topRight': 9 | changeX = newCorners.sw.x - oldCorners.sw.x; 10 | changeY = newCorners.sw.y - oldCorners.sw.y; 11 | break; 12 | case 'bottomLeft': 13 | changeX = newCorners.ne.x - oldCorners.ne.x; 14 | changeY = newCorners.ne.y - oldCorners.ne.y; 15 | break; 16 | case 'top': 17 | case 'left': 18 | case 'topLeft': 19 | changeX = newCorners.se.x - oldCorners.se.x; 20 | changeY = newCorners.se.y - oldCorners.se.y; 21 | break; 22 | default: 23 | changeX = newCorners.nw.x - oldCorners.nw.x; 24 | changeY = newCorners.nw.y - oldCorners.nw.y; 25 | } 26 | return { 27 | changeX, 28 | changeY, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/editor/src/icons/OfflineIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const OfflineIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 20 | 24 | 25 | ); 26 | }; 27 | 28 | export default OfflineIcon; 29 | -------------------------------------------------------------------------------- /packages/editor/src/icons/FormatUppercaseIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const FormatUppercaseIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 16 | 21 | 22 | ); 23 | }; 24 | 25 | export default FormatUppercaseIcon; 26 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/command/textColor.ts: -------------------------------------------------------------------------------- 1 | import { setMark } from './setMark'; 2 | import { Command } from 'prosemirror-state'; 3 | 4 | export const setColor: (color: string) => Command = (color) => { 5 | return (state, dispatch, view) => { 6 | setMark('color', { color })(state, dispatch, view); 7 | return true; 8 | }; 9 | }; 10 | export const setColorForBlock: (color: string) => Command = (color) => { 11 | return (state, dispatch) => { 12 | const tr = state.tr.setSelection(state.selection); 13 | const { $from, $to } = tr.selection; 14 | const nodeRange = $from.blockRange($to); 15 | if (nodeRange && dispatch) { 16 | state.doc.nodesBetween(nodeRange.start, nodeRange.end, (node, pos) => { 17 | if (node.isBlock) { 18 | const nodeType = node.type; 19 | const attrs = { 20 | ...node.attrs, 21 | color, 22 | }; 23 | tr.setNodeMarkup(pos, nodeType, attrs, node.marks); 24 | } 25 | }); 26 | dispatch(tr); 27 | return true; 28 | } 29 | return false; 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/editor/src/icons/FrameIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const FrameIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 15 | 16 | 21 | 22 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default FrameIcon; 34 | -------------------------------------------------------------------------------- /packages/editor/src/layers/core/TransformLayer.tsx: -------------------------------------------------------------------------------- 1 | import { BoxSize, Delta } from 'canva-editor/types'; 2 | import React, { forwardRef, ForwardRefRenderFunction, PropsWithChildren } from 'react'; 3 | import { getTransformStyle } from '../index'; 4 | 5 | export interface TransformLayerProps { 6 | boxSize: BoxSize; 7 | rotate: number; 8 | position: Delta; 9 | transparency?: number; 10 | } 11 | const TransformLayer: ForwardRefRenderFunction> = ( 12 | { boxSize, rotate, position, transparency, children }, 13 | ref, 14 | ) => { 15 | return ( 16 |
30 | {children} 31 |
32 | ); 33 | }; 34 | 35 | export default forwardRef>(TransformLayer); 36 | -------------------------------------------------------------------------------- /packages/editor/src/utils/deserialize.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | LayerComponentProps, 3 | LayerId, 4 | SerializedLayer, 5 | SerializedPage, 6 | } from "canva-editor/types"; 7 | import { createElement, ReactElement } from "react"; 8 | import { resolvers } from "./resolvers"; 9 | import TransformLayer from "canva-editor/layers/core/TransformLayer"; 10 | 11 | const renderLayer = ( 12 | layers: Record, 13 | layerId: LayerId 14 | ): ReactElement => { 15 | const child = layers[layerId].child.map((lId) => 16 | renderWrapperLayer(layers, lId) 17 | ); 18 | return createElement( 19 | resolvers[layers[layerId].type.resolvedName], 20 | { ...(layers[layerId].props as LayerComponentProps), key: layerId }, 21 | child 22 | ); 23 | }; 24 | 25 | const renderWrapperLayer = ( 26 | layers: Record, 27 | layerId: LayerId 28 | ) => { 29 | return createElement( 30 | TransformLayer, 31 | { ...(layers[layerId].props as LayerComponentProps), key: layerId }, 32 | renderLayer(layers, layerId) 33 | ); 34 | }; 35 | export const renderPages = (serializedPages: SerializedPage[]) => { 36 | return serializedPages.map((serializedPage) => { 37 | return renderWrapperLayer(serializedPage.layers, "ROOT"); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/editor/src/components/dialog/QuickBoxDialog.tsx: -------------------------------------------------------------------------------- 1 | import CloseIcon from 'canva-editor/icons/CloseIcon'; 2 | import { forwardRef, ForwardRefRenderFunction, PropsWithChildren } from 'react'; 3 | import EditorButton from '../EditorButton'; 4 | 5 | interface Props { 6 | open: boolean; 7 | onClose: () => void; 8 | } 9 | const QuickBoxDialog: ForwardRefRenderFunction< 10 | HTMLDivElement, 11 | PropsWithChildren 12 | > = ({ open, children, onClose, ...props }, ref) => { 13 | const child = ( 14 |
29 |
30 | 31 | 32 | 33 |
34 | {children} 35 |
36 | ); 37 | return open ? child : null; 38 | }; 39 | 40 | export default forwardRef>( 41 | QuickBoxDialog 42 | ); 43 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ListNumbersIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ListNumbersIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 20 | 21 | ); 22 | }; 23 | 24 | export default ListNumbersIcon; 25 | -------------------------------------------------------------------------------- /packages/editor/src/utils/menu/actions/duplicate.ts: -------------------------------------------------------------------------------- 1 | import { serializeLayers } from '../../layer/layers'; 2 | import { cloneDeep } from 'lodash'; 3 | import { LayerComponentProps, LayerId, SerializedLayerTree } from 'canva-editor/types'; 4 | import { EditorActions, EditorState } from 'canva-editor/types/editor'; 5 | 6 | export const duplicate = ( 7 | state: EditorState, 8 | { 9 | pageIndex, 10 | layerIds, 11 | actions, 12 | }: { 13 | pageIndex: number; 14 | layerIds: LayerId[]; 15 | actions: EditorActions; 16 | }, 17 | ) => { 18 | const data: SerializedLayerTree[] = []; 19 | layerIds.map((layerId) => { 20 | data.push({ 21 | rootId: layerId, 22 | layers: cloneDeep(serializeLayers(state.pages[pageIndex].layers, layerId)), 23 | }); 24 | }); 25 | actions.addLayerTrees( 26 | data.map((serializedLayers) => { 27 | Object.entries(serializedLayers.layers).forEach(([layerId]) => { 28 | (serializedLayers.layers[layerId].props as LayerComponentProps).position.x += 10; 29 | (serializedLayers.layers[layerId].props as LayerComponentProps).position.y += 10; 30 | }); 31 | return serializedLayers; 32 | }), 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/editor/src/utils/2d/visualCorners.tsx: -------------------------------------------------------------------------------- 1 | import { BoxSize, Delta } from 'canva-editor/types'; 2 | import { applyToPoint } from '../applyToPoint'; 3 | 4 | export const visualCorners = (size: BoxSize, matrix: WebKitCSSMatrix, position: Delta) => { 5 | const halfWidth = size.width / 2; 6 | const halfHeight = size.height / 2; 7 | const nw = { x: -halfWidth, y: -halfHeight }; 8 | const ne = { x: halfWidth, y: -halfHeight }; 9 | const sw = { x: -halfWidth, y: halfHeight }; 10 | const se = { x: halfWidth, y: halfHeight }; 11 | const tnw = applyToPoint(matrix, nw); 12 | const tne = applyToPoint(matrix, ne); 13 | const tsw = applyToPoint(matrix, sw); 14 | const tse = applyToPoint(matrix, se); 15 | return { 16 | nw: { 17 | x: tnw.x + halfWidth + position.x, 18 | y: tnw.y + halfHeight + position.y, 19 | }, 20 | ne: { 21 | x: tne.x + halfWidth + position.x, 22 | y: tne.y + halfHeight + position.y, 23 | }, 24 | sw: { 25 | x: tsw.x + halfWidth + position.x, 26 | y: tsw.y + halfHeight + position.y, 27 | }, 28 | se: { 29 | x: tse.x + halfWidth + position.x, 30 | y: tse.y + halfHeight + position.y, 31 | }, 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/editor/src/icons/SyncingIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const SyncingIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 20 | 21 | 29 | 37 | 38 | ); 39 | }; 40 | 41 | export default SyncingIcon; 42 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/EditorContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useRef } from 'react'; 2 | import { createEditor } from './core/helper/createEditor'; 3 | import { useEditor } from '../../hooks'; 4 | import { TextEditor } from './interfaces'; 5 | import { selectText } from './core/command/selectText'; 6 | 7 | interface EditorContentProps { 8 | editor: TextEditor; 9 | } 10 | const EditorContent: FC = ({ editor }) => { 11 | const ref = useRef(null); 12 | const { actions } = useEditor(); 13 | useEffect(() => { 14 | actions.history.new(); 15 | const editingEditor = createEditor({ 16 | content: editor.dom.innerHTML, 17 | ele: ref.current, 18 | handleDOMEvents: { 19 | blur: () => { 20 | actions.closeTextEditor(); 21 | }, 22 | }, 23 | }); 24 | selectText({ from: editingEditor.state.doc.content.size, to: editingEditor.state.doc.content.size })( 25 | editingEditor.state, 26 | editingEditor.dispatch, 27 | ); 28 | editingEditor.focus(); 29 | actions.setOpeningEditor(editingEditor); 30 | }, [actions]); 31 | return
; 32 | }; 33 | export default React.memo(EditorContent); 34 | -------------------------------------------------------------------------------- /packages/editor/src/layers/content/TextContent.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from 'react'; 2 | import { EffectSettings, FontData, LayerComponentProps } from '../../types'; 3 | import { getTextEffectStyle } from '../text/textEffect'; 4 | 5 | export interface TextContentProps extends LayerComponentProps { 6 | id: string; 7 | text: string; 8 | scale: number; 9 | fonts: FontData[]; 10 | colors: string[]; 11 | fontSizes: number[]; 12 | effect: { 13 | name: string; 14 | settings: EffectSettings; 15 | } | null; 16 | } 17 | 18 | export const TextContent: FC = ({ 19 | id, 20 | text, 21 | colors, 22 | fontSizes, 23 | effect, 24 | }) => { 25 | const styles = getTextEffectStyle( 26 | effect?.name || 'none', 27 | effect?.settings as EffectSettings, 28 | colors[0], 29 | fontSizes[0] 30 | ); 31 | const textId = `text-${id}`; 32 | useEffect(() => { 33 | const testEl = document.getElementById(textId); 34 | if (testEl) { 35 | testEl.innerHTML = text; 36 | } 37 | }, [text]); 38 | return ( 39 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/editor/src/drag-and-drop/Manager.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | export default class Manager { 3 | refs = {}; 4 | 5 | add(collection, ref) { 6 | if (!this.refs[collection]) { 7 | this.refs[collection] = []; 8 | } 9 | 10 | this.refs[collection].push(ref); 11 | } 12 | 13 | remove(collection, ref) { 14 | const index = this.getIndex(collection, ref); 15 | 16 | if (index !== -1) { 17 | this.refs[collection].splice(index, 1); 18 | } 19 | } 20 | 21 | isActive() { 22 | return this.active; 23 | } 24 | 25 | getActive() { 26 | return this.refs[this.active.collection].find( 27 | // eslint-disable-next-line eqeqeq 28 | ({node}) => node.sortableInfo.index == this.active.index, 29 | ); 30 | } 31 | 32 | getIndex(collection, ref) { 33 | return this.refs[collection].indexOf(ref); 34 | } 35 | 36 | getOrderedRefs(collection = this.active.collection) { 37 | return this.refs[collection].sort(sortByIndex); 38 | } 39 | } 40 | 41 | function sortByIndex( 42 | { 43 | node: { 44 | sortableInfo: {index: index1}, 45 | }, 46 | }, 47 | { 48 | node: { 49 | sortableInfo: {index: index2}, 50 | }, 51 | }, 52 | ) { 53 | return index1 - index2; 54 | } 55 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/components/Pointer.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | zIndex: number; 3 | top?: number; 4 | left: number; 5 | color: string; 6 | } 7 | 8 | export const Pointer = ({ zIndex, color, left, top = 0.5 }: Props) => { 9 | const style = { 10 | top: `${top * 100}%`, 11 | left: `${left * 100}%`, 12 | }; 13 | 14 | return ( 15 |
29 |
41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/editor/src/utils/2d/rectangleInsideAnother.ts: -------------------------------------------------------------------------------- 1 | import { BoxData } from 'canva-editor/types'; 2 | import { isPointInsideBox } from './isPointInsideBox'; 3 | import { visualCorners } from './visualCorners'; 4 | import { getTransformStyle } from 'canva-editor/layers'; 5 | 6 | export const rectangleInsideAnother = (active: BoxData, target: BoxData) => { 7 | const activeMatrix = new WebKitCSSMatrix(getTransformStyle({ rotate: active.rotate })); 8 | const activeCorners = visualCorners( 9 | { 10 | width: active.boxSize.width, 11 | height: active.boxSize.height, 12 | }, 13 | activeMatrix, 14 | { 15 | x: active.position.x, 16 | y: active.position.y, 17 | }, 18 | ); 19 | const targetMatrix = new WebKitCSSMatrix(getTransformStyle({ rotate: target.rotate })); 20 | const targetCorners = visualCorners( 21 | { 22 | width: target.boxSize.width, 23 | height: target.boxSize.height, 24 | }, 25 | targetMatrix, 26 | { 27 | x: target.position.x, 28 | y: target.position.y, 29 | }, 30 | ); 31 | 32 | const activePoints = [activeCorners.nw, activeCorners.ne, activeCorners.se, activeCorners.sw]; 33 | return !activePoints.find((point) => !isPointInsideBox(point, targetCorners)); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/editor/src/components/button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { ButtonHTMLAttributes, ForwardedRef } from 'react'; 2 | 3 | export type ButtonProps = ButtonHTMLAttributes & { 4 | icon?: JSX.Element; 5 | text?: string; 6 | style?: any; 7 | }; 8 | 9 | const Button = React.forwardRef( 10 | ( 11 | { icon, text, onClick, type = 'button', style, children, ...rest }: ButtonProps, 12 | ref: ForwardedRef 13 | ) => { 14 | return ( 15 | 41 | ); 42 | } 43 | ); 44 | 45 | export default Button; 46 | -------------------------------------------------------------------------------- /packages/editor/src/layers/RootLayer.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, PropsWithChildren } from 'react'; 2 | import { useLayer } from '../hooks'; 3 | import { LayerComponent } from 'canva-editor/types'; 4 | import { RootContentProps, RootContent, ImageContentProps } from '.'; 5 | 6 | export interface RootLayerProps extends Omit { 7 | image?: ImageContentProps['image']|null; 8 | } 9 | const RootLayer: LayerComponent> = ({ 10 | boxSize, 11 | children, 12 | color, 13 | gradientBackground, 14 | image, 15 | position, 16 | rotate, 17 | scale, 18 | }) => { 19 | const { actions } = useLayer(); 20 | return ( 21 | 22 | 31 | (image) && actions.openImageEditor({ boxSize, position, rotate, image }) 32 | } 33 | /> 34 | {children} 35 | 36 | ); 37 | }; 38 | 39 | RootLayer.info = { 40 | name: 'Main', 41 | type: 'Root', 42 | }; 43 | export default RootLayer; 44 | -------------------------------------------------------------------------------- /packages/editor/src/icons/NotesIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const NotesIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 18 | 24 | 25 | ); 26 | }; 27 | 28 | export default NotesIcon; 29 | -------------------------------------------------------------------------------- /packages/editor/src/utils/keyboard/keyboard.ts: -------------------------------------------------------------------------------- 1 | import { isMacOs } from 'react-device-detect'; 2 | 3 | export const normalizeKeyName = (name: string) => { 4 | const parts = name.split(/-(?!$)/); 5 | let result = parts[parts.length - 1]; 6 | if (result == 'Space') result = ' '; 7 | let alt, ctrl, shift, meta; 8 | for (let i = 0; i < parts.length - 1; i++) { 9 | const mod = parts[i]; 10 | if (/^(cmd|meta|m)$/i.test(mod)) meta = true; 11 | else if (/^a(lt)?$/i.test(mod)) alt = true; 12 | else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true; 13 | else if (/^s(hift)?$/i.test(mod)) shift = true; 14 | else if (/^mod$/i.test(mod)) { 15 | if (isMacOs) meta = true; 16 | else ctrl = true; 17 | } else throw new Error('Unrecognized modifier name: ' + mod); 18 | } 19 | if (alt) result = 'Alt-' + result; 20 | if (ctrl) result = 'Ctrl-' + result; 21 | if (meta) result = 'Meta-' + result; 22 | if (shift) result = 'Shift-' + result; 23 | return result; 24 | }; 25 | 26 | export const modifiers = (name: string, event: KeyboardEvent, shift = true) => { 27 | if (event.altKey) name = 'Alt-' + name; 28 | if (event.ctrlKey) name = 'Ctrl-' + name; 29 | if (event.metaKey) name = 'Meta-' + name; 30 | if (shift && event.shiftKey) name = 'Shift-' + name; 31 | return name; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/editor/src/layers/ShapeLayer.tsx: -------------------------------------------------------------------------------- 1 | import { LayerComponent } from 'canva-editor/types'; 2 | import { ShapeContent, ShapeContentProps } from './content/ShapeContent'; 3 | 4 | export type ShapeLayerProps = ShapeContentProps; 5 | const ShapeLayer: LayerComponent = ({ 6 | boxSize, 7 | clipPath, 8 | scale = 1, 9 | rotate, 10 | position, 11 | color, 12 | gradientBackground, 13 | roundedCorners = 0, 14 | border, 15 | shapeSize, 16 | }) => { 17 | const handleDoubleClick = () => { 18 | // TODO: Add text 19 | }; 20 | return ( 21 |
32 | 44 |
45 | ); 46 | }; 47 | 48 | ShapeLayer.info = { 49 | name: 'Shape', 50 | type: 'Shape', 51 | }; 52 | export default ShapeLayer; 53 | -------------------------------------------------------------------------------- /packages/editor/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License with No Resale Clause 2 | 3 | The MIT License (MIT) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | 1. **No Resale Clause:** The person or entity obtaining the Software is allowed to use the Software for creating and running websites, offering services, and selling services based on the Software. However, the Software or any modified version of it cannot be resold or sublicensed, in whole or in part, without explicit written permission from the original author. 8 | 9 | 2. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/plugins/events.ts: -------------------------------------------------------------------------------- 1 | // https://prosemirror.net/docs/ref/#state.PluginView 2 | import { Plugin } from 'prosemirror-state'; 3 | import { TextEditor } from '../../interfaces'; 4 | const events = () => 5 | new Plugin({ 6 | view() { 7 | return { 8 | update: function (view, prevState) { 9 | const state = view.state; 10 | 11 | // No change in the document or selection 12 | if (prevState && prevState.doc.eq(state.doc) && prevState.selection.eq(state.selection)) return; 13 | 14 | // Document or selection has changed 15 | // Perform actions based on the changes 16 | if (prevState && !prevState.doc.eq(state.doc)) { 17 | // const scrollTop = editorView.dom.scrollTop; 18 | (view as unknown as TextEditor).events.emit('update', view); 19 | // editorView.dom.scrollTop = scrollTop; 20 | return; 21 | } 22 | // Additional logic when the selection changes 23 | (view as unknown as TextEditor).events.emit('selectionUpdate', view); 24 | }, 25 | }; 26 | }, 27 | }); 28 | 29 | export default events; 30 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/parser/helper.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { hex2rgb } from './hex2rgb'; 4 | import { rgb2hsv } from './rgb2hsv'; 5 | import { HSVAColor, RGBAColor } from './types'; 6 | 7 | export const parseColor = (color: string): HSVAColor => { 8 | try { 9 | let rgbColor; 10 | if (rgbColorRegex.test(color)) { 11 | rgbColor = parseRgba(color); 12 | } else if (hexColorRegex.test(color)) { 13 | rgbColor = parseHex(color); 14 | } 15 | 16 | if (rgbColor) { 17 | return rgb2hsv(rgbColor); 18 | } 19 | } catch (_) { 20 | console.warn(`Cannot parse ${color}`); 21 | } 22 | return { h: 0, s: 0, v: 0, a: 1 }; 23 | }; 24 | 25 | export const rgbColorRegex = 26 | /rgba?\((?[.\d]+)[, ]+(?[.\d]+)[, ]+(?[.\d]+)(?:\s?[,\/]\s?(?[.\d]+%?))?\)/i; 27 | export const parseRgba = (color: string) => { 28 | const result = rgbColorRegex.exec(color); 29 | if (result?.groups) { 30 | return { 31 | r: parseInt(result.groups.r, 10), 32 | g: parseInt(result.groups.g, 10), 33 | b: parseInt(result.groups.b, 10), 34 | a: typeof result.groups.a !== 'undefined' ? parseInt(result.groups.a) : 1, 35 | } as RGBAColor; 36 | } 37 | }; 38 | 39 | export const hexColorRegex = /^#[0-9A-F]{3,6}[0-9a-f]{0,2}$/i; 40 | export const parseHex = (color: string): RGBAColor => { 41 | return hex2rgb(color); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/editor/src/layout/sidebar/components/PreviewModal.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | // import { Preview } from 'canva-editor/components/editor'; 3 | import CloseIcon from 'canva-editor/icons/CloseIcon'; 4 | import Preview from 'canva-editor/components/editor/Preview'; 5 | 6 | interface PreviewModalProps { 7 | onClose: () => void; 8 | } 9 | 10 | const PreviewModal: FC = ({ onClose }) => { 11 | return ( 12 |
20 | onClose()} /> 21 |
43 | 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default PreviewModal; 50 | -------------------------------------------------------------------------------- /packages/editor/src/types/common.ts: -------------------------------------------------------------------------------- 1 | export type BoxSize = { 2 | width: number; 3 | height: number; 4 | }; 5 | export type Delta = { 6 | x: number; 7 | y: number; 8 | }; 9 | 10 | export type CursorPosition = { 11 | clientX: number; 12 | clientY: number; 13 | }; 14 | 15 | export type FontStyle = { 16 | name: string; 17 | style: string; 18 | url: string; 19 | }; 20 | 21 | export type FontDataApi = { 22 | family: string; 23 | styles: FontStyle[]; 24 | }; 25 | 26 | export type FontData = { 27 | name: string; 28 | family: string; 29 | style: string; 30 | url: string; 31 | styles?: FontData[]; 32 | }; 33 | 34 | export type HorizontalGuideline = { 35 | y: number; 36 | x1: number; 37 | x2: number; 38 | label?: string; 39 | }; 40 | export type VerticalGuideline = { 41 | x: number; 42 | y1: number; 43 | y2: number; 44 | label?: string; 45 | }; 46 | 47 | export type DeepPartial = T extends object 48 | ? { 49 | [P in keyof T]?: DeepPartial; 50 | } 51 | : T; 52 | export type GetFontQuery = Partial<{ 53 | ps: string; 54 | pi: string; 55 | kw: string; 56 | }>; 57 | 58 | export type Rect = { 59 | x: number; 60 | y: number; 61 | width: number; 62 | height: number; 63 | }; 64 | 65 | export type GestureEvent = UIEvent & { 66 | scale: number; 67 | rotation: number; 68 | }; 69 | 70 | export interface IconProps { 71 | className?: string; 72 | style?: any; 73 | fill?: string; 74 | } 75 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/command/unsetMark.ts: -------------------------------------------------------------------------------- 1 | import { getMarkRange } from '../helper/getMarkRange'; 2 | import { MarkType } from 'prosemirror-model'; 3 | import { Command } from 'prosemirror-state'; 4 | 5 | export const unsetMark: ( 6 | typeOrName: string | MarkType, 7 | options?: { 8 | extendEmptyMarkRange?: boolean; 9 | }, 10 | ) => Command = 11 | (typeOrName, options = {}) => 12 | (state, dispatch) => { 13 | const { extendEmptyMarkRange = false } = options; 14 | const tr = state.tr; 15 | const { selection } = tr; 16 | const mark = state.schema.mark(typeOrName); 17 | const { $from, empty, ranges } = selection; 18 | if (empty && extendEmptyMarkRange) { 19 | let { from, to } = selection; 20 | const attrs = $from.marks().find((m) => m.type === mark.type)?.attrs; 21 | const range = getMarkRange($from, mark.type, attrs); 22 | 23 | if (range) { 24 | from = range.from; 25 | to = range.to; 26 | } 27 | 28 | tr.removeMark(from, to, mark); 29 | } else { 30 | ranges.forEach((range) => { 31 | tr.removeMark(range.$from.pos, range.$to.pos, mark); 32 | }); 33 | } 34 | 35 | tr.removeStoredMark(mark); 36 | if (dispatch) { 37 | dispatch(tr); 38 | return true; 39 | } 40 | return false; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ConfigurationIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ConfigurationIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | export default ConfigurationIcon; 24 | -------------------------------------------------------------------------------- /packages/editor/src/utils/2d/positionOfObjectInsideAnother.ts: -------------------------------------------------------------------------------- 1 | import { BoxData } from 'canva-editor/types'; 2 | import { boundingRect } from './boundingRect'; 3 | 4 | export const positionOfObjectInsideAnother = (parent: BoxData, child: BoxData) => { 5 | const boxRect = boundingRect(parent.boxSize, parent.position, parent.rotate); 6 | const imageRect = boundingRect( 7 | child.boxSize, 8 | { 9 | x: parent.position.x + child.position.x * (parent.scale || 1), 10 | y: parent.position.y + child.position.y * (parent.scale || 1), 11 | }, 12 | parent.rotate, 13 | ); 14 | const centerGroupX = boxRect.centerX; 15 | const centerGroupY = boxRect.centerY; 16 | const centerX = imageRect.centerX; 17 | const centerY = imageRect.centerY; 18 | const cos = Math.cos((parent.rotate * Math.PI) / 180); 19 | const sin = Math.sin((parent.rotate * Math.PI) / 180); 20 | const centerGroup = { 21 | x: centerGroupX + (centerX - centerGroupX) * cos - (centerY - centerGroupY) * sin, 22 | y: centerGroupY + (centerX - centerGroupX) * sin + (centerY - centerGroupY) * cos, 23 | }; 24 | const changeX = centerGroup.x - centerX; 25 | const changeY = centerGroup.y - centerY; 26 | return { 27 | x: parent.position.x + child.position.x * (parent.scale || 1) + changeX, 28 | y: parent.position.y + child.position.y * (parent.scale || 1) + changeY, 29 | rotate: parent.rotate + child.rotate, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/editor/src/icons/LineSpacingIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const LineSpacingIcon: React.FC = ({ 5 | className = '', 6 | }: IconProps) => { 7 | return ( 8 | 16 | 22 | 26 | 32 | 33 | ); 34 | }; 35 | 36 | export default LineSpacingIcon; 37 | -------------------------------------------------------------------------------- /packages/editor/src/utils/settings/sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, ForwardRefRenderFunction, PropsWithChildren, useEffect, useState } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | export interface SidebarProps { 5 | open: boolean; 6 | } 7 | const Sidebar: ForwardRefRenderFunction> = ( 8 | { open, children }, 9 | ref, 10 | ) => { 11 | const [container, setContainer] = useState(window.document.getElementById('settings')); 12 | const child = ( 13 |
31 | {children} 32 |
33 | ); 34 | useEffect(() => { 35 | setContainer(window.document.getElementById('settings')); 36 | }, []); 37 | if (!container) { 38 | return null; 39 | } 40 | return open ? ReactDOM.createPortal(child, container) : null; 41 | }; 42 | export default forwardRef>(Sidebar); 43 | -------------------------------------------------------------------------------- /packages/editor/src/utils/settings/PageGridView.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const PageGridItem = styled.button<{isNew: boolean;}>` 4 | position: relative; 5 | transition: border-color 0.3s; 6 | width: 100%; 7 | border-radius: 8px; 8 | animation: ${({ isNew }) => (isNew ? 'fadeIn 0.6s, floating 0.8s' : 'none')}; 9 | 10 | @keyframes floating { 11 | 0%, 100% { 12 | transform: translateY(0); 13 | } 14 | 50% { 15 | transform: translateY(-20px); 16 | } 17 | } 18 | 19 | > div:nth-of-type(1) { 20 | overflow: hidden; 21 | background-color: #fff; 22 | border: 3px solid transparent; 23 | pointer-events: none; 24 | border-radius: 8px; 25 | box-shadow: 0 0 0 1px rgba(64,87,109,.07),0 2px 8px rgba(57,76,96,.15); 26 | } 27 | 28 | > div:nth-of-type(2) { 29 | padding: 8px 8px 12px; 30 | font-weight: 600; 31 | color: #000; 32 | display: inline-flex; 33 | } 34 | `; 35 | 36 | export const PageGridItemContainer = styled.div` 37 | position: relative; 38 | background-color: transparent; 39 | border-radius: 8px; 40 | 41 | .add-btn { 42 | display: none; 43 | position: absolute; 44 | top: calc(50% - 36px); 45 | right: 8px; 46 | } 47 | 48 | &:hover .add-btn { 49 | display: flex; 50 | } 51 | 52 | &.is-active { 53 | background-color: #8b3dff4f; 54 | .page-btn { 55 | > div:nth-of-type(1) { 56 | box-shadow: none; 57 | border-color: #8b3dff; 58 | } 59 | } 60 | } 61 | `; 62 | 63 | -------------------------------------------------------------------------------- /packages/editor/src/icons/LockOpenIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const LockOpenIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 21 | 25 | 32 | 33 | ); 34 | }; 35 | 36 | export default LockOpenIcon; 37 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/utils/equalColorObjects.ts: -------------------------------------------------------------------------------- 1 | import { ObjectColor } from '../types'; 2 | import { hex2rgb } from './parser/hex2rgb'; 3 | 4 | export const equalColorObjects = (first: ObjectColor, second: ObjectColor): boolean => { 5 | if (first === second) return true; 6 | 7 | for (const prop in first) { 8 | // The following allows for a type-safe calling of this function (first & second have to be HSL, HSV, or RGB) 9 | // with type-unsafe iterating over object keys. TS does not allow this without an index (`[key: string]: number`) 10 | // on an object to define how iteration is normally done. To ensure extra keys are not allowed on our types, 11 | // we must cast our object to unknown (as RGB demands `r` be a key, while `Record` does not care if 12 | // there is or not), and then as a type TS can iterate over. 13 | if ((first as unknown as Record)[prop] !== (second as unknown as Record)[prop]) 14 | return false; 15 | } 16 | 17 | return true; 18 | }; 19 | 20 | export const equalColorString = (first: string, second: string): boolean => { 21 | return first.replace(/\s/g, '') === second.replace(/\s/g, ''); 22 | }; 23 | 24 | export const equalHex = (first: string, second: string): boolean => { 25 | if (first.toLowerCase() === second.toLowerCase()) return true; 26 | 27 | // To compare colors like `#FFF` and `ffffff` we convert them into RGB objects 28 | return equalColorObjects(hex2rgb(first), hex2rgb(second)); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/editor/src/layout/sidebar/CloseButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | const CloseSidebarButton: FC<{ onClose: () => void }> = ({ onClose }) => { 4 | return ( 5 |
14 | 45 |
46 | ); 47 | }; 48 | 49 | export default CloseSidebarButton; 50 | -------------------------------------------------------------------------------- /packages/editor/src/tooltip/components/TooltipProvider/TooltipProviderTypes.d.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode, RefObject } from 'react' 2 | import type { ITooltipController } from '../TooltipController/TooltipControllerTypes' 3 | 4 | /** 5 | * @deprecated Use the `data-tooltip-id` attribute, or the `anchorSelect` prop instead. 6 | * See https://react-tooltip.com/docs/getting-started 7 | */ 8 | export type AnchorRef = RefObject 9 | 10 | export interface TooltipContextData { 11 | anchorRefs: Set 12 | activeAnchor: AnchorRef 13 | attach: (...refs: AnchorRef[]) => void 14 | detach: (...refs: AnchorRef[]) => void 15 | setActiveAnchor: (ref: AnchorRef) => void 16 | } 17 | 18 | export interface TooltipContextDataWrapper { 19 | getTooltipData: (tooltipId?: string) => TooltipContextData 20 | } 21 | 22 | /** 23 | * @deprecated Use the `data-tooltip-id` attribute, or the `anchorSelect` prop instead. 24 | * See https://react-tooltip.com/docs/getting-started 25 | */ 26 | export interface ITooltipWrapper { 27 | tooltipId?: string 28 | children: ReactNode 29 | className?: string 30 | 31 | place?: ITooltipController['place'] 32 | content?: ITooltipController['content'] 33 | html?: ITooltipController['html'] 34 | variant?: ITooltipController['variant'] 35 | offset?: ITooltipController['offset'] 36 | wrapper?: ITooltipController['wrapper'] 37 | events?: ITooltipController['events'] 38 | positionStrategy?: ITooltipController['positionStrategy'] 39 | delayShow?: ITooltipController['delayShow'] 40 | delayHide?: ITooltipController['delayHide'] 41 | } 42 | -------------------------------------------------------------------------------- /packages/editor/src/icons/ClipboardIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from 'canva-editor/types'; 2 | import React from 'react'; 3 | 4 | const ClipboardIcon: React.FC = ({ className = '' }: IconProps) => { 5 | return ( 6 | 14 | 15 | 20 | 21 | 26 | 32 | 33 | ); 34 | }; 35 | 36 | export default ClipboardIcon; 37 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/command/setFontFamily.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeType } from 'prosemirror-model'; 2 | import { Command } from 'prosemirror-state'; 3 | 4 | export const setFontFamily: (fontFamily: string) => Command = (fontFamily) => { 5 | return (state, dispatch) => { 6 | const tr = state.tr.setSelection(state.selection); 7 | const { selection, doc } = tr; 8 | if (!selection || !doc) { 9 | return false; 10 | } 11 | const { from, to } = selection; 12 | const tasks: { node: Node; pos: number; nodeType: NodeType }[] = []; 13 | 14 | const allowedNodeTypes = ['paragraph']; 15 | doc.nodesBetween(from, to, (node, pos) => { 16 | const nodeType = node.type; 17 | if (allowedNodeTypes.includes(nodeType.name)) { 18 | tasks.push({ 19 | node, 20 | pos, 21 | nodeType, 22 | }); 23 | } 24 | return true; 25 | }); 26 | 27 | if (!tasks.length) { 28 | return false; 29 | } 30 | tasks.forEach((job) => { 31 | const { node, pos, nodeType } = job; 32 | let { attrs } = node; 33 | attrs = { 34 | ...attrs, 35 | fontFamily 36 | }; 37 | 38 | tr.setNodeMarkup(pos, nodeType, attrs, node.marks); 39 | }); 40 | if (dispatch) { 41 | dispatch(tr); 42 | return true; 43 | } 44 | return false; 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/editor/src/color-picker/types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface RgbColor { 4 | r: number; 5 | g: number; 6 | b: number; 7 | } 8 | 9 | export interface RgbaColor extends RgbColor { 10 | a: number; 11 | } 12 | 13 | export interface HslColor { 14 | h: number; 15 | s: number; 16 | l: number; 17 | } 18 | 19 | export interface HslaColor extends HslColor { 20 | a: number; 21 | } 22 | 23 | export interface HsvColor { 24 | h: number; 25 | s: number; 26 | v: number; 27 | } 28 | 29 | export interface HsvaColor extends HsvColor { 30 | a: number; 31 | } 32 | 33 | export type ObjectColor = RgbColor | HslColor | HsvColor | RgbaColor | HslaColor | HsvaColor; 34 | 35 | export type AnyColor = string | ObjectColor; 36 | 37 | export interface ColorModel { 38 | defaultColor: T; 39 | toHsva: (defaultColor: T) => HsvaColor; 40 | fromHsva: (hsv: HsvaColor) => T; 41 | equal: (first: T, second: T) => boolean; 42 | } 43 | 44 | type ColorPickerHTMLAttributes = Omit, 'color' | 'onChange' | 'onChangeCapture'>; 45 | 46 | export interface ColorPickerBaseProps extends ColorPickerHTMLAttributes { 47 | color: T; 48 | enableAlpha?: boolean; 49 | onChange: (newColor: T) => void; 50 | } 51 | 52 | type ColorInputHTMLAttributes = Omit, 'onChange' | 'value'>; 53 | 54 | export interface ColorInputBaseProps extends ColorInputHTMLAttributes { 55 | color?: string; 56 | onChange?: (newColor: string) => void; 57 | } 58 | -------------------------------------------------------------------------------- /packages/editor/src/components/text-editor/core/command/setLineHeight.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeType } from 'prosemirror-model'; 2 | import { Command } from 'prosemirror-state'; 3 | 4 | export const setLineHeight: (lineHeight?: number) => Command = (lineHeight) => { 5 | return (state, dispatch) => { 6 | const tr = state.tr.setSelection(state.selection); 7 | const { selection, doc } = tr; 8 | if (!selection || !doc) { 9 | return false; 10 | } 11 | const { from, to } = selection; 12 | const tasks: { node: Node; pos: number; nodeType: NodeType }[] = []; 13 | 14 | const allowedNodeTypes = ['paragraph']; 15 | 16 | doc.nodesBetween(from, to, (node, pos) => { 17 | const nodeType = node.type; 18 | if (allowedNodeTypes.includes(nodeType.name)) { 19 | tasks.push({ 20 | node, 21 | pos, 22 | nodeType, 23 | }); 24 | } 25 | return true; 26 | }); 27 | 28 | if (!tasks.length) { 29 | return false; 30 | } 31 | tasks.forEach((job) => { 32 | const { node, pos, nodeType } = job; 33 | let { attrs } = node; 34 | attrs = { 35 | ...attrs, 36 | lineHeight, 37 | }; 38 | tr.setNodeMarkup(pos, nodeType, attrs, node.marks); 39 | }); 40 | 41 | if (dispatch) { 42 | dispatch(tr); 43 | return true; 44 | } 45 | return false; 46 | }; 47 | }; 48 | --------------------------------------------------------------------------------