Advanced Lookup does not work with the code on the mobile version

Hello everyone,
I’ve encountered an issue when using code together with the Advanced Lookup plugin. This problem only occurs in the mobile version of Kintone—everything works fine on desktop.
My code is designed to make a Text field behave like a Lookup, but still allow free text input without triggering the message:
"Click 'Lookup' to get the latest data."

desktop code

(function () {
  'use strict';

  const TABLES = [
    { selector: '.subtable-5525154', textClass: 'field-5531874', lookupClass: 'field-5531974' },
    { selector: '.subtable-5531890', textClass: 'field-5531876', lookupClass: 'field-5531877' },
    { selector: '.subtable-5531891', textClass: 'field-5531881', lookupClass: 'field-5531882' },
    { selector: '.subtable-5531892', textClass: 'field-5531886', lookupClass: 'field-5531887' }
  ];


  function addButtonsToSingleRow(row, textClass, lookupClass) {
    const textCellContent = row.querySelector(`.${textClass} .input-text-outer-cybozu`);
    const textInput = row.querySelector(`.${textClass} input[type="text"]`); // ใช้สำหรับหาครั้งแรก + ปุ่มค้นหา
    const lookupTriggerBtn = row.querySelector(`.${lookupClass} .input-lookup-gaia`);
    const lookupValueInput = row.querySelector(`.${lookupClass} input[type="text"]`); // ใช้สำหรับปุ่มค้นหา

    if (!textCellContent || !textInput || !lookupTriggerBtn || !lookupValueInput || textCellContent.querySelector('.custom-lookup-trigger')) {
      return;
    }


    const searchBtn = document.createElement('button');
    searchBtn.textContent = '🔍';
    searchBtn.className = 'custom-lookup-trigger kintoneplugin-button-normal';
    searchBtn.type = 'button';
    searchBtn.style.marginLeft = '5px';
    searchBtn.style.cursor = 'pointer';
    searchBtn.title = 'ค้นหา Lookup';

    searchBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      const currentLookupTrigger = row.querySelector(`.${lookupClass} .input-lookup-gaia`);
      if (!currentLookupTrigger) return;
      currentLookupTrigger.click();


      const currentLookupValueInput = row.querySelector(`.${lookupClass} input[type="text"]`);
      const currentTextInput = row.querySelector(`.${textClass} input[type="text"]`);
      if (!currentLookupValueInput || !currentTextInput) return;

      const observer = new MutationObserver((mutationsList, obs) => {
         const updatedLookupValueInput = row.querySelector(`.${lookupClass} input[type="text"]`);
         const updatedTextInput = row.querySelector(`.${textClass} input[type="text"]`);

         if (updatedLookupValueInput && updatedLookupValueInput.value && updatedTextInput) {
            updatedTextInput.value = updatedLookupValueInput.value;
            updatedTextInput.dispatchEvent(new Event('input', { bubbles: true }));
            updatedTextInput.dispatchEvent(new Event('change', { bubbles: true }));
            obs.disconnect();
         }
      });

      const lookupContainer = row.querySelector(`.${lookupClass}`);
      if (lookupContainer) {
        observer.observe(lookupContainer, {
          childList: true, subtree: true, attributes: true, attributeFilter: ['value']
        });
      }
    });


    const clearBtn = document.createElement('button');
    clearBtn.textContent = '❌';
    clearBtn.className = 'custom-clear-trigger kintoneplugin-button-normal';
    clearBtn.type = 'button';
    clearBtn.style.marginLeft = '5px';
    clearBtn.style.cursor = 'pointer';
    clearBtn.title = 'ล้างค่า';

    clearBtn.addEventListener('click', (e) => {
      e.stopPropagation();


      const kintoneClearSelector = `.${lookupClass} .input-clear-gaia`;
      const kintoneClearElement = row.querySelector(kintoneClearSelector);


      const currentTextInput = row.querySelector(`.${textClass} input[type="text"]`);


      if (kintoneClearElement && typeof kintoneClearElement.click === 'function') {
        // console.log('เจอ ปุ่ม Clear ของ Kintone, กำลังคลิก:', kintoneClearElement);
        try {

          kintoneClearElement.click();


          if (currentTextInput) {

            currentTextInput.value = '';
            currentTextInput.dispatchEvent(new Event('input', { bubbles: true }));
            currentTextInput.dispatchEvent(new Event('change', { bubbles: true }));
          } else {
             console.warn('หา Text Input ไม่เจอเพื่อเคลียร์ค่า ในแถว:', row);
          }

        } catch (err) {
          console.error('เกิดข้อผิดพลาดขณะคลิกปุ่ม Clear ของ Kintone:', err);

          if (currentTextInput) { currentTextInput.value = ''; /* ... dispatch events ... */ }
        }
      } else {
        console.warn('ไม่พบปุ่ม Clear ของ Kintone หรือคลิกไม่ได้ ด้วย selector:', kintoneClearSelector, 'ในแถว:', row);

        if (currentTextInput) {
          console.log('Kintone Clear ไม่เจอ แต่ยังเคลียร์ Text Input:', currentTextInput);
          currentTextInput.value = '';
          currentTextInput.dispatchEvent(new Event('input', { bubbles: true }));
          currentTextInput.dispatchEvent(new Event('change', { bubbles: true }));
        }
      }
    });


    textCellContent.appendChild(searchBtn);
    textCellContent.appendChild(clearBtn);
  }




  function addInitialButtons() {
    TABLES.forEach(({ selector, textClass, lookupClass }) => {
      const table = document.querySelector(selector);
      if (!table) return;
      const rows = table.querySelectorAll('tbody tr');
      rows.forEach((row) => {
        addButtonsToSingleRow(row, textClass, lookupClass);
      });
    });
  }



  function observeNewRows() {
    TABLES.forEach(({ selector, textClass, lookupClass }) => {
      const table = document.querySelector(selector);
      if (!table) return;

      const tableBody = table.querySelector('tbody');
      if (!tableBody) return;

      const observer = new MutationObserver((mutationsList) => {
        mutationsList.forEach((mutation) => {
          if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
            mutation.addedNodes.forEach((node) => {
              if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'TR') {
                const NEW_ROW_DELAY = 100;
                setTimeout(() => {
                  addButtonsToSingleRow(node, textClass, lookupClass);
                }, NEW_ROW_DELAY);
              }
            });
          }
        });
      });

      observer.observe(tableBody, { childList: true });
    });
  }


  const INIT_DELAY = 1500;


  kintone.events.on(['app.record.create.show', 'app.record.edit.show'], (event) => {
    setTimeout(() => {
      requestAnimationFrame(() => {
        addInitialButtons();
        observeNewRows();
      });
    }, INIT_DELAY);
    return event;
  });

})();

mobile code

(function () {
  'use strict';

  const MOBILE_TABLE_CONFIGS = [
    {
      TABLE_WRAPPER_SELECTOR: 'div.subtable-gaia.subtable-5525154',
      ROW_SELECTOR: 'div.subtable-row-gaia',
      TEXT_FIELD_CODE: '5531874',
      LOOKUP_FIELD_CODE: '5531974'
    },
    {
      TABLE_WRAPPER_SELECTOR: 'div.subtable-gaia.subtable-5531890',
      ROW_SELECTOR: 'div.subtable-row-gaia',
      TEXT_FIELD_CODE: '5531876',
      LOOKUP_FIELD_CODE: '5531877'
    },
    {
      TABLE_WRAPPER_SELECTOR: 'div.subtable-gaia.subtable-5531891',
      ROW_SELECTOR: 'div.subtable-row-gaia',
      TEXT_FIELD_CODE: '5531881',
      LOOKUP_FIELD_CODE: '5531882'
    },
    {
      TABLE_WRAPPER_SELECTOR: 'div.subtable-gaia.subtable-5531892',
      ROW_SELECTOR: 'div.subtable-row-gaia',
      TEXT_FIELD_CODE: '5531886',
      LOOKUP_FIELD_CODE: '5531887'
    }
  ];

  const LOG_PREFIX = '[📱MOBILE_LOOKUP_SCRIPT]';

  function addButtonsToRow(rowElement, textFC, lookupFC) {
    if (rowElement.classList.contains('custom-btn-added')) return;

    const textValueDiv = rowElement.querySelector(`.control-gaia.field-${textFC} .control-value-gaia.value-${textFC}`);
    const lookupValueDiv = rowElement.querySelector(`.control-gaia.field-${lookupFC} .control-value-gaia.value-${lookupFC}`);

    const textInput = textValueDiv?.querySelector('input, textarea');
    const lookupContainer = lookupValueDiv?.querySelector('.forms-lookup-gaia');
    const lookupInput = lookupContainer?.querySelector('input');
    const lookupBtn = lookupContainer?.querySelector('button.forms-lookup-lookup-gaia');
    const clearBtn = lookupContainer?.querySelector('button.forms-lookup-clear-gaia');

    if (!textInput || !lookupInput || !lookupBtn) return;

    const btnWrap = document.createElement('div');
    btnWrap.style.marginTop = '5px';
    btnWrap.style.display = 'flex';
    btnWrap.style.gap = '6px';

    const searchBtn = document.createElement('button');
    searchBtn.textContent = '🔍';
    searchBtn.type = 'button';
    searchBtn.style.fontSize = '1.2em';
    searchBtn.onclick = () => {
      console.log(LOG_PREFIX, '🔍 Clicked search');
      lookupBtn.click();

      const observer = new MutationObserver(() => {
        if (lookupInput.value && textInput.value !== lookupInput.value) {
          textInput.value = lookupInput.value;
          textInput.dispatchEvent(new Event('input', { bubbles: true }));
          textInput.dispatchEvent(new Event('change', { bubbles: true }));
          observer.disconnect();
        }
      });

      observer.observe(lookupValueDiv, { childList: true, subtree: true });

      setTimeout(() => {
        console.log(LOG_PREFIX, '⏱️ Observer timeout');
        observer.disconnect();
      }, 10000);
    };

    const clearBtnEl = document.createElement('button');
    clearBtnEl.textContent = '❌';
    clearBtnEl.type = 'button';
    clearBtnEl.style.fontSize = '1.2em';
    clearBtnEl.onclick = () => {
      console.log(LOG_PREFIX, '❌ Clicked clear');
      clearBtn?.click();
      textInput.value = '';
      textInput.dispatchEvent(new Event('input', { bubbles: true }));
      textInput.dispatchEvent(new Event('change', { bubbles: true }));
    };

    btnWrap.appendChild(searchBtn);
    btnWrap.appendChild(clearBtnEl);
    textValueDiv.appendChild(btnWrap);
    rowElement.classList.add('custom-btn-added');
  }

  function processAllTables() {
    MOBILE_TABLE_CONFIGS.forEach(cfg => {
      const wrapper = document.querySelector(cfg.TABLE_WRAPPER_SELECTOR);
      if (!wrapper) return;
      const rows = wrapper.querySelectorAll(cfg.ROW_SELECTOR);
      rows.forEach(row => addButtonsToRow(row, cfg.TEXT_FIELD_CODE, cfg.LOOKUP_FIELD_CODE));
    });
  }

  kintone.events.on(['mobile.app.record.create.show', 'mobile.app.record.edit.show'], event => {
    console.log(LOG_PREFIX, `📲 Mobile event triggered: ${event.type}`);
    setTimeout(() => {
      processAllTables();
    }, 800);
    return event;
  });
})();

1 Like

Hello @Master_Over_Sun

It is possible that the plugin is wrapping or modifying the DOM asynchronously, which makes the MutationObserver no longer reliable on mobile. Because of this, it may be more stable to use kintone.mobile.app.record.get() and set() to handle subtable values directly, as this approach works independently of the DOM and remains stable even when plugins are active.

1 Like