@treeui/vue 0.15.0 → 0.17.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.
- package/dist/components/TAccordion.cjs.map +1 -1
- package/dist/components/TAccordion.js.map +1 -1
- package/dist/components/TAccordion.vue_vue_type_script_setup_true_lang.cjs +1 -1
- package/dist/components/TAccordion.vue_vue_type_script_setup_true_lang.cjs.map +1 -1
- package/dist/components/TAccordion.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/components/TAccordion.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/TAccordionItem.cjs.map +1 -1
- package/dist/components/TAccordionItem.js.map +1 -1
- package/dist/components/TAccordionItem.vue_vue_type_script_setup_true_lang.cjs +1 -1
- package/dist/components/TAccordionItem.vue_vue_type_script_setup_true_lang.cjs.map +1 -1
- package/dist/components/TAccordionItem.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/TAccordionItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/TCard.cjs.map +1 -1
- package/dist/components/TCard.js.map +1 -1
- package/dist/components/TCard.vue_vue_type_script_setup_true_lang.cjs +1 -1
- package/dist/components/TCard.vue_vue_type_script_setup_true_lang.cjs.map +1 -1
- package/dist/components/TCard.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/TCard.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/TDropdown.cjs.map +1 -1
- package/dist/components/TDropdown.js.map +1 -1
- package/dist/components/TDropdown.vue.d.ts +6 -2
- package/dist/components/TDropdown.vue.d.ts.map +1 -1
- package/dist/components/TDropdown.vue_vue_type_script_setup_true_lang.cjs +1 -1
- package/dist/components/TDropdown.vue_vue_type_script_setup_true_lang.cjs.map +1 -1
- package/dist/components/TDropdown.vue_vue_type_script_setup_true_lang.js +31 -24
- package/dist/components/TDropdown.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/TFlag.cjs +2 -0
- package/dist/components/TFlag.cjs.map +1 -0
- package/dist/components/TFlag.js +7 -0
- package/dist/components/TFlag.js.map +1 -0
- package/dist/components/TFlag.vue.d.ts +20 -0
- package/dist/components/TFlag.vue.d.ts.map +1 -0
- package/dist/components/TFlag.vue_vue_type_script_setup_true_lang.cjs +2 -0
- package/dist/components/TFlag.vue_vue_type_script_setup_true_lang.cjs.map +1 -0
- package/dist/components/TFlag.vue_vue_type_script_setup_true_lang.js +58 -0
- package/dist/components/TFlag.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/TIcon.vue.d.ts +1 -1
- package/dist/components/TLanguageSelect.cjs +2 -0
- package/dist/components/TLanguageSelect.cjs.map +1 -0
- package/dist/components/TLanguageSelect.js +7 -0
- package/dist/components/TLanguageSelect.js.map +1 -0
- package/dist/components/TLanguageSelect.vue.d.ts +103 -0
- package/dist/components/TLanguageSelect.vue.d.ts.map +1 -0
- package/dist/components/TLanguageSelect.vue_vue_type_script_setup_true_lang.cjs +2 -0
- package/dist/components/TLanguageSelect.vue_vue_type_script_setup_true_lang.cjs.map +1 -0
- package/dist/components/TLanguageSelect.vue_vue_type_script_setup_true_lang.js +299 -0
- package/dist/components/TLanguageSelect.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/TMarkdownEditor.cjs.map +1 -1
- package/dist/components/TMarkdownEditor.js.map +1 -1
- package/dist/components/TMarkdownEditor.vue.d.ts +2 -0
- package/dist/components/TMarkdownEditor.vue.d.ts.map +1 -1
- package/dist/components/TMarkdownEditor.vue_vue_type_script_setup_true_lang.cjs +9 -9
- package/dist/components/TMarkdownEditor.vue_vue_type_script_setup_true_lang.cjs.map +1 -1
- package/dist/components/TMarkdownEditor.vue_vue_type_script_setup_true_lang.js +117 -96
- package/dist/components/TMarkdownEditor.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/TPopover.cjs.map +1 -1
- package/dist/components/TPopover.js.map +1 -1
- package/dist/components/TPopover.vue.d.ts +4 -2
- package/dist/components/TPopover.vue.d.ts.map +1 -1
- package/dist/components/TPopover.vue_vue_type_script_setup_true_lang.cjs +1 -1
- package/dist/components/TPopover.vue_vue_type_script_setup_true_lang.cjs.map +1 -1
- package/dist/components/TPopover.vue_vue_type_script_setup_true_lang.js +10 -5
- package/dist/components/TPopover.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/accordion.cjs.map +1 -1
- package/dist/components/accordion.d.ts +4 -4
- package/dist/components/accordion.d.ts.map +1 -1
- package/dist/components/accordion.js.map +1 -1
- package/dist/components/index.d.ts +13 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +80 -78
- package/dist/plugin.cjs +1 -1
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +146 -142
- package/dist/plugin.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TLanguageSelect.vue.d.ts","sourceRoot":"","sources":["../../src/components/TLanguageSelect.vue"],"names":[],"mappings":"AAifA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAS7D;;;;;;;;GAQG;AACH,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,UAAU,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,KAAK,WAAW,GAAG;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,OAAO,CAAC,EAAE,sBAAsB,CAAC;IACjC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kDAAkD;IAClD,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAmTJ,iBAAS,cAAc;WAyOT,OAAO,IAA6B;;iBA/fvC,MAAM,OAAO;iBACb,CAAC,KAAK,EAAE;YAAE,MAAM,EAAE,eAAe,CAAC;YAAC,QAAQ,EAAE,OAAO,CAAC;YAAC,OAAO,EAAE,OAAO,CAAA;SAAE,KAAK,OAAO;gBACrF,MAAM,OAAO;;iBAFZ,MAAM,OAAO;iBACb,CAAC,KAAK,EAAE;YAAE,MAAM,EAAE,eAAe,CAAC;YAAC,QAAQ,EAAE,OAAO,CAAC;YAAC,OAAO,EAAE,OAAO,CAAA;SAAE,KAAK,OAAO;gBACrF,MAAM,OAAO;;;;;;;;EAkgBtB;AAgCD,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AAC9D,QAAA,MAAM,eAAe;;;;;;;;;gBAnlBJ,MAAM;cAUR,OAAO;UANX,KAAK;WAKJ,WAAW;aAJT,sBAAsB;aAMtB,OAAO;aACP,OAAO;aAXP,eAAe,EAAE;UACpB,OAAO;iBACA,OAAO;iBAUP,MAAM;eAGR,MAAM;cATP,OAAO;iBAQJ,MAAM;;;;;OA6kBtB,CAAC;wBACkB,uBAAuB,CAAC,OAAO,eAAe,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAAnG,wBAAoG;AAapG,KAAK,uBAAuB,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IACxC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KAEV,CAAA;CACD,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./TSpinner.cjs"),t=require("../composables/useControllableOpen.cjs"),n=require("./TFlag.cjs"),r=require("./TIcon.cjs");let i=require("vue"),a=require("@treeui/utils");var o=[`data-state`],s=[`disabled`,`aria-controls`,`aria-expanded`,`aria-invalid`,`aria-busy`],c={key:0,class:`t-language-select__slot t-language-select__slot--prefix`},l=[`data-placeholder`],u={key:6,class:`t-language-select__chevron`,viewBox:`0 0 16 16`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},d=[`id`,`aria-label`],f={key:0,class:`t-language-select__empty`,role:`presentation`},p=[`aria-selected`,`aria-disabled`,`tabindex`,`onClick`,`onKeydown`],m={class:`t-language-select__option-text`},h={class:`t-language-select__option-label`},g={key:0,class:`t-language-select__description`},_={key:0,class:`t-language-select__check`,viewBox:`0 0 16 16`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`,"aria-hidden":`true`},v=(0,i.defineComponent)({inheritAttrs:!1,__name:`TLanguageSelect`,props:{modelValue:{default:``},options:{default:()=>[]},open:{type:Boolean,default:void 0},defaultOpen:{type:Boolean,default:!1},size:{default:`md`},variant:{default:`field`},iconOnly:{type:Boolean,default:!1},width:{default:`full`},disabled:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},invalid:{type:Boolean,default:!1},placeholder:{default:``},flagBaseUrl:{default:void 0},emptyText:{default:`No languages available.`}},emits:[`update:modelValue`,`update:open`,`open-change`],setup(v,{emit:y}){let b=v,x=y,S=(0,i.useAttrs)(),C=(0,a.createId)(`t-language-select`),w=(0,i.ref)(null),T=(0,i.ref)(null),E=(0,i.ref)(null),D=(0,i.ref)(new Map),O=(0,i.ref)(-1),k=(0,i.ref)(!1),A=(0,i.computed)(()=>{let{class:e,style:t,...n}=S;return n}),{value:j,setValue:M}=t.useControllableOpen((0,i.toRef)(b,`open`),b.defaultOpen,e=>{x(`update:open`,e),x(`open-change`,e)}),N=(0,i.computed)(()=>b.variant===`switcher`),P=(0,i.computed)(()=>[`t-language-select`,`t-language-select--${b.size}`,`t-language-select--${b.variant}`,!N.value&&b.width!==`full`?`t-field-width--${b.width}`:null,{"t-language-select--icon-only":N.value&&b.iconOnly,"t-language-select--drop-up":k.value,"is-disabled":b.disabled,"is-invalid":b.invalid,"is-loading":b.loading,"is-open":j.value},S.class]),F=e=>{let t=e.parentElement;for(;t&&t!==document.body;){let{overflowY:e}=window.getComputedStyle(t);if(e===`auto`||e===`scroll`||e===`hidden`)return t;t=t.parentElement}return null},I=()=>{let e=T.value,t=E.value;if(!e||!t){k.value=!1;return}let n=e.getBoundingClientRect(),r=t.getBoundingClientRect(),i=F(e)?.getBoundingClientRect(),a=Math.min(i?.bottom??1/0,window.innerHeight),o=Math.max(i?.top??0,0),s=a-n.bottom,c=n.top-o;k.value=r.bottom>a&&c>s},L=(0,i.computed)(()=>S.style),R=(0,i.computed)(()=>b.options.find(e=>e.value===b.modelValue)),z=(0,i.computed)(()=>R.value?.label??``),B=(0,i.computed)(()=>!(N.value&&b.iconOnly)||!R.value?.code),V=(0,i.computed)(()=>b.options.filter(e=>!e.disabled)),H=()=>{let e=b.options.findIndex(e=>e.value===b.modelValue);O.value=e>=0?e:0,k.value=!1,(0,i.nextTick)(()=>{I(),q(O.value)})},U=()=>{b.disabled||M(!0)},W=(e=!1)=>{M(!1),O.value=-1,e&&(0,i.nextTick)(()=>T.value?.focus())},G=()=>{j.value?W():U()},K=e=>{e.disabled||(x(`update:modelValue`,e.value),W(!0))},q=e=>{let t=b.options[e];t&&D.value.get(t.value)?.focus()},J=(e,t)=>{e instanceof HTMLElement?D.value.set(t,e):D.value.delete(t)},Y=e=>{let t=O.value+e;for(;t>=0&&t<b.options.length;){if(!b.options[t].disabled){O.value=t,q(t);return}t+=e}},X=e=>{if(!b.disabled){if((0,a.isEscapeKey)(e)&&j.value){e.preventDefault(),W();return}((0,a.isActivationKey)(e)||e.key===`ArrowDown`||e.key===`ArrowUp`)&&(e.preventDefault(),U())}},Z=(e,t,n)=>{if((0,a.isEscapeKey)(e)){e.preventDefault(),W(!0);return}if((0,a.isActivationKey)(e)){e.preventDefault(),K(t);return}switch(e.key){case`ArrowDown`:e.preventDefault(),Y(1);break;case`ArrowUp`:e.preventDefault(),Y(-1);break;case`Home`:e.preventDefault(),O.value=V.value.length>0?b.options.indexOf(V.value[0]):0,q(O.value);break;case`End`:e.preventDefault(),O.value=V.value.length>0?b.options.indexOf(V.value[V.value.length-1]):b.options.length-1,q(O.value);break;default:break}},Q=e=>{if(!j.value)return;let t=e.target;t instanceof Node&&(w.value?.contains(t)||W())},$=(0,i.computed)(()=>b.modelValue!==``);return(0,i.watch)(j,e=>{e?(document.addEventListener(`pointerdown`,Q),H()):document.removeEventListener(`pointerdown`,Q)},{immediate:!0}),(0,i.watch)(()=>b.options.length,e=>{j.value&&O.value>=e&&(O.value=e-1)}),(0,i.onBeforeUnmount)(()=>{document.removeEventListener(`pointerdown`,Q)}),(t,a)=>((0,i.openBlock)(),(0,i.createElementBlock)(`div`,{ref_key:`rootRef`,ref:w,class:(0,i.normalizeClass)(P.value),style:(0,i.normalizeStyle)(L.value),"data-state":(0,i.unref)(j)?`open`:`closed`},[(0,i.createElementVNode)(`button`,(0,i.mergeProps)({ref_key:`triggerRef`,ref:T},A.value,{type:`button`,class:`t-language-select__trigger`,disabled:v.disabled,"aria-controls":(0,i.unref)(j)?(0,i.unref)(C):void 0,"aria-haspopup":`listbox`,"aria-expanded":(0,i.unref)(j),"aria-invalid":v.invalid||void 0,"aria-busy":v.loading||void 0,onClick:G,onKeydown:X}),[t.$slots.prefix?((0,i.openBlock)(),(0,i.createElementBlock)(`span`,c,[(0,i.renderSlot)(t.$slots,`prefix`)])):(0,i.createCommentVNode)(``,!0),N.value?((0,i.openBlock)(),(0,i.createBlock)(r.default,{key:1,name:`languages`,class:`t-language-select__translate`})):R.value?.code?((0,i.openBlock)(),(0,i.createBlock)(n.default,{key:2,code:R.value.code,"base-url":v.flagBaseUrl,size:v.size},null,8,[`code`,`base-url`,`size`])):(0,i.createCommentVNode)(``,!0),B.value?((0,i.openBlock)(),(0,i.createElementBlock)(`span`,{key:3,class:`t-language-select__value`,"data-placeholder":!$.value||void 0},(0,i.toDisplayString)(z.value||v.placeholder),9,l)):(0,i.createCommentVNode)(``,!0),N.value&&R.value?.code?((0,i.openBlock)(),(0,i.createBlock)(n.default,{key:4,code:R.value.code,"base-url":v.flagBaseUrl,size:v.size},null,8,[`code`,`base-url`,`size`])):(0,i.createCommentVNode)(``,!0),v.loading?((0,i.openBlock)(),(0,i.createBlock)(e.default,{key:5,size:`sm`,label:`Loading`})):((0,i.openBlock)(),(0,i.createElementBlock)(`svg`,u,[...a[0]||=[(0,i.createElementVNode)(`polyline`,{points:`4 6 8 10 12 6`},null,-1)]]))],16,s),(0,i.createVNode)(i.Transition,{name:`t-fade`},{default:(0,i.withCtx)(()=>[(0,i.unref)(j)&&!v.disabled?((0,i.openBlock)(),(0,i.createElementBlock)(`ul`,{key:0,id:(0,i.unref)(C),ref_key:`listboxRef`,ref:E,role:`listbox`,class:`t-language-select__listbox`,"aria-label":typeof A.value[`aria-label`]==`string`?A.value[`aria-label`]:void 0},[v.options.length===0?((0,i.openBlock)(),(0,i.createElementBlock)(`li`,f,[(0,i.renderSlot)(t.$slots,`empty`,{},()=>[(0,i.createTextVNode)((0,i.toDisplayString)(v.emptyText),1)])])):(0,i.createCommentVNode)(``,!0),((0,i.openBlock)(!0),(0,i.createElementBlock)(i.Fragment,null,(0,i.renderList)(v.options,(e,r)=>((0,i.openBlock)(),(0,i.createElementBlock)(`li`,{key:e.value,ref_for:!0,ref:t=>J(t,e.value),role:`option`,class:(0,i.normalizeClass)([`t-language-select__option`,{"is-selected":e.value===v.modelValue,"is-disabled":e.disabled,"is-focused":r===O.value}]),"aria-selected":e.value===v.modelValue,"aria-disabled":e.disabled||void 0,tabindex:e.disabled?-1:0,onClick:t=>K(e),onKeydown:t=>Z(t,e,r)},[(0,i.renderSlot)(t.$slots,`option`,{option:e,selected:e.value===v.modelValue,focused:r===O.value},()=>[e.code?((0,i.openBlock)(),(0,i.createBlock)(n.default,{key:0,code:e.code,"base-url":v.flagBaseUrl,size:v.size},null,8,[`code`,`base-url`,`size`])):(0,i.createCommentVNode)(``,!0),(0,i.createElementVNode)(`span`,m,[(0,i.createElementVNode)(`span`,h,(0,i.toDisplayString)(e.label),1),e.description?((0,i.openBlock)(),(0,i.createElementBlock)(`span`,g,(0,i.toDisplayString)(e.description),1)):(0,i.createCommentVNode)(``,!0)])]),e.value===v.modelValue?((0,i.openBlock)(),(0,i.createElementBlock)(`svg`,_,[...a[1]||=[(0,i.createElementVNode)(`polyline`,{points:`3.5 8.5 6.5 11.5 12.5 4.5`},null,-1)]])):(0,i.createCommentVNode)(``,!0)],42,p))),128))],8,d)):(0,i.createCommentVNode)(``,!0)]),_:3})],14,o))}});exports.default=v;
|
|
2
|
+
//# sourceMappingURL=TLanguageSelect.vue_vue_type_script_setup_true_lang.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TLanguageSelect.vue_vue_type_script_setup_true_lang.cjs","names":["$slots"],"sources":["../../src/components/TLanguageSelect.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { createId, isActivationKey, isEscapeKey } from '@treeui/utils';\nimport { computed, nextTick, onBeforeUnmount, ref, toRef, useAttrs, watch } from 'vue';\nimport { useControllableOpen } from '../composables/useControllableOpen';\nimport type { TFieldWidth, TSize } from '../types/contracts';\nimport TFlag from './TFlag.vue';\nimport TIcon from './TIcon.vue';\nimport TSpinner from './TSpinner.vue';\n\ndefineOptions({\n inheritAttrs: false,\n});\n\n/**\n * `field` is a form control: it reads as the value of a labelled question, so the\n * flag leads and the trigger fills its container.\n *\n * `switcher` is a page-level control, typically in a navbar, where nothing nearby\n * says what it changes. The translate icon states the purpose up front and the\n * current flag closes the row, so the whole thing reads as \"page language, now\n * set to this\" without an external label.\n */\nexport type TLanguageSelectVariant = 'field' | 'switcher';\n\nexport interface TLanguageOption {\n label: string;\n /** Opaque application key — typically a BCP-47 tag. Never parsed or validated. */\n value: string;\n /** ISO 3166-1 alpha-2 code for the flag. Omit to render the option without one. */\n code?: string;\n /** Secondary line, e.g. the language's endonym. */\n description?: string;\n disabled?: boolean;\n}\n\nconst props = withDefaults(\n defineProps<{\n modelValue?: string;\n options?: TLanguageOption[];\n open?: boolean;\n defaultOpen?: boolean;\n size?: TSize;\n variant?: TLanguageSelectVariant;\n /** Hide the language name, leaving icon + flag. `switcher` only. */\n iconOnly?: boolean;\n /** Inline-size cap. Fluid (`full`) by default. */\n width?: TFieldWidth;\n disabled?: boolean;\n loading?: boolean;\n invalid?: boolean;\n placeholder?: string;\n /** Forwarded to every nested TFlag. Set once here to self-host flag assets. */\n flagBaseUrl?: string;\n emptyText?: string;\n }>(),\n {\n modelValue: '',\n options: () => [],\n open: undefined,\n defaultOpen: false,\n size: 'md',\n variant: 'field',\n iconOnly: false,\n width: 'full',\n disabled: false,\n loading: false,\n invalid: false,\n placeholder: '',\n flagBaseUrl: undefined,\n emptyText: 'No languages available.',\n },\n);\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string];\n 'update:open': [value: boolean];\n 'open-change': [value: boolean];\n}>();\n\ndefineSlots<{\n prefix?: () => unknown;\n option?: (props: { option: TLanguageOption; selected: boolean; focused: boolean }) => unknown;\n empty?: () => unknown;\n}>();\n\nconst attrs = useAttrs();\nconst listboxId = createId('t-language-select');\nconst rootRef = ref<HTMLElement | null>(null);\nconst triggerRef = ref<HTMLButtonElement | null>(null);\nconst listboxRef = ref<HTMLElement | null>(null);\nconst optionRefs = ref<Map<string, HTMLElement>>(new Map());\nconst focusedIndex = ref(-1);\nconst dropUp = ref(false);\n\nconst triggerAttrs = computed(() => {\n const { class: _class, style: _style, ...rest } = attrs;\n return rest;\n});\n\nconst { value: isOpen, setValue } = useControllableOpen(\n toRef(props, 'open'),\n props.defaultOpen,\n (value) => {\n emit('update:open', value);\n emit('open-change', value);\n },\n);\n\nconst isSwitcher = computed(() => props.variant === 'switcher');\n\nconst rootClasses = computed(() => [\n 't-language-select',\n `t-language-select--${props.size}`,\n `t-language-select--${props.variant}`,\n // A switcher sizes to its content, so the field width scale does not apply.\n !isSwitcher.value && props.width !== 'full' ? `t-field-width--${props.width}` : null,\n {\n 't-language-select--icon-only': isSwitcher.value && props.iconOnly,\n 't-language-select--drop-up': dropUp.value,\n 'is-disabled': props.disabled,\n 'is-invalid': props.invalid,\n 'is-loading': props.loading,\n 'is-open': isOpen.value,\n },\n attrs.class,\n]);\n\n// Nearest ancestor that can clip the listbox (scroll container or hidden overflow).\nconst findClippingAncestor = (element: HTMLElement): HTMLElement | null => {\n let parent = element.parentElement;\n while (parent && parent !== document.body) {\n const { overflowY } = window.getComputedStyle(parent);\n if (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'hidden') {\n return parent;\n }\n parent = parent.parentElement;\n }\n return null;\n};\n\n// Flip the listbox above the trigger when its rendered position overflows the\n// nearest clipping ancestor (e.g. a modal body) or the viewport, and there is\n// more room above the trigger than below it. Measuring the rendered rect keeps\n// the decision in sync with the CSS offset (--tree-space-2) automatically.\nconst updateDropDirection = () => {\n const trigger = triggerRef.value;\n const listbox = listboxRef.value;\n if (!trigger || !listbox) {\n dropUp.value = false;\n return;\n }\n const triggerRect = trigger.getBoundingClientRect();\n const listboxRect = listbox.getBoundingClientRect();\n const boundary = findClippingAncestor(trigger)?.getBoundingClientRect();\n const bottomLimit = Math.min(boundary?.bottom ?? Infinity, window.innerHeight);\n const topLimit = Math.max(boundary?.top ?? 0, 0);\n const spaceBelow = bottomLimit - triggerRect.bottom;\n const spaceAbove = triggerRect.top - topLimit;\n dropUp.value = listboxRect.bottom > bottomLimit && spaceAbove > spaceBelow;\n};\n\nconst rootStyle = computed(() => attrs.style);\n\nconst selectedOption = computed(() =>\n props.options.find((o) => o.value === props.modelValue),\n);\n\nconst selectedLabel = computed(() => selectedOption.value?.label ?? '');\n\n// iconOnly leans on the flag to carry the current value. With no flag to show it\n// would leave the trigger stating nothing at all, so the name comes back.\nconst showValue = computed(\n () => !(isSwitcher.value && props.iconOnly) || !selectedOption.value?.code,\n);\n\nconst enabledOptions = computed(() =>\n props.options.filter((o) => !o.disabled),\n);\n\n// Runs for every open, however it was triggered. The listbox can also be opened\n// through `open` / `defaultOpen` without going near openDropdown(), so this lives\n// on the isOpen watcher rather than inside the click handler.\nconst onOpened = () => {\n const selectedIdx = props.options.findIndex((o) => o.value === props.modelValue);\n focusedIndex.value = selectedIdx >= 0 ? selectedIdx : 0;\n // Render downward first so the measurement always starts from the default position.\n dropUp.value = false;\n nextTick(() => {\n updateDropDirection();\n focusOption(focusedIndex.value);\n });\n};\n\nconst openDropdown = () => {\n if (props.disabled) return;\n setValue(true);\n};\n\nconst closeDropdown = (restoreFocus = false) => {\n setValue(false);\n focusedIndex.value = -1;\n if (restoreFocus) {\n nextTick(() => triggerRef.value?.focus());\n }\n};\n\nconst toggleDropdown = () => {\n if (isOpen.value) {\n closeDropdown();\n } else {\n openDropdown();\n }\n};\n\nconst selectOption = (opt: TLanguageOption) => {\n if (opt.disabled) return;\n emit('update:modelValue', opt.value);\n closeDropdown(true);\n};\n\nconst focusOption = (index: number) => {\n const opt = props.options[index];\n if (!opt) return;\n const el = optionRefs.value.get(opt.value);\n el?.focus();\n};\n\nconst setOptionRef = (el: Element | null, value: string) => {\n if (el instanceof HTMLElement) {\n optionRefs.value.set(value, el);\n } else {\n optionRefs.value.delete(value);\n }\n};\n\nconst moveFocus = (direction: 1 | -1) => {\n let nextIndex = focusedIndex.value + direction;\n while (nextIndex >= 0 && nextIndex < props.options.length) {\n if (!props.options[nextIndex].disabled) {\n focusedIndex.value = nextIndex;\n focusOption(nextIndex);\n return;\n }\n nextIndex += direction;\n }\n};\n\nconst onTriggerKeydown = (event: KeyboardEvent) => {\n if (props.disabled) return;\n\n if (isEscapeKey(event) && isOpen.value) {\n event.preventDefault();\n closeDropdown();\n return;\n }\n\n if (isActivationKey(event) || event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault();\n openDropdown();\n }\n};\n\nconst onOptionKeydown = (event: KeyboardEvent, opt: TLanguageOption, _index: number) => {\n if (isEscapeKey(event)) {\n event.preventDefault();\n closeDropdown(true);\n return;\n }\n\n if (isActivationKey(event)) {\n event.preventDefault();\n selectOption(opt);\n return;\n }\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault();\n moveFocus(1);\n break;\n case 'ArrowUp':\n event.preventDefault();\n moveFocus(-1);\n break;\n case 'Home':\n event.preventDefault();\n focusedIndex.value = enabledOptions.value.length > 0\n ? props.options.indexOf(enabledOptions.value[0])\n : 0;\n focusOption(focusedIndex.value);\n break;\n case 'End':\n event.preventDefault();\n focusedIndex.value = enabledOptions.value.length > 0\n ? props.options.indexOf(enabledOptions.value[enabledOptions.value.length - 1])\n : props.options.length - 1;\n focusOption(focusedIndex.value);\n break;\n default:\n break;\n }\n};\n\nconst onDocumentPointerDown = (event: PointerEvent) => {\n if (!isOpen.value) return;\n const target = event.target;\n if (!(target instanceof Node)) return;\n if (rootRef.value?.contains(target)) return;\n closeDropdown();\n};\n\nconst hasSelection = computed(() => props.modelValue !== '');\n\n// immediate, so a listbox that is already open on mount (`defaultOpen`, or a\n// controlled `open` that starts true) still dismisses on outside click and still\n// measures its drop direction.\nwatch(isOpen, (value) => {\n if (value) {\n document.addEventListener('pointerdown', onDocumentPointerDown);\n onOpened();\n } else {\n document.removeEventListener('pointerdown', onDocumentPointerDown);\n }\n}, { immediate: true });\n\n// Options can shrink underneath an open listbox. Leaving focusedIndex past the\n// end would strand moveFocus(), which only ever walks outward from it.\nwatch(\n () => props.options.length,\n (length) => {\n if (isOpen.value && focusedIndex.value >= length) {\n focusedIndex.value = length - 1;\n }\n },\n);\n\nonBeforeUnmount(() => {\n document.removeEventListener('pointerdown', onDocumentPointerDown);\n});\n</script>\n\n<template>\n <div\n ref=\"rootRef\"\n :class=\"rootClasses\"\n :style=\"rootStyle\"\n :data-state=\"isOpen ? 'open' : 'closed'\"\n >\n <button\n ref=\"triggerRef\"\n v-bind=\"triggerAttrs\"\n type=\"button\"\n class=\"t-language-select__trigger\"\n :disabled=\"disabled\"\n :aria-controls=\"isOpen ? listboxId : undefined\"\n aria-haspopup=\"listbox\"\n :aria-expanded=\"isOpen\"\n :aria-invalid=\"invalid || undefined\"\n :aria-busy=\"loading || undefined\"\n @click=\"toggleDropdown\"\n @keydown=\"onTriggerKeydown\"\n >\n <span\n v-if=\"$slots.prefix\"\n class=\"t-language-select__slot t-language-select__slot--prefix\"\n >\n <slot name=\"prefix\" />\n </span>\n <!--\n The switcher leads with the translate icon so the control announces what\n it changes; the field leads with the flag, since a form label already\n does that job.\n -->\n <!-- Sized in CSS from the icon token scale, not TIcon's pixel `size` prop. -->\n <TIcon\n v-if=\"isSwitcher\"\n name=\"languages\"\n class=\"t-language-select__translate\"\n />\n <TFlag\n v-else-if=\"selectedOption?.code\"\n :code=\"selectedOption.code\"\n :base-url=\"flagBaseUrl\"\n :size=\"size\"\n />\n <span\n v-if=\"showValue\"\n class=\"t-language-select__value\"\n :data-placeholder=\"!hasSelection ? true : undefined\"\n >\n {{ selectedLabel || placeholder }}\n </span>\n <TFlag\n v-if=\"isSwitcher && selectedOption?.code\"\n :code=\"selectedOption.code\"\n :base-url=\"flagBaseUrl\"\n :size=\"size\"\n />\n <TSpinner\n v-if=\"loading\"\n size=\"sm\"\n label=\"Loading\"\n />\n <svg\n v-else\n class=\"t-language-select__chevron\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <polyline points=\"4 6 8 10 12 6\" />\n </svg>\n </button>\n <transition name=\"t-fade\">\n <ul\n v-if=\"isOpen && !disabled\"\n :id=\"listboxId\"\n ref=\"listboxRef\"\n role=\"listbox\"\n class=\"t-language-select__listbox\"\n :aria-label=\"typeof triggerAttrs['aria-label'] === 'string' ? triggerAttrs['aria-label'] : undefined\"\n >\n <li\n v-if=\"options.length === 0\"\n class=\"t-language-select__empty\"\n role=\"presentation\"\n >\n <slot name=\"empty\">\n {{ emptyText }}\n </slot>\n </li>\n <li\n v-for=\"(opt, index) in options\"\n :key=\"opt.value\"\n :ref=\"(el) => setOptionRef(el as Element | null, opt.value)\"\n role=\"option\"\n class=\"t-language-select__option\"\n :class=\"{\n 'is-selected': opt.value === modelValue,\n 'is-disabled': opt.disabled,\n 'is-focused': index === focusedIndex,\n }\"\n :aria-selected=\"opt.value === modelValue\"\n :aria-disabled=\"opt.disabled || undefined\"\n :tabindex=\"opt.disabled ? -1 : 0\"\n @click=\"selectOption(opt)\"\n @keydown=\"onOptionKeydown($event, opt, index)\"\n >\n <slot\n name=\"option\"\n :option=\"opt\"\n :selected=\"opt.value === modelValue\"\n :focused=\"index === focusedIndex\"\n >\n <TFlag\n v-if=\"opt.code\"\n :code=\"opt.code\"\n :base-url=\"flagBaseUrl\"\n :size=\"size\"\n />\n <span class=\"t-language-select__option-text\">\n <span class=\"t-language-select__option-label\">{{ opt.label }}</span>\n <span\n v-if=\"opt.description\"\n class=\"t-language-select__description\"\n >\n {{ opt.description }}\n </span>\n </span>\n </slot>\n <svg\n v-if=\"opt.value === modelValue\"\n class=\"t-language-select__check\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <polyline points=\"3.5 8.5 6.5 11.5 12.5 4.5\" />\n </svg>\n </li>\n </ul>\n </transition>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import e from "./TSpinner.js";
|
|
2
|
+
import { useControllableOpen as t } from "../composables/useControllableOpen.js";
|
|
3
|
+
import n from "./TFlag.js";
|
|
4
|
+
import r from "./TIcon.js";
|
|
5
|
+
import { Fragment as i, Transition as a, computed as o, createBlock as s, createCommentVNode as c, createElementBlock as l, createElementVNode as u, createTextVNode as ee, createVNode as d, defineComponent as f, mergeProps as p, nextTick as m, normalizeClass as h, normalizeStyle as te, onBeforeUnmount as g, openBlock as _, ref as v, renderList as y, renderSlot as b, toDisplayString as x, toRef as ne, unref as S, useAttrs as C, watch as w, withCtx as T } from "vue";
|
|
6
|
+
import { createId as E, isActivationKey as D, isEscapeKey as O } from "@treeui/utils";
|
|
7
|
+
//#region src/components/TLanguageSelect.vue?vue&type=script&setup=true&lang.ts
|
|
8
|
+
var k = ["data-state"], re = [
|
|
9
|
+
"disabled",
|
|
10
|
+
"aria-controls",
|
|
11
|
+
"aria-expanded",
|
|
12
|
+
"aria-invalid",
|
|
13
|
+
"aria-busy"
|
|
14
|
+
], ie = {
|
|
15
|
+
key: 0,
|
|
16
|
+
class: "t-language-select__slot t-language-select__slot--prefix"
|
|
17
|
+
}, ae = ["data-placeholder"], oe = {
|
|
18
|
+
key: 6,
|
|
19
|
+
class: "t-language-select__chevron",
|
|
20
|
+
viewBox: "0 0 16 16",
|
|
21
|
+
fill: "none",
|
|
22
|
+
stroke: "currentColor",
|
|
23
|
+
"stroke-width": "2",
|
|
24
|
+
"stroke-linecap": "round",
|
|
25
|
+
"stroke-linejoin": "round",
|
|
26
|
+
"aria-hidden": "true"
|
|
27
|
+
}, se = ["id", "aria-label"], ce = {
|
|
28
|
+
key: 0,
|
|
29
|
+
class: "t-language-select__empty",
|
|
30
|
+
role: "presentation"
|
|
31
|
+
}, le = [
|
|
32
|
+
"aria-selected",
|
|
33
|
+
"aria-disabled",
|
|
34
|
+
"tabindex",
|
|
35
|
+
"onClick",
|
|
36
|
+
"onKeydown"
|
|
37
|
+
], ue = { class: "t-language-select__option-text" }, de = { class: "t-language-select__option-label" }, fe = {
|
|
38
|
+
key: 0,
|
|
39
|
+
class: "t-language-select__description"
|
|
40
|
+
}, pe = {
|
|
41
|
+
key: 0,
|
|
42
|
+
class: "t-language-select__check",
|
|
43
|
+
viewBox: "0 0 16 16",
|
|
44
|
+
fill: "none",
|
|
45
|
+
stroke: "currentColor",
|
|
46
|
+
"stroke-width": "2",
|
|
47
|
+
"stroke-linecap": "round",
|
|
48
|
+
"stroke-linejoin": "round",
|
|
49
|
+
"aria-hidden": "true"
|
|
50
|
+
}, A = /*@__PURE__*/ f({
|
|
51
|
+
inheritAttrs: !1,
|
|
52
|
+
__name: "TLanguageSelect",
|
|
53
|
+
props: {
|
|
54
|
+
modelValue: { default: "" },
|
|
55
|
+
options: { default: () => [] },
|
|
56
|
+
open: {
|
|
57
|
+
type: Boolean,
|
|
58
|
+
default: void 0
|
|
59
|
+
},
|
|
60
|
+
defaultOpen: {
|
|
61
|
+
type: Boolean,
|
|
62
|
+
default: !1
|
|
63
|
+
},
|
|
64
|
+
size: { default: "md" },
|
|
65
|
+
variant: { default: "field" },
|
|
66
|
+
iconOnly: {
|
|
67
|
+
type: Boolean,
|
|
68
|
+
default: !1
|
|
69
|
+
},
|
|
70
|
+
width: { default: "full" },
|
|
71
|
+
disabled: {
|
|
72
|
+
type: Boolean,
|
|
73
|
+
default: !1
|
|
74
|
+
},
|
|
75
|
+
loading: {
|
|
76
|
+
type: Boolean,
|
|
77
|
+
default: !1
|
|
78
|
+
},
|
|
79
|
+
invalid: {
|
|
80
|
+
type: Boolean,
|
|
81
|
+
default: !1
|
|
82
|
+
},
|
|
83
|
+
placeholder: { default: "" },
|
|
84
|
+
flagBaseUrl: { default: void 0 },
|
|
85
|
+
emptyText: { default: "No languages available." }
|
|
86
|
+
},
|
|
87
|
+
emits: [
|
|
88
|
+
"update:modelValue",
|
|
89
|
+
"update:open",
|
|
90
|
+
"open-change"
|
|
91
|
+
],
|
|
92
|
+
setup(f, { emit: A }) {
|
|
93
|
+
let j = f, M = A, N = C(), P = E("t-language-select"), F = v(null), I = v(null), L = v(null), R = v(/* @__PURE__ */ new Map()), z = v(-1), B = v(!1), V = o(() => {
|
|
94
|
+
let { class: e, style: t, ...n } = N;
|
|
95
|
+
return n;
|
|
96
|
+
}), { value: H, setValue: U } = t(ne(j, "open"), j.defaultOpen, (e) => {
|
|
97
|
+
M("update:open", e), M("open-change", e);
|
|
98
|
+
}), W = o(() => j.variant === "switcher"), me = o(() => [
|
|
99
|
+
"t-language-select",
|
|
100
|
+
`t-language-select--${j.size}`,
|
|
101
|
+
`t-language-select--${j.variant}`,
|
|
102
|
+
!W.value && j.width !== "full" ? `t-field-width--${j.width}` : null,
|
|
103
|
+
{
|
|
104
|
+
"t-language-select--icon-only": W.value && j.iconOnly,
|
|
105
|
+
"t-language-select--drop-up": B.value,
|
|
106
|
+
"is-disabled": j.disabled,
|
|
107
|
+
"is-invalid": j.invalid,
|
|
108
|
+
"is-loading": j.loading,
|
|
109
|
+
"is-open": H.value
|
|
110
|
+
},
|
|
111
|
+
N.class
|
|
112
|
+
]), he = (e) => {
|
|
113
|
+
let t = e.parentElement;
|
|
114
|
+
for (; t && t !== document.body;) {
|
|
115
|
+
let { overflowY: e } = window.getComputedStyle(t);
|
|
116
|
+
if (e === "auto" || e === "scroll" || e === "hidden") return t;
|
|
117
|
+
t = t.parentElement;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}, ge = () => {
|
|
121
|
+
let e = I.value, t = L.value;
|
|
122
|
+
if (!e || !t) {
|
|
123
|
+
B.value = !1;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
let n = e.getBoundingClientRect(), r = t.getBoundingClientRect(), i = he(e)?.getBoundingClientRect(), a = Math.min(i?.bottom ?? Infinity, window.innerHeight), o = Math.max(i?.top ?? 0, 0), s = a - n.bottom, c = n.top - o;
|
|
127
|
+
B.value = r.bottom > a && c > s;
|
|
128
|
+
}, _e = o(() => N.style), G = o(() => j.options.find((e) => e.value === j.modelValue)), ve = o(() => G.value?.label ?? ""), ye = o(() => !(W.value && j.iconOnly) || !G.value?.code), K = o(() => j.options.filter((e) => !e.disabled)), be = () => {
|
|
129
|
+
let e = j.options.findIndex((e) => e.value === j.modelValue);
|
|
130
|
+
z.value = e >= 0 ? e : 0, B.value = !1, m(() => {
|
|
131
|
+
ge(), Z(z.value);
|
|
132
|
+
});
|
|
133
|
+
}, q = () => {
|
|
134
|
+
j.disabled || U(!0);
|
|
135
|
+
}, J = (e = !1) => {
|
|
136
|
+
U(!1), z.value = -1, e && m(() => I.value?.focus());
|
|
137
|
+
}, Y = () => {
|
|
138
|
+
H.value ? J() : q();
|
|
139
|
+
}, X = (e) => {
|
|
140
|
+
e.disabled || (M("update:modelValue", e.value), J(!0));
|
|
141
|
+
}, Z = (e) => {
|
|
142
|
+
let t = j.options[e];
|
|
143
|
+
t && R.value.get(t.value)?.focus();
|
|
144
|
+
}, xe = (e, t) => {
|
|
145
|
+
e instanceof HTMLElement ? R.value.set(t, e) : R.value.delete(t);
|
|
146
|
+
}, Q = (e) => {
|
|
147
|
+
let t = z.value + e;
|
|
148
|
+
for (; t >= 0 && t < j.options.length;) {
|
|
149
|
+
if (!j.options[t].disabled) {
|
|
150
|
+
z.value = t, Z(t);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
t += e;
|
|
154
|
+
}
|
|
155
|
+
}, Se = (e) => {
|
|
156
|
+
if (!j.disabled) {
|
|
157
|
+
if (O(e) && H.value) {
|
|
158
|
+
e.preventDefault(), J();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
(D(e) || e.key === "ArrowDown" || e.key === "ArrowUp") && (e.preventDefault(), q());
|
|
162
|
+
}
|
|
163
|
+
}, Ce = (e, t, n) => {
|
|
164
|
+
if (O(e)) {
|
|
165
|
+
e.preventDefault(), J(!0);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (D(e)) {
|
|
169
|
+
e.preventDefault(), X(t);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
switch (e.key) {
|
|
173
|
+
case "ArrowDown":
|
|
174
|
+
e.preventDefault(), Q(1);
|
|
175
|
+
break;
|
|
176
|
+
case "ArrowUp":
|
|
177
|
+
e.preventDefault(), Q(-1);
|
|
178
|
+
break;
|
|
179
|
+
case "Home":
|
|
180
|
+
e.preventDefault(), z.value = K.value.length > 0 ? j.options.indexOf(K.value[0]) : 0, Z(z.value);
|
|
181
|
+
break;
|
|
182
|
+
case "End":
|
|
183
|
+
e.preventDefault(), z.value = K.value.length > 0 ? j.options.indexOf(K.value[K.value.length - 1]) : j.options.length - 1, Z(z.value);
|
|
184
|
+
break;
|
|
185
|
+
default: break;
|
|
186
|
+
}
|
|
187
|
+
}, $ = (e) => {
|
|
188
|
+
if (!H.value) return;
|
|
189
|
+
let t = e.target;
|
|
190
|
+
t instanceof Node && (F.value?.contains(t) || J());
|
|
191
|
+
}, we = o(() => j.modelValue !== "");
|
|
192
|
+
return w(H, (e) => {
|
|
193
|
+
e ? (document.addEventListener("pointerdown", $), be()) : document.removeEventListener("pointerdown", $);
|
|
194
|
+
}, { immediate: !0 }), w(() => j.options.length, (e) => {
|
|
195
|
+
H.value && z.value >= e && (z.value = e - 1);
|
|
196
|
+
}), g(() => {
|
|
197
|
+
document.removeEventListener("pointerdown", $);
|
|
198
|
+
}), (t, o) => (_(), l("div", {
|
|
199
|
+
ref_key: "rootRef",
|
|
200
|
+
ref: F,
|
|
201
|
+
class: h(me.value),
|
|
202
|
+
style: te(_e.value),
|
|
203
|
+
"data-state": S(H) ? "open" : "closed"
|
|
204
|
+
}, [u("button", p({
|
|
205
|
+
ref_key: "triggerRef",
|
|
206
|
+
ref: I
|
|
207
|
+
}, V.value, {
|
|
208
|
+
type: "button",
|
|
209
|
+
class: "t-language-select__trigger",
|
|
210
|
+
disabled: f.disabled,
|
|
211
|
+
"aria-controls": S(H) ? S(P) : void 0,
|
|
212
|
+
"aria-haspopup": "listbox",
|
|
213
|
+
"aria-expanded": S(H),
|
|
214
|
+
"aria-invalid": f.invalid || void 0,
|
|
215
|
+
"aria-busy": f.loading || void 0,
|
|
216
|
+
onClick: Y,
|
|
217
|
+
onKeydown: Se
|
|
218
|
+
}), [
|
|
219
|
+
t.$slots.prefix ? (_(), l("span", ie, [b(t.$slots, "prefix")])) : c("", !0),
|
|
220
|
+
W.value ? (_(), s(r, {
|
|
221
|
+
key: 1,
|
|
222
|
+
name: "languages",
|
|
223
|
+
class: "t-language-select__translate"
|
|
224
|
+
})) : G.value?.code ? (_(), s(n, {
|
|
225
|
+
key: 2,
|
|
226
|
+
code: G.value.code,
|
|
227
|
+
"base-url": f.flagBaseUrl,
|
|
228
|
+
size: f.size
|
|
229
|
+
}, null, 8, [
|
|
230
|
+
"code",
|
|
231
|
+
"base-url",
|
|
232
|
+
"size"
|
|
233
|
+
])) : c("", !0),
|
|
234
|
+
ye.value ? (_(), l("span", {
|
|
235
|
+
key: 3,
|
|
236
|
+
class: "t-language-select__value",
|
|
237
|
+
"data-placeholder": !we.value || void 0
|
|
238
|
+
}, x(ve.value || f.placeholder), 9, ae)) : c("", !0),
|
|
239
|
+
W.value && G.value?.code ? (_(), s(n, {
|
|
240
|
+
key: 4,
|
|
241
|
+
code: G.value.code,
|
|
242
|
+
"base-url": f.flagBaseUrl,
|
|
243
|
+
size: f.size
|
|
244
|
+
}, null, 8, [
|
|
245
|
+
"code",
|
|
246
|
+
"base-url",
|
|
247
|
+
"size"
|
|
248
|
+
])) : c("", !0),
|
|
249
|
+
f.loading ? (_(), s(e, {
|
|
250
|
+
key: 5,
|
|
251
|
+
size: "sm",
|
|
252
|
+
label: "Loading"
|
|
253
|
+
})) : (_(), l("svg", oe, [...o[0] ||= [u("polyline", { points: "4 6 8 10 12 6" }, null, -1)]]))
|
|
254
|
+
], 16, re), d(a, { name: "t-fade" }, {
|
|
255
|
+
default: T(() => [S(H) && !f.disabled ? (_(), l("ul", {
|
|
256
|
+
key: 0,
|
|
257
|
+
id: S(P),
|
|
258
|
+
ref_key: "listboxRef",
|
|
259
|
+
ref: L,
|
|
260
|
+
role: "listbox",
|
|
261
|
+
class: "t-language-select__listbox",
|
|
262
|
+
"aria-label": typeof V.value["aria-label"] == "string" ? V.value["aria-label"] : void 0
|
|
263
|
+
}, [f.options.length === 0 ? (_(), l("li", ce, [b(t.$slots, "empty", {}, () => [ee(x(f.emptyText), 1)])])) : c("", !0), (_(!0), l(i, null, y(f.options, (e, r) => (_(), l("li", {
|
|
264
|
+
key: e.value,
|
|
265
|
+
ref_for: !0,
|
|
266
|
+
ref: (t) => xe(t, e.value),
|
|
267
|
+
role: "option",
|
|
268
|
+
class: h(["t-language-select__option", {
|
|
269
|
+
"is-selected": e.value === f.modelValue,
|
|
270
|
+
"is-disabled": e.disabled,
|
|
271
|
+
"is-focused": r === z.value
|
|
272
|
+
}]),
|
|
273
|
+
"aria-selected": e.value === f.modelValue,
|
|
274
|
+
"aria-disabled": e.disabled || void 0,
|
|
275
|
+
tabindex: e.disabled ? -1 : 0,
|
|
276
|
+
onClick: (t) => X(e),
|
|
277
|
+
onKeydown: (t) => Ce(t, e, r)
|
|
278
|
+
}, [b(t.$slots, "option", {
|
|
279
|
+
option: e,
|
|
280
|
+
selected: e.value === f.modelValue,
|
|
281
|
+
focused: r === z.value
|
|
282
|
+
}, () => [e.code ? (_(), s(n, {
|
|
283
|
+
key: 0,
|
|
284
|
+
code: e.code,
|
|
285
|
+
"base-url": f.flagBaseUrl,
|
|
286
|
+
size: f.size
|
|
287
|
+
}, null, 8, [
|
|
288
|
+
"code",
|
|
289
|
+
"base-url",
|
|
290
|
+
"size"
|
|
291
|
+
])) : c("", !0), u("span", ue, [u("span", de, x(e.label), 1), e.description ? (_(), l("span", fe, x(e.description), 1)) : c("", !0)])]), e.value === f.modelValue ? (_(), l("svg", pe, [...o[1] ||= [u("polyline", { points: "3.5 8.5 6.5 11.5 12.5 4.5" }, null, -1)]])) : c("", !0)], 42, le))), 128))], 8, se)) : c("", !0)]),
|
|
292
|
+
_: 3
|
|
293
|
+
})], 14, k));
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
//#endregion
|
|
297
|
+
export { A as default };
|
|
298
|
+
|
|
299
|
+
//# sourceMappingURL=TLanguageSelect.vue_vue_type_script_setup_true_lang.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TLanguageSelect.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../src/components/TLanguageSelect.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { createId, isActivationKey, isEscapeKey } from '@treeui/utils';\nimport { computed, nextTick, onBeforeUnmount, ref, toRef, useAttrs, watch } from 'vue';\nimport { useControllableOpen } from '../composables/useControllableOpen';\nimport type { TFieldWidth, TSize } from '../types/contracts';\nimport TFlag from './TFlag.vue';\nimport TIcon from './TIcon.vue';\nimport TSpinner from './TSpinner.vue';\n\ndefineOptions({\n inheritAttrs: false,\n});\n\n/**\n * `field` is a form control: it reads as the value of a labelled question, so the\n * flag leads and the trigger fills its container.\n *\n * `switcher` is a page-level control, typically in a navbar, where nothing nearby\n * says what it changes. The translate icon states the purpose up front and the\n * current flag closes the row, so the whole thing reads as \"page language, now\n * set to this\" without an external label.\n */\nexport type TLanguageSelectVariant = 'field' | 'switcher';\n\nexport interface TLanguageOption {\n label: string;\n /** Opaque application key — typically a BCP-47 tag. Never parsed or validated. */\n value: string;\n /** ISO 3166-1 alpha-2 code for the flag. Omit to render the option without one. */\n code?: string;\n /** Secondary line, e.g. the language's endonym. */\n description?: string;\n disabled?: boolean;\n}\n\nconst props = withDefaults(\n defineProps<{\n modelValue?: string;\n options?: TLanguageOption[];\n open?: boolean;\n defaultOpen?: boolean;\n size?: TSize;\n variant?: TLanguageSelectVariant;\n /** Hide the language name, leaving icon + flag. `switcher` only. */\n iconOnly?: boolean;\n /** Inline-size cap. Fluid (`full`) by default. */\n width?: TFieldWidth;\n disabled?: boolean;\n loading?: boolean;\n invalid?: boolean;\n placeholder?: string;\n /** Forwarded to every nested TFlag. Set once here to self-host flag assets. */\n flagBaseUrl?: string;\n emptyText?: string;\n }>(),\n {\n modelValue: '',\n options: () => [],\n open: undefined,\n defaultOpen: false,\n size: 'md',\n variant: 'field',\n iconOnly: false,\n width: 'full',\n disabled: false,\n loading: false,\n invalid: false,\n placeholder: '',\n flagBaseUrl: undefined,\n emptyText: 'No languages available.',\n },\n);\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string];\n 'update:open': [value: boolean];\n 'open-change': [value: boolean];\n}>();\n\ndefineSlots<{\n prefix?: () => unknown;\n option?: (props: { option: TLanguageOption; selected: boolean; focused: boolean }) => unknown;\n empty?: () => unknown;\n}>();\n\nconst attrs = useAttrs();\nconst listboxId = createId('t-language-select');\nconst rootRef = ref<HTMLElement | null>(null);\nconst triggerRef = ref<HTMLButtonElement | null>(null);\nconst listboxRef = ref<HTMLElement | null>(null);\nconst optionRefs = ref<Map<string, HTMLElement>>(new Map());\nconst focusedIndex = ref(-1);\nconst dropUp = ref(false);\n\nconst triggerAttrs = computed(() => {\n const { class: _class, style: _style, ...rest } = attrs;\n return rest;\n});\n\nconst { value: isOpen, setValue } = useControllableOpen(\n toRef(props, 'open'),\n props.defaultOpen,\n (value) => {\n emit('update:open', value);\n emit('open-change', value);\n },\n);\n\nconst isSwitcher = computed(() => props.variant === 'switcher');\n\nconst rootClasses = computed(() => [\n 't-language-select',\n `t-language-select--${props.size}`,\n `t-language-select--${props.variant}`,\n // A switcher sizes to its content, so the field width scale does not apply.\n !isSwitcher.value && props.width !== 'full' ? `t-field-width--${props.width}` : null,\n {\n 't-language-select--icon-only': isSwitcher.value && props.iconOnly,\n 't-language-select--drop-up': dropUp.value,\n 'is-disabled': props.disabled,\n 'is-invalid': props.invalid,\n 'is-loading': props.loading,\n 'is-open': isOpen.value,\n },\n attrs.class,\n]);\n\n// Nearest ancestor that can clip the listbox (scroll container or hidden overflow).\nconst findClippingAncestor = (element: HTMLElement): HTMLElement | null => {\n let parent = element.parentElement;\n while (parent && parent !== document.body) {\n const { overflowY } = window.getComputedStyle(parent);\n if (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'hidden') {\n return parent;\n }\n parent = parent.parentElement;\n }\n return null;\n};\n\n// Flip the listbox above the trigger when its rendered position overflows the\n// nearest clipping ancestor (e.g. a modal body) or the viewport, and there is\n// more room above the trigger than below it. Measuring the rendered rect keeps\n// the decision in sync with the CSS offset (--tree-space-2) automatically.\nconst updateDropDirection = () => {\n const trigger = triggerRef.value;\n const listbox = listboxRef.value;\n if (!trigger || !listbox) {\n dropUp.value = false;\n return;\n }\n const triggerRect = trigger.getBoundingClientRect();\n const listboxRect = listbox.getBoundingClientRect();\n const boundary = findClippingAncestor(trigger)?.getBoundingClientRect();\n const bottomLimit = Math.min(boundary?.bottom ?? Infinity, window.innerHeight);\n const topLimit = Math.max(boundary?.top ?? 0, 0);\n const spaceBelow = bottomLimit - triggerRect.bottom;\n const spaceAbove = triggerRect.top - topLimit;\n dropUp.value = listboxRect.bottom > bottomLimit && spaceAbove > spaceBelow;\n};\n\nconst rootStyle = computed(() => attrs.style);\n\nconst selectedOption = computed(() =>\n props.options.find((o) => o.value === props.modelValue),\n);\n\nconst selectedLabel = computed(() => selectedOption.value?.label ?? '');\n\n// iconOnly leans on the flag to carry the current value. With no flag to show it\n// would leave the trigger stating nothing at all, so the name comes back.\nconst showValue = computed(\n () => !(isSwitcher.value && props.iconOnly) || !selectedOption.value?.code,\n);\n\nconst enabledOptions = computed(() =>\n props.options.filter((o) => !o.disabled),\n);\n\n// Runs for every open, however it was triggered. The listbox can also be opened\n// through `open` / `defaultOpen` without going near openDropdown(), so this lives\n// on the isOpen watcher rather than inside the click handler.\nconst onOpened = () => {\n const selectedIdx = props.options.findIndex((o) => o.value === props.modelValue);\n focusedIndex.value = selectedIdx >= 0 ? selectedIdx : 0;\n // Render downward first so the measurement always starts from the default position.\n dropUp.value = false;\n nextTick(() => {\n updateDropDirection();\n focusOption(focusedIndex.value);\n });\n};\n\nconst openDropdown = () => {\n if (props.disabled) return;\n setValue(true);\n};\n\nconst closeDropdown = (restoreFocus = false) => {\n setValue(false);\n focusedIndex.value = -1;\n if (restoreFocus) {\n nextTick(() => triggerRef.value?.focus());\n }\n};\n\nconst toggleDropdown = () => {\n if (isOpen.value) {\n closeDropdown();\n } else {\n openDropdown();\n }\n};\n\nconst selectOption = (opt: TLanguageOption) => {\n if (opt.disabled) return;\n emit('update:modelValue', opt.value);\n closeDropdown(true);\n};\n\nconst focusOption = (index: number) => {\n const opt = props.options[index];\n if (!opt) return;\n const el = optionRefs.value.get(opt.value);\n el?.focus();\n};\n\nconst setOptionRef = (el: Element | null, value: string) => {\n if (el instanceof HTMLElement) {\n optionRefs.value.set(value, el);\n } else {\n optionRefs.value.delete(value);\n }\n};\n\nconst moveFocus = (direction: 1 | -1) => {\n let nextIndex = focusedIndex.value + direction;\n while (nextIndex >= 0 && nextIndex < props.options.length) {\n if (!props.options[nextIndex].disabled) {\n focusedIndex.value = nextIndex;\n focusOption(nextIndex);\n return;\n }\n nextIndex += direction;\n }\n};\n\nconst onTriggerKeydown = (event: KeyboardEvent) => {\n if (props.disabled) return;\n\n if (isEscapeKey(event) && isOpen.value) {\n event.preventDefault();\n closeDropdown();\n return;\n }\n\n if (isActivationKey(event) || event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault();\n openDropdown();\n }\n};\n\nconst onOptionKeydown = (event: KeyboardEvent, opt: TLanguageOption, _index: number) => {\n if (isEscapeKey(event)) {\n event.preventDefault();\n closeDropdown(true);\n return;\n }\n\n if (isActivationKey(event)) {\n event.preventDefault();\n selectOption(opt);\n return;\n }\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault();\n moveFocus(1);\n break;\n case 'ArrowUp':\n event.preventDefault();\n moveFocus(-1);\n break;\n case 'Home':\n event.preventDefault();\n focusedIndex.value = enabledOptions.value.length > 0\n ? props.options.indexOf(enabledOptions.value[0])\n : 0;\n focusOption(focusedIndex.value);\n break;\n case 'End':\n event.preventDefault();\n focusedIndex.value = enabledOptions.value.length > 0\n ? props.options.indexOf(enabledOptions.value[enabledOptions.value.length - 1])\n : props.options.length - 1;\n focusOption(focusedIndex.value);\n break;\n default:\n break;\n }\n};\n\nconst onDocumentPointerDown = (event: PointerEvent) => {\n if (!isOpen.value) return;\n const target = event.target;\n if (!(target instanceof Node)) return;\n if (rootRef.value?.contains(target)) return;\n closeDropdown();\n};\n\nconst hasSelection = computed(() => props.modelValue !== '');\n\n// immediate, so a listbox that is already open on mount (`defaultOpen`, or a\n// controlled `open` that starts true) still dismisses on outside click and still\n// measures its drop direction.\nwatch(isOpen, (value) => {\n if (value) {\n document.addEventListener('pointerdown', onDocumentPointerDown);\n onOpened();\n } else {\n document.removeEventListener('pointerdown', onDocumentPointerDown);\n }\n}, { immediate: true });\n\n// Options can shrink underneath an open listbox. Leaving focusedIndex past the\n// end would strand moveFocus(), which only ever walks outward from it.\nwatch(\n () => props.options.length,\n (length) => {\n if (isOpen.value && focusedIndex.value >= length) {\n focusedIndex.value = length - 1;\n }\n },\n);\n\nonBeforeUnmount(() => {\n document.removeEventListener('pointerdown', onDocumentPointerDown);\n});\n</script>\n\n<template>\n <div\n ref=\"rootRef\"\n :class=\"rootClasses\"\n :style=\"rootStyle\"\n :data-state=\"isOpen ? 'open' : 'closed'\"\n >\n <button\n ref=\"triggerRef\"\n v-bind=\"triggerAttrs\"\n type=\"button\"\n class=\"t-language-select__trigger\"\n :disabled=\"disabled\"\n :aria-controls=\"isOpen ? listboxId : undefined\"\n aria-haspopup=\"listbox\"\n :aria-expanded=\"isOpen\"\n :aria-invalid=\"invalid || undefined\"\n :aria-busy=\"loading || undefined\"\n @click=\"toggleDropdown\"\n @keydown=\"onTriggerKeydown\"\n >\n <span\n v-if=\"$slots.prefix\"\n class=\"t-language-select__slot t-language-select__slot--prefix\"\n >\n <slot name=\"prefix\" />\n </span>\n <!--\n The switcher leads with the translate icon so the control announces what\n it changes; the field leads with the flag, since a form label already\n does that job.\n -->\n <!-- Sized in CSS from the icon token scale, not TIcon's pixel `size` prop. -->\n <TIcon\n v-if=\"isSwitcher\"\n name=\"languages\"\n class=\"t-language-select__translate\"\n />\n <TFlag\n v-else-if=\"selectedOption?.code\"\n :code=\"selectedOption.code\"\n :base-url=\"flagBaseUrl\"\n :size=\"size\"\n />\n <span\n v-if=\"showValue\"\n class=\"t-language-select__value\"\n :data-placeholder=\"!hasSelection ? true : undefined\"\n >\n {{ selectedLabel || placeholder }}\n </span>\n <TFlag\n v-if=\"isSwitcher && selectedOption?.code\"\n :code=\"selectedOption.code\"\n :base-url=\"flagBaseUrl\"\n :size=\"size\"\n />\n <TSpinner\n v-if=\"loading\"\n size=\"sm\"\n label=\"Loading\"\n />\n <svg\n v-else\n class=\"t-language-select__chevron\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <polyline points=\"4 6 8 10 12 6\" />\n </svg>\n </button>\n <transition name=\"t-fade\">\n <ul\n v-if=\"isOpen && !disabled\"\n :id=\"listboxId\"\n ref=\"listboxRef\"\n role=\"listbox\"\n class=\"t-language-select__listbox\"\n :aria-label=\"typeof triggerAttrs['aria-label'] === 'string' ? triggerAttrs['aria-label'] : undefined\"\n >\n <li\n v-if=\"options.length === 0\"\n class=\"t-language-select__empty\"\n role=\"presentation\"\n >\n <slot name=\"empty\">\n {{ emptyText }}\n </slot>\n </li>\n <li\n v-for=\"(opt, index) in options\"\n :key=\"opt.value\"\n :ref=\"(el) => setOptionRef(el as Element | null, opt.value)\"\n role=\"option\"\n class=\"t-language-select__option\"\n :class=\"{\n 'is-selected': opt.value === modelValue,\n 'is-disabled': opt.disabled,\n 'is-focused': index === focusedIndex,\n }\"\n :aria-selected=\"opt.value === modelValue\"\n :aria-disabled=\"opt.disabled || undefined\"\n :tabindex=\"opt.disabled ? -1 : 0\"\n @click=\"selectOption(opt)\"\n @keydown=\"onOptionKeydown($event, opt, index)\"\n >\n <slot\n name=\"option\"\n :option=\"opt\"\n :selected=\"opt.value === modelValue\"\n :focused=\"index === focusedIndex\"\n >\n <TFlag\n v-if=\"opt.code\"\n :code=\"opt.code\"\n :base-url=\"flagBaseUrl\"\n :size=\"size\"\n />\n <span class=\"t-language-select__option-text\">\n <span class=\"t-language-select__option-label\">{{ opt.label }}</span>\n <span\n v-if=\"opt.description\"\n class=\"t-language-select__description\"\n >\n {{ opt.description }}\n </span>\n </span>\n </slot>\n <svg\n v-if=\"opt.value === modelValue\"\n class=\"t-language-select__check\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <polyline points=\"3.5 8.5 6.5 11.5 12.5 4.5\" />\n </svg>\n </li>\n </ul>\n </transition>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TMarkdownEditor.cjs","names":[],"sources":["../../src/components/TMarkdownEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useAttrs, nextTick } from 'vue';\nimport type { TSize } from '../types/contracts';\n\ndefineOptions({\n inheritAttrs: false,\n});\n\nconst props = withDefaults(\n defineProps<{\n modelValue?: string;\n size?: TSize;\n disabled?: boolean;\n placeholder?: string;\n uploadImage?: (file: File) => Promise<string>;\n }>(),\n {\n modelValue: '',\n size: 'md',\n disabled: false,\n placeholder: 'Write your markdown here...',\n uploadImage: undefined,\n },\n);\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string];\n 'image-upload': [file: File, url: string];\n 'image-upload-error': [file: File, error: Error];\n}>();\n\ndefineSlots<{\n toolbar?: () => unknown;\n}>();\n\nconst attrs = useAttrs();\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\nconst isUploading = ref(false);\n\nconst rootClasses = computed(() => [\n 't-md-editor',\n `t-md-editor--${props.size}`,\n {\n 'is-disabled': props.disabled,\n 'is-uploading': isUploading.value,\n },\n attrs.class,\n]);\n\nconst rootStyle = computed(() => attrs.style);\n\nconst editorAttrs = computed(() => {\n const { class: _class, style: _style, ...rest } = attrs;\n return rest;\n});\n\nconst stringValue = computed(() => `${props.modelValue ?? ''}`);\n\nconst onInput = (event: Event) => {\n emit('update:modelValue', (event.target as HTMLTextAreaElement).value);\n};\n\n/* ──────── Toolbar Actions ──────── */\n\nconst insertAtCursor = (before: string, after = '') => {\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const end = el.selectionEnd;\n const text = stringValue.value;\n const selected = text.slice(start, end);\n\n const newText = text.slice(0, start) + before + selected + after + text.slice(end);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n const cursorPos = selected\n ? start + before.length + selected.length + after.length\n : start + before.length;\n el.setSelectionRange(cursorPos, cursorPos);\n });\n};\n\nconst insertHeading = (level: number) => {\n const prefix = '#'.repeat(level) + ' ';\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const text = stringValue.value;\n\n // Find start of current line\n const lineStart = text.lastIndexOf('\\n', start - 1) + 1;\n const lineEnd = text.indexOf('\\n', start);\n const actualEnd = lineEnd === -1 ? text.length : lineEnd;\n const line = text.slice(lineStart, actualEnd);\n\n // Strip existing heading markers\n const stripped = line.replace(/^#{1,6}\\s*/, '');\n const newLine = prefix + stripped;\n\n const newText = text.slice(0, lineStart) + newLine + text.slice(actualEnd);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n el.setSelectionRange(lineStart + newLine.length, lineStart + newLine.length);\n });\n};\n\nconst insertBold = () => insertAtCursor('**', '**');\nconst insertItalic = () => insertAtCursor('_', '_');\nconst insertCode = () => insertAtCursor('`', '`');\nconst insertCodeBlock = () => insertAtCursor('\\n```\\n', '\\n```\\n');\nconst insertLink = () => insertAtCursor('[', '](url)');\nconst insertList = () => insertAtCursor('\\n- ');\nconst insertOrderedList = () => insertAtCursor('\\n1. ');\nconst insertQuote = () => insertAtCursor('\\n> ');\nconst insertHr = () => insertAtCursor('\\n---\\n');\n\n/* ──────── Markdown → HTML rendering ──────── */\n\nconst renderedHtml = computed(() => {\n return markdownToHtml(stringValue.value);\n});\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction markdownToHtml(md: string): string {\n if (!md) return '';\n\n const lines = md.split('\\n');\n const html: string[] = [];\n let inCodeBlock = false;\n let inList = false;\n let inOrderedList = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Code blocks\n if (line.trim().startsWith('```')) {\n if (inCodeBlock) {\n html.push('</code></pre>');\n inCodeBlock = false;\n } else {\n inCodeBlock = true;\n html.push('<pre class=\"t-md-editor__code-block\"><code>');\n }\n continue;\n }\n\n if (inCodeBlock) {\n html.push(escapeHtml(line));\n html.push('\\n');\n continue;\n }\n\n // Close open lists if needed\n const isUnorderedListItem = /^[-*+]\\s/.test(line.trim());\n const isOrderedListItem = /^\\d+\\.\\s/.test(line.trim());\n\n if (inList && !isUnorderedListItem) {\n html.push('</ul>');\n inList = false;\n }\n if (inOrderedList && !isOrderedListItem) {\n html.push('</ol>');\n inOrderedList = false;\n }\n\n // Horizontal rule\n if (/^(-{3,}|_{3,}|\\*{3,})$/.test(line.trim())) {\n html.push('<hr class=\"t-md-editor__hr\" />');\n continue;\n }\n\n // Headings\n const headingMatch = line.match(/^(#{1,6})\\s+(.+)$/);\n if (headingMatch) {\n const level = headingMatch[1].length;\n html.push(`<h${level} class=\"t-md-editor__heading t-md-editor__heading--${level}\">${inlineMarkdown(escapeHtml(headingMatch[2]))}</h${level}>`);\n continue;\n }\n\n // Blockquote\n if (line.trim().startsWith('> ')) {\n html.push(`<blockquote class=\"t-md-editor__blockquote\">${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</blockquote>`);\n continue;\n }\n\n // Unordered list\n if (isUnorderedListItem) {\n if (!inList) {\n html.push('<ul class=\"t-md-editor__list\">');\n inList = true;\n }\n html.push(`<li>${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</li>`);\n continue;\n }\n\n // Ordered list\n if (isOrderedListItem) {\n if (!inOrderedList) {\n html.push('<ol class=\"t-md-editor__list t-md-editor__list--ordered\">');\n inOrderedList = true;\n }\n const content = line.trim().replace(/^\\d+\\.\\s/, '');\n html.push(`<li>${inlineMarkdown(escapeHtml(content))}</li>`);\n continue;\n }\n\n // Empty line\n if (line.trim() === '') {\n html.push('<br />');\n continue;\n }\n\n // Paragraph\n html.push(`<p class=\"t-md-editor__paragraph\">${inlineMarkdown(escapeHtml(line))}</p>`);\n }\n\n // Close any open blocks\n if (inCodeBlock) html.push('</code></pre>');\n if (inList) html.push('</ul>');\n if (inOrderedList) html.push('</ol>');\n\n return html.join('\\n');\n}\n\nfunction inlineMarkdown(text: string): string {\n // Images: \n text = text.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, '<img src=\"$2\" alt=\"$1\" class=\"t-md-editor__image\" loading=\"lazy\" />');\n // Links: [text](url)\n text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\">$1</a>');\n // Bold: **text**\n text = text.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n // Italic: _text_ or *text*\n text = text.replace(/_(.+?)_/g, '<em>$1</em>');\n text = text.replace(/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, '<em>$1</em>');\n // Inline code: `text`\n text = text.replace(/`(.+?)`/g, '<code class=\"t-md-editor__inline-code\">$1</code>');\n // Strikethrough: ~~text~~\n text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');\n return text;\n}\n\n/* ──────── Image paste / drop handling ──────── */\n\nconst handlePaste = async (event: ClipboardEvent) => {\n if (props.disabled) return;\n\n const items = event.clipboardData?.items;\n if (!items) return;\n\n for (const item of items) {\n if (item.type.startsWith('image/')) {\n event.preventDefault();\n const file = item.getAsFile();\n if (file) await processImageUpload(file);\n return;\n }\n }\n};\n\nconst handleDrop = async (event: DragEvent) => {\n if (props.disabled) return;\n\n const files = event.dataTransfer?.files;\n if (!files) return;\n\n for (const file of files) {\n if (file.type.startsWith('image/')) {\n event.preventDefault();\n await processImageUpload(file);\n }\n }\n};\n\nconst handleDragOver = (event: DragEvent) => {\n if (props.disabled) return;\n event.preventDefault();\n};\n\nconst processImageUpload = async (file: File) => {\n if (!props.uploadImage) {\n // Fallback: insert a placeholder with the file name\n insertAtCursor(`![${file.name}]()`);\n return;\n }\n\n // Insert uploading placeholder\n const placeholder = `![Uploading ${file.name}...]()`;\n insertAtCursor(placeholder);\n isUploading.value = true;\n\n try {\n const url = props.uploadImage(file);\n const resolvedUrl = url instanceof Promise ? await url : url;\n\n // Replace placeholder with actual URL\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, ``);\n emit('update:modelValue', newValue);\n emit('image-upload', file, resolvedUrl);\n } catch (err) {\n // Remove placeholder on error\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, '');\n emit('update:modelValue', newValue);\n emit('image-upload-error', file, err instanceof Error ? err : new Error(String(err)));\n } finally {\n isUploading.value = false;\n }\n};\n\n/* ──────── Keyboard shortcuts ──────── */\n\nconst handleKeydown = (event: KeyboardEvent) => {\n if (props.disabled) return;\n\n const mod = event.metaKey || event.ctrlKey;\n if (!mod) return;\n\n switch (event.key) {\n case 'b':\n event.preventDefault();\n insertBold();\n break;\n case 'i':\n event.preventDefault();\n insertItalic();\n break;\n case 'k':\n event.preventDefault();\n insertLink();\n break;\n case 'e':\n event.preventDefault();\n insertCode();\n break;\n }\n};\n\n</script>\n\n<template>\n <div\n v-bind=\"editorAttrs\"\n :class=\"rootClasses\"\n :style=\"rootStyle\"\n >\n <!-- Toolbar -->\n <div\n class=\"t-md-editor__toolbar\"\n role=\"toolbar\"\n aria-label=\"Markdown formatting\"\n >\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 1 (H1)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(1)\"\n >\n H1\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 2 (H2)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(2)\"\n >\n H2\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 3 (H3)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(3)\"\n >\n H3\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Bold (Ctrl+B)\"\n :disabled=\"disabled\"\n @click=\"insertBold\"\n >\n <strong>B</strong>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Italic (Ctrl+I)\"\n :disabled=\"disabled\"\n @click=\"insertItalic\"\n >\n <em>I</em>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Strikethrough\"\n :disabled=\"disabled\"\n @click=\"insertAtCursor('~~', '~~')\"\n >\n <del>S</del>\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Inline code (Ctrl+E)\"\n :disabled=\"disabled\"\n @click=\"insertCode\"\n >\n </>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Code block\"\n :disabled=\"disabled\"\n @click=\"insertCodeBlock\"\n >\n { }\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Link (Ctrl+K)\"\n :disabled=\"disabled\"\n @click=\"insertLink\"\n >\n 🔗\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Unordered list\"\n :disabled=\"disabled\"\n @click=\"insertList\"\n >\n • List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Ordered list\"\n :disabled=\"disabled\"\n @click=\"insertOrderedList\"\n >\n 1. List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Blockquote\"\n :disabled=\"disabled\"\n @click=\"insertQuote\"\n >\n >\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Horizontal rule\"\n :disabled=\"disabled\"\n @click=\"insertHr\"\n >\n ―\n </button>\n </div>\n\n <slot name=\"toolbar\" />\n </div>\n\n <!-- Editor body -->\n <div class=\"t-md-editor__body\">\n <!-- Write pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--write\"\n >\n <textarea\n ref=\"textareaRef\"\n class=\"t-md-editor__textarea\"\n :value=\"stringValue\"\n :placeholder=\"placeholder\"\n :disabled=\"disabled\"\n :aria-label=\"placeholder\"\n @input=\"onInput\"\n @paste=\"handlePaste\"\n @drop=\"handleDrop\"\n @dragover=\"handleDragOver\"\n @keydown=\"handleKeydown\"\n />\n </div>\n\n <!-- Preview pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--preview\"\n aria-label=\"Markdown preview\"\n >\n <!-- eslint-disable vue/no-v-html -->\n <div\n class=\"t-md-editor__preview\"\n v-html=\"renderedHtml\"\n />\n <!-- eslint-enable vue/no-v-html -->\n </div>\n </div>\n\n <!-- Upload indicator -->\n <div\n v-if=\"isUploading\"\n class=\"t-md-editor__upload-bar\"\n role=\"status\"\n aria-live=\"polite\"\n >\n Uploading image…\n </div>\n </div>\n</template>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"TMarkdownEditor.cjs","names":[],"sources":["../../src/components/TMarkdownEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useAttrs, nextTick } from 'vue';\nimport type { TSize } from '../types/contracts';\n\ndefineOptions({\n inheritAttrs: false,\n});\n\nconst props = withDefaults(\n defineProps<{\n modelValue?: string;\n size?: TSize;\n disabled?: boolean;\n placeholder?: string;\n uploadImage?: (file: File) => Promise<string>;\n sanitize?: (html: string) => string;\n }>(),\n {\n modelValue: '',\n size: 'md',\n disabled: false,\n placeholder: 'Write your markdown here...',\n uploadImage: undefined,\n sanitize: undefined,\n },\n);\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string];\n 'image-upload': [file: File, url: string];\n 'image-upload-error': [file: File, error: Error];\n}>();\n\ndefineSlots<{\n toolbar?: () => unknown;\n}>();\n\nconst attrs = useAttrs();\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\nconst isUploading = ref(false);\n\nconst rootClasses = computed(() => [\n 't-md-editor',\n `t-md-editor--${props.size}`,\n {\n 'is-disabled': props.disabled,\n 'is-uploading': isUploading.value,\n },\n attrs.class,\n]);\n\nconst rootStyle = computed(() => attrs.style);\n\nconst editorAttrs = computed(() => {\n const { class: _class, style: _style, ...rest } = attrs;\n return rest;\n});\n\nconst stringValue = computed(() => `${props.modelValue ?? ''}`);\n\nconst onInput = (event: Event) => {\n emit('update:modelValue', (event.target as HTMLTextAreaElement).value);\n};\n\n/* ──────── Toolbar Actions ──────── */\n\nconst insertAtCursor = (before: string, after = '') => {\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const end = el.selectionEnd;\n const text = stringValue.value;\n const selected = text.slice(start, end);\n\n const newText = text.slice(0, start) + before + selected + after + text.slice(end);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n const cursorPos = selected\n ? start + before.length + selected.length + after.length\n : start + before.length;\n el.setSelectionRange(cursorPos, cursorPos);\n });\n};\n\nconst insertHeading = (level: number) => {\n const prefix = '#'.repeat(level) + ' ';\n const el = textareaRef.value;\n if (!el) return;\n\n const start = el.selectionStart;\n const text = stringValue.value;\n\n // Find start of current line\n const lineStart = text.lastIndexOf('\\n', start - 1) + 1;\n const lineEnd = text.indexOf('\\n', start);\n const actualEnd = lineEnd === -1 ? text.length : lineEnd;\n const line = text.slice(lineStart, actualEnd);\n\n // Strip existing heading markers\n const stripped = line.replace(/^#{1,6}\\s*/, '');\n const newLine = prefix + stripped;\n\n const newText = text.slice(0, lineStart) + newLine + text.slice(actualEnd);\n emit('update:modelValue', newText);\n\n nextTick(() => {\n el.focus();\n el.setSelectionRange(lineStart + newLine.length, lineStart + newLine.length);\n });\n};\n\nconst insertBold = () => insertAtCursor('**', '**');\nconst insertItalic = () => insertAtCursor('_', '_');\nconst insertCode = () => insertAtCursor('`', '`');\nconst insertCodeBlock = () => insertAtCursor('\\n```\\n', '\\n```\\n');\nconst insertLink = () => insertAtCursor('[', '](url)');\nconst insertList = () => insertAtCursor('\\n- ');\nconst insertOrderedList = () => insertAtCursor('\\n1. ');\nconst insertQuote = () => insertAtCursor('\\n> ');\nconst insertHr = () => insertAtCursor('\\n---\\n');\n\n/* ──────── Markdown → HTML rendering ──────── */\n\nconst renderedHtml = computed(() => {\n const html = markdownToHtml(stringValue.value);\n // Escape hatch: the built-in rendering escapes HTML and allowlists URL\n // schemes, but consumers with a stricter policy can pass their own\n // sanitizer (DOMPurify or similar) instead of forking the component.\n return props.sanitize ? props.sanitize(html) : html;\n});\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction markdownToHtml(md: string): string {\n if (!md) return '';\n\n const lines = md.split('\\n');\n const html: string[] = [];\n let inCodeBlock = false;\n let inList = false;\n let inOrderedList = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Code blocks\n if (line.trim().startsWith('```')) {\n if (inCodeBlock) {\n html.push('</code></pre>');\n inCodeBlock = false;\n } else {\n inCodeBlock = true;\n html.push('<pre class=\"t-md-editor__code-block\"><code>');\n }\n continue;\n }\n\n if (inCodeBlock) {\n html.push(escapeHtml(line));\n html.push('\\n');\n continue;\n }\n\n // Close open lists if needed\n const isUnorderedListItem = /^[-*+]\\s/.test(line.trim());\n const isOrderedListItem = /^\\d+\\.\\s/.test(line.trim());\n\n if (inList && !isUnorderedListItem) {\n html.push('</ul>');\n inList = false;\n }\n if (inOrderedList && !isOrderedListItem) {\n html.push('</ol>');\n inOrderedList = false;\n }\n\n // Horizontal rule\n if (/^(-{3,}|_{3,}|\\*{3,})$/.test(line.trim())) {\n html.push('<hr class=\"t-md-editor__hr\" />');\n continue;\n }\n\n // Headings\n const headingMatch = line.match(/^(#{1,6})\\s+(.+)$/);\n if (headingMatch) {\n const level = headingMatch[1].length;\n html.push(`<h${level} class=\"t-md-editor__heading t-md-editor__heading--${level}\">${inlineMarkdown(escapeHtml(headingMatch[2]))}</h${level}>`);\n continue;\n }\n\n // Blockquote\n if (line.trim().startsWith('> ')) {\n html.push(`<blockquote class=\"t-md-editor__blockquote\">${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</blockquote>`);\n continue;\n }\n\n // Unordered list\n if (isUnorderedListItem) {\n if (!inList) {\n html.push('<ul class=\"t-md-editor__list\">');\n inList = true;\n }\n html.push(`<li>${inlineMarkdown(escapeHtml(line.trim().slice(2)))}</li>`);\n continue;\n }\n\n // Ordered list\n if (isOrderedListItem) {\n if (!inOrderedList) {\n html.push('<ol class=\"t-md-editor__list t-md-editor__list--ordered\">');\n inOrderedList = true;\n }\n const content = line.trim().replace(/^\\d+\\.\\s/, '');\n html.push(`<li>${inlineMarkdown(escapeHtml(content))}</li>`);\n continue;\n }\n\n // Empty line\n if (line.trim() === '') {\n html.push('<br />');\n continue;\n }\n\n // Paragraph\n html.push(`<p class=\"t-md-editor__paragraph\">${inlineMarkdown(escapeHtml(line))}</p>`);\n }\n\n // Close any open blocks\n if (inCodeBlock) html.push('</code></pre>');\n if (inList) html.push('</ul>');\n if (inOrderedList) html.push('</ol>');\n\n return html.join('\\n');\n}\n\n/* ──────── URL safety ──────── */\n\n// A URL carries a scheme only when a colon follows a valid scheme name.\n// Anything else (`/path`, `./path`, `#anchor`, `?q=1`) is relative and cannot\n// introduce a new scheme, so it is safe. `//host/path` is protocol-relative:\n// it keeps the page's protocol but points at another host, so it can navigate\n// off-origin. That is allowed — it is a link, not script — and `rel=\"noopener\n// noreferrer\"` on the anchor covers the tab-nabbing side of it.\nconst HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/;\n// Navigable schemes. `javascript:`, `vbscript:`, `data:` and friends are\n// rejected — everything reaching inlineMarkdown is already HTML-escaped, so\n// the URL scheme is the only remaining way to get script into the preview.\nconst SAFE_LINK_SCHEME = /^(https?|mailto|tel):/;\nconst SAFE_IMAGE_SCHEME = /^https?:/;\n// Inline raster images are useful for pasted screenshots. `image/svg+xml` is\n// deliberately excluded: SVG is a document format that can carry script and\n// external references, not just pixels.\nconst SAFE_IMAGE_DATA_URL = /^data:image\\/(png|jpe?g|gif|webp|avif|bmp|x-icon|vnd\\.microsoft\\.icon)[;,]/;\n\nconst fromCodePoint = (code: number) =>\n Number.isFinite(code) && code >= 0 && code <= 0x10ffff ? String.fromCodePoint(code) : '';\n\n// Reduce a URL to the form a browser would act on before testing its scheme:\n// decode character references, drop control characters and whitespace, and\n// lowercase. This defeats `JaVaScRiPt:`, ` javascript:`, `java\\tscript:`,\n// `java\\x00script:` and `javascript:` style evasion.\nfunction normalizeUrl(url: string): string {\n return url\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#x([0-9a-f]+);?/gi, (_match, hex: string) => fromCodePoint(parseInt(hex, 16)))\n .replace(/&#(\\d+);?/g, (_match, dec: string) => fromCodePoint(parseInt(dec, 10)))\n // eslint-disable-next-line no-control-regex -- matching control characters is the point\n .replace(/[\\x00-\\x20\\x7f]/g, '')\n .toLowerCase();\n}\n\nfunction isSafeLinkUrl(url: string): boolean {\n const normalized = normalizeUrl(url);\n return !HAS_SCHEME.test(normalized) || SAFE_LINK_SCHEME.test(normalized);\n}\n\nfunction isSafeImageUrl(url: string): boolean {\n const normalized = normalizeUrl(url);\n if (!HAS_SCHEME.test(normalized)) return true;\n return SAFE_IMAGE_SCHEME.test(normalized) || SAFE_IMAGE_DATA_URL.test(normalized);\n}\n\n// Placeholder delimiter for generated markup. NUL is stripped from the input\n// before any token is created, so a placeholder can never collide with user\n// text, and it survives the emphasis passes because none of them treat it as\n// a delimiter.\nconst TOKEN = '\\x00';\n// eslint-disable-next-line no-control-regex -- matching control characters is the point\nconst TOKEN_PATTERN = /\\x00(\\d+)\\x00/g;\n\nfunction inlineMarkdown(text: string): string {\n const tokens: string[] = [];\n // Park generated markup outside the emphasis passes below. Without this,\n // `_(.+?)_` eats the underscore in the generated target=\"_blank\" and any\n // underscore inside a URL.\n const token = (html: string) => {\n tokens.push(html);\n return `${TOKEN}${tokens.length - 1}${TOKEN}`;\n };\n const inert = (source: string) => token(`<span class=\"t-md-editor__blocked-link\">${source}</span>`);\n\n // eslint-disable-next-line no-control-regex -- matching control characters is the point\n text = text.replace(/\\x00/g, '');\n\n // Images:  — alt is a plain-text attribute, so the whole tag is parked.\n text = text.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, (match, alt: string, url: string) =>\n isSafeImageUrl(url)\n ? token(`<img src=\"${url}\" alt=\"${alt}\" class=\"t-md-editor__image\" loading=\"lazy\" />`)\n : inert(match),\n );\n // Links: [text](url) — only the tags are parked so the label still formats.\n text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (match, label: string, url: string) =>\n isSafeLinkUrl(url)\n ? `${token(`<a href=\"${url}\" target=\"_blank\" rel=\"noopener noreferrer\">`)}${label}${token('</a>')}`\n : inert(match),\n );\n // Bold: **text**\n text = text.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n // Italic: _text_ or *text*\n text = text.replace(/_(.+?)_/g, '<em>$1</em>');\n text = text.replace(/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, '<em>$1</em>');\n // Inline code: `text`\n text = text.replace(/`(.+?)`/g, '<code class=\"t-md-editor__inline-code\">$1</code>');\n // Strikethrough: ~~text~~\n text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');\n\n return text.replace(TOKEN_PATTERN, (_match, index: string) => tokens[Number(index)]);\n}\n\n/* ──────── Image paste / drop handling ──────── */\n\nconst handlePaste = async (event: ClipboardEvent) => {\n if (props.disabled) return;\n\n const items = event.clipboardData?.items;\n if (!items) return;\n\n for (const item of items) {\n if (item.type.startsWith('image/')) {\n event.preventDefault();\n const file = item.getAsFile();\n if (file) await processImageUpload(file);\n return;\n }\n }\n};\n\nconst handleDrop = async (event: DragEvent) => {\n if (props.disabled) return;\n\n const files = event.dataTransfer?.files;\n if (!files) return;\n\n for (const file of files) {\n if (file.type.startsWith('image/')) {\n event.preventDefault();\n await processImageUpload(file);\n }\n }\n};\n\nconst handleDragOver = (event: DragEvent) => {\n if (props.disabled) return;\n event.preventDefault();\n};\n\nconst processImageUpload = async (file: File) => {\n if (!props.uploadImage) {\n // Fallback: insert a placeholder with the file name\n insertAtCursor(`![${file.name}]()`);\n return;\n }\n\n // Insert uploading placeholder\n const placeholder = `![Uploading ${file.name}...]()`;\n insertAtCursor(placeholder);\n isUploading.value = true;\n\n try {\n const url = props.uploadImage(file);\n const resolvedUrl = url instanceof Promise ? await url : url;\n\n // Replace placeholder with actual URL\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, ``);\n emit('update:modelValue', newValue);\n emit('image-upload', file, resolvedUrl);\n } catch (err) {\n // Remove placeholder on error\n const currentValue = stringValue.value;\n const newValue = currentValue.replace(placeholder, '');\n emit('update:modelValue', newValue);\n emit('image-upload-error', file, err instanceof Error ? err : new Error(String(err)));\n } finally {\n isUploading.value = false;\n }\n};\n\n/* ──────── Keyboard shortcuts ──────── */\n\nconst handleKeydown = (event: KeyboardEvent) => {\n if (props.disabled) return;\n\n const mod = event.metaKey || event.ctrlKey;\n if (!mod) return;\n\n switch (event.key) {\n case 'b':\n event.preventDefault();\n insertBold();\n break;\n case 'i':\n event.preventDefault();\n insertItalic();\n break;\n case 'k':\n event.preventDefault();\n insertLink();\n break;\n case 'e':\n event.preventDefault();\n insertCode();\n break;\n }\n};\n\n</script>\n\n<template>\n <div\n v-bind=\"editorAttrs\"\n :class=\"rootClasses\"\n :style=\"rootStyle\"\n >\n <!-- Toolbar -->\n <div\n class=\"t-md-editor__toolbar\"\n role=\"toolbar\"\n aria-label=\"Markdown formatting\"\n >\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 1 (H1)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(1)\"\n >\n H1\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 2 (H2)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(2)\"\n >\n H2\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Heading 3 (H3)\"\n :disabled=\"disabled\"\n @click=\"insertHeading(3)\"\n >\n H3\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Bold (Ctrl+B)\"\n :disabled=\"disabled\"\n @click=\"insertBold\"\n >\n <strong>B</strong>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Italic (Ctrl+I)\"\n :disabled=\"disabled\"\n @click=\"insertItalic\"\n >\n <em>I</em>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Strikethrough\"\n :disabled=\"disabled\"\n @click=\"insertAtCursor('~~', '~~')\"\n >\n <del>S</del>\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Inline code (Ctrl+E)\"\n :disabled=\"disabled\"\n @click=\"insertCode\"\n >\n </>\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Code block\"\n :disabled=\"disabled\"\n @click=\"insertCodeBlock\"\n >\n { }\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Link (Ctrl+K)\"\n :disabled=\"disabled\"\n @click=\"insertLink\"\n >\n 🔗\n </button>\n </div>\n\n <div\n class=\"t-md-editor__toolbar-divider\"\n role=\"separator\"\n />\n\n <div class=\"t-md-editor__toolbar-group\">\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Unordered list\"\n :disabled=\"disabled\"\n @click=\"insertList\"\n >\n • List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Ordered list\"\n :disabled=\"disabled\"\n @click=\"insertOrderedList\"\n >\n 1. List\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Blockquote\"\n :disabled=\"disabled\"\n @click=\"insertQuote\"\n >\n >\n </button>\n <button\n type=\"button\"\n class=\"t-md-editor__toolbar-btn\"\n title=\"Horizontal rule\"\n :disabled=\"disabled\"\n @click=\"insertHr\"\n >\n ―\n </button>\n </div>\n\n <slot name=\"toolbar\" />\n </div>\n\n <!-- Editor body -->\n <div class=\"t-md-editor__body\">\n <!-- Write pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--write\"\n >\n <textarea\n ref=\"textareaRef\"\n class=\"t-md-editor__textarea\"\n :value=\"stringValue\"\n :placeholder=\"placeholder\"\n :disabled=\"disabled\"\n :aria-label=\"placeholder\"\n @input=\"onInput\"\n @paste=\"handlePaste\"\n @drop=\"handleDrop\"\n @dragover=\"handleDragOver\"\n @keydown=\"handleKeydown\"\n />\n </div>\n\n <!-- Preview pane -->\n <div\n class=\"t-md-editor__pane t-md-editor__pane--preview\"\n aria-label=\"Markdown preview\"\n >\n <!-- eslint-disable vue/no-v-html -->\n <div\n class=\"t-md-editor__preview\"\n v-html=\"renderedHtml\"\n />\n <!-- eslint-enable vue/no-v-html -->\n </div>\n </div>\n\n <!-- Upload indicator -->\n <div\n v-if=\"isUploading\"\n class=\"t-md-editor__upload-bar\"\n role=\"status\"\n aria-live=\"polite\"\n >\n Uploading image…\n </div>\n </div>\n</template>\n"],"mappings":""}
|