TextToolComp.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. import {
  2. IconStrikethrough,
  3. IconTextBold,
  4. IconTextCenter,
  5. IconTextItalic,
  6. IconTextJustify,
  7. IconTextLeft,
  8. IconTextLetterSpacing,
  9. IconTextLineHeight,
  10. IconTextRight,
  11. IconTextSize,
  12. IconTextUnderline,
  13. } from "@/assets/icons";
  14. import { css } from "@linaria/core";
  15. import { Button, Checkbox, InputNumber, Tooltip } from "ant-design-vue";
  16. import { useEditor } from "@/modules/editor";
  17. import Select from "@queenjs-modules/queditor/components/FormUI/Items/Select";
  18. import { queenApi } from "queenjs";
  19. import {
  20. defineComponent,
  21. nextTick,
  22. onMounted,
  23. onUnmounted,
  24. reactive,
  25. toRaw,
  26. watch,
  27. } from "vue";
  28. import { any, bool, func, number, object, string } from "vue-types";
  29. import NewColorPicker from "../../formItems/NewColorPicker";
  30. import { isNumber } from "lodash";
  31. interface ColumnItem {
  32. label?: string;
  33. component?: ((...args: any[]) => any) | Record<string, any>;
  34. dataIndex?: string;
  35. props?: { [name: string]: any };
  36. itemProps?: { [name: string]: any };
  37. changeExtra?: (data: any) => any;
  38. }
  39. export const TextColor = defineComponent({
  40. props: {
  41. value: string().def("#666666"),
  42. },
  43. emits: ["change"],
  44. setup(props, { emit }) {
  45. const state = reactive({
  46. color: props.value,
  47. });
  48. watch(
  49. () => props.value,
  50. () => {
  51. state.color = props.value;
  52. }
  53. );
  54. const colorChange = (value: string) => {
  55. emit("change", value);
  56. state.color = value;
  57. };
  58. return () => (
  59. <NewColorPicker
  60. value={state.color}
  61. onChange={colorChange}
  62. showGradient={false}
  63. />
  64. );
  65. },
  66. });
  67. export const AlignComp = defineComponent({
  68. props: {
  69. value: string<"left" | "right" | "center" | "justify">().def("left"),
  70. },
  71. emits: ["change"],
  72. setup(props, { emit }) {
  73. const aligns = [
  74. {
  75. label: "左对齐",
  76. key: "left",
  77. icon: <IconTextLeft />,
  78. },
  79. {
  80. label: "居中对齐",
  81. key: "center",
  82. icon: <IconTextCenter />,
  83. },
  84. {
  85. label: "右对齐",
  86. key: "right",
  87. icon: <IconTextRight />,
  88. },
  89. {
  90. label: "两端对齐",
  91. key: "justify",
  92. icon: <IconTextJustify />,
  93. },
  94. ];
  95. return () => (
  96. <div class={AlignCompWapper}>
  97. {aligns.map((e: any) => {
  98. return (
  99. <Tooltip title={e.label} placement="top">
  100. <Button
  101. class={props.value == e.key ? currStyle : null}
  102. icon={e.icon}
  103. type="text"
  104. onClick={() => {
  105. emit("change", e.key);
  106. }}
  107. ></Button>
  108. </Tooltip>
  109. );
  110. })}
  111. </div>
  112. );
  113. },
  114. });
  115. export const LetterSpacingComp = defineComponent({
  116. props: {
  117. value: any<string | number>().def(0),
  118. },
  119. emits: ["change"],
  120. setup(props, { emit }) {
  121. return () => {
  122. let value =
  123. typeof props.value === "string" ? parseInt(props.value) : props.value;
  124. value = isNumber(value) ? value : 0;
  125. return (
  126. <InputNumber
  127. prefix={<IconTextLetterSpacing class="text-22px mr-6px" />}
  128. defaultValue={0}
  129. min={0}
  130. max={100}
  131. step={1}
  132. value={value}
  133. onChange={(value: any) => {
  134. if (!value) {
  135. emit("change", "0px");
  136. return;
  137. }
  138. emit("change", value + "px");
  139. }}
  140. />
  141. );
  142. };
  143. },
  144. });
  145. export const LineHeightComp = defineComponent({
  146. props: {
  147. value: any<string | number>().def(1.5),
  148. },
  149. emits: ["change"],
  150. setup(props, { emit }) {
  151. return () => {
  152. let value =
  153. typeof props.value === "string" ? parseFloat(props.value) : props.value;
  154. value = isNumber(value) ? value : 1.5;
  155. return (
  156. <InputNumber
  157. prefix={<IconTextLineHeight class="text-22px mr-6px" />}
  158. defaultValue={1.5}
  159. min={0.5}
  160. max={3}
  161. step={0.5}
  162. value={value || 1.5}
  163. onChange={(value: any) => {
  164. if (!value) {
  165. emit("change", 1.5);
  166. return;
  167. }
  168. emit("change", value);
  169. }}
  170. />
  171. );
  172. };
  173. },
  174. });
  175. export const FontStyleWapper = defineComponent({
  176. emits: ["change"],
  177. setup(props, { emit }) {
  178. const fontStyleColumns = [
  179. {
  180. label: "加粗",
  181. dataIndex: "bold",
  182. icon: <IconTextBold />,
  183. },
  184. {
  185. label: "斜体",
  186. dataIndex: "italic",
  187. icon: <IconTextItalic />,
  188. },
  189. {
  190. label: "下划线",
  191. dataIndex: "underline",
  192. icon: <IconTextUnderline />,
  193. },
  194. {
  195. label: "删除线",
  196. dataIndex: "strikethrough",
  197. icon: <IconStrikethrough />,
  198. },
  199. ];
  200. const changeVal = (e: any) => {
  201. emit("change", e);
  202. };
  203. return () => (
  204. <div class={[FontStyleCompWapper]}>
  205. {fontStyleColumns.map((e: any) => {
  206. return (
  207. <TextToolItem
  208. key={e.dataIndex}
  209. class={"!mr-0"}
  210. column={{
  211. label: e.label,
  212. dataIndex: e.dataIndex,
  213. component: (props) => {
  214. return <FontStyleComp icon={e.icon} {...props} />;
  215. },
  216. }}
  217. onChange={changeVal}
  218. />
  219. );
  220. })}
  221. </div>
  222. );
  223. },
  224. });
  225. export const FontStyleComp = defineComponent({
  226. props: {
  227. icon: any(),
  228. value: bool().def(false),
  229. },
  230. emits: ["change"],
  231. setup(props, { emit }) {
  232. return () => {
  233. return (
  234. <Button
  235. type="text"
  236. class={props.value ? currStyle : null}
  237. icon={props.icon}
  238. onClick={() => {
  239. emit("change", !props.value);
  240. }}
  241. ></Button>
  242. );
  243. };
  244. },
  245. });
  246. export const FontFamily = defineComponent({
  247. props: {
  248. value: string().def(""),
  249. },
  250. emits: ["change"],
  251. setup(props, { emit }) {
  252. const options = [
  253. { label: "默认字体", value: "" },
  254. { label: "宋体", value: "宋体,Songti,STSong,serif" },
  255. { label: "黑体", value: "黑体,Heiti,STHeiti,sans-serif" },
  256. { label: "仿宋", value: "仿宋,FangSong,STFangsong,serif" },
  257. { label: "楷体", value: "楷体,KaiTi,STKaiti,sans-serif" },
  258. ];
  259. return () => {
  260. let item = options.find((e) => {
  261. return e.value.indexOf(props.value) != -1;
  262. });
  263. const value = item ? item.value : "";
  264. return (
  265. <Select
  266. options={options}
  267. value={value || ""}
  268. onChange={(v) => {
  269. emit("change", v);
  270. }}
  271. ></Select>
  272. );
  273. };
  274. },
  275. });
  276. export const FontSize = defineComponent({
  277. props: {
  278. value: any().def("12px"),
  279. },
  280. emits: ["change"],
  281. setup(props, { emit }) {
  282. return () => {
  283. let value =
  284. typeof props.value === "string" ? parseInt(props.value) : props.value;
  285. value = isNumber(value) ? value : 12;
  286. return (
  287. <InputNumber
  288. prefix={<IconTextSize class="text-22px mr-6px" />}
  289. defaultValue={12}
  290. min={12}
  291. max={60}
  292. value={value}
  293. onChange={(value: any) => {
  294. if (!value) {
  295. emit("change", "12px");
  296. return;
  297. }
  298. emit("change", value + "px");
  299. }}
  300. />
  301. );
  302. };
  303. },
  304. });
  305. export const TextStroke = defineComponent({
  306. props: {
  307. value: any().def(undefined),
  308. },
  309. emits: ["change"],
  310. setup(props, { emit }) {
  311. const state = reactive({
  312. visible: props.value ? true : false,
  313. width: 1,
  314. color: "#666666",
  315. });
  316. watch(
  317. () => props.value,
  318. () => {
  319. formatVal(props.value);
  320. }
  321. );
  322. const formatVal = (value: any) => {
  323. if (!value) {
  324. state.visible = false;
  325. state.color = "#666666";
  326. state.width = 1;
  327. return;
  328. }
  329. state.visible = true;
  330. const colorReg = /#[a-zA-Z0-9]{6}/g;
  331. let color = value.match(colorReg);
  332. if (color) {
  333. color = color[0];
  334. } else {
  335. color = "#666666";
  336. }
  337. state.color = color;
  338. const widthReg = /\d+px/g;
  339. let width = value.match(widthReg);
  340. if (width) {
  341. width = width[0];
  342. } else {
  343. width = 1;
  344. }
  345. state.width = parseInt(width);
  346. };
  347. const colorChange = (v: string) => {
  348. state.color = v;
  349. buildValueSub();
  350. };
  351. const visibleChange = (e: any) => {
  352. const checked = e.target.checked;
  353. state.visible = checked;
  354. buildValueSub();
  355. };
  356. const widthChange = (e: any) => {
  357. state.width = e;
  358. buildValueSub();
  359. };
  360. const buildValueSub = () => {
  361. if (!state.visible) {
  362. return;
  363. }
  364. const value = `${state.width}px ${state.color}`;
  365. emit("change", value);
  366. };
  367. return () => {
  368. return (
  369. <div class={"flex-1"}>
  370. <div class={"flex justify-between items-center w-full"}>
  371. <div class={"flex-1 flex items-center"}>
  372. <Checkbox checked={state.visible} onChange={visibleChange} />
  373. {state.visible && (
  374. <div class={"flex-1 px-20px"}>
  375. <Tooltip title={"描边宽度"} placement="top">
  376. <InputNumber
  377. class={StrokeStyle}
  378. min={1}
  379. max={5}
  380. step={1}
  381. value={state.width}
  382. onChange={widthChange}
  383. />
  384. </Tooltip>
  385. </div>
  386. )}
  387. </div>
  388. <Tooltip title={"描边颜色"} placement="top">
  389. <TextColor value={state.color} onChange={colorChange} />
  390. </Tooltip>
  391. </div>
  392. </div>
  393. );
  394. };
  395. },
  396. });
  397. // export const LinkButton = defineComponent({
  398. // props: {
  399. // icon: any(),
  400. // value: any(),
  401. // },
  402. // emits: ["change"],
  403. // setup(props, { emit }) {
  404. // const showLinkInput = async () => {
  405. // const res = await queenApi.showInput({
  406. // title: "请输入链接地址",
  407. // defaultValue: "http://",
  408. // });
  409. // emit("change", res);
  410. // };
  411. // return () => (
  412. // <Button type="text" icon={props.icon} onClick={showLinkInput}></Button>
  413. // );
  414. // },
  415. // });
  416. const stylesKey: { [key: string]: any } = {
  417. fontSize: "font-size",
  418. fontFamily: "font-family",
  419. letterSpacing: "letter-spacing",
  420. lineHeight: "line-height",
  421. alignment: "text-align",
  422. fontColor: "color",
  423. textStroke: "-webkit-text-stroke",
  424. };
  425. const tagsKey: { [key: string]: any } = {
  426. bold: "<strong>",
  427. italic: "<i>",
  428. underline: "<u>",
  429. strikethrough: "<s>",
  430. };
  431. export const TextToolItem = defineComponent({
  432. props: {
  433. column: object<ColumnItem>(),
  434. index: number(),
  435. onChange: func(),
  436. },
  437. setup(props) {
  438. const state = reactive({
  439. value: undefined as any,
  440. });
  441. const { controls, store, actions, helper } = useEditor();
  442. let editor: any = null;
  443. watch(
  444. () => controls.textEditorCtrl.state.currEditor,
  445. () => {
  446. editor = toRaw(controls.textEditorCtrl.state.currEditor);
  447. initCommands();
  448. }
  449. );
  450. watch(
  451. () => store.currComp.value,
  452. () => {
  453. editor = toRaw(controls.textEditorCtrl.state.currEditor);
  454. if (!editor && store.currComp.compKey == "Text") {
  455. initCommands();
  456. nextTick(() => {
  457. const element: HTMLElement | null = document.querySelector(
  458. `#editor_${store.currComp.id}`
  459. );
  460. if (!element) {
  461. return;
  462. }
  463. const h = helper.pxToDesignSize(element.clientHeight);
  464. console.log(h);
  465. actions.updateCompData(store.currComp, "layout.size.1", h);
  466. helper.extendStreamCard(store.currStreamCardId);
  467. actions.selectObjs([]);
  468. setTimeout(() => {
  469. actions.selectObjs([store.currComp.id]);
  470. }, 0);
  471. });
  472. }
  473. }
  474. );
  475. function handleValueChange() {
  476. const { column } = props;
  477. if (!editor) {
  478. return;
  479. }
  480. const command = editor.commands.get(column?.dataIndex);
  481. if (command) {
  482. state.value = command.value;
  483. }
  484. }
  485. const initCommands = () => {
  486. const { column } = props;
  487. if (!editor) {
  488. if (!column?.dataIndex) {
  489. return;
  490. }
  491. const compValue = store.currComp.value;
  492. if (tagsKey[column.dataIndex]) {
  493. const hasTag = compValue.indexOf(tagsKey[column.dataIndex]);
  494. if (hasTag != -1) {
  495. state.value = true;
  496. } else {
  497. state.value = false;
  498. }
  499. return;
  500. }
  501. const regString = `(${stylesKey[column.dataIndex]}:)([\\s\\S]*?)(\\;)`;
  502. const styleReg = new RegExp(regString, "ig");
  503. const values = compValue.match(styleReg);
  504. if (!values) {
  505. state.value = undefined;
  506. return;
  507. }
  508. let value = values[0];
  509. value = value.replace(styleReg, "$2");
  510. state.value = value;
  511. return;
  512. }
  513. const command = editor.commands.get(column?.dataIndex);
  514. if (command) {
  515. state.value = command.value;
  516. command.on("change:value", handleValueChange);
  517. }
  518. };
  519. onMounted(() => {
  520. initCommands();
  521. });
  522. onUnmounted(() => {
  523. const { column } = props;
  524. if (!editor) {
  525. return;
  526. }
  527. const command = editor.commands.get(column?.dataIndex);
  528. if (command) {
  529. command.off("change:value", handleValueChange);
  530. }
  531. });
  532. const changeVal = (value: any, ...args: any[]) => {
  533. const { column } = props;
  534. let params = {
  535. dataIndex: column?.dataIndex,
  536. value: { value },
  537. ...args,
  538. };
  539. if (column?.changeExtra) params = column.changeExtra?.(params);
  540. props.onChange?.(params);
  541. return params;
  542. };
  543. const component = props.column?.component || null;
  544. return () => {
  545. const { column, index } = props;
  546. return (
  547. <div
  548. key={column?.dataIndex || "" + index}
  549. class={formItemStyles}
  550. {...column?.itemProps}
  551. onClick={(e) => e.stopPropagation()}
  552. >
  553. {column?.label ? (
  554. <Tooltip title={column.label} placement="top">
  555. <component
  556. value={state.value}
  557. {...column.props}
  558. onChange={changeVal}
  559. />
  560. </Tooltip>
  561. ) : (
  562. <component
  563. value={state.value}
  564. {...column?.props}
  565. onChange={changeVal}
  566. />
  567. )}
  568. </div>
  569. );
  570. };
  571. },
  572. });
  573. const currStyle = css`
  574. color: @inf-primary-color;
  575. &:hover,
  576. &:focus {
  577. color: @inf-primary-color;
  578. }
  579. `;
  580. const StrokeStyle = css`
  581. input {
  582. height: 28px;
  583. }
  584. `;
  585. const AlignCompWapper = css`
  586. display: flex;
  587. background-color: #303030;
  588. .ant-btn {
  589. flex: 1;
  590. width: 100%;
  591. line-height: 1;
  592. .inficon {
  593. font-size: 22px;
  594. }
  595. }
  596. `;
  597. const FontStyleCompWapper = css`
  598. flex: 1;
  599. display: flex;
  600. align-items: center;
  601. margin-right: 12px;
  602. border-radius: 2px;
  603. background-color: #303030;
  604. & > div {
  605. flex: 1;
  606. border-radius: 0;
  607. .ant-btn {
  608. width: 100%;
  609. line-height: 1;
  610. .inficon {
  611. font-size: 22px;
  612. }
  613. }
  614. }
  615. `;
  616. const formItemStyles = css`
  617. height: 100%;
  618. flex: 1;
  619. margin-right: 12px;
  620. .ant-input-number-affix-wrapper,
  621. .ant-select {
  622. background-color: #303030;
  623. }
  624. border-radius: 2px;
  625. &:last-child {
  626. margin-right: 0;
  627. }
  628. &.disabled {
  629. cursor: not-allowed;
  630. }
  631. `;