在 contenteditable div 中聚焦子元素

问题描述 投票:0回答:0

我有一个 Vue 3 组件,其中包含一个 contenteditable div,我正在使用 v-for 循环动态地向其添加子元素。我需要确定当前聚焦于哪个子元素,以便我可以在它之后添加一个新的子元素。添加新的子元素后,我需要将焦点设置到它上面。我怎样才能在 Vue 3 中完成这个?

我正在尝试这样做:

<template>
  <div
    id="editableContainer"
    contenteditable="true"
    class="editable-container"
    @keydown.enter="validateEnterKeyEvent($event)"
  >
    <div
      v-for="(item, index) in textEditorItems"
      :id="item.id"
      :key="item"
    >
      <ContentEditableChild
        :item="item"
        @update:model-value="(newValue) => updateContent(index, newValue)"
      />
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, nextTick, reactive, ref, watch } from "vue";
import ContentEditableChild from "@/components/contentEditable/ContentEditableChild.vue";

export default defineComponent({
  name: "DefaultContentEditable",
  components: { ContentEditableChild },
  setup() {
    const textEditorItems = reactive([
      {
        id: "slug-item0",
        type: "h1",
        content: ref("heading test"),
      },
    ]);

    // it will be the article slug later
    const idPrefix = ref("slug-item");

    const getItemId = (index: number) => {
      return `${idPrefix.value}${index}`;
    };

    watch(
      () => textEditorItems,
      (newValue, oldValue) => {
        if (newValue.length !== oldValue.length) {
          // update the IDs of the remaining elements
          for (let i = 0; i < newValue.length; i++) {
            const element = document.getElementById(getItemId(i));
            if (element) {
              element.id = getItemId(i);
            }
          }
        }
      }
    );

    function getFocusedElement() {
      const selection = window.getSelection();
      if (selection && selection.rangeCount > 0) {
        const range = selection.getRangeAt(0);
        const focused = range.commonAncestorContainer;

        let ancestor = focused;
        while (
          ancestor &&
          ancestor.parentElement &&
          ancestor.parentElement.isSameNode(focused.parentElement)
        ) {
          ancestor = ancestor.parentElement;
        }
        if (
          ancestor &&
          ancestor.parentElement &&
          ancestor.parentElement.tagName === "DIV"
        ) {
          return ancestor.parentElement;
        }
      }
      return null;
    }

    function validateEnterKeyEvent(event) {
      const currentItem = getFocusedElement();
      if (!event.shiftKey) {
        event.preventDefault();
        const currenItemContent = currentItem?.innerHTML.trim();
        if (currentItem && currenItemContent) {
          addNew(currentItem.id);
        }
      }
    }

    function addNew(currentItemId: string) {
      const currentItemIndex = textEditorItems.findIndex(
        (item) => item.id === currentItemId
      );
      creatNewParagraph(currentItemIndex, "");

      // make the newly added item focused and the previous not focused
      const newId = getItemId(currentItemIndex + 1);
      nextTick(() => {
        focusElement(newId);
      });
    }

    function focusElement(elementId: string) {
      const element = document.getElementById(elementId);
      const selection = window.getSelection();
      const range = document.createRange();
      if (element && selection) {
        range.setStart(element, 0);
        range.collapse(true);
        selection.removeAllRanges();
        selection.addRange(range);
      }
    }

    function creatNewParagraph(index: number, content: string) {
      // add a new item to the array after the current item
      textEditorItems.splice(index + 1, 0, {
        id: getItemId(index + 1),
        type: "p",
        content: content,
      });
    }

    function updateContent(index: number, newValue: string) {
      textEditorItems[index].content = newValue;
    }

    return {
      focusElement,
      getItemId,
      validateEnterKeyEvent,
      textEditorItems,
      updateContent,
    };
  },
});
</script>

<style scoped lang="scss">
.editable-container {
  outline: none;

  p {
    margin-top: 16px;
    background-color: #eee;
  }
}
* > {
}
</style>

<template>
  <component
    :is="item.type"
    ref="elementRef"
    class="fs16"
    :value="modelValue"
    tabindex="1"
    @input="$emit('update:modelValue', $event.target.innerHTML)"
  >
  </component>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from "vue";

export default defineComponent({
  name: "ContentEditableChild",
  props: {
    item: {
      type: Object,
      required: true,
    },
  },
  emits: ["update:modelValue"],
  setup(props) {
    const modelValue = ref(props.item.content);
    const elementRef = ref(null);

    onMounted(() => {
      if (elementRef.value) {
        elementRef.value.innerHTML = modelValue.value;
      }
    });

    return { modelValue, elementRef };
  },
});
</script>

<style scoped lang="scss">
* {
  outline: none;
  margin-top: 16px;
  background-color: #eee;
}
</style>

在我之前的代码中,我能够使用选择 API 来获取当前获得焦点的元素。但是,我不确定是否有更好的解决方案来实现这一目标。尽管我能够向 contenteditable div 添加一个新元素,但我无法使用 element.focus() 函数将焦点移动到新添加的元素。似乎这个函数在一个 contenteditable div 中不能正常工作。

typescript vuejs3 contenteditable selection-api
© www.soinside.com 2019 - 2024. All rights reserved.