mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-04 04:57:51 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			205 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			205 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<script setup>
 | 
						|
import { computed, ref, onMounted, nextTick, watch } from 'vue';
 | 
						|
import {
 | 
						|
  appendSignature,
 | 
						|
  removeSignature,
 | 
						|
  extractTextFromMarkdown,
 | 
						|
} from 'dashboard/helper/editorHelper';
 | 
						|
 | 
						|
const props = defineProps({
 | 
						|
  modelValue: { type: String, default: '' },
 | 
						|
  label: { type: String, default: '' },
 | 
						|
  placeholder: { type: String, default: '' },
 | 
						|
  maxLength: { type: Number, default: 200 },
 | 
						|
  id: { type: String, default: '' },
 | 
						|
  disabled: { type: Boolean, default: false },
 | 
						|
  customTextAreaClass: { type: String, default: '' },
 | 
						|
  customTextAreaWrapperClass: { type: String, default: '' },
 | 
						|
  showCharacterCount: { type: Boolean, default: false },
 | 
						|
  autoHeight: { type: Boolean, default: false },
 | 
						|
  resize: { type: Boolean, default: false },
 | 
						|
  minHeight: { type: String, default: '4rem' },
 | 
						|
  maxHeight: { type: String, default: '12rem' },
 | 
						|
  autofocus: { type: Boolean, default: false },
 | 
						|
  message: { type: String, default: '' },
 | 
						|
  messageType: {
 | 
						|
    type: String,
 | 
						|
    default: 'info',
 | 
						|
    validator: value => ['info', 'error', 'success'].includes(value),
 | 
						|
  },
 | 
						|
  signature: { type: String, default: '' },
 | 
						|
  sendWithSignature: { type: Boolean, default: false }, // add this as a prop, so that we won't have to add useUISettings
 | 
						|
  allowSignature: { type: Boolean, default: false }, // allowSignature is a kill switch, ensuring no signature methods are triggered except when this flag is true
 | 
						|
});
 | 
						|
 | 
						|
const emit = defineEmits(['update:modelValue']);
 | 
						|
 | 
						|
const textareaRef = ref(null);
 | 
						|
const isFocused = ref(false);
 | 
						|
 | 
						|
const characterCount = computed(() => props.modelValue.length);
 | 
						|
const cleanedSignature = computed(() =>
 | 
						|
  extractTextFromMarkdown(props.signature)
 | 
						|
);
 | 
						|
 | 
						|
const messageClass = computed(() => {
 | 
						|
  switch (props.messageType) {
 | 
						|
    case 'error':
 | 
						|
      return 'text-n-ruby-9 dark:text-n-ruby-9';
 | 
						|
    case 'success':
 | 
						|
      return 'text-n-teal-10 dark:text-n-teal-10';
 | 
						|
    default:
 | 
						|
      return 'text-n-slate-11 dark:text-n-slate-11';
 | 
						|
  }
 | 
						|
});
 | 
						|
 | 
						|
// TODO - use "field-sizing: content" and "height: auto" in future for auto height, when available.
 | 
						|
const adjustHeight = () => {
 | 
						|
  if (!props.autoHeight || !textareaRef.value) return;
 | 
						|
 | 
						|
  // Reset height to auto to get the correct scrollHeight
 | 
						|
  textareaRef.value.style.height = 'auto';
 | 
						|
  // Set the height to the scrollHeight
 | 
						|
  textareaRef.value.style.height = `${textareaRef.value.scrollHeight}px`;
 | 
						|
};
 | 
						|
 | 
						|
const setCursor = () => {
 | 
						|
  if (!textareaRef.value) return;
 | 
						|
 | 
						|
  const bodyWithoutSignature = removeSignature(
 | 
						|
    props.modelValue,
 | 
						|
    cleanedSignature.value
 | 
						|
  );
 | 
						|
  const bodyEndsAt = bodyWithoutSignature.trimEnd().length;
 | 
						|
 | 
						|
  textareaRef.value.focus();
 | 
						|
  textareaRef.value.setSelectionRange(bodyEndsAt, bodyEndsAt);
 | 
						|
};
 | 
						|
 | 
						|
const toggleSignatureInEditor = signatureEnabled => {
 | 
						|
  if (!props.allowSignature) return;
 | 
						|
  const valueWithSignature = signatureEnabled
 | 
						|
    ? appendSignature(props.modelValue, cleanedSignature.value)
 | 
						|
    : removeSignature(props.modelValue, cleanedSignature.value);
 | 
						|
  emit('update:modelValue', valueWithSignature);
 | 
						|
 | 
						|
  nextTick(() => {
 | 
						|
    adjustHeight();
 | 
						|
    setCursor();
 | 
						|
  });
 | 
						|
};
 | 
						|
 | 
						|
const handleInput = event => {
 | 
						|
  emit('update:modelValue', event.target.value);
 | 
						|
  if (props.autoHeight) {
 | 
						|
    nextTick(adjustHeight);
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
const handleFocus = () => {
 | 
						|
  if (!props.disabled) {
 | 
						|
    isFocused.value = true;
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
const handleBlur = () => {
 | 
						|
  if (!props.disabled) {
 | 
						|
    isFocused.value = false;
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
// Watch for changes in modelValue to adjust height
 | 
						|
watch(
 | 
						|
  () => props.modelValue,
 | 
						|
  () => {
 | 
						|
    if (props.autoHeight) {
 | 
						|
      nextTick(adjustHeight);
 | 
						|
    }
 | 
						|
  }
 | 
						|
);
 | 
						|
 | 
						|
watch(
 | 
						|
  () => props.sendWithSignature,
 | 
						|
  newValue => {
 | 
						|
    if (props.allowSignature) toggleSignatureInEditor(newValue);
 | 
						|
  }
 | 
						|
);
 | 
						|
 | 
						|
onMounted(() => {
 | 
						|
  if (props.autoHeight) {
 | 
						|
    nextTick(adjustHeight);
 | 
						|
  }
 | 
						|
 | 
						|
  if (props.autofocus) {
 | 
						|
    textareaRef.value?.focus();
 | 
						|
  }
 | 
						|
});
 | 
						|
</script>
 | 
						|
 | 
						|
<template>
 | 
						|
  <div class="relative flex flex-col gap-1">
 | 
						|
    <label
 | 
						|
      v-if="label"
 | 
						|
      :for="id"
 | 
						|
      class="mb-0.5 text-sm font-medium text-n-slate-12"
 | 
						|
    >
 | 
						|
      {{ label }}
 | 
						|
    </label>
 | 
						|
    <div
 | 
						|
      class="flex flex-col gap-2 px-3 pt-3 pb-3 transition-all duration-500 ease-in-out border rounded-lg bg-n-alpha-black2"
 | 
						|
      :class="[
 | 
						|
        customTextAreaWrapperClass,
 | 
						|
        {
 | 
						|
          'cursor-not-allowed opacity-50 !bg-n-alpha-black2 disabled:border-n-weak dark:disabled:border-n-weak':
 | 
						|
            disabled,
 | 
						|
          'border-n-brand dark:border-n-brand': isFocused,
 | 
						|
          'hover:border-n-slate-6 dark:hover:border-n-slate-6 border-n-weak dark:border-n-weak':
 | 
						|
            !isFocused && messageType !== 'error',
 | 
						|
          'border-n-ruby-8 dark:border-n-ruby-8 hover:border-n-ruby-9 dark:hover:border-n-ruby-9':
 | 
						|
            messageType === 'error' && !isFocused,
 | 
						|
        },
 | 
						|
      ]"
 | 
						|
    >
 | 
						|
      <slot /><!-- Slot for adding popover -->
 | 
						|
      <textarea
 | 
						|
        :id="id"
 | 
						|
        ref="textareaRef"
 | 
						|
        :value="modelValue"
 | 
						|
        :placeholder="placeholder"
 | 
						|
        :maxlength="showCharacterCount ? maxLength : undefined"
 | 
						|
        :class="[
 | 
						|
          customTextAreaClass,
 | 
						|
          {
 | 
						|
            'resize-none': !resize,
 | 
						|
          },
 | 
						|
        ]"
 | 
						|
        :style="{
 | 
						|
          minHeight: autoHeight ? minHeight : undefined,
 | 
						|
          maxHeight: autoHeight ? maxHeight : undefined,
 | 
						|
        }"
 | 
						|
        :disabled="disabled"
 | 
						|
        rows="1"
 | 
						|
        class="flex w-full reset-base text-sm p-0 !rounded-none !bg-transparent dark:!bg-transparent !border-0 !outline-0 !mb-0 placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 text-n-slate-12 dark:text-n-slate-12 disabled:cursor-not-allowed disabled:opacity-50"
 | 
						|
        @input="handleInput"
 | 
						|
        @focus="handleFocus"
 | 
						|
        @blur="handleBlur"
 | 
						|
      />
 | 
						|
      <div
 | 
						|
        v-if="showCharacterCount"
 | 
						|
        class="flex items-center justify-end h-4 mt-1 bottom-3 ltr:right-3 rtl:left-3"
 | 
						|
      >
 | 
						|
        <span class="text-xs tabular-nums text-n-slate-10">
 | 
						|
          {{ characterCount }} / {{ maxLength }}
 | 
						|
        </span>
 | 
						|
      </div>
 | 
						|
    </div>
 | 
						|
    <p
 | 
						|
      v-if="message"
 | 
						|
      class="min-w-0 mt-1 mb-0 text-xs truncate transition-all duration-500 ease-in-out"
 | 
						|
      :class="messageClass"
 | 
						|
    >
 | 
						|
      {{ message }}
 | 
						|
    </p>
 | 
						|
  </div>
 | 
						|
</template>
 |