TextToolComp.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  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, 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 "@simonwep/pickr/dist/themes/nano.min.css";
  19. import _ from "lodash";
  20. import { queenApi } from "queenjs";
  21. import {
  22. defineComponent,
  23. onMounted,
  24. onUnmounted,
  25. reactive,
  26. ref,
  27. toRaw,
  28. watch,
  29. } from "vue";
  30. import { any, bool, func, number, object, string } from "vue-types";
  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. let pickerRef = ref();
  46. const state = reactive({
  47. color: props.value,
  48. });
  49. watch(
  50. () => props.value,
  51. () => {
  52. state.color = props.value;
  53. }
  54. );
  55. const colorChange = (e: any) => {
  56. const hexa = e.target.value;
  57. emit("change", hexa);
  58. state.color = hexa;
  59. };
  60. return () => (
  61. <div
  62. class={ColorPicker}
  63. onClick={() => {
  64. pickerRef?.value.click();
  65. }}
  66. >
  67. <div class="color_picker" style={{ backgroundColor: state.color }}>
  68. <input
  69. type="color"
  70. class="color_input"
  71. ref={pickerRef}
  72. value={state.color}
  73. onInput={_.debounce(colorChange, 300)}
  74. />
  75. </div>
  76. </div>
  77. );
  78. },
  79. });
  80. export const AlignComp = defineComponent({
  81. props: {
  82. value: string<"left" | "right" | "center" | "justify">().def("left"),
  83. },
  84. emits: ["change"],
  85. setup(props, { emit }) {
  86. const aligns = [
  87. {
  88. label: "左对齐",
  89. key: "left",
  90. icon: <IconTextLeft />,
  91. },
  92. {
  93. label: "居中对齐",
  94. key: "center",
  95. icon: <IconTextCenter />,
  96. },
  97. {
  98. label: "右对齐",
  99. key: "right",
  100. icon: <IconTextRight />,
  101. },
  102. {
  103. label: "两端对齐",
  104. key: "justify",
  105. icon: <IconTextJustify />,
  106. },
  107. ];
  108. return () => (
  109. <div class={AlignCompWapper}>
  110. {aligns.map((e: any) => {
  111. return (
  112. <Tooltip title={e.label} placement="top">
  113. <Button
  114. class={props.value == e.key ? currStyle : null}
  115. icon={e.icon}
  116. type="text"
  117. onClick={() => {
  118. emit("change", e.key);
  119. }}
  120. ></Button>
  121. </Tooltip>
  122. );
  123. })}
  124. </div>
  125. );
  126. },
  127. });
  128. export const LetterSpacingComp = defineComponent({
  129. props: {
  130. value: any<string | number>().def(0),
  131. },
  132. emits: ["change"],
  133. setup(props, { emit }) {
  134. return () => {
  135. const value =
  136. typeof props.value === "string" ? parseInt(props.value) : props.value;
  137. return (
  138. <InputNumber
  139. prefix={<IconTextLetterSpacing class="text-22px mr-6px" />}
  140. defaultValue={0}
  141. min={0}
  142. max={100}
  143. step={1}
  144. value={value}
  145. onChange={(value: any) => {
  146. if (!value) {
  147. emit("change", "0px");
  148. return;
  149. }
  150. emit("change", value + "px");
  151. }}
  152. />
  153. );
  154. };
  155. },
  156. });
  157. export const LineHeightComp = defineComponent({
  158. props: {
  159. value: any<string | number>().def(1.5),
  160. },
  161. emits: ["change"],
  162. setup(props, { emit }) {
  163. return () => {
  164. const value =
  165. typeof props.value === "string" ? parseFloat(props.value) : props.value;
  166. return (
  167. <InputNumber
  168. prefix={<IconTextLineHeight class="text-22px mr-6px" />}
  169. defaultValue={1.5}
  170. min={0.5}
  171. max={3}
  172. step={0.5}
  173. value={value || 1.5}
  174. onChange={(value: any) => {
  175. if (!value) {
  176. emit("change", 1.5);
  177. return;
  178. }
  179. emit("change", value);
  180. }}
  181. />
  182. );
  183. };
  184. },
  185. });
  186. export const FontStyleWapper = defineComponent({
  187. emits: ["change"],
  188. setup(props, { emit }) {
  189. const fontStyleColumns = [
  190. {
  191. label: "加粗",
  192. dataIndex: "bold",
  193. icon: <IconTextBold />,
  194. },
  195. {
  196. label: "斜体",
  197. dataIndex: "italic",
  198. icon: <IconTextItalic />,
  199. },
  200. {
  201. label: "下划线",
  202. dataIndex: "underline",
  203. icon: <IconTextUnderline />,
  204. },
  205. {
  206. label: "删除线",
  207. dataIndex: "strikethrough",
  208. icon: <IconStrikethrough />,
  209. },
  210. ];
  211. const changeVal = (e: any) => {
  212. emit("change", e);
  213. };
  214. return () => (
  215. <div class={[FontStyleCompWapper]}>
  216. {fontStyleColumns.map((e: any) => {
  217. return (
  218. <TextToolItem
  219. key={e.dataIndex}
  220. class={"!mr-0"}
  221. column={{
  222. label: e.label,
  223. dataIndex: e.dataIndex,
  224. component: (props) => {
  225. return <FontStyleComp icon={e.icon} {...props} />;
  226. },
  227. }}
  228. onChange={changeVal}
  229. />
  230. );
  231. })}
  232. </div>
  233. );
  234. },
  235. });
  236. export const FontStyleComp = defineComponent({
  237. props: {
  238. icon: any(),
  239. value: bool().def(false),
  240. },
  241. emits: ["change"],
  242. setup(props, { emit }) {
  243. return () => {
  244. return (
  245. <Button
  246. type="text"
  247. class={props.value ? currStyle : null}
  248. icon={props.icon}
  249. onClick={() => {
  250. emit("change", !props.value);
  251. }}
  252. ></Button>
  253. );
  254. };
  255. },
  256. });
  257. export const FontFamily = defineComponent({
  258. props: {
  259. value: string().def(""),
  260. },
  261. emits: ["change"],
  262. setup(props, { emit }) {
  263. const options = [
  264. { label: "默认字体", value: "" },
  265. { label: "宋体", value: "宋体,Songti,STSong,serif" },
  266. { label: "黑体", value: "黑体,Heiti,STHeiti,sans-serif" },
  267. { label: "仿宋", value: "仿宋,FangSong,STFangsong,serif" },
  268. { label: "楷体", value: "楷体,KaiTi,STKaiti,sans-serif" },
  269. ];
  270. return () => {
  271. return (
  272. <Select
  273. options={options}
  274. value={props.value || ""}
  275. onChange={(v) => {
  276. emit("change", v);
  277. }}
  278. ></Select>
  279. );
  280. };
  281. },
  282. });
  283. export const FontSize = defineComponent({
  284. props: {
  285. value: any().def("12px"),
  286. },
  287. emits: ["change"],
  288. setup(props, { emit }) {
  289. return () => {
  290. return (
  291. <InputNumber
  292. prefix={<IconTextSize class="text-22px mr-6px" />}
  293. defaultValue={12}
  294. min={12}
  295. max={60}
  296. value={parseInt(props.value) || 12}
  297. onChange={(value: any) => {
  298. if (!value) {
  299. emit("change", "12px");
  300. return;
  301. }
  302. emit("change", value + "px");
  303. }}
  304. />
  305. );
  306. };
  307. },
  308. });
  309. export const LinkButton = defineComponent({
  310. props: {
  311. icon: any(),
  312. value: any(),
  313. },
  314. emits: ["change"],
  315. setup(props, { emit }) {
  316. const showLinkInput = async () => {
  317. const res = await queenApi.showInput({
  318. title: "请输入链接地址",
  319. defaultValue: "http://",
  320. });
  321. emit("change", res);
  322. };
  323. return () => (
  324. <Button type="text" icon={props.icon} onClick={showLinkInput}></Button>
  325. );
  326. },
  327. });
  328. export const TextToolItem = defineComponent({
  329. props: {
  330. column: object<ColumnItem>(),
  331. index: number(),
  332. onChange: func(),
  333. },
  334. setup(props) {
  335. const state = reactive({
  336. value: undefined,
  337. });
  338. const { controls } = useEditor();
  339. let editor: any = null;
  340. watch(
  341. () => controls.textEditorCtrl.state.currEditor,
  342. () => {
  343. editor = toRaw(controls.textEditorCtrl.state.currEditor);
  344. initCommands();
  345. }
  346. );
  347. function handleValueChange() {
  348. const { column } = props;
  349. if (!editor) {
  350. return;
  351. }
  352. const command = editor.commands.get(column?.dataIndex);
  353. if (command) {
  354. state.value = command.value;
  355. }
  356. }
  357. const initCommands = () => {
  358. const { column } = props;
  359. if (!editor) {
  360. return;
  361. }
  362. const command = editor.commands.get(column?.dataIndex);
  363. if (command) {
  364. console.log("init", column?.dataIndex, command.value);
  365. state.value = command.value;
  366. command.on("change:value", handleValueChange);
  367. }
  368. };
  369. onMounted(() => {
  370. initCommands();
  371. });
  372. onUnmounted(() => {
  373. const { column } = props;
  374. if (!editor) {
  375. return;
  376. }
  377. const command = editor.commands.get(column?.dataIndex);
  378. if (command) {
  379. command.off("change:value", handleValueChange);
  380. }
  381. });
  382. const changeVal = (value: any, ...args: any[]) => {
  383. const { column } = props;
  384. let params = {
  385. dataIndex: column?.dataIndex,
  386. value: { value },
  387. ...args,
  388. };
  389. if (column?.changeExtra) params = column.changeExtra?.(params);
  390. props.onChange?.(params);
  391. return params;
  392. };
  393. const component = props.column?.component || null;
  394. return () => {
  395. const { column, index } = props;
  396. return (
  397. <div
  398. key={column?.dataIndex || "" + index}
  399. class={formItemStyles}
  400. {...column?.itemProps}
  401. onClick={(e) => e.stopPropagation()}
  402. >
  403. {column?.label ? (
  404. <Tooltip title={column.label} placement="top">
  405. <component
  406. value={state.value}
  407. {...column.props}
  408. onChange={changeVal}
  409. />
  410. </Tooltip>
  411. ) : (
  412. <component
  413. value={state.value}
  414. {...column?.props}
  415. onChange={changeVal}
  416. />
  417. )}
  418. </div>
  419. );
  420. };
  421. },
  422. });
  423. const currStyle = css`
  424. color: @inf-primary-color;
  425. &:hover,
  426. &:focus {
  427. color: @inf-primary-color;
  428. }
  429. `;
  430. const ColorPicker = css`
  431. position: relative;
  432. width: 32px;
  433. height: 32px;
  434. border-radius: 2px;
  435. cursor: pointer;
  436. .color_picker {
  437. width: 100%;
  438. height: 100%;
  439. border-radius: 2px;
  440. border: 1px solid transparent;
  441. &:focus,
  442. &:hover {
  443. border-color: @inf-primary-color;
  444. box-shadow: 0 0 0 2px rgba(232, 139, 0, 0.2);
  445. }
  446. }
  447. .color_input {
  448. position: absolute;
  449. left: 0;
  450. bottom: 0;
  451. width: 100%;
  452. height: 0;
  453. padding: 0;
  454. border: none;
  455. visibility: hidden;
  456. }
  457. `;
  458. const AlignCompWapper = css`
  459. display: flex;
  460. .ant-btn {
  461. flex: 1;
  462. width: 100%;
  463. line-height: 1;
  464. .inficon {
  465. font-size: 22px;
  466. }
  467. }
  468. `;
  469. const FontStyleCompWapper = css`
  470. flex: 1;
  471. display: flex;
  472. align-items: center;
  473. margin-right: 12px;
  474. border-radius: 2px;
  475. & > div {
  476. flex: 1;
  477. border-radius: 0;
  478. .ant-btn {
  479. width: 100%;
  480. line-height: 1;
  481. .inficon {
  482. font-size: 22px;
  483. }
  484. }
  485. }
  486. `;
  487. const formItemStyles = css`
  488. height: 100%;
  489. flex: 1;
  490. margin-right: 12px;
  491. background-color: #303030;
  492. border-radius: 2px;
  493. &:last-child {
  494. margin-right: 0;
  495. }
  496. &.disabled {
  497. cursor: not-allowed;
  498. }
  499. `;