Background.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. /*
  2. * <<
  3. * Davinci
  4. * ==
  5. * Copyright (C) 2016 - 2017 EDP
  6. * ==
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. * >>
  19. */
  20. import React, { useContext, useMemo, useCallback, useEffect } from 'react'
  21. import classnames from 'classnames'
  22. import { useClientRect } from 'utils/hooks'
  23. import { SlideContext } from './SlideContext'
  24. import { ContainerContext } from '../ContainerContext'
  25. import { DISPLAY_CONTAINER_PADDING } from './constants'
  26. import { LayerOperations } from '../../constants'
  27. import { DeltaPosition } from '../../Layer'
  28. import { DragTriggerTypes } from 'app/containers/Display/constants'
  29. import { ILayerOperationInfo } from 'app/containers/Display/components/types'
  30. import { onLabelEditorSelectedRange } from 'app/containers/Display/components/Layer/RichText/util'
  31. type KeyDownKeys =
  32. | 'ArrowUp'
  33. | 'ArrowDown'
  34. | 'ArrowLeft'
  35. | 'ArrowRight'
  36. | 'Delete'
  37. | 'Backspace'
  38. | 'c'
  39. | 'C'
  40. | 'v'
  41. | 'V'
  42. | 'y'
  43. | 'Y'
  44. | 'z'
  45. | 'Z'
  46. const ChangePositionKeys: KeyDownKeys[] = [
  47. 'ArrowUp',
  48. 'ArrowDown',
  49. 'ArrowLeft',
  50. 'ArrowRight'
  51. ]
  52. interface ISlideBackgroundProps {
  53. className?: string
  54. padding?: number
  55. autoFit?: boolean
  56. fullscreen?: boolean
  57. parentRef?: {
  58. current: HTMLDivElement
  59. }
  60. onChangeLayersPosition?: (
  61. deltaPosition: DeltaPosition,
  62. scale: number,
  63. eventTrigger: DragTriggerTypes
  64. ) => void
  65. onDoLayerOperation?: (operation: LayerOperations) => void
  66. onRemoveLayerOperationInfo?: (
  67. changedInfo: Pick<Partial<ILayerOperationInfo>, 'selected' | 'editing'>
  68. ) => void
  69. }
  70. const SlideBackground: React.FC<ISlideBackgroundProps> = (props) => {
  71. const {
  72. className,
  73. padding,
  74. autoFit,
  75. fullscreen,
  76. onChangeLayersPosition,
  77. onDoLayerOperation,
  78. onRemoveLayerOperationInfo,
  79. parentRef
  80. } = props
  81. const { slideParams } = useContext(SlideContext)
  82. const { width: slideWidth, height: slideHeight, scaleMode } = slideParams
  83. const containerContextValue = useContext(ContainerContext)
  84. const {
  85. scale,
  86. grid,
  87. zoomRatio,
  88. slideTranslate,
  89. scaleChange,
  90. slideTranslateChange
  91. } = containerContextValue
  92. const [rect, refBackground] = useClientRect<HTMLDivElement>()
  93. const [containerWidth, containerHeight] = useMemo(() => {
  94. if (!fullscreen && !rect) {
  95. return []
  96. }
  97. let width: number
  98. let height: number
  99. if (fullscreen) {
  100. width = window.document.documentElement.clientWidth
  101. height = window.document.documentElement.clientHeight
  102. } else {
  103. width = rect.width
  104. height = rect.height
  105. }
  106. return [width, height].map((item) => Math.max(zoomRatio, 1) * item)
  107. }, [fullscreen, rect, zoomRatio])
  108. let nextScale = useMemo<[number, number]>(() => {
  109. if (!containerWidth || !containerHeight) {
  110. return [0, 0]
  111. }
  112. const landscapeScale = ((containerWidth - padding) / slideWidth) * zoomRatio
  113. const portraitScale =
  114. ((containerHeight - padding) / slideHeight) * zoomRatio
  115. const newScale: [number, number] = new Array<number>(2) as [number, number]
  116. if (autoFit) {
  117. const fitScale =
  118. slideWidth / slideHeight > containerWidth / containerHeight
  119. ? landscapeScale
  120. : portraitScale
  121. newScale.fill(fitScale)
  122. return newScale
  123. }
  124. switch (scaleMode) {
  125. case 'noScale':
  126. newScale.fill(zoomRatio)
  127. break
  128. case 'scaleWidth':
  129. newScale.fill(landscapeScale)
  130. break
  131. case 'scaleHeight':
  132. newScale.fill(portraitScale)
  133. break
  134. case 'scaleFull':
  135. newScale[0] = landscapeScale
  136. newScale[1] = portraitScale
  137. break
  138. }
  139. return newScale
  140. }, [
  141. autoFit,
  142. containerWidth,
  143. containerHeight,
  144. slideWidth,
  145. slideHeight,
  146. scaleMode,
  147. zoomRatio
  148. ])
  149. nextScale = nextScale || scale
  150. scaleChange(nextScale)
  151. let nextTranslate = useMemo<[number, number]>(() => {
  152. if (!fullscreen && !rect) {
  153. return null
  154. }
  155. let width: number
  156. let height: number
  157. if (fullscreen) {
  158. width = window.document.documentElement.clientWidth
  159. height = window.document.documentElement.clientHeight
  160. } else {
  161. width = rect.width
  162. height = rect.height
  163. }
  164. const translateX =
  165. (Math.max(width - slideWidth * nextScale[0], padding) /
  166. (2 * slideWidth)) *
  167. 100
  168. const translateY =
  169. (Math.max(height - slideHeight * nextScale[1], padding) /
  170. (2 * slideHeight)) *
  171. 100
  172. return [translateX, translateY]
  173. }, [fullscreen, rect, nextScale, slideWidth, slideHeight])
  174. nextTranslate = nextTranslate || slideTranslate
  175. slideTranslateChange(nextTranslate)
  176. const nextBackgroundStyle = useMemo(() => {
  177. const newBackgroundStyle: React.CSSProperties = { overflow: 'hidden' }
  178. if (!containerWidth || !containerHeight) {
  179. return newBackgroundStyle
  180. }
  181. if (
  182. slideWidth * nextScale[0] + padding > containerWidth ||
  183. slideHeight * nextScale[1] + padding > containerHeight
  184. ) {
  185. newBackgroundStyle.overflow = 'auto'
  186. }
  187. return newBackgroundStyle
  188. }, [
  189. slideWidth,
  190. slideHeight,
  191. containerWidth,
  192. containerHeight,
  193. nextScale,
  194. zoomRatio
  195. ])
  196. const keyDown = useCallback(
  197. (e: KeyboardEvent) => {
  198. e.stopPropagation()
  199. const key = e.key as KeyDownKeys
  200. const { ctrlKey, metaKey, shiftKey } = e
  201. if (ChangePositionKeys.includes(key)) {
  202. let deltaPosition: DeltaPosition
  203. switch (key) {
  204. case 'ArrowUp':
  205. deltaPosition = { deltaX: 0, deltaY: -grid[1] }
  206. break
  207. case 'ArrowDown':
  208. deltaPosition = { deltaX: 0, deltaY: grid[1] }
  209. break
  210. case 'ArrowLeft':
  211. deltaPosition = { deltaX: -grid[0], deltaY: 0 }
  212. break
  213. case 'ArrowRight':
  214. deltaPosition = { deltaX: grid[0], deltaY: 0 }
  215. break
  216. }
  217. onChangeLayersPosition(
  218. deltaPosition,
  219. scale[0],
  220. DragTriggerTypes.KeyDown
  221. )
  222. return
  223. }
  224. if (key === 'Delete' || key === 'Backspace') {
  225. onDoLayerOperation(LayerOperations.Delete)
  226. return
  227. }
  228. switch (key) {
  229. case 'c':
  230. case 'C':
  231. if (ctrlKey || metaKey) {
  232. onDoLayerOperation(LayerOperations.Copy)
  233. }
  234. break
  235. case 'v':
  236. case 'V':
  237. if (ctrlKey || metaKey) {
  238. onDoLayerOperation(LayerOperations.Paste)
  239. }
  240. break
  241. case 'y':
  242. case 'Y':
  243. if (ctrlKey && !metaKey) {
  244. onDoLayerOperation(LayerOperations.Redo)
  245. }
  246. break
  247. case 'z':
  248. case 'Z':
  249. if (metaKey) {
  250. onDoLayerOperation(
  251. shiftKey ? LayerOperations.Redo : LayerOperations.Undo
  252. )
  253. } else if (ctrlKey) {
  254. onDoLayerOperation(LayerOperations.Undo)
  255. }
  256. break
  257. }
  258. },
  259. [scale, onChangeLayersPosition]
  260. )
  261. useEffect(() => {
  262. if (refBackground.current && onDoLayerOperation) {
  263. parentRef.current = refBackground.current
  264. refBackground.current.addEventListener('keydown', keyDown, false)
  265. }
  266. return () => {
  267. if (refBackground.current) {
  268. refBackground.current.removeEventListener('keydown', keyDown, false)
  269. }
  270. }
  271. }, [refBackground.current, onDoLayerOperation])
  272. const removeLayerOperationInfo = useCallback(() => {
  273. if (onRemoveLayerOperationInfo) {
  274. onRemoveLayerOperationInfo({ selected: false })
  275. }
  276. if (!onLabelEditorSelectedRange() && onRemoveLayerOperationInfo) {
  277. onRemoveLayerOperationInfo({ editing: false })
  278. }
  279. }, [onRemoveLayerOperationInfo])
  280. const slideBackgroundCls = classnames({
  281. 'display-slide-background': true,
  282. [className]: !!className
  283. })
  284. return (
  285. <div
  286. ref={refBackground}
  287. className={slideBackgroundCls}
  288. style={nextBackgroundStyle}
  289. tabIndex={0}
  290. onClick={removeLayerOperationInfo}
  291. >
  292. {props.children}
  293. </div>
  294. )
  295. }
  296. SlideBackground.defaultProps = {
  297. padding: DISPLAY_CONTAINER_PADDING
  298. }
  299. export default SlideBackground