djm-ui 0.67.4 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +38 -2
  3. package/dist/djm-ui.cjs +1032 -347
  4. package/dist/djm-ui.cjs.map +4 -4
  5. package/dist/djm-ui.css +752 -17
  6. package/dist/djm-ui.esm.js +1105 -420
  7. package/dist/djm-ui.esm.js.map +4 -4
  8. package/dist/djm-ui.esm.min.js +373 -227
  9. package/dist/djm-ui.min.cjs +373 -227
  10. package/dist/djm-ui.min.css +2 -2
  11. package/dist/djm-ui.umd.js +944 -259
  12. package/dist/djm-ui.umd.min.js +373 -227
  13. package/dist/mcp/manifest.json +12195 -0
  14. package/dist/mcp/server.js +90 -0
  15. package/package.json +12 -4
  16. package/src/components/icon/index.js +13 -1
  17. package/src/components/rich-editor/index.js +724 -61
  18. package/src/index.js +1 -1
  19. package/src/locale/en-US.js +33 -1
  20. package/src/locale/zh-CN.js +33 -1
  21. package/src/styles/base.css +2 -3
  22. package/src/styles/components/button.css +2 -1
  23. package/src/styles/components/console.css +5 -0
  24. package/src/styles/components/content.css +517 -0
  25. package/src/styles/components/form.css +5 -0
  26. package/src/styles/components/layout.css +326 -0
  27. package/src/styles/components/media-picker.css +3 -3
  28. package/src/styles/components/menu.css +2 -1
  29. package/src/styles/components/org-picker.css +1 -1
  30. package/src/styles/components/responsive.css +86 -0
  31. package/src/styles/components/rich-editor.css +206 -7
  32. package/src/styles/components/skin.css +54 -0
  33. package/src/styles/components/template.css +5 -0
  34. package/src/styles/index.css +1 -0
  35. package/src/styles/theme-dark.css +2 -0
  36. package/src/styles/tokens.css +8 -0
  37. package/src/theme/index.js +40 -0
  38. package/src/utils/sanitize-html.js +44 -6
  39. package/types/index.d.ts +8 -0
package/dist/djm-ui.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! DJM UI v0.67.4 · MIT · 2026-10-26 */
1
+ /*! DJM UI v0.72.0 · MIT · 2026-10-26 */
2
2
  "use strict";
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1119,7 +1119,19 @@ var PATHS = {
1119
1119
  activity: "M22 12h-4l-3 9L9 3l-3 9H2",
1120
1120
  database: "M12 8c4.42 0 8-1.34 8-3s-3.58-3-8-3-8 1.34-8 3 3.58 3 8 3zM4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3",
1121
1121
  sun: "M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42",
1122
- moon: "M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"
1122
+ moon: "M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z",
1123
+ undo: "M3 10h11a5 5 0 1 1 0 10H8M3 10l4-4M3 10l4 4",
1124
+ redo: "M21 10H10a5 5 0 1 0 0 10h6M21 10l-4-4M21 10l-4 4",
1125
+ "align-left": "M4 6h16M4 12h10M4 18h13",
1126
+ "align-center": "M4 6h16M7 12h10M6 18h12",
1127
+ "align-right": "M4 6h16M10 12h10M7 18h13",
1128
+ "align-justify": "M4 6h16M4 12h16M4 18h16",
1129
+ list: "M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01",
1130
+ "list-ordered": "M10 6h10M10 12h10M10 18h10M4 6h1v4M4 12h2M4 16h2v2H4",
1131
+ outdent: "M4 6h16M10 12h10M10 18h10M8 9l-3 3 3 3",
1132
+ indent: "M4 6h16M10 12h10M10 18h10M5 9l3 3-3 3",
1133
+ unlink: "M10 13a5 5 0 0 0 7.5.5M14 11a5 5 0 0 0-7.5-.5M2 2l20 20",
1134
+ images: "M8 3h12v12M4 7h12v12H4zM7 15l3-3 3 3"
1123
1135
  };
1124
1136
  var icon_default = {
1125
1137
  name: "DjmIcon",
@@ -1446,7 +1458,6 @@ var zh_CN_default = {
1446
1458
  "richEditor.italic": "\u659C\u4F53",
1447
1459
  "richEditor.underline": "\u4E0B\u5212\u7EBF",
1448
1460
  "richEditor.strike": "\u5220\u9664\u7EBF",
1449
- "richEditor.heading": "\u4E8C\u7EA7\u6807\u9898",
1450
1461
  "richEditor.paragraph": "\u6B63\u6587",
1451
1462
  "richEditor.bulletList": "\u65E0\u5E8F\u5217\u8868",
1452
1463
  "richEditor.numberList": "\u6709\u5E8F\u5217\u8868",
@@ -1457,6 +1468,39 @@ var zh_CN_default = {
1457
1468
  "richEditor.redo": "\u91CD\u505A",
1458
1469
  "richEditor.source": "\u6E90\u7801",
1459
1470
  "richEditor.visual": "\u53EF\u89C6\u5316",
1471
+ "richEditor.toolbar": "\u7F16\u8F91\u5DE5\u5177\u680F",
1472
+ "richEditor.block": "\u6BB5\u843D\u683C\u5F0F",
1473
+ "richEditor.heading1": "\u6807\u9898 1",
1474
+ "richEditor.heading2": "\u6807\u9898 2",
1475
+ "richEditor.heading3": "\u6807\u9898 3",
1476
+ "richEditor.quote": "\u5F15\u7528",
1477
+ "richEditor.codeBlock": "\u4EE3\u7801\u5757",
1478
+ "richEditor.fontSize": "\u5B57\u53F7",
1479
+ "richEditor.defaultSize": "\u9ED8\u8BA4",
1480
+ "richEditor.inlineCode": "\u884C\u5185\u4EE3\u7801",
1481
+ "richEditor.textColor": "\u6587\u5B57\u989C\u8272",
1482
+ "richEditor.bgColor": "\u80CC\u666F\u8272",
1483
+ "richEditor.alignLeft": "\u5DE6\u5BF9\u9F50",
1484
+ "richEditor.alignCenter": "\u5C45\u4E2D",
1485
+ "richEditor.alignRight": "\u53F3\u5BF9\u9F50",
1486
+ "richEditor.alignJustify": "\u4E24\u7AEF\u5BF9\u9F50",
1487
+ "richEditor.outdent": "\u51CF\u5C11\u7F29\u8FDB",
1488
+ "richEditor.indent": "\u589E\u52A0\u7F29\u8FDB",
1489
+ "richEditor.unlink": "\u53D6\u6D88\u94FE\u63A5",
1490
+ "richEditor.image": "\u63D2\u5165\u56FE\u7247",
1491
+ "richEditor.singleImage": "\u63D2\u5165\u5355\u56FE",
1492
+ "richEditor.multiImage": "\u63D2\u5165\u591A\u56FE",
1493
+ "richEditor.mediaLibrary": "\u4ECE\u7D20\u6750\u5E93\u9009\u62E9",
1494
+ "richEditor.hr": "\u5206\u5272\u7EBF",
1495
+ "richEditor.fullscreen": "\u5168\u5C4F",
1496
+ "richEditor.exitFullscreen": "\u9000\u51FA\u5168\u5C4F",
1497
+ "richEditor.wordCount": "\u5171 {count} \u5B57",
1498
+ "richEditor.uploading": "\u6B63\u5728\u4E0A\u4F20 {count} \u5F20\u56FE\u7247\u2026",
1499
+ "richEditor.uploadFailed": "\u56FE\u7247\u4E0A\u4F20\u5931\u8D25",
1500
+ "richEditor.imageTypeError": "\u53EA\u80FD\u4E0A\u4F20\u56FE\u7247\u6587\u4EF6",
1501
+ "richEditor.imageSizeError": "\u56FE\u7247\u4E0D\u80FD\u8D85\u8FC7 {size}MB",
1502
+ "richEditor.imageUrlInvalid": "\u56FE\u7247\u5730\u5740\u4E0D\u5408\u6CD5\uFF0C\u5DF2\u62D2\u7EDD\u63D2\u5165",
1503
+ "richEditor.linkInvalid": "\u94FE\u63A5\u53EA\u652F\u6301 http(s)\u3001mailto\u3001tel \u6216\u7AD9\u5185\u76F8\u5BF9\u5730\u5740",
1460
1504
  "approval.start": "\u53D1\u8D77",
1461
1505
  "approval.cc": "\u6284\u9001",
1462
1506
  "approval.approved": "\u5DF2\u901A\u8FC7",
@@ -1665,7 +1709,6 @@ var en_US_default = {
1665
1709
  "richEditor.italic": "Italic",
1666
1710
  "richEditor.underline": "Underline",
1667
1711
  "richEditor.strike": "Strikethrough",
1668
- "richEditor.heading": "Heading 2",
1669
1712
  "richEditor.paragraph": "Paragraph",
1670
1713
  "richEditor.bulletList": "Bullet list",
1671
1714
  "richEditor.numberList": "Numbered list",
@@ -1676,6 +1719,39 @@ var en_US_default = {
1676
1719
  "richEditor.redo": "Redo",
1677
1720
  "richEditor.source": "Source",
1678
1721
  "richEditor.visual": "Visual",
1722
+ "richEditor.toolbar": "Formatting toolbar",
1723
+ "richEditor.block": "Paragraph style",
1724
+ "richEditor.heading1": "Heading 1",
1725
+ "richEditor.heading2": "Heading 2",
1726
+ "richEditor.heading3": "Heading 3",
1727
+ "richEditor.quote": "Quote",
1728
+ "richEditor.codeBlock": "Code block",
1729
+ "richEditor.fontSize": "Font size",
1730
+ "richEditor.defaultSize": "Default",
1731
+ "richEditor.inlineCode": "Inline code",
1732
+ "richEditor.textColor": "Text color",
1733
+ "richEditor.bgColor": "Background color",
1734
+ "richEditor.alignLeft": "Align left",
1735
+ "richEditor.alignCenter": "Align center",
1736
+ "richEditor.alignRight": "Align right",
1737
+ "richEditor.alignJustify": "Justify",
1738
+ "richEditor.outdent": "Decrease indent",
1739
+ "richEditor.indent": "Increase indent",
1740
+ "richEditor.unlink": "Remove link",
1741
+ "richEditor.image": "Insert image",
1742
+ "richEditor.singleImage": "Insert single image",
1743
+ "richEditor.multiImage": "Insert multiple images",
1744
+ "richEditor.mediaLibrary": "Choose from media library",
1745
+ "richEditor.hr": "Divider",
1746
+ "richEditor.fullscreen": "Fullscreen",
1747
+ "richEditor.exitFullscreen": "Exit fullscreen",
1748
+ "richEditor.wordCount": "{count} characters",
1749
+ "richEditor.uploading": "Uploading {count} image(s)\u2026",
1750
+ "richEditor.uploadFailed": "Image upload failed",
1751
+ "richEditor.imageTypeError": "Only image files are allowed",
1752
+ "richEditor.imageSizeError": "Image must be smaller than {size}MB",
1753
+ "richEditor.imageUrlInvalid": "Invalid image URL, insertion rejected",
1754
+ "richEditor.linkInvalid": "Only http(s), mailto, tel or relative URLs are allowed",
1679
1755
  "approval.start": "Started",
1680
1756
  "approval.cc": "CC",
1681
1757
  "approval.approved": "Approved",
@@ -9494,7 +9570,199 @@ var tree_select_default = {
9494
9570
  };
9495
9571
 
9496
9572
  // src/components/rich-editor/index.js
9573
+ var import_vue33 = require("vue");
9574
+
9575
+ // src/components/media-picker/index.js
9497
9576
  var import_vue32 = require("vue");
9577
+ var media_picker_default = {
9578
+ name: "DjmMediaPicker",
9579
+ components: { DjmModal: modal_default, DjmPagination: pagination_default, DjmIcon: icon_default },
9580
+ props: {
9581
+ modelValue: { type: [Array, String, Number], default: "" },
9582
+ items: { type: Array, default: () => [] },
9583
+ multiple: { type: Boolean, default: false },
9584
+ // 最多选几个,0 表示不限
9585
+ limit: { type: Number, default: 0 },
9586
+ // image 只显示图片,file 只显示非图片,空串不过滤
9587
+ accept: { type: String, default: "" },
9588
+ title: { type: String, default: "" },
9589
+ triggerText: { type: String, default: "" },
9590
+ pageSize: { type: Number, default: 12 },
9591
+ disabled: { type: Boolean, default: false }
9592
+ },
9593
+ emits: ["update:modelValue", "change"],
9594
+ setup(props, { emit }) {
9595
+ const open = (0, import_vue32.ref)(false);
9596
+ const keyword = (0, import_vue32.ref)("");
9597
+ const page = (0, import_vue32.ref)(1);
9598
+ const temp = (0, import_vue32.ref)([]);
9599
+ const selectedIds = (0, import_vue32.computed)(() => {
9600
+ if (props.multiple) return Array.isArray(props.modelValue) ? props.modelValue : [];
9601
+ if (props.modelValue === "" || props.modelValue === null || props.modelValue === void 0) return [];
9602
+ return [props.modelValue];
9603
+ });
9604
+ function isImage(item) {
9605
+ if (item.type) return item.type === "image";
9606
+ return /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(item.url || "");
9607
+ }
9608
+ const filtered = (0, import_vue32.computed)(() => {
9609
+ const key = keyword.value.trim().toLowerCase();
9610
+ return props.items.filter((item) => {
9611
+ if (props.accept === "image" && !isImage(item)) return false;
9612
+ if (props.accept === "file" && isImage(item)) return false;
9613
+ if (key && !String(item.name || "").toLowerCase().includes(key)) return false;
9614
+ return true;
9615
+ });
9616
+ });
9617
+ const paged = (0, import_vue32.computed)(() => {
9618
+ const start = (page.value - 1) * props.pageSize;
9619
+ return filtered.value.slice(start, start + props.pageSize);
9620
+ });
9621
+ (0, import_vue32.watch)(keyword, () => {
9622
+ page.value = 1;
9623
+ });
9624
+ (0, import_vue32.watch)(
9625
+ () => filtered.value.length,
9626
+ () => {
9627
+ const max = Math.max(1, Math.ceil(filtered.value.length / props.pageSize));
9628
+ if (page.value > max) page.value = max;
9629
+ }
9630
+ );
9631
+ function isChecked(id) {
9632
+ return temp.value.includes(id);
9633
+ }
9634
+ function atLimit() {
9635
+ return props.limit > 0 && temp.value.length >= props.limit;
9636
+ }
9637
+ function toggle(item) {
9638
+ if (props.multiple) {
9639
+ if (isChecked(item.id)) {
9640
+ temp.value = temp.value.filter((id) => id !== item.id);
9641
+ return;
9642
+ }
9643
+ if (atLimit()) return;
9644
+ temp.value = [...temp.value, item.id];
9645
+ return;
9646
+ }
9647
+ temp.value = [item.id];
9648
+ }
9649
+ function openPanel() {
9650
+ if (props.disabled) return;
9651
+ temp.value = [...selectedIds.value];
9652
+ keyword.value = "";
9653
+ page.value = 1;
9654
+ open.value = true;
9655
+ }
9656
+ function commit() {
9657
+ const next = props.multiple ? [...temp.value] : temp.value[0] !== void 0 ? temp.value[0] : "";
9658
+ emit("update:modelValue", next);
9659
+ emit("change", next);
9660
+ open.value = false;
9661
+ }
9662
+ function cancel() {
9663
+ temp.value = [...selectedIds.value];
9664
+ open.value = false;
9665
+ }
9666
+ return {
9667
+ open,
9668
+ keyword,
9669
+ page,
9670
+ temp,
9671
+ selectedIds,
9672
+ filtered,
9673
+ paged,
9674
+ isImage,
9675
+ isChecked,
9676
+ atLimit,
9677
+ toggle,
9678
+ openPanel,
9679
+ commit,
9680
+ cancel,
9681
+ t
9682
+ };
9683
+ },
9684
+ template: `
9685
+ <span class="djm-media-picker">
9686
+ <slot name="trigger" :open="openPanel" :count="selectedIds.length">
9687
+ <button
9688
+ class="djm-btn"
9689
+ type="button"
9690
+ :disabled="disabled"
9691
+ @click="openPanel"
9692
+ >{{ triggerText || t('mediaPicker.select') }}<span v-if="selectedIds.length" class="djm-media-picker-count">{{ selectedIds.length }}</span></button>
9693
+ </slot>
9694
+
9695
+ <DjmModal
9696
+ :model-value="open"
9697
+ :title="title || t('mediaPicker.title')"
9698
+ width="760px"
9699
+ @update:model-value="cancel"
9700
+ >
9701
+ <div class="djm-media-toolbar">
9702
+ <label class="djm-media-search">
9703
+ <DjmIcon name="search" :size="13" />
9704
+ <input
9705
+ v-model="keyword"
9706
+ class="djm-media-search-input"
9707
+ type="text"
9708
+ :placeholder="t('mediaPicker.search')"
9709
+ :aria-label="t('mediaPicker.search')"
9710
+ />
9711
+ </label>
9712
+ <span class="djm-media-toolbar-slot">
9713
+ <slot name="toolbar"></slot>
9714
+ </span>
9715
+ </div>
9716
+
9717
+ <div class="djm-media-grid" role="listbox" :aria-multiselectable="multiple ? 'true' : 'false'">
9718
+ <div
9719
+ v-for="item in paged"
9720
+ :key="item.id"
9721
+ class="djm-media-item"
9722
+ :class="{
9723
+ 'djm-is-checked': isChecked(item.id),
9724
+ 'djm-is-disabled': multiple && atLimit() && !isChecked(item.id)
9725
+ }"
9726
+ role="option"
9727
+ tabindex="0"
9728
+ :aria-selected="String(isChecked(item.id))"
9729
+ @click="toggle(item)"
9730
+ @keydown.enter.prevent="toggle(item)"
9731
+ @keydown.space.prevent="toggle(item)"
9732
+ >
9733
+ <div class="djm-media-thumb">
9734
+ <img v-if="isImage(item)" :src="item.url" :alt="item.name" loading="lazy" />
9735
+ <DjmIcon v-else name="folder" :size="26" />
9736
+ <span v-if="isChecked(item.id)" class="djm-media-check">
9737
+ <DjmIcon name="check" :size="11" />
9738
+ </span>
9739
+ </div>
9740
+ <div class="djm-media-name" :title="item.name">{{ item.name }}</div>
9741
+ </div>
9742
+ <div v-if="!paged.length" class="djm-media-empty">{{ t('mediaPicker.empty') }}</div>
9743
+ </div>
9744
+
9745
+ <div class="djm-media-foot">
9746
+ <span class="djm-media-total">
9747
+ {{ t('mediaPicker.total', { total: filtered.length }) }}
9748
+ <em v-if="multiple && limit > 0">{{ t('mediaPicker.limit', { limit }) }}</em>
9749
+ </span>
9750
+ <DjmPagination
9751
+ v-if="filtered.length > pageSize"
9752
+ v-model:page="page"
9753
+ :page-size="pageSize"
9754
+ :total="filtered.length"
9755
+ />
9756
+ </div>
9757
+
9758
+ <template #footer>
9759
+ <button class="djm-btn" type="button" @click="cancel">{{ t('common.cancel') }}</button>
9760
+ <button class="djm-btn djm-btn-primary" type="button" @click="commit">{{ t('common.confirm') }}</button>
9761
+ </template>
9762
+ </DjmModal>
9763
+ </span>
9764
+ `
9765
+ };
9498
9766
 
9499
9767
  // src/utils/sanitize-html.js
9500
9768
  var TAGS = /* @__PURE__ */ new Set([
@@ -9534,7 +9802,18 @@ var TAGS = /* @__PURE__ */ new Set([
9534
9802
  "th",
9535
9803
  "td"
9536
9804
  ]);
9537
- var GLOBAL_ATTRS = /* @__PURE__ */ new Set(["class", "title"]);
9805
+ var GLOBAL_ATTRS = /* @__PURE__ */ new Set(["class", "title", "style"]);
9806
+ var STYLE_PROPS = /* @__PURE__ */ new Set([
9807
+ "color",
9808
+ "background-color",
9809
+ "font-size",
9810
+ "font-weight",
9811
+ "font-style",
9812
+ "text-decoration",
9813
+ "text-align",
9814
+ "text-indent",
9815
+ "line-height"
9816
+ ]);
9538
9817
  var TAG_ATTRS = {
9539
9818
  a: /* @__PURE__ */ new Set(["href", "target", "rel"]),
9540
9819
  img: /* @__PURE__ */ new Set(["src", "alt", "width", "height"]),
@@ -9542,11 +9821,29 @@ var TAG_ATTRS = {
9542
9821
  th: /* @__PURE__ */ new Set(["colspan", "rowspan", "scope"]),
9543
9822
  td: /* @__PURE__ */ new Set(["colspan", "rowspan"])
9544
9823
  };
9824
+ function cleanStyle(value) {
9825
+ return String(value || "").split(";").map((item) => {
9826
+ const index = item.indexOf(":");
9827
+ if (index === -1) return "";
9828
+ const name = item.slice(0, index).trim().toLowerCase();
9829
+ const val = item.slice(index + 1).trim();
9830
+ if (!STYLE_PROPS.has(name)) return "";
9831
+ if (/url\s*\(|expression\s*\(|javascript:/i.test(val)) return "";
9832
+ return name + ": " + val;
9833
+ }).filter(Boolean).join("; ");
9834
+ }
9835
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
9545
9836
  function safeUrl(value, tag) {
9546
- const url = String(value || "").replace(/[\u0000-\u0020]+/g, "").toLowerCase();
9547
- if (url.startsWith("javascript:") || url.startsWith("vbscript:")) return false;
9548
- if (url.startsWith("data:")) return tag === "img" && url.startsWith("data:image/");
9549
- return true;
9837
+ const url = String(value || "").replace(/[\u0000-\u0020]+/g, "");
9838
+ const scheme = url.match(/^([a-z][a-z0-9+.-]*):/i);
9839
+ if (!scheme) return true;
9840
+ const name = scheme[1].toLowerCase();
9841
+ if (SAFE_SCHEMES.has(name)) return true;
9842
+ if (tag === "img") {
9843
+ if (name === "blob") return true;
9844
+ if (name === "data" && /^data:image\//i.test(url)) return true;
9845
+ }
9846
+ return false;
9550
9847
  }
9551
9848
  function cleanNode(node) {
9552
9849
  const children = [...node.childNodes];
@@ -9569,6 +9866,12 @@ function cleanNode(node) {
9569
9866
  child.removeAttribute(attr.name);
9570
9867
  return;
9571
9868
  }
9869
+ if (name === "style") {
9870
+ const style = cleanStyle(attr.value);
9871
+ if (style) child.setAttribute("style", style);
9872
+ else child.removeAttribute("style");
9873
+ return;
9874
+ }
9572
9875
  if ((name === "href" || name === "src") && !safeUrl(attr.value, tag)) {
9573
9876
  child.removeAttribute(attr.name);
9574
9877
  }
@@ -9589,79 +9892,475 @@ function sanitizeHtml(html) {
9589
9892
  }
9590
9893
 
9591
9894
  // src/components/rich-editor/index.js
9895
+ var BLOCKS = [
9896
+ { value: "p", label: "paragraph" },
9897
+ { value: "h1", label: "heading1" },
9898
+ { value: "h2", label: "heading2" },
9899
+ { value: "h3", label: "heading3" },
9900
+ { value: "blockquote", label: "quote" },
9901
+ { value: "pre", label: "codeBlock" }
9902
+ ];
9903
+ var SIZES = [
9904
+ { value: "", label: "defaultSize" },
9905
+ { value: "12px", label: "12px" },
9906
+ { value: "14px", label: "14px" },
9907
+ { value: "16px", label: "16px" },
9908
+ { value: "18px", label: "18px" },
9909
+ { value: "24px", label: "24px" }
9910
+ ];
9911
+ var COLORS = ["#1f2d3d", "#5a6b7d", "#2b86d9", "#2fa36b", "#d97706", "#dc2626", "#7c3aed", "#ffffff"];
9912
+ var TOOLBAR_PRESETS = {
9913
+ simple: ["history", "inline", "list", "insert"],
9914
+ basic: ["history", "format", "inline", "align", "list", "insert", "view"],
9915
+ full: ["history", "format", "inline", "color", "align", "list", "insert", "view"]
9916
+ };
9917
+ function isSafeImageUrl(url) {
9918
+ const value = String(url || "").replace(/[\u0000-\u0020]+/g, "");
9919
+ if (!value) return false;
9920
+ const scheme = value.match(/^([a-z][a-z0-9+.-]*):/i);
9921
+ if (!scheme) return true;
9922
+ const name = scheme[1].toLowerCase();
9923
+ if (name === "http" || name === "https" || name === "blob") return true;
9924
+ return name === "data" && /^data:image\//i.test(value);
9925
+ }
9926
+ function isSafeLink(url) {
9927
+ const value = String(url || "").trim();
9928
+ if (!value) return false;
9929
+ const scheme = value.replace(/[\u0000-\u0020]+/g, "").match(/^([a-z][a-z0-9+.-]*):/i);
9930
+ if (!scheme) return true;
9931
+ return ["http", "https", "mailto", "tel"].indexOf(scheme[1].toLowerCase()) > -1;
9932
+ }
9592
9933
  var rich_editor_default = {
9593
9934
  name: "DjmRichEditor",
9594
- components: { Icon: icon_default },
9935
+ components: { Icon: icon_default, MediaPicker: media_picker_default },
9595
9936
  props: {
9596
9937
  modelValue: { type: String, default: "" },
9597
9938
  placeholder: { type: String, default: "" },
9598
9939
  minHeight: { type: Number, default: 200 },
9599
- // 开启后对外输出的 HTML 会走白名单清洗
9600
- sanitize: { type: Boolean, default: false },
9601
- disabled: { type: Boolean, default: false }
9940
+ // 默认开启:对外输出的 HTML 走白名单清洗,内容里不会带出脚本
9941
+ sanitize: { type: Boolean, default: true },
9942
+ disabled: { type: Boolean, default: false },
9943
+ // 工具条:simple / basic / full,或传分组数组(history / format / inline / color / align / list / insert / view)
9944
+ toolbar: { type: [String, Array], default: "full" },
9945
+ // 自定义上传:接收 File,返回图片地址(或 { url }),不传则转成 dataURL 本地预览
9946
+ uploadImage: { type: Function, default: null },
9947
+ // 已上传图片,提供后工具栏出现「从素材库选择」:字符串或 { url, name }
9948
+ media: { type: Array, default: () => [] },
9949
+ accept: { type: String, default: "image/*" },
9950
+ // 单张图片大小上限(MB)
9951
+ maxImageSize: { type: Number, default: 5 },
9952
+ // 是否允许一次选多张
9953
+ multiple: { type: Boolean, default: true },
9954
+ showCount: { type: Boolean, default: true }
9602
9955
  },
9603
- emits: ["update:modelValue"],
9956
+ emits: ["update:modelValue", "upload-error"],
9604
9957
  setup(props, { emit }) {
9605
- const editor = (0, import_vue32.ref)(null);
9606
- const sourceMode = (0, import_vue32.ref)(false);
9607
- const source = (0, import_vue32.ref)(props.modelValue);
9608
- const formats = (0, import_vue32.ref)({ bold: false, italic: false, underline: false, strike: false });
9958
+ const editor = (0, import_vue33.ref)(null);
9959
+ const fileInput = (0, import_vue33.ref)(null);
9960
+ const sourceMode = (0, import_vue33.ref)(false);
9961
+ const source = (0, import_vue33.ref)(props.modelValue);
9962
+ const fullscreen = (0, import_vue33.ref)(false);
9963
+ const panel = (0, import_vue33.ref)("");
9964
+ const uploadCount = (0, import_vue33.ref)(0);
9965
+ const textColor = (0, import_vue33.ref)(COLORS[0]);
9966
+ const bgColor = (0, import_vue33.ref)("#fdf3c4");
9967
+ const fontSize = (0, import_vue33.ref)("");
9968
+ const count2 = (0, import_vue33.ref)(0);
9969
+ const formats = (0, import_vue33.ref)({
9970
+ bold: false,
9971
+ italic: false,
9972
+ underline: false,
9973
+ strike: false,
9974
+ code: false,
9975
+ block: "p",
9976
+ align: "",
9977
+ list: ""
9978
+ });
9979
+ let savedRange = null;
9980
+ const ready = (0, import_vue33.computed)(() => !props.disabled && !sourceMode.value);
9981
+ const toolbarGroups = (0, import_vue33.computed)(() => {
9982
+ const list = Array.isArray(props.toolbar) ? props.toolbar : TOOLBAR_PRESETS[props.toolbar] || TOOLBAR_PRESETS.full;
9983
+ return new Set(list);
9984
+ });
9985
+ function has(group) {
9986
+ return toolbarGroups.value.has(group);
9987
+ }
9988
+ const mediaLibrary = (0, import_vue33.computed)(
9989
+ () => (props.media || []).map((item, index) => {
9990
+ if (typeof item === "string") {
9991
+ return { id: "media-" + index, url: item, name: "", type: "image" };
9992
+ }
9993
+ return {
9994
+ id: item && item.id !== void 0 ? String(item.id) : "media-" + index,
9995
+ url: item && (item.url || item.src) || "",
9996
+ name: item && (item.name || item.alt) || "",
9997
+ type: "image"
9998
+ };
9999
+ }).filter((item) => !!item.url)
10000
+ );
9609
10001
  function clean(html) {
9610
10002
  return props.sanitize ? sanitizeHtml(html) : html;
9611
10003
  }
10004
+ function plainText(html) {
10005
+ return String(html || "").replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").replace(/&[a-z]+;/gi, " ");
10006
+ }
9612
10007
  function syncFromDom() {
9613
10008
  if (!editor.value) return;
9614
- emit("update:modelValue", clean(editor.value.innerHTML));
10009
+ const html = clean(editor.value.innerHTML);
10010
+ count2.value = plainText(html).trim().length;
10011
+ emit("update:modelValue", html);
9615
10012
  }
9616
- function onPaste(event) {
9617
- const data = event.clipboardData;
9618
- const html = data && data.getData("text/html");
9619
- if (!html) return;
9620
- event.preventDefault();
9621
- document.execCommand("insertHTML", false, sanitizeHtml(html));
10013
+ function currentRange() {
10014
+ const selection = window.getSelection();
10015
+ if (!selection || !selection.rangeCount || !editor.value) return null;
10016
+ const range = selection.getRangeAt(0);
10017
+ return editor.value.contains(range.commonAncestorContainer) ? range : null;
10018
+ }
10019
+ function rememberRange() {
10020
+ const range = currentRange();
10021
+ savedRange = range ? range.cloneRange() : null;
10022
+ }
10023
+ function focusAt(range) {
10024
+ if (!editor.value) return;
10025
+ editor.value.focus();
10026
+ const selection = window.getSelection();
10027
+ selection.removeAllRanges();
10028
+ if (range) {
10029
+ selection.addRange(range);
10030
+ return;
10031
+ }
10032
+ const end = document.createRange();
10033
+ end.selectNodeContents(editor.value);
10034
+ end.collapse(false);
10035
+ selection.addRange(end);
10036
+ }
10037
+ function insertNodeAt(range, node) {
10038
+ if (!editor.value) return;
10039
+ focusAt(range);
10040
+ const selection = window.getSelection();
10041
+ if (selection.rangeCount) selection.getRangeAt(0).insertNode(node);
10042
+ else editor.value.appendChild(node);
9622
10043
  syncFromDom();
10044
+ syncFormats();
10045
+ }
10046
+ function ensureRange() {
10047
+ const range = currentRange();
10048
+ if (range) return range;
10049
+ if (!savedRange) return null;
10050
+ focusAt(savedRange);
10051
+ return currentRange();
9623
10052
  }
9624
10053
  function exec(command, value = null) {
9625
- if (props.disabled || sourceMode.value) return;
10054
+ if (!ready.value) return;
10055
+ if (typeof document.execCommand !== "function") return;
10056
+ ensureRange();
10057
+ document.execCommand("styleWithCSS", false, true);
9626
10058
  document.execCommand(command, false, value);
9627
10059
  if (editor.value) editor.value.focus();
9628
10060
  syncFromDom();
9629
10061
  syncFormats();
9630
10062
  }
9631
- function formatBlock(tag) {
9632
- exec("formatBlock", tag);
10063
+ function setBlock(value) {
10064
+ panel.value = "";
10065
+ exec("formatBlock", "<" + value + ">");
10066
+ }
10067
+ function setFontSize(size) {
10068
+ panel.value = "";
10069
+ if (!ready.value || !editor.value) return;
10070
+ fontSize.value = size;
10071
+ const range = ensureRange();
10072
+ if (!range || range.collapsed) return;
10073
+ if (!size) {
10074
+ const walker = document.createTreeWalker(editor.value, NodeFilter.SHOW_ELEMENT);
10075
+ const targets = [];
10076
+ while (walker.nextNode()) {
10077
+ const el = walker.currentNode;
10078
+ if (el.style && el.style.fontSize && range.intersectsNode(el)) targets.push(el);
10079
+ }
10080
+ targets.forEach((el) => {
10081
+ el.style.fontSize = "";
10082
+ });
10083
+ syncFromDom();
10084
+ return;
10085
+ }
10086
+ const span = document.createElement("span");
10087
+ span.style.fontSize = size;
10088
+ span.appendChild(range.extractContents());
10089
+ range.insertNode(span);
10090
+ const selection = window.getSelection();
10091
+ const after = document.createRange();
10092
+ after.setStartAfter(span);
10093
+ after.collapse(true);
10094
+ selection.removeAllRanges();
10095
+ selection.addRange(after);
10096
+ syncFromDom();
10097
+ syncFormats();
10098
+ }
10099
+ function applyColor(kind, color) {
10100
+ panel.value = "";
10101
+ if (kind === "text") textColor.value = color;
10102
+ else bgColor.value = color;
10103
+ if (!ready.value) return;
10104
+ if (typeof document.execCommand !== "function") return;
10105
+ document.execCommand("styleWithCSS", false, true);
10106
+ const command = kind === "text" ? "foreColor" : "hiliteColor";
10107
+ if (!document.execCommand(command, false, color) && kind === "bg") {
10108
+ document.execCommand("backColor", false, color);
10109
+ }
10110
+ if (editor.value) editor.value.focus();
10111
+ syncFromDom();
10112
+ syncFormats();
10113
+ }
10114
+ function togglePanel(name) {
10115
+ if (!ready.value) return;
10116
+ panel.value = panel.value === name ? "" : name;
10117
+ }
10118
+ function toggleInlineCode() {
10119
+ if (!ready.value || !editor.value) return;
10120
+ const range = currentRange();
10121
+ if (!range) return;
10122
+ const anchor = range.commonAncestorContainer;
10123
+ const node = anchor.nodeType === 1 ? anchor : anchor.parentElement;
10124
+ const code = node && node.closest ? node.closest("code") : null;
10125
+ if (code && editor.value.contains(code)) {
10126
+ const parent = code.parentNode;
10127
+ while (code.firstChild) parent.insertBefore(code.firstChild, code);
10128
+ parent.removeChild(code);
10129
+ syncFromDom();
10130
+ return;
10131
+ }
10132
+ if (range.collapsed) return;
10133
+ const wrapper = document.createElement("code");
10134
+ wrapper.appendChild(range.extractContents());
10135
+ range.insertNode(wrapper);
10136
+ const selection = window.getSelection();
10137
+ const after = document.createRange();
10138
+ after.setStartAfter(wrapper);
10139
+ after.collapse(true);
10140
+ selection.removeAllRanges();
10141
+ selection.addRange(after);
10142
+ syncFromDom();
10143
+ syncFormats();
9633
10144
  }
9634
10145
  function addLink() {
10146
+ if (!ready.value) return;
9635
10147
  const url = window.prompt(t("richEditor.linkPrompt"), "https://");
9636
- if (url) exec("createLink", url);
10148
+ if (!url) return;
10149
+ if (!isSafeLink(url)) {
10150
+ window.alert(t("richEditor.linkInvalid"));
10151
+ return;
10152
+ }
10153
+ const range = currentRange();
10154
+ if (range && range.collapsed) {
10155
+ const link = document.createElement("a");
10156
+ link.href = url;
10157
+ link.target = "_blank";
10158
+ link.rel = "noopener noreferrer";
10159
+ link.textContent = url;
10160
+ insertNodeAt(range, link);
10161
+ return;
10162
+ }
10163
+ exec("createLink", url);
10164
+ if (editor.value) {
10165
+ editor.value.querySelectorAll("a[href]").forEach((link) => {
10166
+ if (!link.getAttribute("target")) link.setAttribute("target", "_blank");
10167
+ link.setAttribute("rel", "noopener noreferrer");
10168
+ });
10169
+ }
10170
+ syncFromDom();
10171
+ }
10172
+ function readAsDataUrl(file) {
10173
+ return new Promise((resolve, reject) => {
10174
+ const reader = new FileReader();
10175
+ reader.onload = () => resolve(reader.result);
10176
+ reader.onerror = () => reject(reader.error || new Error("read failed"));
10177
+ reader.readAsDataURL(file);
10178
+ });
10179
+ }
10180
+ async function uploadOne(file) {
10181
+ if (props.uploadImage) {
10182
+ const result = await props.uploadImage(file);
10183
+ const url = typeof result === "string" ? result : result && result.url;
10184
+ if (!url) throw new Error(t("richEditor.imageUrlInvalid"));
10185
+ if (!isSafeImageUrl(url)) throw new Error(t("richEditor.imageUrlInvalid"));
10186
+ return String(url).trim();
10187
+ }
10188
+ return readAsDataUrl(file);
10189
+ }
10190
+ function isImage(file) {
10191
+ return file && /^image\//.test(file.type);
10192
+ }
10193
+ function pickImage(allowMultiple) {
10194
+ if (!ready.value || !fileInput.value) return;
10195
+ rememberRange();
10196
+ fileInput.value.multiple = !!allowMultiple && props.multiple;
10197
+ fileInput.value.click();
10198
+ }
10199
+ function insertMedia(ids) {
10200
+ if (!ready.value) return;
10201
+ const picked = (Array.isArray(ids) ? ids : [ids]).map((id) => mediaLibrary.value.find((item) => item.id === String(id))).filter(Boolean);
10202
+ const safe = picked.filter((item) => {
10203
+ if (isSafeImageUrl(item.url)) return true;
10204
+ emit("upload-error", { file: null, message: t("richEditor.imageUrlInvalid") });
10205
+ return false;
10206
+ });
10207
+ if (!safe.length) return;
10208
+ focusAt(savedRange);
10209
+ const fragment = document.createDocumentFragment();
10210
+ safe.forEach((item) => {
10211
+ const img = document.createElement("img");
10212
+ img.src = item.url;
10213
+ img.alt = item.name || "image";
10214
+ img.className = "djm-rich-editor-img";
10215
+ fragment.appendChild(img);
10216
+ });
10217
+ const selection = window.getSelection();
10218
+ if (selection.rangeCount) selection.getRangeAt(0).insertNode(fragment);
10219
+ else editor.value.appendChild(fragment);
10220
+ syncFromDom();
10221
+ syncFormats();
10222
+ }
10223
+ function insertImages(files) {
10224
+ if (!files.length || !editor.value) return;
10225
+ uploadCount.value = files.length;
10226
+ const holder = document.createElement("span");
10227
+ holder.className = "djm-rich-editor-uploading";
10228
+ holder.textContent = t("richEditor.uploading", { count: files.length });
10229
+ const anchor = savedRange && editor.value.contains(savedRange.commonAncestorContainer) ? savedRange : null;
10230
+ focusAt(anchor);
10231
+ const selection = window.getSelection();
10232
+ if (selection.rangeCount) selection.getRangeAt(0).insertNode(holder);
10233
+ else editor.value.appendChild(holder);
10234
+ Promise.all(files.map(async (file) => {
10235
+ if (!isImage(file)) return { file, error: t("richEditor.imageTypeError") };
10236
+ if (props.maxImageSize > 0 && file.size > props.maxImageSize * 1024 * 1024) {
10237
+ return { file, error: t("richEditor.imageSizeError", { size: props.maxImageSize }) };
10238
+ }
10239
+ try {
10240
+ return { file, url: await uploadOne(file) };
10241
+ } catch (error) {
10242
+ return { file, error: error && error.message || t("richEditor.uploadFailed") };
10243
+ }
10244
+ })).then((results) => {
10245
+ uploadCount.value = 0;
10246
+ const done = results.filter((item) => item.url);
10247
+ const failed = results.filter((item) => item.error);
10248
+ const alive = editor.value && editor.value.contains(holder);
10249
+ if (done.length && editor.value) {
10250
+ done.forEach((item) => {
10251
+ const img = document.createElement("img");
10252
+ img.src = item.url;
10253
+ img.alt = item.file.name || "image";
10254
+ img.className = "djm-rich-editor-img";
10255
+ if (alive) holder.parentNode.insertBefore(img, holder);
10256
+ else editor.value.appendChild(img);
10257
+ });
10258
+ }
10259
+ if (alive) holder.remove();
10260
+ failed.forEach((item) => emit("upload-error", { file: item.file, message: item.error }));
10261
+ syncFromDom();
10262
+ });
10263
+ }
10264
+ function onFilesChange(event) {
10265
+ const files = Array.from(event.target.files || []);
10266
+ event.target.value = "";
10267
+ insertImages(files);
10268
+ }
10269
+ function onPaste(event) {
10270
+ const data = event.clipboardData;
10271
+ if (!data) return;
10272
+ const images = Array.from(data.files || []).filter(isImage);
10273
+ if (images.length) {
10274
+ event.preventDefault();
10275
+ insertImages(images);
10276
+ return;
10277
+ }
10278
+ const html = data.getData("text/html");
10279
+ if (!html) return;
10280
+ event.preventDefault();
10281
+ document.execCommand("insertHTML", false, sanitizeHtml(html));
10282
+ syncFromDom();
10283
+ }
10284
+ function onDragOver(event) {
10285
+ if (!ready.value) return;
10286
+ if (event.dataTransfer && Array.from(event.dataTransfer.types || []).includes("Files")) {
10287
+ event.preventDefault();
10288
+ }
10289
+ }
10290
+ function onDrop(event) {
10291
+ if (!ready.value) return;
10292
+ const files = Array.from(event.dataTransfer && event.dataTransfer.files || []).filter(isImage);
10293
+ if (!files.length) return;
10294
+ event.preventDefault();
10295
+ rememberRange();
10296
+ insertImages(files);
10297
+ }
10298
+ function closestTag(node, tag) {
10299
+ const el = node && (node.nodeType === 1 ? node : node.parentElement);
10300
+ return el && el.closest ? el.closest(tag) : null;
10301
+ }
10302
+ function detectBlock() {
10303
+ const selection = window.getSelection();
10304
+ if (!selection || !selection.anchorNode) return "p";
10305
+ let node = selection.anchorNode;
10306
+ if (node.nodeType !== 1) node = node.parentElement;
10307
+ while (node && node !== editor.value) {
10308
+ const tag = node.tagName.toLowerCase();
10309
+ if (["p", "h1", "h2", "h3", "blockquote", "pre"].includes(tag)) return tag;
10310
+ node = node.parentElement;
10311
+ }
10312
+ return "p";
9637
10313
  }
9638
10314
  function syncFormats() {
9639
- if (props.disabled || sourceMode.value) return;
9640
- if (!editor.value || !editor.value.contains(document.activeElement)) return;
10315
+ if (props.disabled || sourceMode.value || !editor.value) return;
10316
+ if (!editor.value.contains(document.activeElement)) return;
10317
+ const selection = window.getSelection();
9641
10318
  formats.value = {
9642
10319
  bold: document.queryCommandState("bold"),
9643
10320
  italic: document.queryCommandState("italic"),
9644
10321
  underline: document.queryCommandState("underline"),
9645
- strike: document.queryCommandState("strikeThrough")
10322
+ strike: document.queryCommandState("strikeThrough"),
10323
+ code: !!closestTag(selection.anchorNode, "code"),
10324
+ block: detectBlock(),
10325
+ align: document.queryCommandState("justifyCenter") ? "center" : document.queryCommandState("justifyRight") ? "right" : document.queryCommandState("justifyFull") ? "justify" : "left",
10326
+ list: document.queryCommandState("insertOrderedList") ? "ol" : document.queryCommandState("insertUnorderedList") ? "ul" : ""
9646
10327
  };
9647
10328
  }
9648
10329
  function onSourceInput() {
10330
+ count2.value = plainText(source.value).trim().length;
9649
10331
  emit("update:modelValue", clean(source.value));
9650
10332
  }
9651
10333
  function toggleSource() {
9652
10334
  if (props.disabled) return;
10335
+ panel.value = "";
9653
10336
  if (sourceMode.value) {
9654
10337
  emit("update:modelValue", clean(source.value));
9655
10338
  sourceMode.value = false;
9656
10339
  requestAnimationFrame(() => {
9657
10340
  if (editor.value) editor.value.innerHTML = clean(source.value);
10341
+ syncFromDom();
9658
10342
  });
9659
10343
  } else {
9660
10344
  source.value = editor.value ? editor.value.innerHTML : props.modelValue;
9661
10345
  sourceMode.value = true;
9662
10346
  }
9663
10347
  }
9664
- (0, import_vue32.watch)(
10348
+ function toggleFullscreen() {
10349
+ if (props.disabled) return;
10350
+ fullscreen.value = !fullscreen.value;
10351
+ panel.value = "";
10352
+ }
10353
+ function onDocumentClick(event) {
10354
+ if (!panel.value) return;
10355
+ if (!(event.target.closest && event.target.closest(".djm-rich-editor-color"))) panel.value = "";
10356
+ }
10357
+ function onKeydown(event) {
10358
+ if (event.key === "Escape" && fullscreen.value) {
10359
+ fullscreen.value = false;
10360
+ panel.value = "";
10361
+ }
10362
+ }
10363
+ (0, import_vue33.watch)(
9665
10364
  () => props.modelValue,
9666
10365
  (value) => {
9667
10366
  if (sourceMode.value) {
@@ -9670,40 +10369,117 @@ var rich_editor_default = {
9670
10369
  }
9671
10370
  const next = clean(value);
9672
10371
  if (editor.value && editor.value.innerHTML !== next) editor.value.innerHTML = next;
10372
+ count2.value = plainText(next).trim().length;
9673
10373
  }
9674
10374
  );
9675
- (0, import_vue32.onMounted)(() => {
10375
+ (0, import_vue33.onMounted)(() => {
9676
10376
  if (editor.value) editor.value.innerHTML = clean(props.modelValue);
10377
+ count2.value = plainText(props.modelValue).trim().length;
9677
10378
  document.addEventListener("selectionchange", syncFormats);
10379
+ document.addEventListener("click", onDocumentClick);
10380
+ document.addEventListener("keydown", onKeydown);
10381
+ });
10382
+ (0, import_vue33.onBeforeUnmount)(() => {
10383
+ document.removeEventListener("selectionchange", syncFormats);
10384
+ document.removeEventListener("click", onDocumentClick);
10385
+ document.removeEventListener("keydown", onKeydown);
9678
10386
  });
9679
- (0, import_vue32.onBeforeUnmount)(() => document.removeEventListener("selectionchange", syncFormats));
9680
10387
  return {
9681
10388
  editor,
10389
+ fileInput,
9682
10390
  sourceMode,
9683
10391
  source,
10392
+ fullscreen,
10393
+ panel,
10394
+ uploadCount,
10395
+ textColor,
10396
+ bgColor,
10397
+ fontSize,
10398
+ count: count2,
9684
10399
  formats,
10400
+ blocks: BLOCKS,
10401
+ sizes: SIZES,
10402
+ colors: COLORS,
10403
+ mediaLibrary,
10404
+ has,
9685
10405
  exec,
9686
- formatBlock,
10406
+ setBlock,
10407
+ setFontSize,
10408
+ applyColor,
10409
+ togglePanel,
10410
+ toggleInlineCode,
9687
10411
  addLink,
9688
- onSourceInput,
10412
+ pickImage,
10413
+ insertMedia,
10414
+ onFilesChange,
9689
10415
  onPaste,
10416
+ onDragOver,
10417
+ onDrop,
10418
+ rememberRange,
10419
+ onSourceInput,
9690
10420
  toggleSource,
10421
+ toggleFullscreen,
9691
10422
  syncFromDom,
9692
10423
  syncFormats,
9693
10424
  t
9694
10425
  };
9695
10426
  },
9696
10427
  template: `
9697
- <div class="djm-rich-editor" :class="{ 'djm-is-disabled': disabled }">
9698
- <div class="djm-rich-editor-toolbar">
9699
- <div class="djm-rich-editor-group">
10428
+ <div
10429
+ class="djm-rich-editor"
10430
+ :class="{ 'djm-is-disabled': disabled, 'djm-is-fullscreen': fullscreen }"
10431
+ >
10432
+ <div class="djm-rich-editor-toolbar" role="toolbar" :aria-label="t('richEditor.toolbar')" @mousedown="rememberRange">
10433
+ <div class="djm-rich-editor-group" v-if="has('history')">
10434
+ <button
10435
+ class="djm-rich-editor-btn"
10436
+ type="button"
10437
+ :title="t('richEditor.undo')"
10438
+ :aria-label="t('richEditor.undo')"
10439
+ :disabled="disabled || sourceMode"
10440
+ @mousedown.prevent
10441
+ @click="exec('undo')"
10442
+ ><Icon name="undo" :size="13" /></button>
10443
+ <button
10444
+ class="djm-rich-editor-btn"
10445
+ type="button"
10446
+ :title="t('richEditor.redo')"
10447
+ :aria-label="t('richEditor.redo')"
10448
+ :disabled="disabled || sourceMode"
10449
+ @mousedown.prevent
10450
+ @click="exec('redo')"
10451
+ ><Icon name="redo" :size="13" /></button>
10452
+ </div>
10453
+
10454
+ <div class="djm-rich-editor-group" v-if="has('format')">
10455
+ <select
10456
+ class="djm-rich-editor-select"
10457
+ :value="formats.block"
10458
+ :aria-label="t('richEditor.block')"
10459
+ :disabled="disabled || sourceMode"
10460
+ @change="setBlock($event.target.value)"
10461
+ >
10462
+ <option v-for="item in blocks" :key="item.value" :value="item.value">{{ t('richEditor.' + item.label) }}</option>
10463
+ </select>
10464
+ <select
10465
+ class="djm-rich-editor-select djm-rich-editor-select-size"
10466
+ :value="fontSize"
10467
+ :aria-label="t('richEditor.fontSize')"
10468
+ :disabled="disabled || sourceMode"
10469
+ @change="setFontSize($event.target.value)"
10470
+ >
10471
+ <option v-for="item in sizes" :key="item.value" :value="item.value">{{ t('richEditor.' + item.label) }}</option>
10472
+ </select>
10473
+ </div>
10474
+
10475
+ <div class="djm-rich-editor-group" v-if="has('inline')">
9700
10476
  <button
9701
10477
  class="djm-rich-editor-btn djm-rich-editor-btn-bold"
9702
10478
  type="button"
9703
10479
  :title="t('richEditor.bold')"
9704
10480
  :aria-label="t('richEditor.bold')"
9705
10481
  :aria-pressed="String(formats.bold)"
9706
- :disabled="disabled"
10482
+ :disabled="disabled || sourceMode"
9707
10483
  @mousedown.prevent
9708
10484
  @click="exec('bold')"
9709
10485
  >B</button>
@@ -9713,7 +10489,7 @@ var rich_editor_default = {
9713
10489
  :title="t('richEditor.italic')"
9714
10490
  :aria-label="t('richEditor.italic')"
9715
10491
  :aria-pressed="String(formats.italic)"
9716
- :disabled="disabled"
10492
+ :disabled="disabled || sourceMode"
9717
10493
  @mousedown.prevent
9718
10494
  @click="exec('italic')"
9719
10495
  >I</button>
@@ -9723,7 +10499,7 @@ var rich_editor_default = {
9723
10499
  :title="t('richEditor.underline')"
9724
10500
  :aria-label="t('richEditor.underline')"
9725
10501
  :aria-pressed="String(formats.underline)"
9726
- :disabled="disabled"
10502
+ :disabled="disabled || sourceMode"
9727
10503
  @mousedown.prevent
9728
10504
  @click="exec('underline')"
9729
10505
  >U</button>
@@ -9733,46 +10509,130 @@ var rich_editor_default = {
9733
10509
  :title="t('richEditor.strike')"
9734
10510
  :aria-label="t('richEditor.strike')"
9735
10511
  :aria-pressed="String(formats.strike)"
9736
- :disabled="disabled"
10512
+ :disabled="disabled || sourceMode"
10513
+ @mousedown.prevent
10514
+ @click="exec('strikeThrough')"
10515
+ >S</button>
10516
+ <button
10517
+ class="djm-rich-editor-btn"
10518
+ type="button"
10519
+ :title="t('richEditor.inlineCode')"
10520
+ :aria-label="t('richEditor.inlineCode')"
10521
+ :aria-pressed="String(formats.code)"
10522
+ :disabled="disabled || sourceMode"
10523
+ @mousedown.prevent
10524
+ @click="toggleInlineCode"
10525
+ ><Icon name="code" :size="13" /></button>
10526
+ </div>
10527
+
10528
+ <div class="djm-rich-editor-group djm-rich-editor-color" v-if="has('color')">
10529
+ <button
10530
+ class="djm-rich-editor-btn"
10531
+ type="button"
10532
+ :title="t('richEditor.textColor')"
10533
+ :aria-label="t('richEditor.textColor')"
10534
+ :aria-expanded="String(panel === 'text')"
10535
+ :disabled="disabled || sourceMode"
10536
+ @mousedown.prevent
10537
+ @click.stop="togglePanel('text')"
10538
+ >
10539
+ <span class="djm-rich-editor-letter" :style="{ borderBottomColor: textColor }">A</span>
10540
+ </button>
10541
+ <button
10542
+ class="djm-rich-editor-btn"
10543
+ type="button"
10544
+ :title="t('richEditor.bgColor')"
10545
+ :aria-label="t('richEditor.bgColor')"
10546
+ :aria-expanded="String(panel === 'bg')"
10547
+ :disabled="disabled || sourceMode"
9737
10548
  @mousedown.prevent
9738
- @click="exec('strikeThrough')"
9739
- >S</button>
10549
+ @click.stop="togglePanel('bg')"
10550
+ >
10551
+ <span class="djm-rich-editor-letter djm-rich-editor-letter-bg" :style="{ background: bgColor }">A</span>
10552
+ </button>
10553
+ <div v-if="panel === 'text' || panel === 'bg'" class="djm-rich-editor-panel" role="group" :aria-label="t('richEditor.textColor')">
10554
+ <button
10555
+ v-for="color in colors"
10556
+ :key="color"
10557
+ class="djm-rich-editor-swatch"
10558
+ type="button"
10559
+ :title="color"
10560
+ :aria-label="color"
10561
+ :style="{ background: color }"
10562
+ @mousedown.prevent
10563
+ @click="applyColor(panel, color)"
10564
+ ></button>
10565
+ </div>
9740
10566
  </div>
9741
10567
 
9742
- <div class="djm-rich-editor-group">
9743
- <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.heading')" :disabled="disabled" @mousedown.prevent @click="formatBlock('h2')">H2</button>
9744
- <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.paragraph')" :disabled="disabled" @mousedown.prevent @click="formatBlock('p')">{{ t('richEditor.paragraph') }}</button>
10568
+ <div class="djm-rich-editor-group" v-if="has('align')">
10569
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.alignLeft')" :aria-label="t('richEditor.alignLeft')" :aria-pressed="String(formats.align === 'left')" :disabled="disabled || sourceMode" @mousedown.prevent @click="exec('justifyLeft')"><Icon name="align-left" :size="13" /></button>
10570
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.alignCenter')" :aria-label="t('richEditor.alignCenter')" :aria-pressed="String(formats.align === 'center')" :disabled="disabled || sourceMode" @mousedown.prevent @click="exec('justifyCenter')"><Icon name="align-center" :size="13" /></button>
10571
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.alignRight')" :aria-label="t('richEditor.alignRight')" :aria-pressed="String(formats.align === 'right')" :disabled="disabled || sourceMode" @mousedown.prevent @click="exec('justifyRight')"><Icon name="align-right" :size="13" /></button>
10572
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.alignJustify')" :aria-label="t('richEditor.alignJustify')" :aria-pressed="String(formats.align === 'justify')" :disabled="disabled || sourceMode" @mousedown.prevent @click="exec('justifyFull')"><Icon name="align-justify" :size="13" /></button>
9745
10573
  </div>
9746
10574
 
9747
- <div class="djm-rich-editor-group">
9748
- <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.bulletList')" :disabled="disabled" @mousedown.prevent @click="exec('insertUnorderedList')">
9749
- <span class="djm-rich-editor-glyph">\u2022 \u2261</span>
9750
- </button>
9751
- <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.numberList')" :disabled="disabled" @mousedown.prevent @click="exec('insertOrderedList')">
9752
- <span class="djm-rich-editor-glyph">1. \u2261</span>
9753
- </button>
9754
- <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.link')" :aria-label="t('richEditor.link')" :disabled="disabled" @mousedown.prevent @click="addLink">
9755
- <Icon name="link" :size="13" />
9756
- </button>
10575
+ <div class="djm-rich-editor-group" v-if="has('list')">
10576
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.bulletList')" :aria-label="t('richEditor.bulletList')" :aria-pressed="String(formats.list === 'ul')" :disabled="disabled || sourceMode" @mousedown.prevent @click="exec('insertUnorderedList')"><Icon name="list" :size="13" /></button>
10577
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.numberList')" :aria-label="t('richEditor.numberList')" :aria-pressed="String(formats.list === 'ol')" :disabled="disabled || sourceMode" @mousedown.prevent @click="exec('insertOrderedList')"><Icon name="list-ordered" :size="13" /></button>
10578
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.outdent')" :aria-label="t('richEditor.outdent')" :disabled="disabled || sourceMode" @mousedown.prevent @click="exec('outdent')"><Icon name="outdent" :size="13" /></button>
10579
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.indent')" :aria-label="t('richEditor.indent')" :disabled="disabled || sourceMode" @mousedown.prevent @click="exec('indent')"><Icon name="indent" :size="13" /></button>
9757
10580
  </div>
9758
10581
 
9759
- <div class="djm-rich-editor-group">
9760
- <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.clearFormat')" :disabled="disabled" @mousedown.prevent @click="exec('removeFormat')">{{ t('richEditor.clearFormat') }}</button>
9761
- <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.undo')" :aria-label="t('richEditor.undo')" :disabled="disabled" @mousedown.prevent @click="exec('undo')">\u21B6</button>
9762
- <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.redo')" :aria-label="t('richEditor.redo')" :disabled="disabled" @mousedown.prevent @click="exec('redo')">\u21B7</button>
10582
+ <div class="djm-rich-editor-group" v-if="has('insert')">
10583
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.link')" :aria-label="t('richEditor.link')" :disabled="disabled || sourceMode" @mousedown.prevent @click="addLink"><Icon name="link" :size="13" /></button>
10584
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.unlink')" :aria-label="t('richEditor.unlink')" :disabled="disabled || sourceMode" @mousedown.prevent @click="exec('unlink')"><Icon name="unlink" :size="13" /></button>
10585
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.singleImage')" :aria-label="t('richEditor.singleImage')" :disabled="disabled || sourceMode" @mousedown.prevent @click="pickImage(false)"><Icon name="image" :size="13" /></button>
10586
+ <button v-if="multiple" class="djm-rich-editor-btn" type="button" :title="t('richEditor.multiImage')" :aria-label="t('richEditor.multiImage')" :disabled="disabled || sourceMode" @mousedown.prevent @click="pickImage(true)"><Icon name="images" :size="13" /></button>
10587
+ <MediaPicker
10588
+ v-if="mediaLibrary.length"
10589
+ class="djm-rich-editor-picker"
10590
+ multiple
10591
+ accept="image"
10592
+ :items="mediaLibrary"
10593
+ :title="t('richEditor.mediaLibrary')"
10594
+ :disabled="disabled || sourceMode"
10595
+ @change="insertMedia"
10596
+ >
10597
+ <template #trigger="{ open }">
10598
+ <button
10599
+ class="djm-rich-editor-btn"
10600
+ type="button"
10601
+ :title="t('richEditor.mediaLibrary')"
10602
+ :aria-label="t('richEditor.mediaLibrary')"
10603
+ :disabled="disabled || sourceMode"
10604
+ @mousedown.prevent="rememberRange"
10605
+ @click="open"
10606
+ ><Icon name="grid" :size="13" /></button>
10607
+ </template>
10608
+ </MediaPicker>
10609
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.hr')" :aria-label="t('richEditor.hr')" :disabled="disabled || sourceMode" @mousedown.prevent @click="exec('insertHorizontalRule')">\u2014</button>
10610
+ <button class="djm-rich-editor-btn" type="button" :title="t('richEditor.clearFormat')" :aria-label="t('richEditor.clearFormat')" :disabled="disabled || sourceMode" @mousedown.prevent @click="exec('removeFormat')"><span class="djm-rich-editor-glyph">T\xD7</span></button>
9763
10611
  </div>
9764
10612
 
9765
- <div class="djm-rich-editor-spacer"></div>
9766
- <button
9767
- class="djm-rich-editor-btn"
9768
- :class="{ 'djm-is-active': sourceMode }"
9769
- type="button"
9770
- :aria-pressed="String(sourceMode)"
9771
- :disabled="disabled"
9772
- @click="toggleSource"
9773
- >
9774
- <Icon name="code" :size="13" />{{ sourceMode ? t('richEditor.visual') : t('richEditor.source') }}
9775
- </button>
10613
+ <div class="djm-rich-editor-spacer" v-if="has('view')"></div>
10614
+ <div class="djm-rich-editor-group djm-rich-editor-group-end" v-if="has('view')">
10615
+ <button
10616
+ class="djm-rich-editor-btn"
10617
+ :class="{ 'djm-is-active': sourceMode }"
10618
+ type="button"
10619
+ :title="sourceMode ? t('richEditor.visual') : t('richEditor.source')"
10620
+ :aria-label="sourceMode ? t('richEditor.visual') : t('richEditor.source')"
10621
+ :aria-pressed="String(sourceMode)"
10622
+ :disabled="disabled"
10623
+ @click="toggleSource"
10624
+ ><Icon name="code" :size="13" /></button>
10625
+ <button
10626
+ class="djm-rich-editor-btn"
10627
+ :class="{ 'djm-is-active': fullscreen }"
10628
+ type="button"
10629
+ :title="fullscreen ? t('richEditor.exitFullscreen') : t('richEditor.fullscreen')"
10630
+ :aria-label="fullscreen ? t('richEditor.exitFullscreen') : t('richEditor.fullscreen')"
10631
+ :aria-pressed="String(fullscreen)"
10632
+ :disabled="disabled"
10633
+ @click="toggleFullscreen"
10634
+ ><Icon :name="fullscreen ? 'minimize' : 'maximize'" :size="13" /></button>
10635
+ </div>
9776
10636
  </div>
9777
10637
 
9778
10638
  <div
@@ -9788,6 +10648,8 @@ var rich_editor_default = {
9788
10648
  :style="{ minHeight: minHeight + 'px' }"
9789
10649
  @input="syncFromDom"
9790
10650
  @paste="onPaste"
10651
+ @dragover="onDragOver"
10652
+ @drop="onDrop"
9791
10653
  ></div>
9792
10654
 
9793
10655
  <textarea
@@ -9800,12 +10662,27 @@ var rich_editor_default = {
9800
10662
  spellcheck="false"
9801
10663
  @input="onSourceInput"
9802
10664
  ></textarea>
10665
+
10666
+ <div v-if="showCount" class="djm-rich-editor-foot">
10667
+ <span v-if="uploadCount" class="djm-rich-editor-foot-item">{{ t('richEditor.uploading', { count: uploadCount }) }}</span>
10668
+ <span class="djm-rich-editor-foot-item">{{ t('richEditor.wordCount', { count }) }}</span>
10669
+ </div>
10670
+
10671
+ <input
10672
+ ref="fileInput"
10673
+ class="djm-rich-editor-file"
10674
+ type="file"
10675
+ :accept="accept"
10676
+ tabindex="-1"
10677
+ aria-hidden="true"
10678
+ @change="onFilesChange"
10679
+ />
9803
10680
  </div>
9804
10681
  `
9805
10682
  };
9806
10683
 
9807
10684
  // src/components/floating-window/index.js
9808
- var import_vue33 = require("vue");
10685
+ var import_vue34 = require("vue");
9809
10686
  function clamp(value, min, max) {
9810
10687
  return Math.min(Math.max(value, min), Math.max(min, max));
9811
10688
  }
@@ -9823,12 +10700,12 @@ var floating_window_default = {
9823
10700
  },
9824
10701
  emits: ["close", "focus"],
9825
10702
  setup(props, { emit }) {
9826
- const root = (0, import_vue33.ref)(null);
9827
- const pos = (0, import_vue33.ref)({ x: props.initialX, y: props.initialY });
9828
- const size = (0, import_vue33.ref)({ w: props.width, h: props.height });
9829
- const minimized = (0, import_vue33.ref)(false);
9830
- const maximized = (0, import_vue33.ref)(false);
9831
- const saved = (0, import_vue33.ref)(null);
10703
+ const root = (0, import_vue34.ref)(null);
10704
+ const pos = (0, import_vue34.ref)({ x: props.initialX, y: props.initialY });
10705
+ const size = (0, import_vue34.ref)({ w: props.width, h: props.height });
10706
+ const minimized = (0, import_vue34.ref)(false);
10707
+ const maximized = (0, import_vue34.ref)(false);
10708
+ const saved = (0, import_vue34.ref)(null);
9832
10709
  let drag = null;
9833
10710
  let resize = null;
9834
10711
  function stage() {
@@ -9916,7 +10793,7 @@ var floating_window_default = {
9916
10793
  }
9917
10794
  emit("focus");
9918
10795
  }
9919
- (0, import_vue33.onBeforeUnmount)(() => {
10796
+ (0, import_vue34.onBeforeUnmount)(() => {
9920
10797
  window.removeEventListener("pointermove", onDrag);
9921
10798
  window.removeEventListener("pointerup", endDrag);
9922
10799
  window.removeEventListener("pointermove", onResize);
@@ -9964,7 +10841,7 @@ var floating_window_default = {
9964
10841
  };
9965
10842
 
9966
10843
  // src/components/upload/index.js
9967
- var import_vue34 = require("vue");
10844
+ var import_vue35 = require("vue");
9968
10845
  var uidSeed6 = 0;
9969
10846
  function extension(name) {
9970
10847
  const index = name.lastIndexOf(".");
@@ -10014,11 +10891,11 @@ var upload_default = {
10014
10891
  },
10015
10892
  emits: ["update:modelValue", "change", "success", "error", "remove", "exceed"],
10016
10893
  setup(props, { emit }) {
10017
- const files = (0, import_vue34.ref)([]);
10018
- const dragActive = (0, import_vue34.ref)(false);
10019
- const inputRef = (0, import_vue34.ref)(null);
10894
+ const files = (0, import_vue35.ref)([]);
10895
+ const dragActive = (0, import_vue35.ref)(false);
10896
+ const inputRef = (0, import_vue35.ref)(null);
10020
10897
  const tasks = /* @__PURE__ */ new Map();
10021
- (0, import_vue34.watch)(
10898
+ (0, import_vue35.watch)(
10022
10899
  () => props.modelValue,
10023
10900
  (list) => {
10024
10901
  const next = list || [];
@@ -10112,7 +10989,7 @@ var upload_default = {
10112
10989
  if (pass === false) continue;
10113
10990
  }
10114
10991
  uidSeed6 += 1;
10115
- const item = (0, import_vue34.reactive)({
10992
+ const item = (0, import_vue35.reactive)({
10116
10993
  uid: uidSeed6,
10117
10994
  name: raw.name,
10118
10995
  size: raw.size,
@@ -10172,7 +11049,7 @@ var upload_default = {
10172
11049
  files.value = [];
10173
11050
  emitList();
10174
11051
  }
10175
- (0, import_vue34.onBeforeUnmount)(() => {
11052
+ (0, import_vue35.onBeforeUnmount)(() => {
10176
11053
  files.value.forEach((item) => {
10177
11054
  if (item.url) URL.revokeObjectURL(item.url);
10178
11055
  });
@@ -10320,7 +11197,7 @@ var upload_default = {
10320
11197
  };
10321
11198
 
10322
11199
  // src/components/tree/index.js
10323
- var import_vue35 = require("vue");
11200
+ var import_vue36 = require("vue");
10324
11201
  var tree_default = {
10325
11202
  name: "DjmTree",
10326
11203
  components: { Icon: icon_default },
@@ -10352,17 +11229,17 @@ var tree_default = {
10352
11229
  },
10353
11230
  emits: ["update:checked", "update:expanded", "node-click", "check", "node-drop", "load-error"],
10354
11231
  setup(props, { emit }) {
10355
- const lazyChildren = (0, import_vue35.ref)({});
10356
- const loadingKeys = (0, import_vue35.ref)([]);
10357
- const innerExpanded = (0, import_vue35.ref)([...props.expanded]);
10358
- const innerChecked = (0, import_vue35.ref)([...props.checked]);
10359
- (0, import_vue35.watch)(
11232
+ const lazyChildren = (0, import_vue36.ref)({});
11233
+ const loadingKeys = (0, import_vue36.ref)([]);
11234
+ const innerExpanded = (0, import_vue36.ref)([...props.expanded]);
11235
+ const innerChecked = (0, import_vue36.ref)([...props.checked]);
11236
+ (0, import_vue36.watch)(
10360
11237
  () => props.expanded,
10361
11238
  (value) => {
10362
11239
  if (Array.isArray(value)) innerExpanded.value = value;
10363
11240
  }
10364
11241
  );
10365
- (0, import_vue35.watch)(
11242
+ (0, import_vue36.watch)(
10366
11243
  () => props.checked,
10367
11244
  (value) => {
10368
11245
  if (Array.isArray(value)) innerChecked.value = value;
@@ -10400,7 +11277,7 @@ var tree_default = {
10400
11277
  });
10401
11278
  return out;
10402
11279
  }
10403
- (0, import_vue35.watch)(
11280
+ (0, import_vue36.watch)(
10404
11281
  () => props.data,
10405
11282
  () => {
10406
11283
  if (props.defaultExpandAll) innerExpanded.value = collectExpandable(props.data);
@@ -10417,7 +11294,7 @@ var tree_default = {
10417
11294
  const children = childrenOf(node);
10418
11295
  return Boolean(children && children.some((child) => matchSubtree(child)));
10419
11296
  }
10420
- const rows = (0, import_vue35.computed)(() => {
11297
+ const rows = (0, import_vue36.computed)(() => {
10421
11298
  const result = [];
10422
11299
  const walk = (nodes, level) => {
10423
11300
  nodes.forEach((node) => {
@@ -10488,8 +11365,8 @@ var tree_default = {
10488
11365
  function isActive(node) {
10489
11366
  return props.activeKey !== "" && node[props.nodeKey] === props.activeKey;
10490
11367
  }
10491
- const rootEl = (0, import_vue35.ref)(null);
10492
- const focusIndex = (0, import_vue35.ref)(-1);
11368
+ const rootEl = (0, import_vue36.ref)(null);
11369
+ const focusIndex = (0, import_vue36.ref)(-1);
10493
11370
  function tabIndexOf(index) {
10494
11371
  return (focusIndex.value === -1 ? 0 : focusIndex.value) === index ? 0 : -1;
10495
11372
  }
@@ -10546,9 +11423,9 @@ var tree_default = {
10546
11423
  function rowClassOf(node, level) {
10547
11424
  return typeof props.rowClass === "function" ? props.rowClass(node, level) : props.rowClass;
10548
11425
  }
10549
- const dragKey = (0, import_vue35.ref)("");
10550
- const dropKey = (0, import_vue35.ref)("");
10551
- const dropPosition = (0, import_vue35.ref)("");
11426
+ const dragKey = (0, import_vue36.ref)("");
11427
+ const dropKey = (0, import_vue36.ref)("");
11428
+ const dropPosition = (0, import_vue36.ref)("");
10552
11429
  function findNodeByKey(key, nodes = props.data) {
10553
11430
  for (const node of nodes) {
10554
11431
  if (node[props.nodeKey] === key) return node;
@@ -10660,7 +11537,7 @@ var tree_default = {
10660
11537
  onDrop,
10661
11538
  onDragEnd,
10662
11539
  labelOf,
10663
- emptyLabel: (0, import_vue35.computed)(() => props.emptyText || t("tree.empty")),
11540
+ emptyLabel: (0, import_vue36.computed)(() => props.emptyText || t("tree.empty")),
10664
11541
  t
10665
11542
  };
10666
11543
  },
@@ -10729,7 +11606,7 @@ var tree_default = {
10729
11606
  };
10730
11607
 
10731
11608
  // src/components/editable-table/index.js
10732
- var import_vue36 = require("vue");
11609
+ var import_vue37 = require("vue");
10733
11610
  var ACTIONS_KEY = "__editable_actions";
10734
11611
  function isEmpty(value) {
10735
11612
  if (value === "" || value === null || value === void 0) return true;
@@ -10752,13 +11629,13 @@ var editable_table_default = {
10752
11629
  },
10753
11630
  emits: ["edit", "save", "cancel"],
10754
11631
  setup(props, { emit }) {
10755
- const slots = (0, import_vue36.useSlots)();
10756
- const editingKey = (0, import_vue36.ref)("");
10757
- const draft = (0, import_vue36.reactive)({});
10758
- const errors = (0, import_vue36.reactive)({});
10759
- const tableRef = (0, import_vue36.ref)(null);
10760
- const editableColumns = (0, import_vue36.computed)(() => props.columns.filter((column) => column.editor));
10761
- const renderColumns = (0, import_vue36.computed)(() => {
11632
+ const slots = (0, import_vue37.useSlots)();
11633
+ const editingKey = (0, import_vue37.ref)("");
11634
+ const draft = (0, import_vue37.reactive)({});
11635
+ const errors = (0, import_vue37.reactive)({});
11636
+ const tableRef = (0, import_vue37.ref)(null);
11637
+ const editableColumns = (0, import_vue37.computed)(() => props.columns.filter((column) => column.editor));
11638
+ const renderColumns = (0, import_vue37.computed)(() => {
10762
11639
  if (!props.showActions) return props.columns;
10763
11640
  return props.columns.concat({
10764
11641
  key: ACTIONS_KEY,
@@ -10770,7 +11647,7 @@ var editable_table_default = {
10770
11647
  exportable: false
10771
11648
  });
10772
11649
  });
10773
- const forwardedSlotNames = (0, import_vue36.computed)(() => {
11650
+ const forwardedSlotNames = (0, import_vue37.computed)(() => {
10774
11651
  const taken = new Set(editableColumns.value.map((column) => "cell-" + column.key));
10775
11652
  taken.add("cell-" + ACTIONS_KEY);
10776
11653
  return Object.keys(slots).filter((name) => !taken.has(name));
@@ -10845,7 +11722,7 @@ var editable_table_default = {
10845
11722
  return false;
10846
11723
  }
10847
11724
  if (!validate()) {
10848
- (0, import_vue36.nextTick)(() => {
11725
+ (0, import_vue37.nextTick)(() => {
10849
11726
  const el = tableRef.value && tableRef.value.$el ? tableRef.value.$el.querySelector(".djm-cell-error") : null;
10850
11727
  if (el) el.scrollIntoView({ block: "nearest" });
10851
11728
  });
@@ -10865,7 +11742,7 @@ var editable_table_default = {
10865
11742
  clearErrors();
10866
11743
  return true;
10867
11744
  }
10868
- (0, import_vue36.watch)(
11745
+ (0, import_vue37.watch)(
10869
11746
  () => props.rows,
10870
11747
  () => {
10871
11748
  if (editingKey.value && !findRow(editingKey.value)) {
@@ -11001,8 +11878,8 @@ var descriptions_default = {
11001
11878
  };
11002
11879
 
11003
11880
  // src/components/message/index.js
11004
- var import_vue37 = require("vue");
11005
- var djmMessages = (0, import_vue37.reactive)([]);
11881
+ var import_vue38 = require("vue");
11882
+ var djmMessages = (0, import_vue38.reactive)([]);
11006
11883
  var seq = 0;
11007
11884
  function close(id) {
11008
11885
  const index = djmMessages.findIndex((item) => item.id === id);
@@ -11047,7 +11924,7 @@ var message_default = {
11047
11924
  };
11048
11925
 
11049
11926
  // src/components/org-picker/index.js
11050
- var import_vue38 = require("vue");
11927
+ var import_vue39 = require("vue");
11051
11928
  var org_picker_default = {
11052
11929
  name: "DjmOrgPicker",
11053
11930
  components: { DjmIcon: icon_default },
@@ -11066,13 +11943,13 @@ var org_picker_default = {
11066
11943
  },
11067
11944
  emits: ["update:modelValue", "change", "visible-change"],
11068
11945
  setup(props, { emit }) {
11069
- const open = (0, import_vue38.ref)(false);
11070
- const root = (0, import_vue38.ref)(null);
11071
- const searchRef = (0, import_vue38.ref)(null);
11072
- const keyword = (0, import_vue38.ref)("");
11073
- const activeDept = (0, import_vue38.ref)("");
11074
- const temp = (0, import_vue38.ref)([]);
11075
- const expandedIds = (0, import_vue38.ref)(/* @__PURE__ */ new Set());
11946
+ const open = (0, import_vue39.ref)(false);
11947
+ const root = (0, import_vue39.ref)(null);
11948
+ const searchRef = (0, import_vue39.ref)(null);
11949
+ const keyword = (0, import_vue39.ref)("");
11950
+ const activeDept = (0, import_vue39.ref)("");
11951
+ const temp = (0, import_vue39.ref)([]);
11952
+ const expandedIds = (0, import_vue39.ref)(/* @__PURE__ */ new Set());
11076
11953
  let detachPanel = null;
11077
11954
  function collectDeptIds(nodes, out = []) {
11078
11955
  nodes.forEach((node) => {
@@ -11102,22 +11979,22 @@ var org_picker_default = {
11102
11979
  walk([root2]);
11103
11980
  return ids;
11104
11981
  }
11105
- const staffMap = (0, import_vue38.computed)(() => {
11982
+ const staffMap = (0, import_vue39.computed)(() => {
11106
11983
  const map = /* @__PURE__ */ new Map();
11107
11984
  props.staff.forEach((item) => map.set(item.id, item));
11108
11985
  return map;
11109
11986
  });
11110
- const appliedStaff = (0, import_vue38.computed)(
11987
+ const appliedStaff = (0, import_vue39.computed)(
11111
11988
  () => props.modelValue.map((id) => staffMap.value.get(id)).filter(Boolean)
11112
11989
  );
11113
- const visibleTags = (0, import_vue38.computed)(() => {
11990
+ const visibleTags = (0, import_vue39.computed)(() => {
11114
11991
  if (props.maxTagCount <= 0) return appliedStaff.value;
11115
11992
  return appliedStaff.value.slice(0, props.maxTagCount);
11116
11993
  });
11117
- const restTagCount = (0, import_vue38.computed)(
11994
+ const restTagCount = (0, import_vue39.computed)(
11118
11995
  () => Math.max(0, appliedStaff.value.length - visibleTags.value.length)
11119
11996
  );
11120
- const visibleStaff = (0, import_vue38.computed)(() => {
11997
+ const visibleStaff = (0, import_vue39.computed)(() => {
11121
11998
  const key = keyword.value.trim().toLowerCase();
11122
11999
  if (key) {
11123
12000
  return props.staff.filter(
@@ -11128,7 +12005,7 @@ var org_picker_default = {
11128
12005
  const scope = deptScope(activeDept.value);
11129
12006
  return props.staff.filter((item) => scope.has(item.deptId));
11130
12007
  });
11131
- const deptRows = (0, import_vue38.computed)(() => {
12008
+ const deptRows = (0, import_vue39.computed)(() => {
11132
12009
  const rows = [];
11133
12010
  const walk = (nodes, depth) => {
11134
12011
  nodes.forEach((node) => {
@@ -11140,11 +12017,11 @@ var org_picker_default = {
11140
12017
  walk(props.departments, 0);
11141
12018
  return rows;
11142
12019
  });
11143
- const selectedCount = (0, import_vue38.computed)(() => temp.value.length);
11144
- const placeholderText = (0, import_vue38.computed)(() => props.placeholder || t("orgPicker.placeholder"));
11145
- const emptyLabel = (0, import_vue38.computed)(() => props.emptyText || t("orgPicker.empty"));
11146
- const panel = (0, import_vue38.ref)(null);
11147
- const panelStyle = (0, import_vue38.ref)({ ...PANEL_HIDDEN_STYLE });
12020
+ const selectedCount = (0, import_vue39.computed)(() => temp.value.length);
12021
+ const placeholderText = (0, import_vue39.computed)(() => props.placeholder || t("orgPicker.placeholder"));
12022
+ const emptyLabel = (0, import_vue39.computed)(() => props.emptyText || t("orgPicker.empty"));
12023
+ const panel = (0, import_vue39.ref)(null);
12024
+ const panelStyle = (0, import_vue39.ref)({ ...PANEL_HIDDEN_STYLE });
11148
12025
  const PANEL_HEIGHT = 342;
11149
12026
  function updatePosition() {
11150
12027
  if (!open.value || !root.value) return;
@@ -11161,7 +12038,7 @@ var org_picker_default = {
11161
12038
  expandedIds.value = new Set(collectDeptIds(props.departments));
11162
12039
  }
11163
12040
  expandAll();
11164
- (0, import_vue38.watch)(() => props.departments, () => {
12041
+ (0, import_vue39.watch)(() => props.departments, () => {
11165
12042
  expandedIds.value = /* @__PURE__ */ new Set([...expandedIds.value, ...collectDeptIds(props.departments)]);
11166
12043
  });
11167
12044
  function initialOf(item) {
@@ -11250,7 +12127,7 @@ var org_picker_default = {
11250
12127
  function clearTemp() {
11251
12128
  temp.value = [];
11252
12129
  }
11253
- (0, import_vue38.onBeforeUnmount)(() => {
12130
+ (0, import_vue39.onBeforeUnmount)(() => {
11254
12131
  if (detachPanel) {
11255
12132
  detachPanel();
11256
12133
  detachPanel = null;
@@ -11436,7 +12313,7 @@ var org_picker_default = {
11436
12313
  };
11437
12314
 
11438
12315
  // src/components/schema-form/index.js
11439
- var import_vue39 = require("vue");
12316
+ var import_vue40 = require("vue");
11440
12317
  var CONTROL_MAP = {
11441
12318
  input: input_default,
11442
12319
  number: input_number_default,
@@ -11472,26 +12349,26 @@ var schema_form_default = {
11472
12349
  },
11473
12350
  emits: ["update:modelValue", "change"],
11474
12351
  setup(props, { emit }) {
11475
- const innerForm = (0, import_vue39.ref)(null);
11476
- const model = (0, import_vue39.reactive)({ ...props.modelValue });
11477
- (0, import_vue39.watch)(() => props.fields, (fields) => {
12352
+ const innerForm = (0, import_vue40.ref)(null);
12353
+ const model = (0, import_vue40.reactive)({ ...props.modelValue });
12354
+ (0, import_vue40.watch)(() => props.fields, (fields) => {
11478
12355
  fields.forEach((field) => {
11479
12356
  if (model[field.key] === void 0 && field.default !== void 0) {
11480
12357
  model[field.key] = typeof field.default === "function" ? field.default() : field.default;
11481
12358
  }
11482
12359
  });
11483
12360
  }, { immediate: true, deep: true });
11484
- (0, import_vue39.watch)(() => props.modelValue, (value) => {
12361
+ (0, import_vue40.watch)(() => props.modelValue, (value) => {
11485
12362
  if (!value) return;
11486
12363
  Object.keys(value).forEach((key) => {
11487
12364
  if (model[key] !== value[key]) model[key] = value[key];
11488
12365
  });
11489
12366
  }, { deep: true });
11490
- (0, import_vue39.watch)(model, () => {
12367
+ (0, import_vue40.watch)(model, () => {
11491
12368
  emit("update:modelValue", { ...model });
11492
12369
  emit("change", { ...model });
11493
12370
  }, { deep: true });
11494
- const formRules = (0, import_vue39.computed)(() => {
12371
+ const formRules = (0, import_vue40.computed)(() => {
11495
12372
  const merged = { ...props.rules };
11496
12373
  props.fields.forEach((field) => {
11497
12374
  if (!field.required && !field.rules) return;
@@ -11504,7 +12381,7 @@ var schema_form_default = {
11504
12381
  });
11505
12382
  return merged;
11506
12383
  });
11507
- const visibleFields = (0, import_vue39.computed)(
12384
+ const visibleFields = (0, import_vue40.computed)(
11508
12385
  () => props.fields.filter((field) => {
11509
12386
  if (typeof field.visibleWhen !== "function") return true;
11510
12387
  return Boolean(field.visibleWhen(model));
@@ -11698,7 +12575,7 @@ var approval_flow_default = {
11698
12575
  };
11699
12576
 
11700
12577
  // src/components/query-filter/index.js
11701
- var import_vue40 = require("vue");
12578
+ var import_vue41 = require("vue");
11702
12579
  var CONTROL_MAP2 = {
11703
12580
  input: input_default,
11704
12581
  number: input_number_default,
@@ -11723,26 +12600,26 @@ var query_filter_default = {
11723
12600
  },
11724
12601
  emits: ["update:modelValue", "search", "reset"],
11725
12602
  setup(props, { emit }) {
11726
- const expanded = (0, import_vue40.ref)(false);
11727
- const model = (0, import_vue40.reactive)({ ...props.modelValue });
11728
- (0, import_vue40.watch)(() => props.fields, (fields) => {
12603
+ const expanded = (0, import_vue41.ref)(false);
12604
+ const model = (0, import_vue41.reactive)({ ...props.modelValue });
12605
+ (0, import_vue41.watch)(() => props.fields, (fields) => {
11729
12606
  fields.forEach((field) => {
11730
12607
  if (model[field.key] === void 0 && field.default !== void 0) {
11731
12608
  model[field.key] = typeof field.default === "function" ? field.default() : field.default;
11732
12609
  }
11733
12610
  });
11734
12611
  }, { immediate: true, deep: true });
11735
- (0, import_vue40.watch)(() => props.modelValue, (value) => {
12612
+ (0, import_vue41.watch)(() => props.modelValue, (value) => {
11736
12613
  if (!value) return;
11737
12614
  Object.keys(value).forEach((key) => {
11738
12615
  if (model[key] !== value[key]) model[key] = value[key];
11739
12616
  });
11740
12617
  }, { deep: true });
11741
- (0, import_vue40.watch)(model, () => {
12618
+ (0, import_vue41.watch)(model, () => {
11742
12619
  emit("update:modelValue", { ...model });
11743
12620
  }, { deep: true });
11744
- const collapsible = (0, import_vue40.computed)(() => props.fields.length > props.visibleCount);
11745
- const visibleFields = (0, import_vue40.computed)(() => {
12621
+ const collapsible = (0, import_vue41.computed)(() => props.fields.length > props.visibleCount);
12622
+ const visibleFields = (0, import_vue41.computed)(() => {
11746
12623
  if (!collapsible.value || expanded.value) return props.fields;
11747
12624
  return props.fields.slice(0, props.visibleCount);
11748
12625
  });
@@ -11871,198 +12748,6 @@ var filter_tags_default = {
11871
12748
  `
11872
12749
  };
11873
12750
 
11874
- // src/components/media-picker/index.js
11875
- var import_vue41 = require("vue");
11876
- var media_picker_default = {
11877
- name: "DjmMediaPicker",
11878
- components: { DjmModal: modal_default, DjmPagination: pagination_default, DjmIcon: icon_default },
11879
- props: {
11880
- modelValue: { type: [Array, String, Number], default: "" },
11881
- items: { type: Array, default: () => [] },
11882
- multiple: { type: Boolean, default: false },
11883
- // 最多选几个,0 表示不限
11884
- limit: { type: Number, default: 0 },
11885
- // image 只显示图片,file 只显示非图片,空串不过滤
11886
- accept: { type: String, default: "" },
11887
- title: { type: String, default: "" },
11888
- triggerText: { type: String, default: "" },
11889
- pageSize: { type: Number, default: 12 },
11890
- disabled: { type: Boolean, default: false }
11891
- },
11892
- emits: ["update:modelValue", "change"],
11893
- setup(props, { emit }) {
11894
- const open = (0, import_vue41.ref)(false);
11895
- const keyword = (0, import_vue41.ref)("");
11896
- const page = (0, import_vue41.ref)(1);
11897
- const temp = (0, import_vue41.ref)([]);
11898
- const selectedIds = (0, import_vue41.computed)(() => {
11899
- if (props.multiple) return Array.isArray(props.modelValue) ? props.modelValue : [];
11900
- if (props.modelValue === "" || props.modelValue === null || props.modelValue === void 0) return [];
11901
- return [props.modelValue];
11902
- });
11903
- function isImage(item) {
11904
- if (item.type) return item.type === "image";
11905
- return /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(item.url || "");
11906
- }
11907
- const filtered = (0, import_vue41.computed)(() => {
11908
- const key = keyword.value.trim().toLowerCase();
11909
- return props.items.filter((item) => {
11910
- if (props.accept === "image" && !isImage(item)) return false;
11911
- if (props.accept === "file" && isImage(item)) return false;
11912
- if (key && !String(item.name || "").toLowerCase().includes(key)) return false;
11913
- return true;
11914
- });
11915
- });
11916
- const paged = (0, import_vue41.computed)(() => {
11917
- const start = (page.value - 1) * props.pageSize;
11918
- return filtered.value.slice(start, start + props.pageSize);
11919
- });
11920
- (0, import_vue41.watch)(keyword, () => {
11921
- page.value = 1;
11922
- });
11923
- (0, import_vue41.watch)(
11924
- () => filtered.value.length,
11925
- () => {
11926
- const max = Math.max(1, Math.ceil(filtered.value.length / props.pageSize));
11927
- if (page.value > max) page.value = max;
11928
- }
11929
- );
11930
- function isChecked(id) {
11931
- return temp.value.includes(id);
11932
- }
11933
- function atLimit() {
11934
- return props.limit > 0 && temp.value.length >= props.limit;
11935
- }
11936
- function toggle(item) {
11937
- if (props.multiple) {
11938
- if (isChecked(item.id)) {
11939
- temp.value = temp.value.filter((id) => id !== item.id);
11940
- return;
11941
- }
11942
- if (atLimit()) return;
11943
- temp.value = [...temp.value, item.id];
11944
- return;
11945
- }
11946
- temp.value = [item.id];
11947
- }
11948
- function openPanel() {
11949
- if (props.disabled) return;
11950
- temp.value = [...selectedIds.value];
11951
- keyword.value = "";
11952
- page.value = 1;
11953
- open.value = true;
11954
- }
11955
- function commit() {
11956
- const next = props.multiple ? [...temp.value] : temp.value[0] !== void 0 ? temp.value[0] : "";
11957
- emit("update:modelValue", next);
11958
- emit("change", next);
11959
- open.value = false;
11960
- }
11961
- function cancel() {
11962
- temp.value = [...selectedIds.value];
11963
- open.value = false;
11964
- }
11965
- return {
11966
- open,
11967
- keyword,
11968
- page,
11969
- temp,
11970
- selectedIds,
11971
- filtered,
11972
- paged,
11973
- isImage,
11974
- isChecked,
11975
- atLimit,
11976
- toggle,
11977
- openPanel,
11978
- commit,
11979
- cancel,
11980
- t
11981
- };
11982
- },
11983
- template: `
11984
- <span class="djm-media-picker">
11985
- <slot name="trigger" :open="openPanel" :count="selectedIds.length">
11986
- <button
11987
- class="djm-btn"
11988
- type="button"
11989
- :disabled="disabled"
11990
- @click="openPanel"
11991
- >{{ triggerText || t('mediaPicker.select') }}<span v-if="selectedIds.length" class="djm-media-picker-count">{{ selectedIds.length }}</span></button>
11992
- </slot>
11993
-
11994
- <DjmModal
11995
- :model-value="open"
11996
- :title="title || t('mediaPicker.title')"
11997
- width="760px"
11998
- @update:model-value="cancel"
11999
- >
12000
- <div class="djm-media-toolbar">
12001
- <label class="djm-media-search">
12002
- <DjmIcon name="search" :size="13" />
12003
- <input
12004
- v-model="keyword"
12005
- class="djm-media-search-input"
12006
- type="text"
12007
- :placeholder="t('mediaPicker.search')"
12008
- :aria-label="t('mediaPicker.search')"
12009
- />
12010
- </label>
12011
- <span class="djm-media-toolbar-slot">
12012
- <slot name="toolbar"></slot>
12013
- </span>
12014
- </div>
12015
-
12016
- <div class="djm-media-grid" role="listbox" :aria-multiselectable="multiple ? 'true' : 'false'">
12017
- <div
12018
- v-for="item in paged"
12019
- :key="item.id"
12020
- class="djm-media-item"
12021
- :class="{
12022
- 'djm-is-checked': isChecked(item.id),
12023
- 'djm-is-disabled': multiple && atLimit() && !isChecked(item.id)
12024
- }"
12025
- role="option"
12026
- tabindex="0"
12027
- :aria-selected="String(isChecked(item.id))"
12028
- @click="toggle(item)"
12029
- @keydown.enter.prevent="toggle(item)"
12030
- @keydown.space.prevent="toggle(item)"
12031
- >
12032
- <div class="djm-media-thumb">
12033
- <img v-if="isImage(item)" :src="item.url" :alt="item.name" loading="lazy" />
12034
- <DjmIcon v-else name="folder" :size="26" />
12035
- <span v-if="isChecked(item.id)" class="djm-media-check">
12036
- <DjmIcon name="check" :size="11" />
12037
- </span>
12038
- </div>
12039
- <div class="djm-media-name" :title="item.name">{{ item.name }}</div>
12040
- </div>
12041
- <div v-if="!paged.length" class="djm-media-empty">{{ t('mediaPicker.empty') }}</div>
12042
- </div>
12043
-
12044
- <div class="djm-media-foot">
12045
- <span class="djm-media-total">
12046
- {{ t('mediaPicker.total', { total: filtered.length }) }}
12047
- <em v-if="multiple && limit > 0">{{ t('mediaPicker.limit', { limit }) }}</em>
12048
- </span>
12049
- <DjmPagination
12050
- v-if="filtered.length > pageSize"
12051
- v-model:page="page"
12052
- :page-size="pageSize"
12053
- :total="filtered.length"
12054
- />
12055
- </div>
12056
-
12057
- <template #footer>
12058
- <button class="djm-btn" type="button" @click="cancel">{{ t('common.cancel') }}</button>
12059
- <button class="djm-btn djm-btn-primary" type="button" @click="commit">{{ t('common.confirm') }}</button>
12060
- </template>
12061
- </DjmModal>
12062
- </span>
12063
- `
12064
- };
12065
-
12066
12751
  // src/components/kanban/index.js
12067
12752
  var import_vue42 = require("vue");
12068
12753
  function cloneColumns(list) {
@@ -15403,7 +16088,7 @@ var components = [
15403
16088
  org_chart_default,
15404
16089
  page_tabs_default
15405
16090
  ];
15406
- var version = "0.67.4";
16091
+ var version = "0.72.0";
15407
16092
  function install(app, options = {}) {
15408
16093
  if (options.locale) setLocale(options.locale);
15409
16094
  components.forEach((component) => app.component(component.name, component));