Что происходит между использованием renderMarkdown и фактической записью в DOM в ReactWebChat?

Недавно мы перешли с WebChatV3 на V4. Мы постоянно работаем над переносом всех наших пользовательских функций на нового клиента. Одна из этих функций - проверка URL-адресов для определенного домена и установка целевого значения тега a на "_parent".

Чтобы реализовать это, мы добавили зависимость для markdown-it, поскольку элемент ReactWebChat может принимать ее в качестве аргумента, как описано здесь: Промежуточное ПО BotFramework-Webchat для renderMarkdown Вместо добавления средства визуализации эмодзи мы встроили в него правило и передал его в ReactWebChat в соответствии с примером, приведенным в ответе выше. Этот код выглядит так:

export const getConfiguredMarkdownIt = () => {
    const markdownIt = new MarkdownIt.default({ html: false, xhtmlOut: true, breaks: true, linkify: true, typographer: true });
    const defaultRender = markdownIt.renderer.rules.link_open || ((tokens, idx, options, env, self) => {
        return self.renderToken(tokens, idx, options);
    });

    markdownIt.renderer.rules.link_open = (tokens, idx, options, env, self) => {
        let href = '';
        const hrefIndex = tokens[idx].attrIndex('href');
        if (hrefIndex >= 0) {
            href = tokens[idx].attrs[hrefIndex][1];
        }
        const newTarget = Helper.getTargetForUrl(href);
        const targetIndex = tokens[idx].attrIndex('target');
        if (targetIndex < 0) {
            tokens[idx].attrPush(['target', newTarget]);
        } else {
            tokens[idx].attrs[targetIndex][1] = newTarget;
        }
        const relIndex = tokens[idx].attrIndex('rel');
        const rel = 'noopener noreferrer';
        if (relIndex < 0) {
            tokens[idx].attrPush(['rel', rel]);
        } else {
            tokens[idx].attrs[relIndex][1] = rel;
        }
        console.log(tokens[idx]);
        return defaultRender(tokens, idx, options, env, self);
    };
    return markdownIt;
}

Затем это используется для передачи в элемент ReactWebChat как таковой (для краткости многое опущено):

import { getConfiguredMarkdownIt } from './MarkdownSetup'
const md = getConfiguredMarkdownIt();
...
<ReactWebChat renderMarkdown={ md.render.bind(md) } />   

Первое сообщение, которое наш бот возвращает пользователю, отправляет URL-адрес, который должен быть нацелен на "_parent". Однако он постоянно отображается как "_blank", в то время как атрибут "rel" полностью устанавливается с помощью нашего специального метода. Для меня это подтверждает, что наше настраиваемое правило работает, но происходит что-то странное. Я отладил, что происходит, и обработанный HTML, включая атрибут "target", некоторое время сохраняет правильное значение, но в конечном итоге переключается на "_blank". Все последующие сообщения получают свою цель правильно, я заменил URL-адрес в открытии на один из них, чтобы увидеть, что произойдет, и результат тот же: "_blank".

Javascript на самом деле не является моим опытом, и мне трудно понять, что происходит, когда я просматриваю код в инструментах отладки Chrome. Но мне удалось соблюсти правильный HTML вплоть до card-elements.ts. Когда я добираюсь туда, в конце функции isBleedingAtBottom обнаруживаемый мной HTML-код внезапно содержит "_blank" в атрибуте "target". Я совершенно не понимаю, почему это происходит.

Это ошибка или что-то мне не хватает?

Версии:

"botframework-webchat": "^4.7.0",
"markdown-it": "8.3.1",

Вот (слегка измененный) JSON сообщения:

{
  "type": "message",
  "serviceUrl": "http://localhost:57714",
  "channelId": "emulator",
  "from": {
    "id": "63700ba0-e2ca-11ea-8243-4773a3b07af6",
    "name": "Bot",
    "role": "bot"
  },
  "conversation": {
    "id": "63727ca0-e2ca-11ea-b639-bf8d0ffe9da8|livechat"
  },
  "recipient": {
    "id": "3952a99d-87de-4b22-a1b3-04fd8c9f141b",
    "role": "user"
  },
  "locale": "en-US",
  "inputHint": "acceptingInput",
  "attachments": [
    {
      "contentType": "application/vnd.microsoft.card.hero",
      "content": {
        "text": "Go to [this page](REMOVED URL) bla bla blah. click start to continue",
        "buttons": [
          {
            "type": "imBack",
            "title": "Start",
            "value": "Start"
          }
        ]
      }
    }
  ],
  "entities": [],
  "replyToId": "654f52f0-e2ca-11ea-b639-bf8d0ffe9da8",
  "id": "688a0b90-e2ca-11ea-b639-bf8d0ffe9da8",
  "localTimestamp": "2020-08-20T11:49:15+02:00",
  "timestamp": "2020-08-20T09:49:15.336Z"
}

0 ответов

Поскольку веб-чат преобразует все карточки в адаптивные карточки, вам нужно будет решить эту проблему с помощью адаптивных карточек. Вы можете увидеть здесь, что адаптивные карты SDK, что веб - чат с помощью преобразует все якоря, чтобы "_blank" после того, как применяется уценка.

       let anchors = element.getElementsByTagName("a");

for (let i = 0; i < anchors.length; i++) {
    let anchor = <HTMLAnchorElement>anchors[i];
    anchor.classList.add(hostConfig.makeCssClassName("ac-anchor"));
    anchor.target = "_blank";
    anchor.onclick = (e) => {
        if (raiseAnchorClickedEvent(this, e.target as HTMLAnchorElement)) {
            e.preventDefault();
            e.cancelBubble = true;
        }
    }
}

Я думаю, у вас есть несколько вариантов, как заставить ссылку открываться с помощью "_parent".

Вариант 1. Перехватить событие клика

Вам нужно прочитать кое-что о том, как реализовать настраиваемые обработчики для вашего средства визуализации Adaptive Cards в веб -чате: BotFramework-WebChat - Adaptive Card

Первое, что нужно понять, это то, что веб-чат использует SDK Adaptive Cards JavaScript, доступный в виде пакета npm. Веб-чат в основном использует готовые к работе функции визуализации SDK, но он меняет одну важную вещь - способ обработки действий. Без настраиваемого обработчика действия отправки боту не будут отправляться.

         adaptiveCard.onExecuteAction = handleExecuteAction;

Вот как приложения должны использовать адаптивные карты. Хотя большая часть функций обрабатывается на стороне SDK, есть несколько вещей, которые приложение должно сделать, чтобы адаптивные карты работали для этого конкретного приложения. Хотя вы можете видеть, что веб-чат назначает функцию onExecuteAction "event" свойства конкретного экземпляра Adaptive Card, существует также статический аналог onExecuteAction к которому можно получить доступ так:

        AdaptiveCard.onExecuteAction = handleExecuteAction;

Использование статического события применит обработчик для всех адаптивных карточек, а не только для одной, но он будет переопределен любыми обработчиками, примененными к конкретным экземплярам. Причина, по которой я говорю вам это, заключается в том, что существует гораздо больше статических событий, и некоторые из них, в частности, будут полезны в вашей ситуации:

         static onAnchorClicked: (element: CardElement, anchor: HTMLAnchorElement) => boolean = null;
static onExecuteAction: (action: Action) => void = null;
static onElementVisibilityChanged: (element: CardElement) => void = null;
static onImageLoaded: (image: Image) => void = null;
static onInlineCardExpanded: (action: ShowCardAction, isExpanded: boolean) => void = null;
static onInputValueChanged: (input: Input) => void = null;
static onParseElement: (element: CardElement, json: any, errors?: Array<HostConfig.IValidationError>) => void = null;
static onParseAction: (element: Action, json: any, errors?: Array<HostConfig.IValidationError>) => void = null;
static onParseError: (error: HostConfig.IValidationError) => void = null;
static onProcessMarkdown: (text: string, result: IMarkdownProcessingResult) => void = null;

Вы, наверное, догадались, что мы хотим onAnchorClicked, и мы можем использовать это так:

       adaptiveCardsPackage.AdaptiveCard.onAnchorClicked = (element, anchor) => {
  console.log('anchor clicked', anchor);
  
  // Since it looks like you only want to use _parent for certain links
  // you can put that logic here
  window.open(anchor.href, '_parent', 'noreferrer');
  
  // Returning true will prevent the default behavior
  return true;
}

Вариант 2. Создайте собственный элемент

Если вы действительно хотите, чтобы тег привязки выглядел так, как вы хотите, при просмотре HTML, и вы не хотите открывать ссылку с помощью JavaScript, вам нужно будет создать свой собственный тип элемента, потому что мы можем видеть, что текстовые блоки и текст пробежки не позволяют вам делать то, что вы пытаетесь сделать. Если вы создаете свой собственный тип текстового элемента, например текстовый блок, вы можете переопределить internalRenderи примените Markdown как хотите, не меняя цель на _blank. Пожалуйста, обратитесь к документации для получения дополнительной информации об этой опции. Обратите внимание, что в этом случае вам нужно будет явно использовать адаптивную карту, чтобы использовать свой настраиваемый элемент, потому что веб-чат не будет знать, как поместить настраиваемый элемент в адаптивную карту, если вы дадите ему карту героя.

Другие вопросы по тегам