Filtrowanie HTML przy pomocy selektorów CSS

Niecodzienne aplikacje wymagają dość niecodziennych rozwiązań w kodzie. Nie inaczej jest ze wszelkiego rodzaju RTE – Rich Text Editors. Jednym z wyzwań stojącym przed twórcami tego typu komponentów (czyli także i przede mną, dumnego programisty CKEditora 4) jest filtrowanie wprowadzanej przez użytkownika treści – tak, aby nie dało się przypadkiem wprowadzić żadnego, złośliwego kodu.

Advanced Content Filtering

W CKEditorze 4 rozwiązane jest to przy pomocy mechanizmu zwanego Advanced Content Filtering, który to – nie oszukujmy się – nie posiada najbardziej przyjaznej użytkownikowi składni:

// Akceptujmy tylko akapity z klasą important oraz nagłówki h1 z opcjonalnym stylem inline określającym kolor tła
p(!important); h1{background-color}

Jest jeszcze co prawda składnia oparta o obiekty, ale ona cierpi na inną przypadłość – jest niepotrzebnie rozwlekła.

CSS na ratunek!

Od dłuższego czasu chodziła mi po głowie myśl, czy podobnego filtra nie da się zrobić w sposób o wiele prostszy. Pewnego dnia siadłem do pisania prostego skryptu przy użyciu jQuery i… olśniło mnie. Rozwiązaniem wręcz idealnym dla tego typu filtra mogą być przecież selektory CSS!

Zasada działania jest bardzo prosta: bierzemy dowolny element, z którego chcemy odsiać niepożądane przez nas elementy. Następnie przy pomocy document.querySelectorAll pobieramy dwa zbiory elementów: wszystkie elementy z naszego filtrowanego kontenera oraz wszystkie dozwolone elementy (czyli te, które chcemy zostawić). Jeśli odsiejemy następnie wszystkie dozwolone elementy ze zbioru wszystkich elementów, to dostaniemy te elementy, które powinniśmy wyrzucić.

Brzmi cholernie zawile, więc pokażę to może na kodzie. Zatem mamy taki HTML:

Ja zostaję Ja wylatuję, ale mój środek zostaje

I chcemy, żeby w naszym div dozwolone były tylko te linki, które mają atrybut [href]. Zatem, żeby dobrać się do tablicy niedozwolonych elementów, możemy zrobić tak:

( function() {
    // Pobieramy div, którego zawartość chcemy przefiltrować.
    const toFilter = document.querySelector( 'div' );

    // Pobieramy wszystkie dozwolone elementy z diva i wrzucamy do tablicy.
    const allowedElements = [ ...toFilter.querySelectorAll( 'a[href]' ) ];

    // Pobieramy wszystkie elementy z diva.
    const allElements = [ ...toFilter.querySelectorAll( '*' ) ];

    // Wyciągamy wszystkie niedozwolonego elementy.
    const disallowedElements = allElements.filter( ( element ) => {
        return allowedElements.indexOf( element ) === -1;
    } );
}() );

Powyższy kod jest napisany przy użyciu ECMAScript 6, które na dzień dzisiejszy wypada znać (polecam w zakresie nauki pozycję Exploring JS Axela Rauschmayera; i tutaj uwaga: przez fakt zastosowania ES6 kod nie działa w Internet Explorerze!). Przeznaczenia i sposobu działania const i specyficznego sposobu zapisu funkcji raczej nie trzeba wyjaśniać. Niemniej na słowo komentarza na pewno zasługuje konstrukcja:

const allowedElements = [ ...toFilter.querySelectorAll( 'a[href]' ) ];

Jak wiadomo (albo i nie), document.querySelectorAll nie zwraca pełnoprawnej tablicy, a obiekt NodeList, który nie ma wielu przydatnych, tablicowych metod. Sęk w tym, że my ich potrzebujemy (w końcu używamy Array.prototype.filter i Array.prototype.indexOf). Z tego też powodu musimy jakoś z NodeList tę tablicę uzyskać. I właśnie to robi ten kod. Być może jego o wiele bardziej zrozumiałą wersją byłby zapis:

const allowedElements = Array.from( toFilter.querySelectorAll( 'a[href]' ) );

…ale wówczas nie mógłbym dać odnośnika do iteratorów w ES6, z których powyższe rozwiązanie korzysta.

Cała magia powyższego kodu sprowadza się do wywołania allElements.filter, które tworzy nam tablicę z wynikami spełniającymi warunek. Przyjrzyjmy się temu warunkowi:

allowedElements.indexOf( element ) === -1

Można go przeczytać jako „element spełnia warunek jeśli nie należy do zbioru allowedElements„. Innymi słowy: tylko niedozwolony element spełni warunek i tym samym znajdzie się w tablicy disallowedElements.

Skoro już mamy tablicę niedozwolonych elementów, to je usuńmy, prawda?

disallowedElements.forEach( ( element ) => {
    // Tak, od dłuższego czasu każdy element ma metodę remove.
    // https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove
    element.remove();
} );

I tutaj pojawia się pewien problem: skrypt usunął element, ale wraz z treścią. A nie do końca o to nam chodzi: chcemy, żeby treść była jedynie pozbawiona niedozwolonych znaczników HTML (tak, jak robi to np. strip_tags w PHP). Z tego też względu musimy wyjąc wszystkie dzieci usuwanego elementu z niego i dopiero wówczas go usunąć:

disallowedElements.forEach( ( element ) => {
    while ( element.firstChild ) {
        element.parentNode.insertBefore( element.firstChild, element );
    }

    element.remove();
} );

Pętla będzie się wykonywać tak długo jak element będzie miał dzieci. Każde dziecko jest wyciągane ze swojego rodzica, po czym wstawiane tuż przed nim (brzmi to makabrycznie, czyż nie?). Zatem dla takiego elementu:

Coś taminnego

po pierwszym przelocie pętli otrzymamy:

Coś tam

innego

a po drugim:

Coś tam innego

I w tym momencie pętla się zakończy i akapit zostanie usunięty.

Voilà! Jeśli teraz odpalimy nasz kod, zauważymy, że po odfiltrowaniu we wnętrzu div został wyłącznie jeden link. Cały przykład jest dostępny na jsfiddle.net. Natomiast na moim GitHubie znajduje się podrasowana wersja skryptu.

Miłego filtrowania!

Spodobał Ci się artykuł? Dzięki naciśnięciu serduszka poniżej będę wiedział jakie treści tworzyć. Dzięki! :)

6 najlepszych bibliotek JavaScript do tworzenia prezentacji Wysuwane boczne menu 13 innowacyjnych formularzy logowania i rejestracji
View Comments
  • witam czy będziecie organizowali kursy nie wiem, stacjonarne ? i nie chodzi mi o online
    tylko wynajęta sala gdzie by się to odbyło ??