index.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <template>
  2. <div
  3. class="editable-element-shape"
  4. :class="{
  5. lock: elementInfo.lock,
  6. 'format-painter': shapeFormatPainter,
  7. }"
  8. :style="{
  9. top: elementInfo.top + 'px',
  10. left: elementInfo.left + 'px',
  11. width: elementInfo.width + 'px',
  12. height: elementInfo.height + 'px',
  13. }"
  14. >
  15. <div
  16. class="rotate-wrapper"
  17. :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
  18. >
  19. <div
  20. class="element-content"
  21. :style="{
  22. opacity: elementInfo.opacity,
  23. filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
  24. transform: flipStyle,
  25. color: text.defaultColor,
  26. fontFamily: text.defaultFontName,
  27. }"
  28. v-contextmenu="contextmenus"
  29. @mousedown="($event) => handleSelectElement($event)"
  30. @mouseup="execFormatPainter()"
  31. @touchstart="($event) => handleSelectElement($event)"
  32. @dblclick="startEdit()"
  33. >
  34. <svg
  35. overflow="visible"
  36. :width="elementInfo.width"
  37. :height="elementInfo.height"
  38. >
  39. <defs>
  40. <PatternDefs
  41. v-if="elementInfo.pattern"
  42. :id="`editable-pattern-${elementInfo.id}`"
  43. :src="elementInfo.pattern"
  44. />
  45. <GradientDefs
  46. v-else-if="elementInfo.gradient"
  47. :id="`editable-gradient-${elementInfo.id}`"
  48. :type="elementInfo.gradient.type"
  49. :colors="elementInfo.gradient.colors"
  50. :rotate="elementInfo.gradient.rotate"
  51. />
  52. </defs>
  53. <g
  54. :transform="`scale(${elementInfo.width / elementInfo.viewBox[0]}, ${
  55. elementInfo.height / elementInfo.viewBox[1]
  56. }) translate(0,0) matrix(1,0,0,1,0,0)`"
  57. >
  58. <path
  59. class="shape-path"
  60. vector-effect="non-scaling-stroke"
  61. stroke-linecap="butt"
  62. stroke-linejoin="round"
  63. stroke-miterlimit="8"
  64. transform="translate(1, 1)"
  65. stroke-opacity="1"
  66. :d="elementInfo.path"
  67. :fill="fill"
  68. :stroke="outlineColor"
  69. :stroke-width="outlineWidth"
  70. :stroke-dasharray="strokeDashArray"
  71. ></path>
  72. </g>
  73. </svg>
  74. <div
  75. class="shape-text"
  76. :style="text.style"
  77. :class="[text.align, { editable: editable || text.content }]"
  78. >
  79. <ProsemirrorEditor
  80. ref="prosemirrorEditorRef"
  81. v-if="editable || text.content"
  82. :elementId="elementInfo.id"
  83. :defaultColor="text.defaultColor"
  84. :defaultFontName="text.defaultFontName"
  85. :editable="!elementInfo.lock"
  86. :value="text.content"
  87. @update="({ value, ignore }) => updateText(value, ignore)"
  88. @blur="checkEmptyText()"
  89. @mousedown="($event) => handleSelectElement($event, false)"
  90. />
  91. </div>
  92. </div>
  93. </div>
  94. </div>
  95. </template>
  96. <script lang="ts" setup>
  97. import { computed, nextTick, ref, watch, useTemplateRef } from "vue";
  98. import { storeToRefs } from "pinia";
  99. import { useMainStore, useSlidesStore } from "@/store";
  100. import type { PPTShapeElement, ShapeText } from "@/types/slides";
  101. import type { ContextmenuItem } from "@/components/Contextmenu/types";
  102. import useElementOutline from "@/views/components/element/hooks/useElementOutline";
  103. import useElementShadow from "@/views/components/element/hooks/useElementShadow";
  104. import useElementFlip from "@/views/components/element/hooks/useElementFlip";
  105. import useElementFill from "@/views/components/element/hooks/useElementFill";
  106. import useHistorySnapshot from "@/hooks/useHistorySnapshot";
  107. import GradientDefs from "./GradientDefs.vue";
  108. import PatternDefs from "./PatternDefs.vue";
  109. import ProsemirrorEditor from "@/views/components/element/ProsemirrorEditor.vue";
  110. const props = defineProps<{
  111. elementInfo: PPTShapeElement;
  112. selectElement: (
  113. e: MouseEvent | TouchEvent,
  114. element: PPTShapeElement,
  115. canMove?: boolean
  116. ) => void;
  117. contextmenus: () => ContextmenuItem[] | null;
  118. }>();
  119. const mainStore = useMainStore();
  120. const slidesStore = useSlidesStore();
  121. const { theme } = storeToRefs(slidesStore);
  122. const { handleElementId, shapeFormatPainter } = storeToRefs(mainStore);
  123. const { addHistorySnapshot } = useHistorySnapshot();
  124. const handleSelectElement = (e: MouseEvent | TouchEvent, canMove = true) => {
  125. if (props.elementInfo.lock) return;
  126. e.stopPropagation();
  127. props.selectElement(e, props.elementInfo, canMove);
  128. };
  129. const execFormatPainter = () => {
  130. if (!shapeFormatPainter.value) return;
  131. const { keep, ...newProps } = shapeFormatPainter.value;
  132. slidesStore.updateElement({
  133. id: props.elementInfo.id,
  134. props: newProps,
  135. });
  136. addHistorySnapshot();
  137. if (!keep) mainStore.setShapeFormatPainter(null);
  138. };
  139. const element = computed(() => props.elementInfo);
  140. const { fill } = useElementFill(element, "editable");
  141. const outline = computed(() => props.elementInfo.outline);
  142. const { outlineWidth, outlineColor, strokeDashArray } =
  143. useElementOutline(outline);
  144. const shadow = computed(() => props.elementInfo.shadow);
  145. const { shadowStyle } = useElementShadow(shadow);
  146. const flipH = computed(() => props.elementInfo.flipH);
  147. const flipV = computed(() => props.elementInfo.flipV);
  148. const { flipStyle } = useElementFlip(flipH, flipV);
  149. const editable = ref(false);
  150. watch(handleElementId, () => {
  151. if (handleElementId.value !== props.elementInfo.id) {
  152. if (editable.value) editable.value = false;
  153. }
  154. });
  155. const text = computed<ShapeText>(() => {
  156. const defaultText: ShapeText = {
  157. content: "",
  158. align: "middle",
  159. defaultFontName: theme.value.fontName,
  160. defaultColor: theme.value.fontColor,
  161. };
  162. if (!props.elementInfo.text) return defaultText;
  163. return props.elementInfo.text;
  164. });
  165. const updateText = (content: string, ignore = false) => {
  166. const _text = { ...text.value, content };
  167. slidesStore.updateElement({
  168. id: props.elementInfo.id,
  169. props: { text: _text },
  170. });
  171. if (!ignore) addHistorySnapshot();
  172. };
  173. const checkEmptyText = () => {
  174. if (!props.elementInfo.text) return;
  175. const pureText = props.elementInfo.text.content.replace(/<[^>]+>/g, "");
  176. if (!pureText) {
  177. slidesStore.removeElementProps({
  178. id: props.elementInfo.id,
  179. propName: "text",
  180. });
  181. addHistorySnapshot();
  182. }
  183. };
  184. const prosemirrorEditorRef = useTemplateRef<
  185. InstanceType<typeof ProsemirrorEditor>
  186. >("prosemirrorEditorRef");
  187. const startEdit = () => {
  188. editable.value = true;
  189. nextTick(
  190. () => prosemirrorEditorRef.value && prosemirrorEditorRef.value.focus()
  191. );
  192. };
  193. </script>
  194. <style lang="scss" scoped>
  195. .editable-element-shape {
  196. position: absolute;
  197. pointer-events: none;
  198. background-size: contain;
  199. &.lock .element-content {
  200. cursor: default;
  201. }
  202. &.format-painter .element-content {
  203. cursor: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzQiIGhlaWdodD0iMTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIuNzUgMTMuNzY0VjEuNDIxYS4zLjMgMCAwMS40NDgtLjI2bDEwLjkxIDYuMTk3YS4zLjMgMCAwMS0uMTE2LjU1OWwtNC4xOTYuNDQyIDIuNTgyIDQuNDcyYS4zLjMgMCAwMS0uMTEuNDFsLTMuMTg0IDEuODM4YS4zLjMgMCAwMS0uNDEtLjExbC0yLjU4MS00LjQ3Mi0yLjgxIDMuNDU2YS4zLjMgMCAwMS0uNTMzLS4xODl6IiBmaWxsPSIjZmZmIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMjYgMTQuNWw0LjUtNC41LTYtNmMtMiAyLTMgMi01LjUgMi41LjQgMy4yIDQuODMzIDYuNjY3IDcgOHptNC41ODgtNC40OTRhLjMuMyAwIDAwLjQyNCAwbC42OC0uNjhhMS41IDEuNSAwIDAwMC0yLjEyMUwzMC4zNCA1Ljg1MmwyLjAyNi0xLjU4MmExLjYyOSAxLjYyOSAwIDEwLTIuMjgtMi4yOTZsLTEuNjAzIDIuMDIxLTEuMzU3LTEuMzU2YTEuNSAxLjUgMCAwMC0yLjEyIDBsLS42ODEuNjhhLjMuMyAwIDAwMCAuNDI0bDYuMjYzIDYuMjYzeiIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0yNC41NDMgMy45NjFzLTEuMDMgMS4yMDItMi40OTQgMS44OTFjLTEuMDA2LjQ3NC0yLjE4MS41ODUtMi43MzQuNjI3LS4yLjAxNC0uMzQ0LjIwOS0uMjc3LjM5OC4yOTMuODIgMS4xMTIgMi44MDEgMi42NTggNC4zNDcgMi4xMjYgMi4xMjYgMy42NTkgMi45NjggNC4xNDIgMy4yMDIuMS4wNDguMjE1LjAzLjI5OS0uMDQxLjM4NS0uMzI2IDEuNS0xLjI3NyAyLjIxLTEuOTg2Ljg5MS0uODkgMi4xODYtMi40NDggMi4xODYtMi40NDhtLjQ4LjA1NWEuMy4zIDAgMDEtLjQyNSAwbC02LjI2My02LjI2M2EuMy4zIDAgMDEwLS40MjRsLjY4LS42OGExLjUgMS41IDAgMDEyLjEyMiAwbDEuMzU2IDEuMzU2IDEuNjA0LTIuMDIxYTEuNjI5IDEuNjI5IDAgMTEyLjI3OSAyLjI5NkwzMC4zNCA1Ljg1MmwxLjM1MyAxLjM1M2ExLjUgMS41IDAgMDEwIDIuMTIxbC0uNjguNjh6IiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L3N2Zz4=)
  204. 2 5,
  205. default !important;
  206. }
  207. }
  208. .rotate-wrapper {
  209. width: 100%;
  210. height: 100%;
  211. }
  212. .element-content {
  213. width: 100%;
  214. height: 100%;
  215. position: relative;
  216. cursor: move;
  217. svg {
  218. transform-origin: 0 0;
  219. overflow: visible;
  220. display: block;
  221. }
  222. .shape-path {
  223. pointer-events: all;
  224. }
  225. }
  226. .shape-text {
  227. width: 100%;
  228. height: 100%;
  229. position: absolute;
  230. top: 0;
  231. bottom: 0;
  232. left: 0;
  233. right: 0;
  234. display: flex;
  235. flex-direction: column;
  236. padding: 5px;
  237. word-break: break-word;
  238. pointer-events: none;
  239. &.editable {
  240. pointer-events: all;
  241. }
  242. &.top {
  243. justify-content: flex-start;
  244. }
  245. &.middle {
  246. justify-content: center;
  247. left: 50%;
  248. top: 50%;
  249. -webkit-transform: translate(-50%, -50%);
  250. transform: translate(-50%, -50%);
  251. }
  252. &.bottom {
  253. justify-content: flex-end;
  254. }
  255. }
  256. </style>