decaporg / decap-cms

A Git-based CMS for Static Site Generators
https://decapcms.org
MIT License
17.65k stars 1 forks source link

TypeError: Cannot read properties of null (reading 'href') #7224

Closed BrunoAFK closed 4 weeks ago

BrunoAFK commented 4 weeks ago

Describe the bug TypeError: Cannot read properties of null (reading 'href') at /admin/:170:44 at Array.map () at fromBlock (/admin/:168:87) at s. (https://unpkg.com/decap-cms@3.1.10/dist/decap-cms.js:477:54593) at s.tokenizeBlock (https://unpkg.com/decap-cms@3.1.10/dist/decap-cms.js:485:2582080) at e.exports [as parse] (https://unpkg.com/decap-cms@3.1.10/dist/decap-cms.js:485:2558013) at e.exports.w.parse (https://unpkg.com/decap-cms@3.1.10/dist/decap-cms.js:491:39920) at C (https://unpkg.com/decap-cms@3.1.10/dist/decap-cms.js:477:44350) at t.markdownToSlate (https://unpkg.com/decap-cms@3.1.10/dist/decap-cms.js:477:43534) at j (https://unpkg.com/decap-cms@3.1.10/dist/decap-cms.js:473:9761)

To Reproduce I don't know how to reproduce and what is triggering this.

Applicable Versions:

CMS configuration


backend:
  name: github
  repo: repo/site
  branch: main
  base_url: https://url.com
  auth_endpoint: /api/auth
media_folder: static/images
public_folder: /images
display_url: https://url.com
search: true
editor:
  preview: false
collections:
  - name: novosti
    label: Novosti
    folder: content/novosti
    create: true
    slug: "{{slug}}"
    summary: "{{title}} - {{date | date('DD.MM.YYYY')}} – {{body | truncate(30,
      '...')}}"
    filter:
      field: format
      value: post
    media_folder: /static/images/articles
    public_folder: /images/articles
    view_groups:
      - label: Year
        field: date
        pattern: \d{4}
        id: date__\d{4}
    fields:
      - label: Format
        name: format
        widget: hidden
        default: post
      - label: Naslov
        name: title
        hint: Naslov članka
        widget: string
      - label: Autor
        name: author
        widget: hidden
      - label: Gallery Mode
        name: fancybox
        widget: hidden
        default: "true"
      - label: Datum objave
        name: date
        widget: datetime
        format: YYYY-MM-DD
      - label: Opis
        name: description
        hint: Opis članka, bit će vidljiv na listi članaka
        widget: string
      - label: Slika članka
        name: thumbnail
        hint: Slika članka, bit će vidljiva na listi članaka. Dimenzije trebaju biti 500
          x 231 (idealno).
        widget: image
        default: /images/default/feature.jpg
      - label: Istaknuta slika članka
        name: featuredImage
        hint: Glavna slika članka, idealne dimenzije su 1280x853. Prikazuje se unutar
          članka.
        widget: image
        required: false
        default: /images/default/feature.jpg
      - label: Tagovi
        name: tags
        widget: hidden
        default:
          - novosti
        allow_add: true
      - label: Sadržaj
        name: body
        widget: markdown
    publish: true
    type: folder_based_collection
    sortable_fields:
      - commit_date
      - title
      - date
      - commit_author
      - description
    view_filters: []
    editor:
      preview: false
  - name: drustva
    label: Društva
    folder: content/drustva
    create: true
    slug: "{{slug}}"
    filter:
      field: format
      value: post
    fields:
      - label: Format
        name: format
        widget: hidden
        default: post
      - label: Naslov
        name: title
        hint: Naziv društva
        widget: string
      - label: Grb drutšva
        name: thumbnail
        hint: Grb društva, bit će vidljiva na listi društava. Dimenzije trebaju biti 500
          x 231 (idealno).
        widget: image
        default: /images/default/grb-vatrogasci.png
      - label: Istaknuta slika društva
        name: featuredImage
        hint: Glavna slika stranice društva, idealne dimenzije su 1280x853. Prikazuje se
          unutar stranice društva.
        widget: image
        required: false
      - label: Sadržaj
        name: body
        widget: markdown
    publish: true
    type: folder_based_collection
    sortable_fields:
      - commit_date
      - title
      - commit_author
    view_filters: []
    view_groups: []
    editor:
      preview: false
  - name: nabava
    label: Nabava
    folder: content/nabava
    create: true
    slug: "{{slug}}"
    summary: "{{title | upper}} - {{date | date('DD.MM.YYYY')}} – {{body |
      truncate(30, '...')}}"
    filter:
      field: format
      value: post
    view_groups:
      - label: Yea
...

index.html
...
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Content Manager</title>
  </head>
  <body>
    <script src="https://unpkg.com/decap-cms@3.1.10/dist/decap-cms.js"></script>
    <script>
      CMS.registerEditorComponent({
        id: "youtube",
        label: "Youtube",
        fields: [{name: 'id', label: 'Youtube Video ID'}],
        pattern: /^{{<\s?youtube (\S+)\s?>}}/,
        fromBlock: function(match) {
          return {
            id: match[1]
          };
        },
        toBlock: function(obj) {
          return `
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; height: auto;">
  <iframe src="https://www.youtube.com/embed/${obj.id}?rel=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
          `;
        },
        toPreview: function(obj) {
          return `
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; height: auto;">
  <iframe src="https://www.youtube.com/embed/${obj.id}?rel=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
          `;
        }
      });
      CMS.registerEditorComponent({
        id: "position-list",
        label: "Lista pozicija",
        fields: [
          {
            name: 'items',
            label: 'Items',
            widget: 'list',
            fields: [
              { name: 'positionName', label: 'Naziv Pozicije' }, // positionName for the position
              { name: 'personName', label: 'Ime i prezime' }    // personName for the person
            ]
          }
        ],
        pattern: /^<div class="object-list">([\s\S]*?)<\/div>$/,
        fromBlock: function(match) {
          const html = match[1];  // The captured group which is inner HTML of the object-list div
          const dummyDiv = document.createElement('div');
          dummyDiv.innerHTML = html;
          const items = Array.from(dummyDiv.querySelectorAll('.object-item')).map((div) => {
            return {
              positionName: div.querySelector('.object-position').innerText,
              personName: div.querySelector('.object-name').innerText
            };
          });
          return { items: items };
        },
        toBlock: function(obj) {
          const itemsHtml = obj.items.map(item =>
            `<div class="object-item"><div class="object-position">${item.positionName}</div><div class="object-name">${item.personName}</div></div>`
          ).join("");
          return `<div class="object-list">${itemsHtml}</div>`;
        }
      });
      CMS.registerEditorComponent({
        id: "contact-list",
        label: "Kontakti Lista",
        fields: [
          {
            name: 'contacts',
            label: 'Kontakti',
            widget: 'list',
            fields: [
              { label: 'Naziv', name: 'title', widget: 'string' },
              { label: 'Titula / Jedinica', name: 'subtitle', widget: 'string', required: false },
              { label: 'Adresa', name: 'address', widget: 'string', required: false },
              { label: 'OIB', name: 'oib', widget: 'number', hint: 'OIB 11 znamenki', pattern: ['^\\d{11}$', 'Mora biti točno 11 znamenki'], required: false },
              { label: 'MB', name: 'mb', widget: 'number', hint: 'MB ima 7 znamenki', pattern: ['^\\d{7}$', 'Mora biti točno 7 znamenki'], required: false },
              { label: 'IBAN', name: 'iban', widget: 'string', hint: 'Unesite IBAN', pattern: ['^[A-Z]{2}(\\s*\\d\\s*){19}$', 'Mora započeti s dva slova i sadržavati ukupno 19 znamenki'], required: false },
              { label: 'Telefon', name: 'phone', widget: 'string', required: false },
              { label: 'Email', name: 'email', widget: 'string', pattern: ['^[\\w.%+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$', 'Unesite važeću email adresu'], required: false },
              { label: 'Dodatna Informacija', name: 'extraInfo', widget: 'text', required: false }
            ]
          }
        ],
        pattern: /^<div class="contact-list">([\s\S]*?)<\/div>$/,
        fromBlock: function(match) {
          const html = match[1];
          const dummyDiv = document.createElement('div');
          dummyDiv.innerHTML = html;
          const contacts = Array.from(dummyDiv.querySelectorAll('.contact-card')).map((div) => {
            return {
              title: div.querySelector('.contact-title').innerText,
              subtitle: div.querySelector('.contact-subtitle') ? div.querySelector('.contact-subtitle').innerText : '',
              address: div.querySelector('.contact-address') ? div.querySelector('.contact-address').innerText.replace('Adresa: ', '') : '',
              oib: div.querySelector('.contact-oib') ? div.querySelector('.contact-oib').innerText.replace('OIB: ', '') : '',
              mb: div.querySelector('.contact-mb') ? div.querySelector('.contact-mb').innerText.replace('MB: ', '') : '',
              iban: div.querySelector('.contact-iban') ? div.querySelector('.contact-iban').innerText.replace('IBAN: ', '') : '',
              phone: div.querySelector('.contact-phone a') ? div.querySelector('.contact-phone a').innerText : '',
              email: div.querySelector('.contact-email a') ? div.querySelector('.contact-email a').innerText : '',
              extraInfo: div.querySelector('.contact-extra-info') ? div.querySelector('.contact-extra-info').innerText : ''
            };
          });
          return { contacts: contacts };
        },
        toBlock: function(obj) {
          const contactsHtml = obj.contacts.map(contact =>
`<div class="contact-card">
  <div class="title-area">
    <h2 class="contact-title">${contact.title}</h2>
    ${contact.subtitle ? `<p class="contact-subtitle">${contact.subtitle}</p>` : ''}
  </div>
  ${contact.address ? `<p class="contact-address"><span>Adresa: </span>${contact.address}</p>` : ''}
  ${contact.oib ? `<p class="contact-oib"><span>OIB: </span>${contact.oib}</p>` : ''}
  ${contact.mb ? `<p class="contact-mb"><span>MB: </span>${contact.mb}</p>` : ''}
  ${contact.iban ? `<p class="contact-iban"><span>IBAN: </span>${contact.iban}</p>` : ''}
  ${contact.phone ? `<p class="contact-phone"><span>Telefon: </span><a href="tel:${contact.phone}">${contact.phone}</a></p>` : ''}
  ${contact.email ? `<p class="contact-email"><span>Email: </span><a href="mailto:${contact.email}">${contact.email}</a></p>` : ''}
  ${contact.extraInfo ? `<p class="contact-extra-info">${contact.extraInfo}</p>` : ''}
</div>`
          ).join("");
          return `<div class="contact-list">${contactsHtml}</div>`;
        },
        toPreview: function(obj) {
          const contactsHtml = obj.contacts.map(contact =>
`<div class="contact-card">
  <div class="title-area">
    <h2 class="contact-title">${contact.title}</h2>
    ${contact.subtitle ? `<p class="contact-subtitle">${contact.subtitle}</p>` : ''}
  </div>
  ${contact.address ? `<p class="contact-address"><span>Adresa: </span>${contact.address}</p>` : ''}
  ${contact.oib ? `<p class="contact-oib"><span>OIB: </span>${contact.oib}</p>` : ''}
  ${contact.mb ? `<p class="contact-mb"><span>MB: </span>${contact.mb}</p>` : ''}
  ${contact.iban ? `<p class="contact-iban"><span>IBAN: </span>${contact.iban}</p>` : ''}
  ${contact.phone ? `<p class="contact-phone"><span>Telefon: </span><a href="tel:${contact.phone}">${contact.phone}</a></p>` : ''}
  ${contact.email ? `<p class="contact-email"><span>Email: </span><a href="mailto:${contact.email}">${contact.email}</a></p>` : ''}
  ${contact.extraInfo ? `<p class="contact-extra-info">${contact.extraInfo}</p>` : ''}
</div>`
          ).join("");
          return `<div class="contact-list">${contactsHtml}</div>`;
        }
      });
      CMS.registerEditorComponent({
        id: "gallery",
        label: "Galerija",
        fields: [
          { name: 'fancybox', label: 'Ime galerije', hint: "Naziv galerije, nije obavezan. Ako želite imati više galerija na stranici a da su odvojene prilikom pregleda slika, onda je poželjno staviti.", widget: 'string', required: false },
          {
            name: 'gallery',
            label: 'Galerija',
            widget: 'list',
            fields: [
              { name: 'image', label: 'Slika', widget: 'image' },
              { name: 'alt', label: 'Alt tekst', widget: 'string', required: false }
            ]
          }
        ],
        pattern: /^<div class="gallery">([\s\S]*?)<\/div>$/,
        fromBlock: function(match) {
          const html = match[1];
          const dummyDiv = document.createElement('div');
          dummyDiv.innerHTML = html;
          const galleryItems = Array.from(dummyDiv.querySelectorAll('.gallery-item')).map((div) => {
            return {
              image: div.querySelector('a').href,
              alt: div.querySelector('img').alt
            };
          });
          return { 
            fancybox: dummyDiv.querySelector('.gallery-item') ? dummyDiv.querySelector('.gallery-item').getAttribute('data-fancybox') : 'gallery',
            gallery: galleryItems 
          };
        },
        toBlock: function(obj) {
          const fancybox = obj.fancybox || 'gallery';
          const galleryHtml = obj.gallery.map(item =>
`<a href="${item.image}" data-fancybox="${fancybox}" data-caption="${item.alt}" class="gallery-item">
    <img src="${item.image}" alt="${item.alt}" class="rounded shadow-md mb-4" />
  </a>`
          ).join("");
          return `<div class="gallery">
                    ${galleryHtml}
                  </div>`;
        },
        toPreview: function(obj) {
          const fancybox = obj.fancybox || 'gallery';
          const galleryHtml = obj.gallery.map(item =>
`<a href="${item.image}" data-fancybox="${fancybox}" data-caption="${item.alt}" class="gallery-item">
    <img src="${item.image}" alt="${item.alt}" class="rounded shadow-md mb-4" />
  </a>`
          ).join("");
          return `<div class="gallery">
                    ${galleryHtml}
                  </div>`;
        }
      });

    </script>
  </body>
</html>
...
BrunoAFK commented 4 weeks ago

Issue is closed; I made a mistake when configuring fromBlock in the gallery.