motla / vue-document-editor

:page_facing_up: Paper-sized WYSIWYG document editor for Vue apps
https://motla.github.io/vue-document-editor/
MIT License
313 stars 70 forks source link

dynamyc editable fields #34

Closed ingmontoya closed 1 year ago

ingmontoya commented 1 year ago

Is your feature request related to a problem? Please describe. No

Describe the solution you'd like in there a way to add dynamic editable fields? I'm wondering how I can add them to the editor so the user can add dynamic fields on demand.

Additional context Working on a contract template creator, but I want the user be able to create templates and add dynamic fields according to his needs.

motla commented 1 year ago

Hi @ingmontoya, yes it should be possible but it needs you to code it from the ground, as nothing like it is built-in. what do you have in mind? like the user clicks on a menu element then drags on the page to define an editable zone? should the zone be resizable? should it snap to a grid?

ingmontoya commented 1 year ago

Hey @motla, thanks for your quick response!! Sounds good to have some sort of icon or menu item to define an editable zone, shouldn't be fixed, I'm intending to use it for full names, addresses, or email fields...

ariana-nemes commented 1 year ago

Im looking for the same thing! Anyone found a way?

ariana-nemes commented 1 year ago

Im getting closer but how can I change the caret position? Thanks a lot!

motla commented 1 year ago

@ingmontoya

Sounds good to have some sort of icon or menu item to define an editable zone, shouldn't be fixed, I'm intending to use it for full names, addresses, or email fields...

Ok I see. You will have to fiddle a lot with HTML, CSS and JavaScript. In terms of page content management, most of it is managed by the web browser, so we are quite limited by HTML capabilities. So for instance if you want to set an element that can be moved inside the page, and the page content goes automatically around it, I think this is not easily feasible.

If you just want a field that stays "over" the page content, what you could do is to insert an HTML element (let's say a simple \<div> for instance) with CSS position:absolute, either directly to the page HTML content data (you can access it directly inside your Vue.js app), or at the current caret position with document.execCommand("insertHTML", false, "<div ...). To have drag&drop and resize capabilities you can change its CSS properties dynamically (left, top, width, height) by setting JavaScript mouse events on the \<div> itself and/or on a "resize handle" set inside the \<div>. You will have to limit these values so the user can't move the \<div> outside the page. However I'm pretty sure absolute-positioned elements will not work well with the page split system of this library. It has not been designed for that and that would be quite a challenge...

@ariana-nemes

Im getting closer but how can I change the caret position? Thanks a lot!

Not trivial but apparently you can do it: https://stackoverflow.com/questions/6249095/how-to-set-the-caret-cursor-position-in-a-contenteditable-element-div

ariana-nemes commented 1 year ago

Thanks @motla!! I am trying to use document.execCommand("insertHTML", false, "<div ...) to insert some custom block elements.. But somehow it simply does not add the div that i want in the content. Is there a possibility to assist me further?

motla commented 1 year ago

@ariana-nemes can you share me your code or your project?

ariana-nemes commented 1 year ago

No, because of some internal rules :(. But i can share some snippets with you!

 async addTag(tag) {
      document.execCommand(
        'insertHTML',
        false,
        `<span contenteditable="false" class="template-document__tag" data-custom-field-type=${tag.value}><span contenteditable="false">${tag.name}</span></span>`,
      );
    },`

This function happens when i click on of the buttons attached in the screenshot.

Screenshot 2023-08-17 at 21 27 31

motla commented 1 year ago

@ariana-nemes is there an error on the web-browser console? did you try putting a console.log("test"); inside your addTag function to make sure it is executed when you click on your button?

ariana-nemes commented 1 year ago

No errors on the console. And yes, i'm 100% that it reaches that function.

<template>
  <div class="main">
    <!-- Top bar -->
    <vue-file-toolbar-menu :content="menu" class="bar" />

    <!-- Custom tags -->
    <div class="template-document__tag-container">
      <span v-for="(tag, index) in tags" :key="index">
        <span
          v-if="
            (tag.type.includes(templateType) && parentTemplateType != 'EV' && parentTemplateType != 'OL') ||
            tag.type.includes('ALL') ||
            tag.type.includes(parentTemplateType)
          "
        >
          <Tag
            class="mr-2 template-document__tag-list-element"
            :value="tag.name"
            v-tooltip.top="`${locale.documents.this_field} ${tag.name.replace()}`"
            @click="addTag(tag)"
          ></Tag>
        </span>
      </span>
    </div>

    <!-- Document editor -->
    <vue-document-editor
      class="editor"
      id="document-editor"
      ref="editor"
      v-model:content="content"
      :overlay="overlay"
      :zoom="zoom"
      :page_format_mm="page_format_mm"
      :page_margins="page_margins"
      :display="display"
    />
  </div>
</template>
motla commented 1 year ago

@ariana-nemes I think you missed the " " around ${tag.value} (it should be data-custom-field-type="${tag.value}") and make sure tag.value / tag.name do not contain special characters or HTML. I saw from the spec the HTML content must be valid for insertHTML to work, maybe this is the issue.

ariana-nemes commented 1 year ago

Thank but sadly this didn't fix the issue :(

motla commented 1 year ago

@ariana-nemes also could you console.log(tag.name) to make sure it is not empty in your function?

ariana-nemes commented 1 year ago

Yea. I think i found the issue.. Somehow the contenteditable is getting out of focus when im pressing one of my divs.

ariana-nemes commented 1 year ago

@motla do you have any idea how to refocus it?

motla commented 1 year ago

@ariana-nemes Well I'm not sure what's going on... Here is an example that works:

index.html
<html>
<head>
  <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue-document-editor@2/dist/VueDocumentEditor.umd.min.js"></script>
  <link href="https://cdn.jsdelivr.net/npm/vue-document-editor@2/dist/VueDocumentEditor.css" rel="stylesheet">
</head>
<body>
  <div id="app">
    <div>
      <input v-for="(tag, index) in tags" :key="index" type="button" :value="tag.name" @click="addTag(tag)" />
    </div>
    <div style="font-family: Avenir, sans-serif">
      <vue-document-editor v-model:content="content" />
    </div>
  </div>
  <script>
  const app = Vue.createApp({
    components: { VueDocumentEditor },
    data () {
      return {
        tags: [{ name: "Insert hello!", value: "hello!" }, { name: "Insert world!", value: "world!" }],
        content: ["<h1>Hello!</h1>Fill this page with text and new pages will be created as it overflows."]
      }
    },
    methods: {
      async addTag(tag) {
        document.execCommand('insertHTML', false, `<span>${tag.value}</span>`);
      }
    }
  }).mount('#app');
  </script>
</body>
</html>

From there you can get to your example step by step (or simplify your example step by step to get there) to understand what is wrong. I also noticed that setting <span contenteditable="false">${tag.value}</span> inputs the value twice on Google Chrome, but not on Firefox and Safari... document.execCommand() is not standard so we have to deal with it unfortunately... It's weird territory. Good luck!

motla commented 1 year ago

@ariana-nemes OK I found your issue, you were right about the fact that it unfocuses when you click on your button.

That is because inside your Tag component, they use an element which is neither a <button> or <input type="button">, but likely a stylished <div>.

In this case the behavior is that when the web browser detects a mousedown event on any element, it unfocuses.

The mousedown event is always triggered before the click event (which is a combination of mousedown-mouseup).

A workaround is then to trigger your function using the @mousedown event instead of the @click one. So it will stay focused when document.execCommand is called. Then to prevent the unfocus you can call event.preventDefault() inside your function. By security, to prevent elements below your button (if any) to trigger a mousedown event you can call event.stopPropagation()

Example here:

index.html
<html>
<head>
  <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue-document-editor@2/dist/VueDocumentEditor.umd.min.js"></script>
  <link href="https://cdn.jsdelivr.net/npm/vue-document-editor@2/dist/VueDocumentEditor.css" rel="stylesheet">
  <style>
    .custom-btn {
      font-family: sans-serif;
      padding: 5px;
      margin: 5px;
      border: solid 1px black;
      border-radius: 5px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div id="app">
    <div>
      <span v-for="(tag, index) in tags" :key="index" type="button" @mousedown="addTag(tag)" class="custom-btn">{{tag.name}}</span>
    </div>
    <div style="font-family: Avenir, sans-serif">
      <vue-document-editor v-model:content="content" />
    </div>
  </div>
  <script>
  const app = Vue.createApp({
    components: { VueDocumentEditor },
    data () {
      return {
        tags: [{ name: "Insert hello!", value: "hello!" }, { name: "Insert world!", value: "world!" }],
        content: ["<h1>Hello!</h1>Fill this page with text and new pages will be created as it overflows."]
      }
    },
    methods: {
      async addTag(tag) {
        event.preventDefault();
        event.stopPropagation();
        document.execCommand('insertHTML', false, `<span>${tag.value}</span>`);
      }
    }
  }).mount('#app');
  </script>
</body>
</html>

I hope that it helps. I have to get back to my work now 😊 Good luck!