HTMLShape.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. /* eslint-disable @typescript-eslint/no-explicit-any */
  2. import { delay, TLBoxShape, TLBoxShapeProps, TLResetBoundsInfo } from '@tldraw/core'
  3. import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react'
  4. import Vec from '@tldraw/vec'
  5. import { action, computed } from 'mobx'
  6. import { observer } from 'mobx-react-lite'
  7. import * as React from 'react'
  8. import type { SizeLevel, Shape } from '.'
  9. import { useCameraMovingRef } from '../../hooks/useCameraMoving'
  10. import { withClampedStyles } from './style-props'
  11. export interface HTMLShapeProps extends TLBoxShapeProps {
  12. type: 'html'
  13. html: string
  14. scaleLevel?: SizeLevel
  15. }
  16. const levelToScale = {
  17. xs: 0.5,
  18. sm: 0.8,
  19. md: 1,
  20. lg: 1.5,
  21. xl: 2,
  22. xxl: 3,
  23. }
  24. export class HTMLShape extends TLBoxShape<HTMLShapeProps> {
  25. static id = 'html'
  26. static defaultProps: HTMLShapeProps = {
  27. id: 'html',
  28. type: 'html',
  29. parentId: 'page',
  30. point: [0, 0],
  31. size: [600, 0],
  32. html: '',
  33. }
  34. canChangeAspectRatio = true
  35. canFlip = false
  36. canEdit = true
  37. htmlAnchorRef = React.createRef<HTMLDivElement>()
  38. @computed get scaleLevel() {
  39. return this.props.scaleLevel ?? 'md'
  40. }
  41. @action setScaleLevel = async (v?: SizeLevel) => {
  42. const newSize = Vec.mul(
  43. this.props.size,
  44. levelToScale[(v as SizeLevel) ?? 'md'] / levelToScale[this.props.scaleLevel ?? 'md']
  45. )
  46. this.update({
  47. scaleLevel: v,
  48. })
  49. await delay()
  50. this.update({
  51. size: newSize,
  52. })
  53. }
  54. onResetBounds = (info?: TLResetBoundsInfo) => {
  55. if (this.htmlAnchorRef.current) {
  56. const rect = this.htmlAnchorRef.current.getBoundingClientRect()
  57. const [w, h] = Vec.div([rect.width, rect.height], info?.zoom ?? 1)
  58. const clamp = (v: number) => Math.max(Math.min(v || 400, 1400), 10)
  59. this.update({
  60. size: [clamp(w), clamp(h)],
  61. })
  62. }
  63. return this
  64. }
  65. ReactComponent = observer(({ events, isErasing, isEditing }: TLComponentProps) => {
  66. const {
  67. props: { html, scaleLevel },
  68. } = this
  69. const isMoving = useCameraMovingRef()
  70. const app = useApp<Shape>()
  71. const isSelected = app.selectedIds.has(this.id)
  72. const tlEventsEnabled =
  73. isMoving || (isSelected && !isEditing) || app.selectedTool.id !== 'select'
  74. const stop = React.useCallback(
  75. e => {
  76. if (!tlEventsEnabled) {
  77. // TODO: pinching inside Logseq Shape issue
  78. e.stopPropagation()
  79. }
  80. },
  81. [tlEventsEnabled]
  82. )
  83. const scaleRatio = levelToScale[scaleLevel ?? 'md']
  84. React.useEffect(() => {
  85. if (this.props.size[1] === 0) {
  86. this.onResetBounds({ zoom: app.viewport.camera.zoom })
  87. app.persist(true)
  88. }
  89. }, [])
  90. return (
  91. <HTMLContainer
  92. style={{
  93. overflow: 'hidden',
  94. pointerEvents: 'all',
  95. opacity: isErasing ? 0.2 : 1,
  96. }}
  97. {...events}
  98. >
  99. <div
  100. onWheelCapture={stop}
  101. onPointerDown={stop}
  102. onPointerUp={stop}
  103. className="tl-html-container"
  104. style={{
  105. pointerEvents: !isMoving && (isEditing || isSelected) ? 'all' : 'none',
  106. overflow: isEditing ? 'auto' : 'hidden',
  107. width: `calc(100% / ${scaleRatio})`,
  108. height: `calc(100% / ${scaleRatio})`,
  109. transform: `scale(${scaleRatio})`,
  110. }}
  111. >
  112. <div
  113. ref={this.htmlAnchorRef}
  114. className="tl-html-anchor"
  115. dangerouslySetInnerHTML={{ __html: html.trim() }}
  116. />
  117. </div>
  118. </HTMLContainer>
  119. )
  120. })
  121. ReactIndicator = observer(() => {
  122. const {
  123. props: {
  124. size: [w, h],
  125. },
  126. } = this
  127. return <rect width={w} height={h} fill="transparent" />
  128. })
  129. validateProps = (props: Partial<HTMLShapeProps>) => {
  130. if (props.size !== undefined) {
  131. props.size[0] = Math.max(props.size[0], 1)
  132. props.size[1] = Math.max(props.size[1], 1)
  133. }
  134. return withClampedStyles(this, props)
  135. }
  136. }