Add billboard for bottom of page with allowance to delay until after reading is finished. (#20616)
* Add delayed behavior * Add special behavior to billboards * Fix order for test * Test clicking outside element when close event is present * Update app/javascript/packs/billboardAfterRenderActions.js
This commit is contained in:
parent
8f4a6b832b
commit
205aee8c78
18 changed files with 195 additions and 20 deletions
|
|
@ -53,6 +53,21 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.popover-billboard {
|
||||
z-index: var(--z-popover);
|
||||
animation: popoverEnter 0.5s;
|
||||
box-shadow: 0 0 100px 60px rgba(0, 0, 0, 0.2) !important;
|
||||
.text-styles {
|
||||
padding-top: var(--su-4);
|
||||
padding-bottom: var(--su-4);
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.sponsorship-dropdown {
|
||||
top: auto;
|
||||
bottom: var(--su-8);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-billboard {
|
||||
margin: var(--su-2) 0 0;
|
||||
h1,
|
||||
|
|
@ -99,3 +114,15 @@
|
|||
padding-right: var(--su-7);
|
||||
padding-left: var(--su-7);
|
||||
}
|
||||
|
||||
// fadeIn
|
||||
@keyframes popoverEnter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ module Api
|
|||
params.permit :approved, :body_markdown, :creator_id, :display_to,
|
||||
:name, :organization_id, :placement_area, :published,
|
||||
:tag_list, :type_of, :exclude_article_ids, :weight, :requires_cookies,
|
||||
:audience_segment_type, :audience_segment_id, :priority,
|
||||
:audience_segment_type, :audience_segment_id, :priority, :special_behavior,
|
||||
:custom_display_label, :template, :render_mode, :preferred_article_ids,
|
||||
# Permitting twice allows both comma-separated string and array values
|
||||
:target_geolocations, target_geolocations: []
|
||||
|
|
|
|||
|
|
@ -140,8 +140,9 @@ function clearExcludeIds() {
|
|||
*/
|
||||
document.ready.then(() => {
|
||||
const select = document.getElementsByClassName('js-placement-area')[0];
|
||||
const articleSpecificPlacement = ['post_comments', 'post_sidebar'];
|
||||
const articleSpecificPlacement = ['post_comments', 'post_sidebar', 'post_fixed_bottom'];
|
||||
const targetedTagPlacements = [
|
||||
'post_fixed_bottom',
|
||||
'post_comments',
|
||||
'post_sidebar',
|
||||
'sidebar_right',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { h, render } from 'preact';
|
|||
import { Snackbar, addSnackbarItem } from '../Snackbar';
|
||||
import { addFullScreenModeControl } from '../utilities/codeFullscreenModeSwitcher';
|
||||
import { initializeDropdown } from '../utilities/dropdownUtils';
|
||||
import { setupBillboardDropdown } from '../utilities/billboardDropdown';
|
||||
import { setupBillboardInteractivity } from '../utilities/billboardInteractivity';
|
||||
import { embedGists } from '../utilities/gist';
|
||||
import { initializeUserSubscriptionLiquidTagContent } from '../liquidTags/userSubscriptionLiquidTag';
|
||||
import { trackCommentClicks } from '@utilities/ahoy/trackEvents';
|
||||
|
|
@ -159,7 +159,7 @@ getCsrfToken().then(async () => {
|
|||
const targetNode = document.querySelector('#comments');
|
||||
targetNode && embedGists(targetNode);
|
||||
|
||||
setupBillboardDropdown();
|
||||
setupBillboardInteractivity();
|
||||
initializeUserSubscriptionLiquidTagContent();
|
||||
focusOnComments();
|
||||
// Temporary Ahoy Stats for comment section clicks on controls
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { setupBillboardDropdown } from '../utilities/billboardDropdown';
|
||||
import { setupBillboardInteractivity } from '../utilities/billboardInteractivity';
|
||||
import {
|
||||
observeBillboards,
|
||||
executeBBScripts,
|
||||
implementSpecialBehavior,
|
||||
} from './billboardAfterRenderActions';
|
||||
|
||||
export async function getBillboard() {
|
||||
|
|
@ -29,10 +30,8 @@ async function generateBillboard(element) {
|
|||
try {
|
||||
const response = await window.fetch(asyncUrl);
|
||||
const htmlContent = await response.text();
|
||||
|
||||
const generatedElement = document.createElement('div');
|
||||
generatedElement.innerHTML = htmlContent;
|
||||
|
||||
element.innerHTML = '';
|
||||
element.appendChild(generatedElement);
|
||||
element.querySelectorAll('img').forEach((img) => {
|
||||
|
|
@ -41,7 +40,8 @@ async function generateBillboard(element) {
|
|||
};
|
||||
});
|
||||
executeBBScripts(element);
|
||||
setupBillboardDropdown();
|
||||
implementSpecialBehavior(element);
|
||||
setupBillboardInteractivity();
|
||||
// This is called here because the ad is loaded asynchronously.
|
||||
// The original code is still in the asset pipeline, so is not importable.
|
||||
// This could be refactored to be importable as we continue that migration.
|
||||
|
|
|
|||
|
|
@ -42,6 +42,22 @@ export function executeBBScripts(el) {
|
|||
}
|
||||
}
|
||||
|
||||
export function implementSpecialBehavior(element) {
|
||||
if (element.querySelector('.js-billboard') && element.querySelector('.js-billboard').dataset.special === 'delayed') {
|
||||
element.classList.add('hidden');
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '-150px',
|
||||
threshold: 0.2
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(showDelayed, observerOptions);
|
||||
|
||||
const target = document.getElementById('comments');
|
||||
observer.observe(target);
|
||||
}
|
||||
}
|
||||
|
||||
export function observeBillboards() {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
|
|
@ -66,7 +82,20 @@ export function observeBillboards() {
|
|||
document.querySelectorAll('[data-display-unit]').forEach((ad) => {
|
||||
observer.observe(ad);
|
||||
ad.removeEventListener('click', trackAdClick, false);
|
||||
ad.addEventListener('click', () => trackAdClick(ad));
|
||||
ad.addEventListener('click', () => trackAdClick(ad, event));
|
||||
});
|
||||
}
|
||||
|
||||
function showDelayed(entries) {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
// The target element has come into the viewport
|
||||
// Place the behavior you want to trigger here
|
||||
// query for data-special "delayed"
|
||||
document.querySelectorAll("[data-special='delayed']").forEach((el) => {
|
||||
el.closest('.hidden').classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +136,10 @@ function trackAdImpression(adBox) {
|
|||
adBox.dataset.impressionRecorded = true;
|
||||
}
|
||||
|
||||
function trackAdClick(adBox) {
|
||||
function trackAdClick(adBox, event) {
|
||||
if (!event.target.closest('a')) {
|
||||
return;
|
||||
}
|
||||
const isBot =
|
||||
/bot|google|baidu|bing|msn|duckduckbot|teoma|slurp|yandex/i.test(
|
||||
navigator.userAgent,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
initializeBillboardVisibility,
|
||||
} from '../packs/billboardAfterRenderActions';
|
||||
import { observeFeedElements } from '../packs/feedEvents';
|
||||
import { setupBillboardDropdown } from '@utilities/billboardDropdown';
|
||||
import { setupBillboardInteractivity } from '@utilities/billboardInteractivity';
|
||||
import { trackCreateAccountClicks } from '@utilities/ahoy/trackEvents';
|
||||
|
||||
/* global userData */
|
||||
|
|
@ -82,7 +82,7 @@ function renderSidebar() {
|
|||
.then((res) => res.text())
|
||||
.then((response) => {
|
||||
sidebarContainer.innerHTML = response;
|
||||
setupBillboardDropdown();
|
||||
setupBillboardInteractivity();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -105,7 +105,7 @@ if (!document.getElementById('featured-story-marker')) {
|
|||
const callback = () => {
|
||||
initializeBillboardVisibility();
|
||||
observeBillboards();
|
||||
setupBillboardDropdown();
|
||||
setupBillboardInteractivity();
|
||||
observeFeedElements();
|
||||
};
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ if (!document.getElementById('featured-story-marker')) {
|
|||
const callback = () => {
|
||||
initializeBillboardVisibility();
|
||||
observeBillboards();
|
||||
setupBillboardDropdown();
|
||||
setupBillboardInteractivity();
|
||||
observeFeedElements();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
// Import the function to test and any necessary parts
|
||||
import { setupBillboardInteractivity } from '@utilities/billboardInteractivity';
|
||||
|
||||
describe('billboard close functionality', () => {
|
||||
beforeEach(() => {
|
||||
// Setup a simple DOM structure that includes only the elements needed for the close functionality
|
||||
document.body.innerHTML = `
|
||||
<div class="another-element"></div>
|
||||
<div class="js-billboard" style="display: block;">
|
||||
<button id="sponsorship-close-trigger-1"></button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
it('hides billboard on close button click', () => {
|
||||
setupBillboardInteractivity();
|
||||
|
||||
// Simulate clicking the close button
|
||||
const closeButton = document.querySelector('#sponsorship-close-trigger-1');
|
||||
closeButton.click();
|
||||
|
||||
// Assert the billboard is hidden
|
||||
const billboard = document.querySelector('.js-billboard');
|
||||
expect(billboard.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('hides billboard when clicking outside of it', () => {
|
||||
setupBillboardInteractivity();
|
||||
|
||||
// Simulate a click outside the billboard
|
||||
const anotherElement = document.querySelector('.another-element');
|
||||
anotherElement.click();
|
||||
|
||||
// Assert the billboard is hidden
|
||||
const billboard = document.querySelector('.js-billboard');
|
||||
expect(billboard.style.display).toBe('none');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { initializeDropdown } from './dropdownUtils';
|
||||
|
||||
export function setupBillboardDropdown() {
|
||||
export function setupBillboardInteractivity() {
|
||||
const sponsorshipDropdownButtons = document.querySelectorAll(
|
||||
'button[id^=sponsorship-dropdown-trigger-]',
|
||||
);
|
||||
|
|
@ -23,6 +23,22 @@ export function setupBillboardDropdown() {
|
|||
}
|
||||
});
|
||||
}
|
||||
const sponsorshipCloseButtons = document.querySelectorAll(
|
||||
'button[id^=sponsorship-close-trigger-]',
|
||||
);
|
||||
if (sponsorshipCloseButtons.length) {
|
||||
sponsorshipCloseButtons.forEach((sponsorshipCloseButton) => {
|
||||
sponsorshipCloseButton.addEventListener('click', () => {
|
||||
sponsorshipCloseButton.closest('.js-billboard').style.display = 'none';
|
||||
});
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!event.target.closest('.js-billboard')) {
|
||||
sponsorshipCloseButton.closest('.js-billboard').style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -6,7 +6,17 @@ class Billboard < ApplicationRecord
|
|||
belongs_to :audience_segment, optional: true
|
||||
|
||||
# rubocop:disable Layout/LineLength
|
||||
ALLOWED_PLACEMENT_AREAS = %w[sidebar_left sidebar_left_2 sidebar_right sidebar_right_second sidebar_right_third feed_first feed_second feed_third home_hero post_sidebar post_comments].freeze
|
||||
ALLOWED_PLACEMENT_AREAS = %w[sidebar_left
|
||||
sidebar_left_2
|
||||
sidebar_right
|
||||
sidebar_right_second
|
||||
sidebar_right_third
|
||||
feed_first feed_second
|
||||
feed_third
|
||||
home_hero
|
||||
post_fixed_bottom
|
||||
post_sidebar
|
||||
post_comments].freeze
|
||||
# rubocop:enable Layout/LineLength
|
||||
ALLOWED_PLACEMENT_AREAS_HUMAN_READABLE = ["Sidebar Left (First Position)",
|
||||
"Sidebar Left (Second Position)",
|
||||
|
|
@ -17,6 +27,7 @@ class Billboard < ApplicationRecord
|
|||
"Home Feed Second",
|
||||
"Home Feed Third",
|
||||
"Home Hero",
|
||||
"Fixed Bottom (Individual Post)",
|
||||
"Sidebar Right (Individual Post)",
|
||||
"Below the comment section"].freeze
|
||||
|
||||
|
|
@ -34,6 +45,7 @@ class Billboard < ApplicationRecord
|
|||
enum type_of: { in_house: 0, community: 1, external: 2 }
|
||||
enum render_mode: { forem_markdown: 0, raw: 1 }
|
||||
enum template: { authorship_box: 0, plain: 1 }
|
||||
enum :special_behavior, { nothing: 0, delayed: 1 }
|
||||
|
||||
belongs_to :organization, optional: true
|
||||
has_many :billboard_events, foreign_key: :display_ad_id, inverse_of: :billboard, dependent: :destroy
|
||||
|
|
|
|||
|
|
@ -245,3 +245,4 @@
|
|||
<%== RunkitTag.script %>
|
||||
<%== TweetTag.script %>
|
||||
</script>
|
||||
<div class="js-billboard-container pb-4 crayons-layout__comments-billboard" data-async-url="<%= article_billboard_path(username: @article.username, slug: @article.slug, placement_area: :post_fixed_bottom) %>"></div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
data-category-click="<%= BillboardEvent::CATEGORY_CLICK %>"
|
||||
data-category-impression="<%= BillboardEvent::CATEGORY_IMPRESSION %>"
|
||||
data-context-type="<%= data_context_type %>"
|
||||
data-special="<%= billboard.special_behavior %>"
|
||||
data-article-id="<%= @article&.id %>"
|
||||
data-type-of="<%= billboard.type_of %>">
|
||||
<%= billboard.processed_html.html_safe %>
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
data-category-click="<%= BillboardEvent::CATEGORY_CLICK %>"
|
||||
data-category-impression="<%= BillboardEvent::CATEGORY_IMPRESSION %>"
|
||||
data-context-type="<%= data_context_type %>"
|
||||
data-special="<%= billboard.special_behavior %>"
|
||||
data-article-id="<%= @article&.id %>"
|
||||
data-type-of="<%= billboard.type_of %>">
|
||||
<div class="crayons-story__body">
|
||||
|
|
@ -33,6 +35,7 @@
|
|||
data-category-click="<%= BillboardEvent::CATEGORY_CLICK %>"
|
||||
data-category-impression="<%= BillboardEvent::CATEGORY_IMPRESSION %>"
|
||||
data-context-type="<%= data_context_type %>"
|
||||
data-special="<%= billboard.special_behavior %>"
|
||||
data-article-id="<%= @article&.id %>"
|
||||
data-type-of="<%= billboard.type_of %>">
|
||||
<%= render partial: "shared/billboard_header", locals: { billboard: billboard } %>
|
||||
|
|
@ -40,12 +43,33 @@
|
|||
<%= billboard.processed_html.html_safe %>
|
||||
</div>
|
||||
</div>
|
||||
<% elsif billboard.placement_area == "post_fixed_bottom" %>
|
||||
<div class="crayons-card crayons-card--secondary crayons-sponsorship popover-billboard crayons-story__billboard js-billboard fixed bottom-0 left-0 right-0"
|
||||
data-display-unit data-id="<%= billboard.id %>"
|
||||
data-category-click="<%= BillboardEvent::CATEGORY_CLICK %>"
|
||||
data-category-impression="<%= BillboardEvent::CATEGORY_IMPRESSION %>"
|
||||
data-context-type="<%= data_context_type %>"
|
||||
data-special="<%= billboard.special_behavior %>"
|
||||
data-article-id="<%= @article&.id %>"
|
||||
data-type-of="<%= billboard.type_of %>">
|
||||
<div class="flex">
|
||||
<%= render partial: "shared/billboard_header", locals: { billboard: billboard } %>
|
||||
<button id="sponsorship-close-trigger-<%= billboard.id %>" aria-controls="sponsorship-close-<%= billboard.id %>"
|
||||
class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon">
|
||||
<%= crayons_icon_tag("x", class: "pointer-events-none", title: t("billboard.menu.close")) %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-1 text-styles text-styles--billboard">
|
||||
<%= billboard.processed_html.html_safe %>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="crayons-card crayons-card--secondary crayons-sponsorship billboard js-billboard"
|
||||
data-display-unit data-id="<%= billboard.id %>"
|
||||
data-category-click="<%= BillboardEvent::CATEGORY_CLICK %>"
|
||||
data-category-impression="<%= BillboardEvent::CATEGORY_IMPRESSION %>"
|
||||
data-context-type="<%= data_context_type %>"
|
||||
data-special="<%= billboard.special_behavior %>"
|
||||
data-article-id="<%= @article&.id %>"
|
||||
data-type-of="<%= billboard.type_of %>">
|
||||
<%= render partial: "shared/billboard_header", locals: { billboard: billboard } %>
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ en:
|
|||
menu:
|
||||
aria_label: Toggle dropdown menu
|
||||
icon: Dropdown menu
|
||||
close: Close
|
||||
manage_preferences: Manage preferences
|
||||
what_is_a_billboard: What's a billboard?
|
||||
report_billboard: Report billboard
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ fr:
|
|||
menu:
|
||||
aria_label: Basculer le menu déroulant
|
||||
icon: Menu déroulant
|
||||
close: Fermer
|
||||
manage_preferences: Gérer les préférences
|
||||
what_is_a_billboard: Qu'est-ce qu'un panneau publicitaire?
|
||||
report_billboard: Report billboard
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
class AddSpecialBehaviorToBillboards < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :display_ads, :special_behavior, :integer, default: 0, null: false
|
||||
end
|
||||
end
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2024_01_11_144401) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2024_02_12_142953) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "citext"
|
||||
enable_extension "ltree"
|
||||
|
|
@ -490,6 +490,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_01_11_144401) do
|
|||
t.boolean "published", default: false
|
||||
t.integer "render_mode", default: 0
|
||||
t.boolean "requires_cookies", default: false
|
||||
t.integer "special_behavior", default: 0, null: false
|
||||
t.float "success_rate", default: 0.0
|
||||
t.ltree "target_geolocations", default: [], array: true
|
||||
t.integer "template", default: 0
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ RSpec.describe "Api::V1::Billboards" do
|
|||
"creator_id", "exclude_article_ids",
|
||||
"audience_segment_type", "audience_segment_id",
|
||||
"custom_display_label", "template", "render_mode", "preferred_article_ids",
|
||||
"priority", "weight", "target_geolocations", "requires_cookies")
|
||||
"priority", "weight", "target_geolocations", "requires_cookies", "special_behavior")
|
||||
expect(response.parsed_body["target_geolocations"]).to contain_exactly("US-WA", "CA-BC")
|
||||
end
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ RSpec.describe "Api::V1::Billboards" do
|
|||
"creator_id", "exclude_article_ids",
|
||||
"audience_segment_type", "audience_segment_id",
|
||||
"custom_display_label", "template", "render_mode", "preferred_article_ids",
|
||||
"priority", "weight", "target_geolocations", "requires_cookies")
|
||||
"priority", "weight", "target_geolocations", "requires_cookies", "special_behavior")
|
||||
expect(response.parsed_body["target_geolocations"]).to contain_exactly("US-WA", "CA-BC")
|
||||
end
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ RSpec.describe "Api::V1::Billboards" do
|
|||
"placement_area", "processed_html", "published",
|
||||
"success_rate", "tag_list", "type_of", "updated_at",
|
||||
"creator_id", "exclude_article_ids", "requires_cookies",
|
||||
"audience_segment_type", "audience_segment_id",
|
||||
"audience_segment_type", "audience_segment_id", "special_behavior",
|
||||
"custom_display_label", "template", "render_mode", "preferred_article_ids",
|
||||
"priority", "weight", "target_geolocations")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -172,6 +172,22 @@ RSpec.describe "Billboards" do
|
|||
end
|
||||
end
|
||||
|
||||
context "when the placement area is post_fixed_bottom" do
|
||||
it "contains close button" do
|
||||
billboard = create_billboard(placement_area: "post_fixed_bottom")
|
||||
get article_billboard_path(username: article.username, slug: article.slug, placement_area: "post_fixed_bottom")
|
||||
expect(response.body).to include "sponsorship-close-trigger-#{billboard.id}"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the placement area is post_sidebar" do
|
||||
it "does not contain close button" do
|
||||
billboard = create_billboard(placement_area: "post_sidebar")
|
||||
get article_billboard_path(username: article.username, slug: article.slug, placement_area: "post_sidebar")
|
||||
expect(response.body).not_to include "sponsorship-close-trigger-#{billboard.id}"
|
||||
end
|
||||
end
|
||||
|
||||
context "when requesting test billboard" do
|
||||
let(:admin) { create(:user, :admin) }
|
||||
let!(:test_billboard) { create_billboard(id: 123, placement_area: "post_sidebar", approved: false) }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue