Предотвращение одновременной прокрутки браузера и flash-контента, «расширенный курс»

в 17:12, , рубрики: Action Script, actionscript 3.0, javascript, метки: ,

Совсем не думал, что решая такой простой (как мне казалось) вопрос, придется здорово потрудиться и пораскинуть мозгами. Но в итоге получилась полноценная библиотека, которая адекватно работает со всеми видами flash — плагинов на странице (включая Pepper flash в Chrome, о котором позже).

Но обо всем по порядку.

Проблема одновременной прокрутки флеш контента и браузера существует давно, поэтому я был уверен, что есть готовое решение «из коробки».
Немного погуглив получаем желаемый результат — MouseWheelTrap. Библиотека отслеживает положение курсора, и если он находится над flash-контентом, отключает прокрутку в браузере, оставляя flash право единолично использованть ролик мыши. Скачиваем, подключаем, все работает.
Просто?
Отнюдь.
Первым неработоспособность блокировки заметил коллега — спросил, почему не работает скролл мышкой. Решил проверить на других машинах — проблема оказалась серьезней чем я думал сначала — на большинстве скролл так же не работал, причем только в Chrome.
Возникло два вопроса:

  1. Почему не работает готовая библиотека?
  2. Почему только в Chrome?

Chrome в этом плане здорово переплюнул IE8, поскольку IE с задачей справлялся на ура

Виной всему оказался плагин Pepper flash, который включается в сборку браузера Chrome начиная с версии 11.3. У Pepper flash своя API и «повышенная безопасность». Видимо как раз эта «безопасность» и стала корнем проблемы. Продолжив поиски решения, нашел большое количество упоминаний словосочетаний «Buggy pepper flash» и прочих не очень лестных замечаний в адрес этого флеш плагина. И очень удивило, что на Stackoverflow не было ни одного полноценного ответа по этой теме:
вопрос 1
вопрос 2

Это меня здорово вдохновило на поиск и написание собственного решения.

Первым что пришло в голову было следующее: не блокировать стандартную прокрутку браузера с помощью стандартной

event.preventDefault();     //Для всех браузеров, кроме IE
event.returnValue = false;//Для IE

а отслеживать передвижение колесика мыши и прокручивать браузер в противоположном направлении.
Идея, конечно, костыль с гвоздями, но как то начинать решение проблемы надо.
Оказалось что вызывать функции прокрутки страницы ни из обработчика события перемещения ролика мыши, ни из flash с помощью ExternalInterface нельзя.
Оно и к лучшему, решение в любом случае было далеко от изящного и оригинального.

Но немного разобрав ту самую библиотеку, удалось ее здорово улучшить. Основная её проблема была в том, что после каждого движения мыши она отправляла на выполнение довольно здоровый кусок кода на javascript, что в данном случае не имело совершенно никакого смысла:

if (ExternalInterface.available) {
    ExternalInterface.call("eval", JAVASCRIPT);
    return;
}

JAVASCRIPT — это «строка» кода, которая в развернутом состоянии занимает 41 строчку. Немного расточительно при каждом движении курсора мыши отправлять код с определением функций на страницу.
Пришлось перенести его в инициализацию библиотеки и в последствии там же и оставить.

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

Оказалось что Pepper flash (читай flash в Chrome) просто не получает событий передвижения колесика, если мы отключаем стандартную прокрутку с помощью event.preventDefault();

Решение оказалось простым до безумия — если браузер не хочет отправлять события о прокрутке — возьмем эту обязанность на себя.
Логика решения:

  1. Ловим событие прокрутки колесика мыши
  2. Отправляем детали события (дельта прокрутки) во Flash
  3. Блокируем прокрутку браузера
  4. Собираем событие прокрутки в flash с нуля
  5. Диспетчиризуем событие на сцену

А на сцене событие уже ловится подписанными на него компонентами.
Для отладки я использовал внешний .js скрипт, в конечной версии все функции js регистрируются на странице из Flash.
Отлично, приступим.
Основная часть работы возлагается на «внешний» код на странице:

var browserScrollAllow = true;            //По умолчанию разрешаем прокручивать страницу

/**
* Функция вызывается один раз, регистрируем слушателей всевозможных событий прокрутки колесика
*/
function registerEventListeners() {
    if (window.addEventListener) {
        window.addEventListener('mousewheel', wheelHandler, true);
        window.addEventListener('DOMMouseScroll', wheelHandler, true);
        window.addEventListener('scroll', wheelHandler, true);
    }
    window.onmousewheel = wheelHandler;
    document.onmousewheel = wheelHandler;
}

/**
* Наша основная функция, в которой ловим прокрутку и опционально заставляем flash 
* сгенерировать свое событие прокрутки
*/
function wheelHandler(event) {
	var delta = event.wheelDeltaY;
    if (!event) {
        event = window.event
    }
    if (!browserScrollAllow) {
        if (window.chrome) {
            document.getElementById('flashObject').scrollHappened(delta);
        }
        if (event.preventDefault) {
            event.preventDefault();
        } else {
            event.returnValue = false;
        }
    }
}

/**
* Эту функцию вызываем из flash каждый раз, когда курсор мыши заходит на область flash или покидает ее
*/
function allowBrowserScroll(allow) {
    browserScrollAllow = allow;
}

Ключевой момент в данном случае:
if (window.chrome) {
document.getElementById('flashObject').scrollHappened(delta);
}

Если мы имеем дело с Chrome, то уведомляем флешку о том, что произошла прокрутка и передаем значение перемещения.

Теперь то, что находится внутри flash (код обработчиков перемещения мыши и ухода курсора со сцены в данном случае особой значимости не несет и может быть опущен):

/**
*Функция инициализации "библиотеки"
*/
function initialize(stage: Stage): void {
    if (ExternalInterface.available) {
        nativeStage = stage;
        stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseOverStage); //Курсор мыши над сценой
        stage.addEventListener(Event.MOUSE_LEAVE, mouseLeavesStage);       //Курсор мыши за пределами сцены
        stage.addEventListener(MouseEvent.MOUSE_WHEEL, onMouseWheel); //Проверочная функция прокрутки колесиком

        ExternalInterface.addCallback("scrollHappened", scrollHappened);
      
    } else {
        throw new UninitializedError(NO_EXTERNAL_INTERFACE_ERROR);
    }
}

/**
* Функция диспетчиризации нового события прокрутки
*/
function scrollHappened(wheelDelta: Number): void {
    nativeStage.dispatchEvent(new MouseEvent(MouseEvent.MOUSE_WHEEL, 
											true, 
											false, 
											nativeStage.mouseX, 
											nativeStage.mouseY, 
											null, 
											false, 
											false, 
											false, 
											false, 
											wheelDelta / 40));
}

Таким образом мы получаем и обрабатываем событие прокрутки до того, как вызываем event.preventDefault();. Флеш теперь не чувствует себя обделенным и исправно обрабатывает свои же события.

Все?
Не совсем.

Чуть позже оказалось, что на Mac OS-x присутствует та же проблема. При этом уже не в Chrome а во всех браузерах по умолчанию. Тут уже не получилось бы отделаться простой проверкой на тип браузера.
На помощь пришла стандартная функция .os из библиотеки flash.system.Capabilities
Определяем, сидит ли пользователь с OS-X (AS3):

isMac = Capabilities.os.toLowerCase().indexOf("mac") != -1;

Немного поправляем функцию регистрации слушателей, заставляем принимать булеву переменную isMac в качестве параметра (JS):

function registerEventListeners(inputIsMac) {
    isMac = inputIsMac;
}

И добавляем дополнительное условие для диспетчиризации собственного события:

if (window.chrome || isMac) {
     document.getElementById('flashObject').scrollHappened(delta);
}

В итоге, если немного отполировать код(добавить весь JS код в AS3 в виде строковой величины и вызывать с помощью ExternalInterface.call("eval",...), и добавить входной параметр — flashID для большей гибкости), получим работоспособную библиотеку, полноценного аналога которой я почему-то не смог найти на просторах интернета

Ссылка на репозиторий GitHub

Автор: KumoKairo

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js