Polish block editor work
closes RCX-2048 flag=block_editor test plan: - with the block_editor flag on - edit a page - scroll down > expect the top bar to stop scrolling when it hits the top of the page > expect the About, Hero, and Navigation sections to have a toolbar with a background color button - click the button and change the color from the popup > expect to see the new background color - click on a text block > expect a new font size menu button in the toolbar > expect the menu to change the size of the text - select a section - move the mouse around > expect a tag to pop up labeling the block you're hovering over - click on a Heading block - open the heading level menu from the heading block toolbar > expect the current level to have a checkmark - create a new page using the stepper > expect the page to be scrolled to the top, and no section selected once the page is created - grab a section or block from the tray > expect a grabbing cursor - start creating a new page - cancel from the Create a new page modal > expect to be back on the index page Change-Id: Ib6735e139b34ca84ca71e54f32360e468999c3ff Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/350004 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Jacob DeWar <jacob.dewar@instructure.com> QA-Review: Jacob DeWar <jacob.dewar@instructure.com> Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
parent
8e957aa398
commit
895f75549d
|
@ -5,7 +5,7 @@
|
|||
"author": "neme",
|
||||
"main": "./react/index.tsx",
|
||||
"dependencies": {
|
||||
"@craftjs/core": "^0.2.7",
|
||||
"@craftjs/core": "^0.2.8",
|
||||
"@instructure/ui-color-picker": "^8",
|
||||
"styled-components": ">= 4",
|
||||
"react-contenteditable": "^3.3.7",
|
||||
|
|
|
@ -50,11 +50,17 @@ type BlockEditorProps = {
|
|||
enabled?: boolean
|
||||
version: string
|
||||
content: string
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export default function BlockEditor({enabled = true, version, content}: BlockEditorProps) {
|
||||
export default function BlockEditor({
|
||||
enabled = true,
|
||||
version,
|
||||
content,
|
||||
onCancel,
|
||||
}: BlockEditorProps) {
|
||||
const [json] = useState(content || DEFAULT_CONTENT)
|
||||
const [toolboxOpen, setToolboxOpen] = useState(true)
|
||||
const [toolboxOpen, setToolboxOpen] = useState(false)
|
||||
const [stepperOpen, setStepperOpen] = useState(!content)
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -80,8 +86,14 @@ export default function BlockEditor({enabled = true, version, content}: BlockEdi
|
|||
|
||||
const handleCloseStepper = useCallback(() => {
|
||||
setStepperOpen(false)
|
||||
setToolboxOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleCancelStepper = useCallback(() => {
|
||||
setStepperOpen(false)
|
||||
onCancel()
|
||||
}, [onCancel])
|
||||
|
||||
return (
|
||||
<View
|
||||
as="div"
|
||||
|
@ -101,16 +113,20 @@ export default function BlockEditor({enabled = true, version, content}: BlockEdi
|
|||
onRender={RenderNode}
|
||||
>
|
||||
<Flex direction="column" alignItems="stretch" justifyItems="start" gap="small" width="100%">
|
||||
<Flex.Item shouldGrow={false}>
|
||||
<div style={{position: 'sticky', top: 0, zIndex: 9999}}>
|
||||
<Topbar onToolboxChange={handleOpenToolbox} toolboxOpen={toolboxOpen} />
|
||||
</Flex.Item>
|
||||
</div>
|
||||
<Flex.Item id="editor-area" shouldGrow={true}>
|
||||
<Frame data={json} />
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
|
||||
<Toolbox open={toolboxOpen} onClose={handleCloseToolbox} />
|
||||
<NewPageStepper open={stepperOpen} onDismiss={handleCloseStepper} />
|
||||
<NewPageStepper
|
||||
open={stepperOpen}
|
||||
onFinish={handleCloseStepper}
|
||||
onCancel={handleCancelStepper}
|
||||
/>
|
||||
</Editor>
|
||||
</View>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (C) 2024 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {SVGIcon} from '@instructure/ui-svg-images'
|
||||
import {type IconProps} from '../iconTypes'
|
||||
|
||||
export default ({elementRef, size = 'small'}: IconProps) => {
|
||||
return (
|
||||
<SVGIcon
|
||||
elementRef={elementRef}
|
||||
src={`<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_13_7929)">
|
||||
<path d="M5.52715 0L4.48118 1.01914L5.82475 2.33149L0.672221 7.36027C-0.224074 8.23479 -0.224074 9.6186 0.672221 10.4931L0.74662 10.5671L5.45129 15.1575C6.34759 16.032 7.7673 16.032 8.6636 15.1575L14.3398 9.61917L14.8636 9.1096L7.61763 2.0397L7.02097 1.45754L6.87072 1.31093L5.52715 0ZM6.87072 3.35205L12.7716 9.1096L11.129 10.7123H2.9859L1.71673 9.47398C1.41797 9.18248 1.41797 8.67234 1.71673 8.38083L6.87072 3.35205ZM15.7593 11.6603L15.1611 12.5342C15.1611 12.5342 14.788 13.1167 14.3398 13.7726C14.1158 14.137 13.967 14.4288 13.8176 14.7931C13.6682 15.1575 13.5185 15.3764 13.5185 15.8137C13.5185 16.9797 14.5642 18 15.7593 18C16.9543 18 18 16.9797 18 15.8137C18 15.3764 17.8503 15.0849 17.7009 14.7205C17.5516 14.3562 17.3281 13.9915 17.1787 13.7C16.8052 13.0441 16.3574 12.4616 16.3574 12.4616L15.7593 11.6603Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_13_7929">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
`}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -18,5 +18,6 @@
|
|||
|
||||
import IconUndo from './undo'
|
||||
import IconRedo from './redo'
|
||||
import IconBackgroundColor from './background-color'
|
||||
|
||||
export {IconUndo, IconRedo}
|
||||
export {IconUndo, IconRedo, IconBackgroundColor}
|
||||
|
|
|
@ -20,10 +20,11 @@ import React, {useCallback, useState} from 'react'
|
|||
import tinycolor from 'tinycolor2'
|
||||
import {ColorIndicator, ColorMixer, ColorPreset} from '@instructure/ui-color-picker'
|
||||
import {FormFieldGroup, type FormMessage} from '@instructure/ui-form-field'
|
||||
import {TextInput} from '@instructure/ui-text-input'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
|
||||
import {TextInput, type TextInputOwnProps} from '@instructure/ui-text-input'
|
||||
|
||||
type ColorPickerProps = {
|
||||
label: string
|
||||
label: React.ReactNode
|
||||
disabled: boolean
|
||||
value: string
|
||||
onChange: (color: string) => void
|
||||
|
@ -57,7 +58,7 @@ const ColorPicker = ({label, disabled, value, onChange}: ColorPickerProps) => {
|
|||
)
|
||||
|
||||
const handleHexKey = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
(event: React.KeyboardEvent<TextInputOwnProps>) => {
|
||||
if (event.key === 'Enter') {
|
||||
const color = tinycolor(typedColor)
|
||||
if (color.isValid(typedColor)) {
|
||||
|
@ -79,7 +80,7 @@ const ColorPicker = ({label, disabled, value, onChange}: ColorPickerProps) => {
|
|||
return (
|
||||
<FormFieldGroup layout="stacked" description={label} rowSpacing="small">
|
||||
<TextInput
|
||||
renderLabel="Enter a color"
|
||||
renderLabel={<ScreenReaderContent>Custom color</ScreenReaderContent>}
|
||||
interaction={disabled ? 'disabled' : 'enabled'}
|
||||
value={typedColor}
|
||||
messages={messages}
|
||||
|
|
|
@ -30,15 +30,16 @@ import {PageSections} from './PageSections'
|
|||
import {ColorPalette} from './ColorPalette'
|
||||
import {FontPairings} from './FontPairings'
|
||||
import {PageTemplates} from './PageTemplates'
|
||||
import {buildPageContent} from '../../../utils'
|
||||
import {buildPageContent, getScrollParent} from '../../../utils'
|
||||
import {type PageSection} from './types'
|
||||
|
||||
type NewPageStepperProps = {
|
||||
open: boolean
|
||||
onDismiss: () => void
|
||||
onFinish: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const NewPageStepper = ({open, onDismiss}: NewPageStepperProps) => {
|
||||
const NewPageStepper = ({open, onFinish, onCancel}: NewPageStepperProps) => {
|
||||
const {actions, query} = useEditor()
|
||||
const [step, setStep] = useState(0)
|
||||
const [startingPoint, setStartingPoint] = useState<Step1Selection>('scratch')
|
||||
|
@ -51,9 +52,20 @@ const NewPageStepper = ({open, onDismiss}: NewPageStepperProps) => {
|
|||
setStep(step + 1)
|
||||
} else {
|
||||
buildPageContent(actions, query, selectedSections, paletteId, fontName)
|
||||
onDismiss()
|
||||
onFinish()
|
||||
}
|
||||
}, [actions, fontName, onDismiss, paletteId, query, selectedSections, step])
|
||||
}, [actions, fontName, onFinish, paletteId, query, selectedSections, step])
|
||||
|
||||
// buildPageContent returns before the Editor renders all the new stuff.
|
||||
// I think that because of javascript's single-threaded nature, onDismiss doesn't
|
||||
// unmount the modal until craftjs is finished rendering all the new nodes.
|
||||
// Use that opportunity to unselect the last created node and scroll to the top
|
||||
const handleClosed = useCallback(() => {
|
||||
// @ts-expect-error (null is OK)
|
||||
actions.selectNode(null)
|
||||
const scrollingContainer = getScrollParent()
|
||||
scrollingContainer.scrollTo({top: 0, behavior: 'instant'})
|
||||
}, [actions])
|
||||
|
||||
const handlePrevStep = useCallback(() => {
|
||||
setStep(step - 1)
|
||||
|
@ -100,12 +112,12 @@ const NewPageStepper = ({open, onDismiss}: NewPageStepperProps) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} label="Create a new page" onDismiss={onDismiss}>
|
||||
<Modal open={open} label="Create a new page" onDismiss={onCancel} onClose={handleClosed}>
|
||||
<Modal.Header>
|
||||
<Heading>Create a new page</Heading>
|
||||
<CloseButton
|
||||
data-instui-modal-close-button="true"
|
||||
onClick={onDismiss}
|
||||
onClick={onCancel}
|
||||
screenReaderLabel="Close"
|
||||
placement="end"
|
||||
offset="medium"
|
||||
|
@ -126,7 +138,7 @@ const NewPageStepper = ({open, onDismiss}: NewPageStepperProps) => {
|
|||
</View>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button color="secondary" onClick={onDismiss}>
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="primary" margin="0 0 0 small" onClick={handleNextStep}>
|
||||
|
|
|
@ -48,8 +48,9 @@ import {ROOT_NODE} from '@craftjs/utils'
|
|||
import {IconArrowUpLine, IconTrashLine, IconDragHandleLine} from '@instructure/ui-icons'
|
||||
import {IconButton} from '@instructure/ui-buttons'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {type ViewProps} from '@instructure/ui-view'
|
||||
import {View, type ViewProps} from '@instructure/ui-view'
|
||||
import {ToolbarSeparator} from './ToolbarSeparator'
|
||||
import {getScrollParent} from '../../utils'
|
||||
|
||||
const findUpNode = (node: Node, query: any): Node | undefined => {
|
||||
let upnode = node.data.parent ? query.node(node.data.parent).get() : undefined
|
||||
|
@ -68,11 +69,17 @@ const findContainingSection = (node: Node, query: any): Node | undefined => {
|
|||
return upnode && upnode.data.custom?.isSection ? upnode : undefined
|
||||
}
|
||||
|
||||
type RenderNodeProps = {
|
||||
interface RenderNodeProps {
|
||||
render: React.ReactElement
|
||||
}
|
||||
|
||||
export const RenderNode = ({render}: RenderNodeProps) => {
|
||||
interface RenderNodeComponent extends React.FC<RenderNodeProps> {
|
||||
globals: {
|
||||
selectedSectionId: string
|
||||
}
|
||||
}
|
||||
|
||||
export const RenderNode: RenderNodeComponent = ({render}: RenderNodeProps) => {
|
||||
const {actions, query} = useEditor(state => {
|
||||
if (state.events.selected.size === 0) {
|
||||
RenderNode.globals.selectedSectionId = ''
|
||||
|
@ -100,7 +107,7 @@ export const RenderNode = ({render}: RenderNodeProps) => {
|
|||
props: n.data.props,
|
||||
}))
|
||||
|
||||
const [currentToolbarRef, setCurrentToolbarRef] = useState<HTMLDivElement | null>(null)
|
||||
const [currentToolbarOrTagRef, setCurrentToolbarOrTagRef] = useState<HTMLDivElement | null>(null)
|
||||
const [currentMenuRef, setCurrentMenuRef] = useState<HTMLDivElement | null>(null)
|
||||
const [upnodeId] = useState<string | undefined>(findUpNode(node, query)?.id)
|
||||
|
||||
|
@ -150,16 +157,21 @@ export const RenderNode = ({render}: RenderNodeProps) => {
|
|||
|
||||
const getToolbarPos = useCallback(
|
||||
(domNode: HTMLElement | null) => {
|
||||
const {top, left, bottom} = domNode
|
||||
const {top, left, height} = domNode
|
||||
? domNode.getBoundingClientRect()
|
||||
: {top: 0, left: 0, bottom: 0}
|
||||
const offset = currentToolbarRef ? currentToolbarRef.getBoundingClientRect().height : 0
|
||||
: {top: 0, left: 0, height: 0}
|
||||
const bottom = top + height
|
||||
|
||||
// 5 is the offset of the hover/focus rings
|
||||
const offset = currentToolbarOrTagRef
|
||||
? currentToolbarOrTagRef.getBoundingClientRect().height + 5
|
||||
: 0
|
||||
return {
|
||||
top: `${top > 0 ? top - offset : bottom - offset}px`,
|
||||
left: `${left}px`,
|
||||
}
|
||||
},
|
||||
[currentToolbarRef]
|
||||
[currentToolbarOrTagRef]
|
||||
)
|
||||
const getMenuPos = useCallback(
|
||||
(domNode: HTMLElement | null) => {
|
||||
|
@ -181,19 +193,19 @@ export const RenderNode = ({render}: RenderNodeProps) => {
|
|||
currentMenuRef.style.top = top
|
||||
currentMenuRef.style.left = left
|
||||
}
|
||||
if (currentToolbarRef) {
|
||||
if (currentToolbarOrTagRef) {
|
||||
const {top, left} = getToolbarPos(dom)
|
||||
currentToolbarRef.style.top = top
|
||||
currentToolbarRef.style.left = left
|
||||
currentToolbarOrTagRef.style.top = top
|
||||
currentToolbarOrTagRef.style.left = left
|
||||
}
|
||||
}, [currentMenuRef, currentToolbarRef, dom, getMenuPos, getToolbarPos])
|
||||
}, [currentMenuRef, currentToolbarOrTagRef, dom, getMenuPos, getToolbarPos])
|
||||
|
||||
useEffect(() => {
|
||||
const scroller = document.getElementById('drawer-layout-content') || document
|
||||
scroller.addEventListener('scroll', scroll)
|
||||
const scrollingContainer = getScrollParent()
|
||||
scrollingContainer.addEventListener('scroll', scroll)
|
||||
|
||||
return () => {
|
||||
scroller.removeEventListener('scroll', scroll)
|
||||
scrollingContainer.removeEventListener('scroll', scroll)
|
||||
}
|
||||
}, [dom, scroll])
|
||||
|
||||
|
@ -222,15 +234,16 @@ export const RenderNode = ({render}: RenderNodeProps) => {
|
|||
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
ref={(el: HTMLDivElement) => setCurrentToolbarRef(el)}
|
||||
ref={(el: HTMLDivElement) => setCurrentToolbarOrTagRef(el)}
|
||||
className="block-toolbar"
|
||||
style={{
|
||||
left: getToolbarPos(dom).left,
|
||||
top: getToolbarPos(dom).top,
|
||||
}}
|
||||
data-timestamp={Date.now()}
|
||||
>
|
||||
<Text size="small">{name}</Text>
|
||||
<View as="div" background="brand" padding="0 xx-small" borderRadius="small">
|
||||
<Text size="small">{name}</Text>
|
||||
</View>
|
||||
{moveable ? (
|
||||
<IconButton
|
||||
cursor="move"
|
||||
|
@ -302,6 +315,35 @@ export const RenderNode = ({render}: RenderNodeProps) => {
|
|||
: null
|
||||
}
|
||||
|
||||
const renderHoverTag = () => {
|
||||
if (node.data?.custom?.noToolbar) return null
|
||||
|
||||
const parentSection = findContainingSection(node, query)
|
||||
if (!parentSection) return null
|
||||
|
||||
const isMySectionSelected = RenderNode.globals.selectedSectionId === parentSection.id
|
||||
if (!isMySectionSelected) return null
|
||||
|
||||
const mountPoint = document.querySelector('.block-editor-editor')
|
||||
if (!mountPoint) return null
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
ref={(el: HTMLDivElement) => setCurrentToolbarOrTagRef(el)}
|
||||
className="block-tag"
|
||||
style={{
|
||||
left: getToolbarPos(dom).left,
|
||||
top: getToolbarPos(dom).top,
|
||||
}}
|
||||
>
|
||||
<View as="div" background="secondary" padding="0 xx-small" borderRadius="small">
|
||||
<Text size="small">{name}</Text>
|
||||
</View>
|
||||
</div>,
|
||||
mountPoint
|
||||
)
|
||||
}
|
||||
|
||||
const renderRelated = () => {
|
||||
return (
|
||||
<>
|
||||
|
@ -314,6 +356,7 @@ export const RenderNode = ({render}: RenderNodeProps) => {
|
|||
return (
|
||||
<>
|
||||
{selected && node.related && renderRelated()}
|
||||
{!selected && hovered && renderHoverTag()}
|
||||
{render}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -19,12 +19,12 @@
|
|||
import React, {useCallback} from 'react'
|
||||
import {useEditor, type Node} from '@craftjs/core'
|
||||
import {Menu} from '@instructure/ui-menu'
|
||||
import {getCloneTree, scrollIntoViewWithCallback} from '../../utils'
|
||||
import {getCloneTree, scrollIntoViewWithCallback, getScrollParent} from '../../utils'
|
||||
|
||||
function triggerScrollEvent() {
|
||||
const scroller = document.getElementById('drawer-layout-content') || document
|
||||
const scrollingContainer = getScrollParent()
|
||||
const scrollEvent = new Event('scroll')
|
||||
scroller.dispatchEvent(scrollEvent)
|
||||
scrollingContainer.dispatchEvent(scrollEvent)
|
||||
}
|
||||
|
||||
type SectionMenuProps = {
|
||||
|
@ -49,7 +49,9 @@ const SectionMenu = ({
|
|||
})
|
||||
|
||||
const handleEditSection = useCallback(() => {
|
||||
onEditSection(selected.get())
|
||||
if (onEditSection) {
|
||||
onEditSection(selected.get())
|
||||
}
|
||||
}, [onEditSection, selected])
|
||||
|
||||
const handleDuplicateSection = useCallback(() => {
|
||||
|
@ -118,8 +120,10 @@ const SectionMenu = ({
|
|||
const handleRemove = useCallback(() => {
|
||||
if (onRemove) {
|
||||
onRemove(selected.get())
|
||||
} else {
|
||||
actions.delete(selected.get().id)
|
||||
} else if (selected.get()?.id) {
|
||||
window.setTimeout(() => {
|
||||
actions.delete(selected.get().id)
|
||||
}, 0)
|
||||
}
|
||||
}, [actions, onRemove, selected])
|
||||
|
||||
|
|
|
@ -46,8 +46,6 @@ import {QuizSection, QuizSectionIcon} from '../user/sections/QuizSection'
|
|||
import {FooterSection, FooterSectionIcon} from '../user/sections/FooterSection'
|
||||
import {BlankSection, BlankSectionIcon} from '../user/sections/BlankSection'
|
||||
|
||||
import {getTrayHeight} from '../../utils'
|
||||
|
||||
type ToolboxProps = {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
|
@ -117,64 +115,63 @@ export const Toolbox = ({open, onClose}: ToolboxProps) => {
|
|||
<Tray
|
||||
contentRef={el => setTrayRef(el)}
|
||||
label="Toolbox"
|
||||
mountNode={document.querySelector('.block-editor-editor') as HTMLElement}
|
||||
open={open}
|
||||
placement="end"
|
||||
size="small"
|
||||
onClose={handleCloseTray}
|
||||
>
|
||||
<Flex direction="column" height={getTrayHeight()}>
|
||||
<Flex.Item shouldGrow={true} shouldShrink={true}>
|
||||
<CloseButton placement="end" onClick={handleCloseTray} screenReaderLabel="Close" />
|
||||
<Tabs onRequestTabChange={handleTabChange}>
|
||||
<Tabs.Panel renderTitle="Blocks" isSelected={activeTab === 0}>
|
||||
<Flex
|
||||
gap="x-small"
|
||||
justifyItems="space-between"
|
||||
alignItems="center"
|
||||
wrap="wrap"
|
||||
padding="x-small"
|
||||
>
|
||||
{renderBox('Button', ButtonBlockIcon, <ButtonBlock text="Click me" />)}
|
||||
{renderBox('Text', TextBlockIcon, <TextBlock text="" />)}
|
||||
{renderBox('RCE', RCEBlockIcon, <RCEBlock text="" />)}
|
||||
{/* renderBox(
|
||||
<View as="div">
|
||||
<CloseButton placement="end" onClick={handleCloseTray} screenReaderLabel="Close" />
|
||||
<Tabs onRequestTabChange={handleTabChange}>
|
||||
<Tabs.Panel renderTitle="Blocks" isSelected={activeTab === 0}>
|
||||
<Flex
|
||||
gap="x-small"
|
||||
justifyItems="space-between"
|
||||
alignItems="center"
|
||||
wrap="wrap"
|
||||
padding="x-small"
|
||||
>
|
||||
{renderBox('Button', ButtonBlockIcon, <ButtonBlock text="Click me" />)}
|
||||
{renderBox('Text', TextBlockIcon, <TextBlock text="" />)}
|
||||
{renderBox('RCE', RCEBlockIcon, <RCEBlock text="" />)}
|
||||
{/* renderBox(
|
||||
'Container',
|
||||
ContainerIcon,
|
||||
<Element is={Container} background="#fff" canvas={true} layout="row" />
|
||||
) */}
|
||||
{renderBox('Icon', IconBlockIcon, <IconBlock iconName="apple" />)}
|
||||
{renderBox('Heading', HeadingBlockIcon, <HeadingBlock />)}
|
||||
{renderBox('Resource Card', ResourceCardIcon, <ResourceCard />)}
|
||||
{renderBox('Image', ImageBlockIcon, <ImageBlock />)}
|
||||
{renderBox('Iframe', IframeBlockIcon, <IframeBlock />)}
|
||||
</Flex>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel renderTitle="Sections" isSelected={activeTab === 1}>
|
||||
<Flex
|
||||
gap="x-small"
|
||||
justifyItems="space-between"
|
||||
alignItems="center"
|
||||
wrap="wrap"
|
||||
width="320px"
|
||||
padding="x-small"
|
||||
>
|
||||
{renderBox('Resources', ResourcesSectionIcon, <ResourcesSection />)}
|
||||
{renderBox(
|
||||
'Columns',
|
||||
ColumnsSectionIcon,
|
||||
<ColumnsSection columns={2} variant="fixed" />
|
||||
)}
|
||||
{renderBox('Blank', BlankSectionIcon, <BlankSection />)}
|
||||
{renderBox('Hero', ImageBlockIcon, <HeroSection />)}
|
||||
{renderBox('Navigation', NavigationSectionIcon, <NavigationSection />)}
|
||||
{renderBox('About', AboutSectionIcon, <AboutSection />)}
|
||||
{renderBox('Quiz', QuizSectionIcon, <QuizSection />)}
|
||||
{renderBox('Footer', FooterSectionIcon, <FooterSection />)}
|
||||
</Flex>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
{renderBox('Icon', IconBlockIcon, <IconBlock iconName="apple" />)}
|
||||
{renderBox('Heading', HeadingBlockIcon, <HeadingBlock />)}
|
||||
{renderBox('Resource Card', ResourceCardIcon, <ResourceCard />)}
|
||||
{renderBox('Image', ImageBlockIcon, <ImageBlock />)}
|
||||
{renderBox('Iframe', IframeBlockIcon, <IframeBlock />)}
|
||||
</Flex>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel renderTitle="Sections" isSelected={activeTab === 1}>
|
||||
<Flex
|
||||
gap="x-small"
|
||||
justifyItems="space-between"
|
||||
alignItems="center"
|
||||
wrap="wrap"
|
||||
width="320px"
|
||||
padding="x-small"
|
||||
>
|
||||
{renderBox('Resources', ResourcesSectionIcon, <ResourcesSection />)}
|
||||
{renderBox(
|
||||
'Columns',
|
||||
ColumnsSectionIcon,
|
||||
<ColumnsSection columns={2} variant="fixed" />
|
||||
)}
|
||||
{renderBox('Blank', BlankSectionIcon, <BlankSection />)}
|
||||
{renderBox('Hero', ImageBlockIcon, <HeroSection />)}
|
||||
{renderBox('Navigation', NavigationSectionIcon, <NavigationSection />)}
|
||||
{renderBox('About', AboutSectionIcon, <AboutSection />)}
|
||||
{renderBox('Quiz', QuizSectionIcon, <QuizSection />)}
|
||||
{renderBox('Footer', FooterSectionIcon, <FooterSection />)}
|
||||
</Flex>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</View>
|
||||
</Tray>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
} from '@instructure/ui-icons'
|
||||
|
||||
import {LinkModal} from '../../../editor/LinkModal'
|
||||
import {ColorModal} from './ColorModal'
|
||||
import {ColorModal} from '../../common/ColorModal'
|
||||
|
||||
import {isInstuiButtonColor} from './common'
|
||||
import type {ButtonBlockProps, ButtonSize, ButtonVariant} from './common'
|
||||
|
@ -196,6 +196,7 @@ const ButtonBlockToolbar = () => {
|
|||
<ColorModal
|
||||
open={colorModalOpen}
|
||||
color={props.color}
|
||||
variant="button"
|
||||
onClose={handleCloseColorModal}
|
||||
onSubmit={handleColorChange}
|
||||
/>
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2024 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {Button, CloseButton} from '@instructure/ui-buttons'
|
||||
import {RadioInput} from '@instructure/ui-radio-input'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import {Heading} from '@instructure/ui-heading'
|
||||
import {Modal} from '@instructure/ui-modal'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import {isInstuiButtonColor} from './common'
|
||||
import {ColorPicker} from '../../../editor/ColorPicker'
|
||||
import {FormFieldGroup} from '@instructure/ui-form-field'
|
||||
|
||||
type LinkModalProps = {
|
||||
open: boolean
|
||||
color: string
|
||||
onClose: () => void
|
||||
onSubmit: (color: string) => void
|
||||
}
|
||||
|
||||
const ColorModal = ({open, color, onClose, onSubmit}: LinkModalProps) => {
|
||||
const [currColor, setCurrColor] = useState(color)
|
||||
|
||||
const handleColorChange = useCallback((newColor: string) => {
|
||||
setCurrColor(newColor)
|
||||
}, [])
|
||||
|
||||
const handleButtonColorChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
handleColorChange(event.target.value)
|
||||
},
|
||||
[handleColorChange]
|
||||
)
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
onSubmit(currColor)
|
||||
}, [onSubmit, currColor])
|
||||
|
||||
return (
|
||||
<Modal open={open} onDismiss={onClose} label="Link" size="small">
|
||||
<Modal.Header>
|
||||
<Heading level="h2">Select an Button Color</Heading>
|
||||
<CloseButton placement="end" onClick={onClose} screenReaderLabel="Close" />
|
||||
</Modal.Header>
|
||||
<Modal.Body padding="medium">
|
||||
<FormFieldGroup description="Standard Button Colors">
|
||||
<Flex as="div" margin="small" gap="small" wrap="wrap">
|
||||
<RadioInput
|
||||
value="primary"
|
||||
label="Primary"
|
||||
inline={true}
|
||||
onChange={handleButtonColorChange}
|
||||
checked={currColor === 'primary'}
|
||||
/>
|
||||
<RadioInput
|
||||
value="secondary"
|
||||
label="Secondary"
|
||||
inline={true}
|
||||
onChange={handleButtonColorChange}
|
||||
checked={currColor === 'secondary'}
|
||||
/>
|
||||
<RadioInput
|
||||
value="success"
|
||||
label="Success"
|
||||
inline={true}
|
||||
onChange={handleButtonColorChange}
|
||||
checked={currColor === 'success'}
|
||||
/>
|
||||
<RadioInput
|
||||
value="danger"
|
||||
label="Danger"
|
||||
inline={true}
|
||||
onChange={handleButtonColorChange}
|
||||
checked={currColor === 'danger'}
|
||||
/>
|
||||
<RadioInput
|
||||
value="primary-inverse"
|
||||
label="Primary Inverse"
|
||||
inline={true}
|
||||
onChange={handleButtonColorChange}
|
||||
checked={currColor === 'primary-inverse'}
|
||||
/>
|
||||
<RadioInput
|
||||
value="custom"
|
||||
label="Custom"
|
||||
inline={true}
|
||||
onChange={handleButtonColorChange}
|
||||
checked={!isInstuiButtonColor(currColor)}
|
||||
/>
|
||||
</Flex>
|
||||
</FormFieldGroup>
|
||||
|
||||
<View as="div" margin="small 0 0 0" borderWidth="small 0 0 0" padding="small 0 0 0">
|
||||
<ColorPicker
|
||||
label="Custom Color"
|
||||
onChange={handleColorChange}
|
||||
value={currColor}
|
||||
disabled={isInstuiButtonColor(currColor)}
|
||||
/>
|
||||
</View>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button color="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="primary" onClick={handleSubmit} margin="0 0 0 small">
|
||||
Set Color
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export {ColorModal}
|
|
@ -19,17 +19,7 @@ import React from 'react'
|
|||
import {useEditor, useNode} from '@craftjs/core'
|
||||
import {useClassNames} from '../../../../utils'
|
||||
import {ContainerSettings} from './ContainerSettings'
|
||||
|
||||
export type ContainerLayout = 'default' | 'row' | 'column'
|
||||
export type ContainerProps = {
|
||||
id?: string
|
||||
className?: string
|
||||
'data-placeholder'?: string
|
||||
background?: string
|
||||
style?: React.CSSProperties
|
||||
layout?: ContainerLayout
|
||||
children?: React.ReactNode
|
||||
}
|
||||
import {type ContainerProps} from './types'
|
||||
|
||||
export const Container = ({
|
||||
id,
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import {Container} from './Container'
|
||||
import {ContainerSettings} from './ContainerSettings'
|
||||
import {type ContainerProps, type ContainerLayout} from './types'
|
||||
|
||||
const ContainerIcon = `
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
@ -27,4 +28,4 @@ const ContainerIcon = `
|
|||
</svg>
|
||||
`
|
||||
|
||||
export {Container, ContainerSettings, ContainerIcon}
|
||||
export {Container, ContainerSettings, ContainerIcon, type ContainerProps, type ContainerLayout}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (C) 2024 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export type ContainerLayout = 'default' | 'row' | 'column'
|
||||
export type ContainerProps = {
|
||||
id?: string
|
||||
className?: string
|
||||
'data-placeholder'?: string
|
||||
background?: string
|
||||
style?: React.CSSProperties
|
||||
layout?: ContainerLayout
|
||||
children?: React.ReactNode
|
||||
}
|
|
@ -39,7 +39,7 @@ import {useNode} from '@craftjs/core'
|
|||
import {Button} from '@instructure/ui-buttons'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import {Menu, type MenuItemProps, type MenuItem} from '@instructure/ui-menu'
|
||||
import {IconArrowOpenDownLine} from '@instructure/ui-icons'
|
||||
import {IconMiniArrowDownLine} from '@instructure/ui-icons'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {type ViewOwnProps} from '@instructure/ui-view'
|
||||
|
||||
|
@ -68,20 +68,35 @@ const HeadingBlockToolbar = () => {
|
|||
label="Heading level"
|
||||
trigger={
|
||||
<Button size="small">
|
||||
<Flex gap="small">
|
||||
<Flex gap="x-small">
|
||||
<Text size="small">Level</Text>
|
||||
<IconArrowOpenDownLine size="x-small" />
|
||||
<IconMiniArrowDownLine size="x-small" />
|
||||
</Flex>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Menu.Item value="h2" onSelect={handleLevelChange}>
|
||||
<Menu.Item
|
||||
type="checkbox"
|
||||
value="h2"
|
||||
onSelect={handleLevelChange}
|
||||
selected={props.level === 'h2'}
|
||||
>
|
||||
<Text size="small">Heading 2</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item value="h3" onSelect={handleLevelChange}>
|
||||
<Menu.Item
|
||||
type="checkbox"
|
||||
value="h3"
|
||||
onSelect={handleLevelChange}
|
||||
selected={props.level === 'h3'}
|
||||
>
|
||||
<Text size="small">Heading 3</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item value="h4" onSelect={handleLevelChange}>
|
||||
<Menu.Item
|
||||
type="checkbox"
|
||||
value="h4"
|
||||
onSelect={handleLevelChange}
|
||||
selected={props.level === 'h4'}
|
||||
>
|
||||
<Text size="small">Heading 4</Text>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
import React, {useCallback, useEffect} from 'react'
|
||||
import {useEditor, useNode, type Node} from '@craftjs/core'
|
||||
import {useClassNames} from '../../../../utils'
|
||||
import {useClassNames, getScrollParent} from '../../../../utils'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
|
||||
|
||||
export type PageBlockProps = {
|
||||
|
@ -37,6 +37,20 @@ export const PageBlock = ({children}: PageBlockProps) => {
|
|||
} = useNode()
|
||||
const clazz = useClassNames(enabled, {empty: !children}, ['page-block'])
|
||||
|
||||
// So that a section newly dropped in the editor gets selected,
|
||||
// RenderNode selects them on initial render. As a side-effect this also
|
||||
// happens as the initial json is loaded.
|
||||
// This unselects whatever was last and scrolls to the top.
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// @ts-expect-error (null is allowed)
|
||||
actions.selectNode(null)
|
||||
const scrollingContainer = getScrollParent()
|
||||
scrollingContainer.scrollTo({top: 0, behavior: 'instant'})
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handlePagekey = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
|
|
|
@ -32,14 +32,14 @@ import {TextBlockToolbar} from './TextBlockToolbar'
|
|||
|
||||
type TextBlockProps = {
|
||||
text?: string
|
||||
fontSize?: number
|
||||
fontSize?: string
|
||||
textAlign?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
export const TextBlock = ({
|
||||
text = '',
|
||||
fontSize,
|
||||
fontSize = '12pt',
|
||||
textAlign = 'start',
|
||||
color = black,
|
||||
}: TextBlockProps) => {
|
||||
|
@ -124,7 +124,7 @@ export const TextBlock = ({
|
|||
onChange={handleChange}
|
||||
onKeyUp={handleKey}
|
||||
tagName="div"
|
||||
style={{fontSize: `${fontSize}px`, textAlign, color}}
|
||||
style={{fontSize, textAlign, color}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -132,7 +132,7 @@ export const TextBlock = ({
|
|||
return (
|
||||
<div
|
||||
className={clazz}
|
||||
style={{fontSize: `${fontSize}px`, textAlign, color}}
|
||||
style={{fontSize, textAlign, color}}
|
||||
dangerouslySetInnerHTML={{__html: text}}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -36,13 +36,18 @@
|
|||
|
||||
import React, {useCallback} from 'react'
|
||||
import {useNode} from '@craftjs/core'
|
||||
import {IconButton} from '@instructure/ui-buttons'
|
||||
import {Button, IconButton} from '@instructure/ui-buttons'
|
||||
import {
|
||||
IconBoldLine,
|
||||
IconItalicLine,
|
||||
IconUnderlineLine,
|
||||
IconStrikethroughLine,
|
||||
IconMiniArrowDownLine,
|
||||
} from '@instructure/ui-icons'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import {Menu, type MenuItemProps, type MenuItem} from '@instructure/ui-menu'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {type ViewOwnProps} from '@instructure/ui-view'
|
||||
import {
|
||||
isSelectionAllStyled,
|
||||
isElementBold,
|
||||
|
@ -70,6 +75,18 @@ const TextBlockToolbar = () => {
|
|||
setProp(prps => (prps.text = node.dom?.firstElementChild?.innerHTML))
|
||||
}, [node.dom, setProp])
|
||||
|
||||
const handleFontSizeChange = useCallback(
|
||||
(
|
||||
e: React.MouseEvent<ViewOwnProps, MouseEvent>,
|
||||
value: MenuItemProps['value'] | MenuItemProps['value'][],
|
||||
_selected: MenuItemProps['selected'],
|
||||
_args: MenuItem
|
||||
) => {
|
||||
setProp(prps => (prps.fontSize = value))
|
||||
},
|
||||
[setProp]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
|
@ -89,6 +106,29 @@ const TextBlockToolbar = () => {
|
|||
<IconButton screenReaderLabel="Strikethrough" withBackground={false} withBorder={false}>
|
||||
<IconStrikethroughLine size="x-small" />
|
||||
</IconButton>
|
||||
<Menu
|
||||
label="Font size"
|
||||
trigger={
|
||||
<Button size="small">
|
||||
<Flex gap="x-small">
|
||||
<Text size="small">{props.fontSize || 'Size'}</Text>
|
||||
<IconMiniArrowDownLine size="x-small" />
|
||||
</Flex>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{['8pt', '10pt', '12pt', '14pt', '18ps', '24pt', '36pt'].map(size => (
|
||||
<Menu.Item
|
||||
type="checkbox"
|
||||
key={size}
|
||||
value={size}
|
||||
onSelect={handleFontSizeChange}
|
||||
selected={props.fontSize === size}
|
||||
>
|
||||
<Text size="small">{size}pt</Text>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* Copyright (C) 2024 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {Button, CloseButton} from '@instructure/ui-buttons'
|
||||
import {RadioInput} from '@instructure/ui-radio-input'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import {Heading} from '@instructure/ui-heading'
|
||||
import {Modal} from '@instructure/ui-modal'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import {isInstuiButtonColor} from '../blocks/ButtonBlock/common'
|
||||
import {ColorPicker} from '../../editor/ColorPicker'
|
||||
import {FormFieldGroup} from '@instructure/ui-form-field'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
|
||||
|
||||
export type ColorModalVariant = 'background' | 'button'
|
||||
type ColorModalProps = {
|
||||
open: boolean
|
||||
color: string
|
||||
variant: ColorModalVariant
|
||||
onClose: () => void
|
||||
onSubmit: (color: string) => void
|
||||
}
|
||||
|
||||
const ColorModal = ({open, color, variant, onClose, onSubmit}: ColorModalProps) => {
|
||||
const [currColor, setCurrColor] = useState(color)
|
||||
|
||||
const handleColorChange = useCallback((newColor: string) => {
|
||||
setCurrColor(newColor)
|
||||
}, [])
|
||||
|
||||
const handleButtonColorChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
handleColorChange(event.target.value)
|
||||
},
|
||||
[handleColorChange]
|
||||
)
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
onSubmit(currColor)
|
||||
}, [onSubmit, currColor])
|
||||
|
||||
const renderModalHeading = () => {
|
||||
switch (variant) {
|
||||
case 'button':
|
||||
return <Heading level="h2">Select a Button Color</Heading>
|
||||
case 'background':
|
||||
return <Heading level="h2">Select a Background Color</Heading>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} onDismiss={onClose} label="Link" size="small">
|
||||
<Modal.Header>
|
||||
{renderModalHeading()}
|
||||
<CloseButton placement="end" onClick={onClose} screenReaderLabel="Close" />
|
||||
</Modal.Header>
|
||||
<Modal.Body padding="medium">
|
||||
{variant === 'button' && (
|
||||
<View as="div" margin="0 0 small 0" borderWidth="0 0 small 0" padding="0 0 small 0">
|
||||
<FormFieldGroup description="Standard Button Colors">
|
||||
<Flex as="div" margin="small" gap="small" wrap="wrap">
|
||||
<RadioInput
|
||||
value="primary"
|
||||
label="Primary"
|
||||
inline={true}
|
||||
onChange={handleButtonColorChange}
|
||||
checked={currColor === 'primary'}
|
||||
/>
|
||||
<RadioInput
|
||||
value="secondary"
|
||||
label="Secondary"
|
||||
inline={true}
|
||||
onChange={handleButtonColorChange}
|
||||
checked={currColor === 'secondary'}
|
||||
/>
|
||||
<RadioInput
|
||||
value="success"
|
||||
label="Success"
|
||||
inline={true}
|
||||
onChange={handleButtonColorChange}
|
||||
checked={currColor === 'success'}
|
||||
/>
|
||||
<RadioInput
|
||||
value="danger"
|
||||
label="Danger"
|
||||
inline={true}
|
||||
onChange={handleButtonColorChange}
|
||||
checked={currColor === 'danger'}
|
||||
/>
|
||||
<RadioInput
|
||||
value="primary-inverse"
|
||||
label="Primary Inverse"
|
||||
inline={true}
|
||||
onChange={handleButtonColorChange}
|
||||
checked={currColor === 'primary-inverse'}
|
||||
/>
|
||||
<RadioInput
|
||||
value="custom"
|
||||
label="Custom"
|
||||
inline={true}
|
||||
onChange={handleButtonColorChange}
|
||||
checked={!isInstuiButtonColor(currColor)}
|
||||
/>
|
||||
</Flex>
|
||||
</FormFieldGroup>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ColorPicker
|
||||
label="Enter a hex color value"
|
||||
onChange={handleColorChange}
|
||||
value={currColor}
|
||||
disabled={isInstuiButtonColor(currColor)}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button color="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="primary" onClick={handleSubmit} margin="0 0 0 small">
|
||||
Set Color
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export {ColorModal}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (C) 2024 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {useNode} from '@craftjs/core'
|
||||
|
||||
import {IconButton} from '@instructure/ui-buttons'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import {IconBackgroundColor} from '../../../assets/internal-icons'
|
||||
|
||||
import {ColorModal} from './ColorModal'
|
||||
import {type ContainerProps} from '../blocks/Container/types'
|
||||
|
||||
const SectionToolbar = () => {
|
||||
const {
|
||||
actions: {setProp},
|
||||
props,
|
||||
} = useNode(node => ({
|
||||
props: node.data.props,
|
||||
}))
|
||||
const [colorModalOpen, setColorModalOpen] = useState(false)
|
||||
|
||||
const handleBackgroundColorChange = useCallback(
|
||||
(color: string) => {
|
||||
setProp((prps: ContainerProps) => (prps.background = color))
|
||||
setColorModalOpen(false)
|
||||
},
|
||||
[setProp]
|
||||
)
|
||||
|
||||
const handleBackgroundColorButtonClick = useCallback(() => {
|
||||
setColorModalOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseColorModal = useCallback(() => {
|
||||
setColorModalOpen(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Flex gap="small">
|
||||
<IconButton
|
||||
size="small"
|
||||
withBackground={false}
|
||||
withBorder={false}
|
||||
screenReaderLabel="Color"
|
||||
disabled={props.variant === 'condensed'}
|
||||
onClick={handleBackgroundColorButtonClick}
|
||||
>
|
||||
<IconBackgroundColor size="x-small" />
|
||||
</IconButton>
|
||||
|
||||
<ColorModal
|
||||
open={colorModalOpen}
|
||||
color={props.background}
|
||||
variant="background"
|
||||
onClose={handleCloseColorModal}
|
||||
onSubmit={handleBackgroundColorChange}
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export {SectionToolbar}
|
|
@ -25,6 +25,7 @@ import {ImageBlock} from '../../blocks/ImageBlock'
|
|||
import {NoSections} from '../../common'
|
||||
import {useClassNames, getContrastingColor} from '../../../../utils'
|
||||
import {SectionMenu} from '../../../editor/SectionMenu'
|
||||
import {SectionToolbar} from '../../common/SectionToolbar'
|
||||
|
||||
type AboutSectionProps = {
|
||||
background?: string
|
||||
|
@ -62,7 +63,6 @@ export const AboutSection = ({background}: AboutSectionProps) => {
|
|||
<Element
|
||||
id={`${cid}_image`}
|
||||
is={ImageBlock}
|
||||
canvas={true}
|
||||
constraint="contain"
|
||||
src="/images/block_editor/default_about_image.png"
|
||||
/>
|
||||
|
@ -73,7 +73,7 @@ export const AboutSection = ({background}: AboutSectionProps) => {
|
|||
canvas={true}
|
||||
className="about-section__inner-start"
|
||||
>
|
||||
<Element id={`${cid}_text`} is={AboutTextHalf} canvas={true} color={textColor} />
|
||||
<Element id={`${cid}_text`} is={AboutTextHalf} color={textColor} />
|
||||
</Element>
|
||||
</Container>
|
||||
)
|
||||
|
@ -89,5 +89,6 @@ AboutSection.craft = {
|
|||
},
|
||||
related: {
|
||||
sectionMenu: SectionMenu,
|
||||
toolbar: SectionToolbar,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -52,7 +52,6 @@ const FooterSection = ({background}: FooterSectionProps) => {
|
|||
<Element
|
||||
id={`${cid}__footer-canvas-icon`}
|
||||
is={ImageBlock}
|
||||
canvas={true}
|
||||
src="/images/block_editor/canvas_logo_white.svg"
|
||||
width={113}
|
||||
height={28}
|
||||
|
@ -60,7 +59,6 @@ const FooterSection = ({background}: FooterSectionProps) => {
|
|||
<Element
|
||||
id={`${cid}__footer-canvas-to-top`}
|
||||
is={ButtonBlock}
|
||||
canvas={true}
|
||||
text="Back to top"
|
||||
variant="condensed"
|
||||
color={buttonColor}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -24,6 +24,7 @@ import {ImageBlock} from '../../blocks/ImageBlock'
|
|||
import {NoSections} from '../../common'
|
||||
import {useClassNames, getContrastingColor} from '../../../../utils'
|
||||
import {SectionMenu} from '../../../editor/SectionMenu'
|
||||
import {SectionToolbar} from '../../common/SectionToolbar'
|
||||
|
||||
type HeroSectionProps = {
|
||||
background?: string
|
||||
|
@ -52,24 +53,13 @@ export const HeroSection = ({background}: HeroSectionProps) => {
|
|||
// what's already there.
|
||||
return (
|
||||
<Container className={clazz} background={backgroundColor}>
|
||||
<Element
|
||||
id={`${cid}_nosection1`}
|
||||
is={NoSections}
|
||||
canvas={true}
|
||||
className="hero-section__inner-start"
|
||||
>
|
||||
<Element id={`${cid}_text`} is={HeroTextHalf} canvas={true} color={textColor} />
|
||||
<Element id={`${cid}_nosection1`} is={NoSections} className="hero-section__inner-start">
|
||||
<Element id={`${cid}_text`} is={HeroTextHalf} color={textColor} />
|
||||
</Element>
|
||||
<Element
|
||||
id={`${cid}_nosection2`}
|
||||
is={NoSections}
|
||||
canvas={true}
|
||||
className="hero-section__inner-end"
|
||||
>
|
||||
<Element id={`${cid}_nosection2`} is={NoSections} className="hero-section__inner-end">
|
||||
<Element
|
||||
id={`${cid}_image`}
|
||||
is={ImageBlock}
|
||||
canvas={true}
|
||||
constraint="contain"
|
||||
src="/images/block_editor/default_hero_image.png"
|
||||
/>
|
||||
|
@ -88,5 +78,6 @@ HeroSection.craft = {
|
|||
},
|
||||
related: {
|
||||
sectionMenu: SectionMenu,
|
||||
toolbar: SectionToolbar,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ const HeroTextHalf = ({
|
|||
text={title}
|
||||
level="h2"
|
||||
custom={{
|
||||
displayName: 'Headline',
|
||||
themeOverride: {
|
||||
h2FontFamily:
|
||||
'Georgia, LatoWeb, Lato, "Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||
|
@ -63,7 +64,14 @@ const HeroTextHalf = ({
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Element id={`${id}__text`} is={TextBlock} text={text} textAlign="start" color={color} />
|
||||
<Element
|
||||
id={`${id}__text`}
|
||||
is={TextBlock}
|
||||
text={text}
|
||||
textAlign="start"
|
||||
color={color}
|
||||
custom={{displayName: 'Details'}}
|
||||
/>
|
||||
<Element
|
||||
id={`${id}__link`}
|
||||
is={ButtonBlock}
|
||||
|
|
|
@ -22,6 +22,7 @@ import {Container} from '../../blocks/Container'
|
|||
import {ButtonBlock} from '../../blocks/ButtonBlock'
|
||||
import {useClassNames, getContrastingColor} from '../../../../utils'
|
||||
import {SectionMenu} from '../../../editor/SectionMenu'
|
||||
import {SectionToolbar} from '../../common/SectionToolbar'
|
||||
|
||||
export type NavigationSectionInnerProps = {
|
||||
children?: React.ReactNode
|
||||
|
@ -125,6 +126,7 @@ NavigationSection.craft = {
|
|||
},
|
||||
related: {
|
||||
sectionMenu: SectionMenu,
|
||||
toolbar: SectionToolbar,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -16,8 +16,14 @@
|
|||
}
|
||||
|
||||
|
||||
.toolbox-item:hover {
|
||||
outline: 1px solid var(--ic-brand-primary);
|
||||
.toolbox-item {
|
||||
cursor: grab;
|
||||
&:hover {
|
||||
outline: 1px solid var(--ic-brand-primary);
|
||||
}
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.font0 {font-family: lato, 'Helvetica Neue', Helvetica, Arial, sans-serif;}
|
||||
|
@ -54,7 +60,7 @@
|
|||
border-style: solid solid solid none;
|
||||
}
|
||||
|
||||
.block-editor-editor .block-toolbar {
|
||||
.block-editor-editor .block-toolbar, .block-editor-editor .block-tag {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
// code copied from https://github.com/prevwong/craft.js/issues/209#issuecomment-795221484
|
||||
// but with a couple modifications
|
||||
|
||||
import {type Node} from '@craftjs/core'
|
||||
import {uid} from '@instructure/uid'
|
||||
|
@ -26,13 +27,13 @@ export const getCloneTree = (idToClone: string, query: any) => {
|
|||
const newNodes: Record<string, Node> = {}
|
||||
|
||||
const changeNodeId = (node: Node, newParentId?: string) => {
|
||||
let newNodeId = uid('node', 2)
|
||||
const newNodeId = uid('node', 2)
|
||||
const childNodes = node.data.nodes.map(childId => changeNodeId(tree.nodes[childId], newNodeId))
|
||||
const linkedNodes = Object.keys(node.data.linkedNodes).reduce((accum, id) => {
|
||||
newNodeId = changeNodeId(tree.nodes[node.data.linkedNodes[id]], newNodeId)
|
||||
const linkedNodeId = changeNodeId(tree.nodes[node.data.linkedNodes[id]], newNodeId)
|
||||
return {
|
||||
...accum,
|
||||
[id]: newNodeId,
|
||||
[id]: linkedNodeId,
|
||||
}
|
||||
}, {})
|
||||
|
||||
|
|
|
@ -16,14 +16,19 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function getScrollParent(node: Element | null) {
|
||||
if (node == null) {
|
||||
return null
|
||||
}
|
||||
// export function getScrollParent(node: Element | null) {
|
||||
// if (node == null) {
|
||||
// return null
|
||||
// }
|
||||
|
||||
if (node.scrollHeight > node.clientHeight) {
|
||||
return node
|
||||
} else {
|
||||
return getScrollParent(node.parentNode as Element | null)
|
||||
}
|
||||
// if (node.scrollHeight > node.clientHeight) {
|
||||
// return node
|
||||
// } else {
|
||||
// return getScrollParent(node.parentNode as Element | null)
|
||||
// }
|
||||
// }
|
||||
|
||||
export function getScrollParent() {
|
||||
// depends on whether new user tutorial is enabled or not
|
||||
return document.getElementById('drawer-layout-content') || document.documentElement
|
||||
}
|
||||
|
|
|
@ -233,7 +233,11 @@ export default class WikiPageEditView extends ValidatedFormView {
|
|||
}
|
||||
ReactDOM.render(
|
||||
<Suspense fallback={<div>{I18n.t('Loading...')}</div>}>
|
||||
<BlockEditor version={blockEditorData.version} content={blockEditorData.blocks[0].data} />
|
||||
<BlockEditor
|
||||
version={blockEditorData.version}
|
||||
content={blockEditorData.blocks[0].data}
|
||||
onCancel={this.cancel.bind(this)}
|
||||
/>
|
||||
</Suspense>,
|
||||
document.getElementById('block_editor')
|
||||
)
|
||||
|
|
|
@ -1312,10 +1312,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
|
||||
|
||||
"@craftjs/core@^0.2.7":
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@craftjs/core/-/core-0.2.7.tgz#1d2ceed72ca2441ff211fe33ce44e051d71adb93"
|
||||
integrity sha512-tD0m2OwnG9n3S6OPmPq6ZXjFuTvr/zthgpoyGIyKqtX1hVVh4I/6mKKw8zghJEU57mrETlOpwXLoWoL40c8iHA==
|
||||
"@craftjs/core@^0.2.8":
|
||||
version "0.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@craftjs/core/-/core-0.2.8.tgz#d16bb0b5dd84417d631bf181adc491db7048307f"
|
||||
integrity sha512-gVZgBWyhJBD3TYdFT60yZcJWhmLHx7SnAU+EjnXGMbOeU4EDr9iTnCIBhYAovGArG22UPo/sdTJqrE2MFR7Yjg==
|
||||
dependencies:
|
||||
"@craftjs/utils" "^0.2.2"
|
||||
debounce "^1.2.0"
|
||||
|
|
Loading…
Reference in New Issue