...

Динамика на форме при помощи MutationObserver

Тема в разделе "Примеры сценариев", создана пользователем kirillovykh, 28 май 2024.

Метки:
  1. kirillovykh

    kirillovykh Участник

    Бизнес кейс
    При завершении Активности CRM всплывающее сообщение о необходимости создать новую

    Постановка
    При нажатии на кнопку сделано Активности CRM отображать модальное окно с уведомлением «Необходимо создать следующее Действие». Отображение модального окна выполняется после закрытия крайней задачи.

    Решение
    Данное решение актуально для версии 2023.8.5. На более поздних версиях работоспособность не гарантирована, т.к. вёрстка виджета Связанные задачи и логика формирования может измениться.

    Основная задача сводится к отслеживанию изменений в DOM дереве. Для отслеживания изменений используется MutationObserver. Данная тема частично разбиралась и обсуждалась:
    https://community.elma365.com/ru/threads/2192/
    https://community.elma365.com/ru/threads/2868/

    Цель отслеживания изменений – получить всплывающее окно выполнения задачи и добавить обработку нажатия на кнопку Сделано с проверкой валидного комментария и запуском сценария по отображению уведомления.

    На форму просмотра приложения добавлена вкладка Действия, на которую вынесен виджет Связанные задачи и добавлен виджет Модальное окно с уведомлением.
    При переходе на данную вкладку запускается сценарий с отслеживанием изменений.
    Пример отоборажения уведомления после закрытия крайней задачи.
    [​IMG]

    Стоит отметить, что выполняется отслеживание изменений всего поддерева (блок со связанными задачами). На это несколько причин:
    • при изменениях в виджете Связанные задачи (закрытие задачи, изменение и т.п.) пересобирается вся вёрстка с задачами
    • чтобы не запускать несколько наблюдений при наличии нескольких активных задач

    Листинг
    Код:
    
    /**
    * При нажатии на кнопку ОК Модального окна с напоминанием
    */
    async function onButtonClickOk(): Promise<void> {
        
    ViewContext.data.show_reminder_modal_body false;
    }

    /**
    * Событие после отображения вкладки Действия
    */
    async function onRenderActionTab(): Promise<void> {
        
    // Селектор на активную вкладку
        
    let selector_string 'elma-tabset .nav-item .active';

        
    // Проверка активной вкладки. Для вкладки "Действия" запуск обработки связанных задач
        
    const active_tab document.querySelector(selector_string);

        if (!
    active_tab || active_tab.innerText !== 'Действия') {
            return;
        }

        
    // Селектор на блок со связанными задачами
        
    selector_string 'div.linked-tasks__items';

        
    // Поиск блока со связанными задачами
        // Поиск выполняется на головной div, т.к. в момент загрузки в DOM данные только об linked-tasks__items
        
    const linked_tasks_div document.querySelector(selector_string);

        if (!
    linked_tasks_div) {
            return;
        }

        
    // Options for the observer (which mutations to observe)
        
    const config = { childListtruesubtreetrue };

        
    // Callback function to execute when mutations are observed
        
    const callback = (mutationListany[], observerany) => {
            for (const 
    mutation of mutationList) {
                
    // Обработка только добавленных элементов
                
    if (!mutation.addedNodes.length) {
                    continue;
                }

                const 
    added_node mutation.addedNodes[0];

                if (
    added_node?.className !== 'task-actions ng-star-inserted') {
                    continue;
                }

                
    // Если в мутации добавлен виджет с действиями по активности, то добавить listener на нажатие кнопки Сделано (call)
                
    const call_action searchTree(added_node'action-button ready btn btn-primary');

                if (!
    call_action) {
                    continue;
                }

                
    call_action.addEventListener('click', function () {
                    
    // При наличии активного popover с кнопкой Сделано добавить listener на нажатие кнопки
                    
    const done_button document.querySelector("body > div.popover-outer > div.popover.popover_is-visible elma-form button");

                    if (!
    done_button) {
                        return;
                    }

                    
    done_button.addEventListener('click', function () {
                        
    // При наличии валидного комментария в popover отображение модального окна с напоминанием
                        
    const valid_comment document.querySelector("body > div.popover-outer.visible elma-form elma-form-control > textarea.ng-valid");

                        if (!
    valid_comment) {
                            return;
                        }

                        
    showReminderModalBody();
                    });

                });
            }
        };

        
    // Create an observer instance linked to the callback function
        
    const observer = new MutationObserver(callback);

        
    // Start observing the target node for configured mutations
        
    observer.observe(linked_tasks_divconfig);
    }

    /**
    * Отображение модального окна с напоминанием
    */
    async function showReminderModalBody() {
        
    // Отображение модального окна выполняется после скрытия popover с подтверждением
        
    const done_button document.querySelector("body > div.popover-outer.visible elma-form button");

        if (
    done_button) {
            
    window.setTimeout(showReminderModalBody300);

            return;
        }

        
    // Отображение только после закрытия крайней активной задачи
        
    const current_tasks_count await System.processes._searchTasks()
            .
    where((fanyg) => g.and(
                
    f.__item.eq(Context),
                
    f.state.in([ProcessTaskState.assignmentProcessTaskState.inProgress])
            ))
            .
    count();

        if (
    current_tasks_count != 0) {
            return;
        }

        
    ViewContext.data.show_reminder_modal_body true;
    }

    /**
    * Поиск элемента в дереве
    * @param element Элемент, по которому выполняется поиск
    * @param matchingTitle Наименование класса искомого элемента
    * @returns Найденный элемент, либо null
    */
    function searchTree(elementanymatchingTitlestring): any {
        if (
    element.className == matchingTitle) {
            return 
    element;
        } else if (
    element.children != null) {
            
    let result null;

            for (
    let i 0result == null && element.children.lengthi++) {
                
    result searchTree(element.children[i], matchingTitle);
            }

            return 
    result;
        }

        return 
    null;
    }

    Может возникнуть желание не выполнять запрос на поиск задач, а сразу анализировать вёрстку виджета с задачами, т.к. в данных вёрстки возможно подсчитать количество активных задач и при отслеживании мутаций актуализировать это значение. Тут нюанс в том, что задач по элементу приложения может быть много и будет добавляться сворачивание. Соответственно в мутациях будет только информация об отображаемых задачах, что не позволит дать определённый ответ о закрытии крайней задачи по элементу.

    Касаемо отключения отслеживания изменений. После удаления наблюдаемого объекта из DOM, а затем освобождения сборщиком мусора браузера, MutationObserver прекращает отслеживание элемента. Однако сам MutationObserver может продолжать существовать, отслеживая другие элементы (так понимаю, если было несколько вызовов observe() для различных элементов DOM).
    Если отслеживается поддерево элементов целевого элемента и часть этого поддерева отделяется (detached) и перемещается в другое место в DOM, то отслеживание отделенной части поддерева продолжается.
    https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/disconnect
    https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe

    В данном случае, отделение части поддерева возможно не учитывать, т.к. поддерево пересобирается при каждом изменении, и оставить отключение отслеживания на MutationObserver. Конечно, при быстром переключении с вкладки на вкладку есть вероятность, что MutationObserver будет множиться, т.к. сборщик мусора не будет успевать освобождать удалённые элементы. Но событие быстрого переключения вкладок было принято считать маловероятным, и возможные изменения отслеживаемых элементов, ожидающих очистки, также не учитываются.

    Дополнительно возможно при переходе с вкладки отключать отслеживание.

    Стоило сразу сказать, что данный подход не гарант надёжности. Как минимум, может просто измениться вёрстка и решение не будет корректно работать. Что в данном случае и произошло, потребовалось внесение изменений в обработку.