@folklore/ads 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cjs.js +428 -22
  2. package/dist/es.js +410 -22
  3. package/package.json +2 -2
package/dist/cjs.js CHANGED
@@ -2,24 +2,99 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var debounce = require('lodash/debounce');
6
5
  var PropTypes = require('prop-types');
6
+ var classNames = require('classnames');
7
7
  var React = require('react');
8
+ var isArray = require('lodash/isArray');
9
+ var uniqBy = require('lodash/uniqBy');
10
+ var sortBy = require('lodash/sortBy');
11
+ var debounce = require('lodash/debounce');
8
12
  var EventEmitter = require('wolfy87-eventemitter');
9
13
  var isObject = require('lodash/isObject');
10
14
  var createDebug = require('debug');
11
- var isArray = require('lodash/isArray');
12
15
  var jsxRuntime = require('react/jsx-runtime');
16
+ var hooks = require('@folklore/hooks');
17
+ var tracking = require('@folklore/tracking');
13
18
 
14
19
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
15
20
 
16
- var debounce__default = /*#__PURE__*/_interopDefaultLegacy(debounce);
17
21
  var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
22
+ var classNames__default = /*#__PURE__*/_interopDefaultLegacy(classNames);
18
23
  var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
24
+ var isArray__default = /*#__PURE__*/_interopDefaultLegacy(isArray);
25
+ var uniqBy__default = /*#__PURE__*/_interopDefaultLegacy(uniqBy);
26
+ var sortBy__default = /*#__PURE__*/_interopDefaultLegacy(sortBy);
27
+ var debounce__default = /*#__PURE__*/_interopDefaultLegacy(debounce);
19
28
  var EventEmitter__default = /*#__PURE__*/_interopDefaultLegacy(EventEmitter);
20
29
  var isObject__default = /*#__PURE__*/_interopDefaultLegacy(isObject);
21
30
  var createDebug__default = /*#__PURE__*/_interopDefaultLegacy(createDebug);
22
- var isArray__default = /*#__PURE__*/_interopDefaultLegacy(isArray);
31
+
32
+ const adPath = PropTypes__default["default"].string;
33
+ const adSize = PropTypes__default["default"].oneOfType([PropTypes__default["default"].arrayOf(PropTypes__default["default"].number), PropTypes__default["default"].arrayOf(PropTypes__default["default"].string), PropTypes__default["default"].string, PropTypes__default["default"].arrayOf(PropTypes__default["default"].oneOfType([PropTypes__default["default"].arrayOf(PropTypes__default["default"].number), PropTypes__default["default"].arrayOf(PropTypes__default["default"].string), PropTypes__default["default"].string]))]);
34
+ const adSizeMapping = PropTypes__default["default"].arrayOf(PropTypes__default["default"].arrayOf(PropTypes__default["default"].oneOfType([PropTypes__default["default"].arrayOf(PropTypes__default["default"].number), adSize])));
35
+ const adPosition = PropTypes__default["default"].shape({
36
+ size: adSize,
37
+ sizeMapping: PropTypes__default["default"].objectOf(adSize)
38
+ });
39
+ const adPositions = PropTypes__default["default"].objectOf(adPosition);
40
+ const adViewports = PropTypes__default["default"].objectOf(PropTypes__default["default"].arrayOf(PropTypes__default["default"].number));
41
+ const adTargeting = PropTypes__default["default"].shape({
42
+ domain: PropTypes__default["default"].string
43
+ });
44
+
45
+ var propTypes$3 = /*#__PURE__*/Object.freeze({
46
+ __proto__: null,
47
+ adPath: adPath,
48
+ adSize: adSize,
49
+ adSizeMapping: adSizeMapping,
50
+ adPosition: adPosition,
51
+ adPositions: adPositions,
52
+ adViewports: adViewports,
53
+ adTargeting: adTargeting
54
+ });
55
+
56
+ const getAdSizes = sizes => {
57
+ if (isArray__default["default"](sizes)) {
58
+ return uniqBy__default["default"](isArray__default["default"](sizes[0]) || sizes[0] === 'fluid' ? sizes.filter(size => size !== 'fluid').reduce((allSizes, size) => [...allSizes, ...getAdSizes(size)], []) : [sizes].filter(size => size !== 'fluid'), size => size.join('x'));
59
+ }
60
+ return sizes.split('x').map(it => parseInt(it, 10));
61
+ };
62
+ const getMinimumAdSize = sizes => getAdSizes(sizes).reduce((minimumSize, size) => ({
63
+ width: Math.min(minimumSize.width, size[0]),
64
+ height: Math.min(minimumSize.height, size[1])
65
+ }), {
66
+ width: Infinity,
67
+ height: Infinity
68
+ });
69
+ const sizeFitsInViewport = (size, viewport) => size === 'fluid' && viewport[0] > 600 || size !== 'fluid' && (viewport[0] === 0 || size[0] <= viewport[0]) && (viewport[1] === 0 || size[1] <= viewport[1]);
70
+ const getSortedViewports = viewports => sortBy__default["default"](Object.keys(viewports).map(name => ({
71
+ name,
72
+ size: viewports[name]
73
+ })), viewport => viewport.size[0]).reverse();
74
+ const buildSizeMappingFromViewports = (sizeMapping, viewports) => getSortedViewports(viewports).reduce((newSizeMapping, _ref) => {
75
+ let {
76
+ name,
77
+ size: viewPortSize
78
+ } = _ref;
79
+ return typeof sizeMapping[name] !== 'undefined' ? [...newSizeMapping, [viewPortSize, sizeMapping[name]]] : newSizeMapping;
80
+ }, []);
81
+ const buildSizeMappingFromSizes = (sizes, viewports) => getSortedViewports(viewports).map(_ref2 => {
82
+ let {
83
+ name,
84
+ size: viewPortSize
85
+ } = _ref2;
86
+ return [viewPortSize, sizes.filter(size => sizeFitsInViewport(size, name === 'default' ? [300, 300] : viewPortSize))];
87
+ });
88
+ const getSizeMappingFromPosition = (_ref3, viewports) => {
89
+ let {
90
+ size: allSizes = [],
91
+ sizeMapping = null
92
+ } = _ref3;
93
+ if (sizeMapping === true) {
94
+ return buildSizeMappingFromSizes(allSizes, viewports);
95
+ }
96
+ return sizeMapping !== null ? buildSizeMappingFromViewports(sizeMapping, viewports) : null;
97
+ };
23
98
 
24
99
  class AdSlot extends EventEmitter__default["default"] {
25
100
  constructor(id, path, size) {
@@ -189,7 +264,7 @@ class AdSlot extends EventEmitter__default["default"] {
189
264
  }
190
265
 
191
266
  /* globals refreshDisabledLineItems: [] */
192
- const debug = createDebug__default["default"]('site:ads');
267
+ const debug = createDebug__default["default"]('folklore:ads');
193
268
  class AdsManager extends EventEmitter__default["default"] {
194
269
  static index = 0;
195
270
  static createAdId() {
@@ -545,23 +620,10 @@ const positions = {
545
620
  }
546
621
  };
547
622
 
548
- PropTypes__default["default"].string;
549
- const adSize = PropTypes__default["default"].oneOfType([PropTypes__default["default"].arrayOf(PropTypes__default["default"].number), PropTypes__default["default"].arrayOf(PropTypes__default["default"].string), PropTypes__default["default"].string, PropTypes__default["default"].arrayOf(PropTypes__default["default"].oneOfType([PropTypes__default["default"].arrayOf(PropTypes__default["default"].number), PropTypes__default["default"].arrayOf(PropTypes__default["default"].string), PropTypes__default["default"].string]))]);
550
- PropTypes__default["default"].arrayOf(PropTypes__default["default"].arrayOf(PropTypes__default["default"].oneOfType([PropTypes__default["default"].arrayOf(PropTypes__default["default"].number), adSize])));
551
- const adPosition = PropTypes__default["default"].shape({
552
- size: adSize,
553
- sizeMapping: PropTypes__default["default"].objectOf(adSize)
554
- });
555
- PropTypes__default["default"].objectOf(adPosition);
556
- const adViewports = PropTypes__default["default"].objectOf(PropTypes__default["default"].arrayOf(PropTypes__default["default"].number));
557
- PropTypes__default["default"].shape({
558
- domain: PropTypes__default["default"].string
559
- });
560
-
561
623
  /* eslint-disable react/jsx-props-no-spreading */
562
624
  const AdsContext = /*#__PURE__*/React__default["default"].createContext(null);
563
625
  const useAdsContext = () => React.useContext(AdsContext);
564
- const propTypes = {
626
+ const propTypes$2 = {
565
627
  children: PropTypes__default["default"].node.isRequired,
566
628
  defaultSlotPath: PropTypes__default["default"].string,
567
629
  slotsPath: PropTypes__default["default"].objectOf(PropTypes__default["default"].string),
@@ -572,7 +634,7 @@ const propTypes = {
572
634
  positions: adViewports,
573
635
  viewports: adViewports
574
636
  };
575
- const defaultProps = {
637
+ const defaultProps$2 = {
576
638
  defaultSlotPath: null,
577
639
  slotsPath: null,
578
640
  enableSingleRequest: true,
@@ -653,10 +715,354 @@ function AdsProvider(_ref) {
653
715
  children: children
654
716
  });
655
717
  }
656
- AdsProvider.propTypes = propTypes;
657
- AdsProvider.defaultProps = defaultProps;
718
+ AdsProvider.propTypes = propTypes$2;
719
+ AdsProvider.defaultProps = defaultProps$2;
720
+
721
+ const AdsTargetingContext = /*#__PURE__*/React__default["default"].createContext(null);
722
+ const useAdsTargeting = () => React.useContext(AdsTargetingContext);
723
+ const propTypes$1 = {
724
+ children: PropTypes__default["default"].node.isRequired,
725
+ // eslint-disable-next-line react/forbid-prop-types
726
+ targeting: PropTypes__default["default"].object
727
+ };
728
+ const defaultProps$1 = {
729
+ targeting: {
730
+ domain: typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.host}` : null
731
+ }
732
+ };
733
+ function AdsTargetingProvider(_ref) {
734
+ let {
735
+ children,
736
+ targeting
737
+ } = _ref;
738
+ return /*#__PURE__*/jsxRuntime.jsx(AdsTargetingContext.Provider, {
739
+ value: targeting,
740
+ children: children
741
+ });
742
+ }
743
+ AdsTargetingProvider.propTypes = propTypes$1;
744
+ AdsTargetingProvider.defaultProps = defaultProps$1;
745
+
746
+ function useTrackAd() {
747
+ const tracking$1 = tracking.useTracking() || null;
748
+ const trackEvent = React.useCallback((action, slot, renderEvent) => {
749
+ if (tracking$1 !== null && typeof tracking$1.trackAd !== 'undefined') {
750
+ tracking$1.trackAd(action, slot, renderEvent);
751
+ }
752
+ }, [tracking$1]);
753
+ return trackEvent;
754
+ }
755
+
756
+ function useAd(path, size) {
757
+ let {
758
+ sizeMapping = null,
759
+ targeting = null,
760
+ categoryExclusions = null,
761
+ refreshInterval = null,
762
+ alwaysRender = false,
763
+ onRender = null,
764
+ disabled = false,
765
+ trackEvents = true,
766
+ rootMargin = '300px'
767
+ } = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
768
+ const {
769
+ ads: adsManager,
770
+ ready: adsReady
771
+ } = useAdsContext();
772
+ const trackAd = useTrackAd();
773
+ const track = React.useCallback(function () {
774
+ if (trackEvents) {
775
+ trackAd(...arguments);
776
+ }
777
+ }, [trackEvents, trackAd]);
778
+
779
+ // Check for visibility
780
+ const {
781
+ ref: refObserver,
782
+ entry: {
783
+ isIntersecting
784
+ }
785
+ } = hooks.useIntersectionObserver({
786
+ rootMargin,
787
+ disabled
788
+ });
789
+
790
+ // Window blur
791
+ const [windowActive, setWindowActive] = React.useState(true); // eslint-disable-line
792
+ const onWindowBlur = React.useCallback(() => setWindowActive(false), [setWindowActive]);
793
+ const onWindowFocus = React.useCallback(() => setWindowActive(true), [setWindowActive]);
794
+ hooks.useWindowEvent('blur', onWindowBlur);
795
+ hooks.useWindowEvent('focus', onWindowFocus);
796
+ const isVisible = isIntersecting; /* && windowActive */
797
+
798
+ // Current render event
799
+ const [renderEvent, setRenderEvent] = React.useState(null);
800
+
801
+ // Create slot
802
+ const currentSlot = React.useRef(null);
803
+ const slot = React.useMemo(() => {
804
+ if (currentSlot.current !== null) {
805
+ adsManager.destroySlot(currentSlot.current);
806
+ currentSlot.current = null;
807
+ }
808
+ currentSlot.current = path !== null && !disabled ? adsManager.createSlot(path, size, {
809
+ visible: isVisible,
810
+ sizeMapping,
811
+ targeting,
812
+ categoryExclusions
813
+ }) : null;
814
+ // if (currentSlot.current !== null && adsReady) {
815
+ // adsManager.defineSlot(currentSlot.current);
816
+ // }
817
+ return currentSlot.current;
818
+ }, [adsManager, path, disabled, size, sizeMapping, targeting, alwaysRender, categoryExclusions]);
819
+
820
+ // Set visibility
821
+ React.useEffect(() => {
822
+ if (slot !== null) {
823
+ slot.setVisible(isVisible);
824
+ }
825
+ }, [slot, isVisible]);
826
+
827
+ // Render ad when visible
828
+ React.useEffect(() => {
829
+ const slotReady = slot !== null && !slot.isDefined();
830
+ if (adsReady && slotReady && (alwaysRender || isVisible)) {
831
+ adsManager.defineSlot(slot);
832
+ adsManager.displaySlot(slot);
833
+ track('Init', slot);
834
+ }
835
+ }, [adsManager, adsReady, slot, alwaysRender, isIntersecting, track]);
836
+
837
+ // Refresh ads slot
838
+ React.useEffect(() => {
839
+ let interval = null;
840
+ const slotReady = slot !== null && slot.isDefined();
841
+ if (adsReady && slotReady && isVisible && refreshInterval !== null) {
842
+ interval = setInterval(() => {
843
+ adsManager.refreshSlot(slot);
844
+ track('Refresh', slot);
845
+ }, refreshInterval);
846
+ }
847
+ return () => {
848
+ if (interval !== null) {
849
+ clearInterval(interval);
850
+ }
851
+ };
852
+ }, [adsManager, adsReady, slot, isVisible, refreshInterval, track]);
853
+
854
+ // Listen to render event
855
+ React.useEffect(() => {
856
+ if (slot === null) {
857
+ return () => {};
858
+ }
859
+ const onSlotRender = event => {
860
+ setRenderEvent(event);
861
+ if (onRender !== null) {
862
+ onRender(event);
863
+ }
864
+ const {
865
+ isEmpty = true
866
+ } = event || {};
867
+ if (isEmpty) {
868
+ track('Empty', slot);
869
+ } else {
870
+ track('Render', slot, event);
871
+ }
872
+ };
873
+ slot.on('render', onSlotRender);
874
+ return () => slot.off('render', onSlotRender);
875
+ }, [slot, disabled, setRenderEvent, onRender, track]);
876
+
877
+ // Destroy slot
878
+ React.useEffect(() => () => {
879
+ if (slot !== null) {
880
+ currentSlot.current = null;
881
+ adsManager.destroySlot(slot);
882
+ }
883
+ }, []);
884
+ return {
885
+ refObserver,
886
+ disabled: adsManager.isDisabled(),
887
+ id: slot !== null ? slot.getElementId() : null,
888
+ isRendered: slot !== null && slot.isRendered(),
889
+ isEmpty: slot !== null ? slot.isEmpty() : true,
890
+ isVisible: slot !== null ? slot.isVisible() : true,
891
+ width: null,
892
+ height: null,
893
+ renderEvent,
894
+ ...(slot !== null ? slot.getRenderedSize() : null)
895
+ };
896
+ }
897
+
898
+ const propTypes = {
899
+ position: PropTypes__default["default"].string.isRequired,
900
+ slot: PropTypes__default["default"].string,
901
+ path: adPath,
902
+ size: adSize,
903
+ sizeMapping: adSizeMapping,
904
+ targeting: adTargeting,
905
+ refreshInterval: PropTypes__default["default"].number,
906
+ alwaysRender: PropTypes__default["default"].bool,
907
+ disabled: PropTypes__default["default"].bool,
908
+ className: PropTypes__default["default"].string,
909
+ emptyClassName: PropTypes__default["default"].string,
910
+ adClassName: PropTypes__default["default"].string,
911
+ onRender: PropTypes__default["default"].func
912
+ };
913
+ const defaultProps = {
914
+ path: null,
915
+ slot: 'default',
916
+ size: null,
917
+ sizeMapping: null,
918
+ targeting: null,
919
+ refreshInterval: null,
920
+ alwaysRender: true,
921
+ disabled: false,
922
+ className: null,
923
+ emptyClassName: null,
924
+ adClassName: null,
925
+ onRender: null
926
+ };
927
+ function Ad(_ref) {
928
+ let {
929
+ position: positionName,
930
+ slot: slotName,
931
+ path,
932
+ size,
933
+ sizeMapping,
934
+ targeting,
935
+ refreshInterval,
936
+ alwaysRender,
937
+ disabled,
938
+ className,
939
+ emptyClassName,
940
+ adClassName,
941
+ onRender
942
+ } = _ref;
943
+ const {
944
+ viewports,
945
+ positions,
946
+ slotsPath
947
+ } = useAdsContext();
948
+ const position = positionName !== null ? positions[positionName] || null : null;
949
+ const finalPath = path || (slotName !== null ? slotsPath[slotName] : null) || (positionName !== null ? slotsPath[positionName] : null);
950
+ const finalSize = size !== null ? size : position.size;
951
+
952
+ // Targeting
953
+ const contextTargeting = useAdsTargeting();
954
+ const finalSizeMapping = React.useMemo(() => sizeMapping !== null ? sizeMapping : getSizeMappingFromPosition(position, viewports), [sizeMapping, position, viewports]);
955
+ const allTargeting = React.useMemo(() => contextTargeting !== null || targeting !== null || positionName !== null ? {
956
+ position: positionName,
957
+ ...contextTargeting,
958
+ ...targeting
959
+ } : null, [contextTargeting, targeting, positionName]);
960
+ const finalAdTargeting = React.useMemo(() => {
961
+ const {
962
+ refreshAds = null,
963
+ ...otherProps
964
+ } = allTargeting || {};
965
+ return {
966
+ refreshInterval: refreshAds !== null && refreshAds === 'inactive' ? null : refreshInterval,
967
+ targeting: otherProps || {}
968
+ };
969
+ }, [allTargeting, refreshInterval]);
970
+
971
+ // Create ad
972
+ const {
973
+ disabled: adsDisabled,
974
+ id,
975
+ width,
976
+ height,
977
+ isEmpty,
978
+ isRendered,
979
+ refObserver
980
+ } = useAd(finalPath, finalSize, {
981
+ sizeMapping: finalSizeMapping,
982
+ targeting: finalAdTargeting.targeting,
983
+ refreshInterval: finalAdTargeting.refreshInterval,
984
+ alwaysRender,
985
+ onRender,
986
+ disabled
987
+ });
988
+
989
+ // Get size
990
+ const lastRenderedSize = React.useRef(null);
991
+ const wasDisabled = React.useRef(disabled);
992
+ const waitingNextRender = React.useMemo(() => {
993
+ if (disabled) {
994
+ wasDisabled.current = true;
995
+ } else if (!disabled && isRendered) {
996
+ wasDisabled.current = false;
997
+ }
998
+ return wasDisabled.current && !isRendered;
999
+ }, [isRendered]);
1000
+ const sizeStyle = React.useMemo(() => {
1001
+ if (disabled || waitingNextRender) {
1002
+ return lastRenderedSize.current;
1003
+ }
1004
+ const {
1005
+ width: minimumWidth,
1006
+ height: minimumHeight
1007
+ } = getMinimumAdSize(finalSizeMapping !== null ? finalSizeMapping.reduce((allSizes, sizeMap) => [...allSizes, sizeMap[1]], [finalSize]) : finalSize);
1008
+ if (isRendered) {
1009
+ lastRenderedSize.current = !isEmpty ? {
1010
+ width,
1011
+ height
1012
+ } : null;
1013
+ }
1014
+ return {
1015
+ width: isRendered ? width : minimumWidth,
1016
+ height: isRendered ? height : minimumHeight
1017
+ };
1018
+ }, [id, disabled, finalSize, finalSizeMapping, width, height, isRendered, isEmpty]);
1019
+ const keepSize = (disabled || waitingNextRender) && lastRenderedSize.current !== null;
1020
+ if (id === null && !keepSize) {
1021
+ return null;
1022
+ }
1023
+ return /*#__PURE__*/jsxRuntime.jsx("div", {
1024
+ id: id !== null ? `${id}-container` : null,
1025
+ className: classNames__default["default"]([className, {
1026
+ [emptyClassName]: emptyClassName !== null && isEmpty && !keepSize
1027
+ }]),
1028
+ ref: refObserver,
1029
+ style: isEmpty && !keepSize ? {
1030
+ height: 0,
1031
+ paddingBottom: 0,
1032
+ overflow: 'hidden',
1033
+ opacity: 0
1034
+ } : null,
1035
+ children: /*#__PURE__*/jsxRuntime.jsx("div", {
1036
+ className: adClassName,
1037
+ style: {
1038
+ ...sizeStyle,
1039
+ margin: 'auto'
1040
+ },
1041
+ children: /*#__PURE__*/jsxRuntime.jsx("div", {
1042
+ id: id
1043
+ })
1044
+ })
1045
+ });
1046
+ }
1047
+ Ad.propTypes = propTypes;
1048
+ Ad.defaultProps = defaultProps;
658
1049
 
1050
+ exports.Ad = Ad;
659
1051
  exports.AdSlot = AdSlot;
660
1052
  exports.AdsManager = AdsManager;
661
1053
  exports.AdsProvider = AdsProvider;
1054
+ exports.AdsTargetingProvider = AdsTargetingProvider;
1055
+ exports.PropTypes = propTypes$3;
1056
+ exports.buildSizeMappingFromSizes = buildSizeMappingFromSizes;
1057
+ exports.buildSizeMappingFromViewports = buildSizeMappingFromViewports;
1058
+ exports.getAdSizes = getAdSizes;
1059
+ exports.getMinimumAdSize = getMinimumAdSize;
1060
+ exports.getSizeMappingFromPosition = getSizeMappingFromPosition;
1061
+ exports.getSortedViewports = getSortedViewports;
1062
+ exports.positions = positions;
1063
+ exports.sizeFitsInViewport = sizeFitsInViewport;
1064
+ exports.useAd = useAd;
662
1065
  exports.useAdsContext = useAdsContext;
1066
+ exports.useAdsTargeting = useAdsTargeting;
1067
+ exports.useAdsTracking = useTrackAd;
1068
+ exports.viewports = viewports;
package/dist/es.js CHANGED
@@ -1,11 +1,83 @@
1
- import debounce from 'lodash/debounce';
2
1
  import PropTypes from 'prop-types';
3
- import React, { useContext, useState, useRef, useMemo, useEffect } from 'react';
2
+ import classNames from 'classnames';
3
+ import React, { useContext, useState, useRef, useMemo, useEffect, useCallback } from 'react';
4
+ import isArray from 'lodash/isArray';
5
+ import uniqBy from 'lodash/uniqBy';
6
+ import sortBy from 'lodash/sortBy';
7
+ import debounce from 'lodash/debounce';
4
8
  import EventEmitter from 'wolfy87-eventemitter';
5
9
  import isObject from 'lodash/isObject';
6
10
  import createDebug from 'debug';
7
- import isArray from 'lodash/isArray';
8
11
  import { jsx } from 'react/jsx-runtime';
12
+ import { useIntersectionObserver, useWindowEvent } from '@folklore/hooks';
13
+ import { useTracking } from '@folklore/tracking';
14
+
15
+ const adPath = PropTypes.string;
16
+ const adSize = PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.string), PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.string), PropTypes.string]))]);
17
+ const adSizeMapping = PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), adSize])));
18
+ const adPosition = PropTypes.shape({
19
+ size: adSize,
20
+ sizeMapping: PropTypes.objectOf(adSize)
21
+ });
22
+ const adPositions = PropTypes.objectOf(adPosition);
23
+ const adViewports = PropTypes.objectOf(PropTypes.arrayOf(PropTypes.number));
24
+ const adTargeting = PropTypes.shape({
25
+ domain: PropTypes.string
26
+ });
27
+
28
+ var propTypes$3 = /*#__PURE__*/Object.freeze({
29
+ __proto__: null,
30
+ adPath: adPath,
31
+ adSize: adSize,
32
+ adSizeMapping: adSizeMapping,
33
+ adPosition: adPosition,
34
+ adPositions: adPositions,
35
+ adViewports: adViewports,
36
+ adTargeting: adTargeting
37
+ });
38
+
39
+ const getAdSizes = sizes => {
40
+ if (isArray(sizes)) {
41
+ return uniqBy(isArray(sizes[0]) || sizes[0] === 'fluid' ? sizes.filter(size => size !== 'fluid').reduce((allSizes, size) => [...allSizes, ...getAdSizes(size)], []) : [sizes].filter(size => size !== 'fluid'), size => size.join('x'));
42
+ }
43
+ return sizes.split('x').map(it => parseInt(it, 10));
44
+ };
45
+ const getMinimumAdSize = sizes => getAdSizes(sizes).reduce((minimumSize, size) => ({
46
+ width: Math.min(minimumSize.width, size[0]),
47
+ height: Math.min(minimumSize.height, size[1])
48
+ }), {
49
+ width: Infinity,
50
+ height: Infinity
51
+ });
52
+ const sizeFitsInViewport = (size, viewport) => size === 'fluid' && viewport[0] > 600 || size !== 'fluid' && (viewport[0] === 0 || size[0] <= viewport[0]) && (viewport[1] === 0 || size[1] <= viewport[1]);
53
+ const getSortedViewports = viewports => sortBy(Object.keys(viewports).map(name => ({
54
+ name,
55
+ size: viewports[name]
56
+ })), viewport => viewport.size[0]).reverse();
57
+ const buildSizeMappingFromViewports = (sizeMapping, viewports) => getSortedViewports(viewports).reduce((newSizeMapping, _ref) => {
58
+ let {
59
+ name,
60
+ size: viewPortSize
61
+ } = _ref;
62
+ return typeof sizeMapping[name] !== 'undefined' ? [...newSizeMapping, [viewPortSize, sizeMapping[name]]] : newSizeMapping;
63
+ }, []);
64
+ const buildSizeMappingFromSizes = (sizes, viewports) => getSortedViewports(viewports).map(_ref2 => {
65
+ let {
66
+ name,
67
+ size: viewPortSize
68
+ } = _ref2;
69
+ return [viewPortSize, sizes.filter(size => sizeFitsInViewport(size, name === 'default' ? [300, 300] : viewPortSize))];
70
+ });
71
+ const getSizeMappingFromPosition = (_ref3, viewports) => {
72
+ let {
73
+ size: allSizes = [],
74
+ sizeMapping = null
75
+ } = _ref3;
76
+ if (sizeMapping === true) {
77
+ return buildSizeMappingFromSizes(allSizes, viewports);
78
+ }
79
+ return sizeMapping !== null ? buildSizeMappingFromViewports(sizeMapping, viewports) : null;
80
+ };
9
81
 
10
82
  class AdSlot extends EventEmitter {
11
83
  constructor(id, path, size) {
@@ -175,7 +247,7 @@ class AdSlot extends EventEmitter {
175
247
  }
176
248
 
177
249
  /* globals refreshDisabledLineItems: [] */
178
- const debug = createDebug('site:ads');
250
+ const debug = createDebug('folklore:ads');
179
251
  class AdsManager extends EventEmitter {
180
252
  static index = 0;
181
253
  static createAdId() {
@@ -531,23 +603,10 @@ const positions = {
531
603
  }
532
604
  };
533
605
 
534
- PropTypes.string;
535
- const adSize = PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.string), PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.string), PropTypes.string]))]);
536
- PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), adSize])));
537
- const adPosition = PropTypes.shape({
538
- size: adSize,
539
- sizeMapping: PropTypes.objectOf(adSize)
540
- });
541
- PropTypes.objectOf(adPosition);
542
- const adViewports = PropTypes.objectOf(PropTypes.arrayOf(PropTypes.number));
543
- PropTypes.shape({
544
- domain: PropTypes.string
545
- });
546
-
547
606
  /* eslint-disable react/jsx-props-no-spreading */
548
607
  const AdsContext = /*#__PURE__*/React.createContext(null);
549
608
  const useAdsContext = () => useContext(AdsContext);
550
- const propTypes = {
609
+ const propTypes$2 = {
551
610
  children: PropTypes.node.isRequired,
552
611
  defaultSlotPath: PropTypes.string,
553
612
  slotsPath: PropTypes.objectOf(PropTypes.string),
@@ -558,7 +617,7 @@ const propTypes = {
558
617
  positions: adViewports,
559
618
  viewports: adViewports
560
619
  };
561
- const defaultProps = {
620
+ const defaultProps$2 = {
562
621
  defaultSlotPath: null,
563
622
  slotsPath: null,
564
623
  enableSingleRequest: true,
@@ -639,7 +698,336 @@ function AdsProvider(_ref) {
639
698
  children: children
640
699
  });
641
700
  }
642
- AdsProvider.propTypes = propTypes;
643
- AdsProvider.defaultProps = defaultProps;
701
+ AdsProvider.propTypes = propTypes$2;
702
+ AdsProvider.defaultProps = defaultProps$2;
703
+
704
+ const AdsTargetingContext = /*#__PURE__*/React.createContext(null);
705
+ const useAdsTargeting = () => useContext(AdsTargetingContext);
706
+ const propTypes$1 = {
707
+ children: PropTypes.node.isRequired,
708
+ // eslint-disable-next-line react/forbid-prop-types
709
+ targeting: PropTypes.object
710
+ };
711
+ const defaultProps$1 = {
712
+ targeting: {
713
+ domain: typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.host}` : null
714
+ }
715
+ };
716
+ function AdsTargetingProvider(_ref) {
717
+ let {
718
+ children,
719
+ targeting
720
+ } = _ref;
721
+ return /*#__PURE__*/jsx(AdsTargetingContext.Provider, {
722
+ value: targeting,
723
+ children: children
724
+ });
725
+ }
726
+ AdsTargetingProvider.propTypes = propTypes$1;
727
+ AdsTargetingProvider.defaultProps = defaultProps$1;
728
+
729
+ function useTrackAd() {
730
+ const tracking = useTracking() || null;
731
+ const trackEvent = useCallback((action, slot, renderEvent) => {
732
+ if (tracking !== null && typeof tracking.trackAd !== 'undefined') {
733
+ tracking.trackAd(action, slot, renderEvent);
734
+ }
735
+ }, [tracking]);
736
+ return trackEvent;
737
+ }
738
+
739
+ function useAd(path, size) {
740
+ let {
741
+ sizeMapping = null,
742
+ targeting = null,
743
+ categoryExclusions = null,
744
+ refreshInterval = null,
745
+ alwaysRender = false,
746
+ onRender = null,
747
+ disabled = false,
748
+ trackEvents = true,
749
+ rootMargin = '300px'
750
+ } = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
751
+ const {
752
+ ads: adsManager,
753
+ ready: adsReady
754
+ } = useAdsContext();
755
+ const trackAd = useTrackAd();
756
+ const track = useCallback(function () {
757
+ if (trackEvents) {
758
+ trackAd(...arguments);
759
+ }
760
+ }, [trackEvents, trackAd]);
761
+
762
+ // Check for visibility
763
+ const {
764
+ ref: refObserver,
765
+ entry: {
766
+ isIntersecting
767
+ }
768
+ } = useIntersectionObserver({
769
+ rootMargin,
770
+ disabled
771
+ });
772
+
773
+ // Window blur
774
+ const [windowActive, setWindowActive] = useState(true); // eslint-disable-line
775
+ const onWindowBlur = useCallback(() => setWindowActive(false), [setWindowActive]);
776
+ const onWindowFocus = useCallback(() => setWindowActive(true), [setWindowActive]);
777
+ useWindowEvent('blur', onWindowBlur);
778
+ useWindowEvent('focus', onWindowFocus);
779
+ const isVisible = isIntersecting; /* && windowActive */
780
+
781
+ // Current render event
782
+ const [renderEvent, setRenderEvent] = useState(null);
783
+
784
+ // Create slot
785
+ const currentSlot = useRef(null);
786
+ const slot = useMemo(() => {
787
+ if (currentSlot.current !== null) {
788
+ adsManager.destroySlot(currentSlot.current);
789
+ currentSlot.current = null;
790
+ }
791
+ currentSlot.current = path !== null && !disabled ? adsManager.createSlot(path, size, {
792
+ visible: isVisible,
793
+ sizeMapping,
794
+ targeting,
795
+ categoryExclusions
796
+ }) : null;
797
+ // if (currentSlot.current !== null && adsReady) {
798
+ // adsManager.defineSlot(currentSlot.current);
799
+ // }
800
+ return currentSlot.current;
801
+ }, [adsManager, path, disabled, size, sizeMapping, targeting, alwaysRender, categoryExclusions]);
802
+
803
+ // Set visibility
804
+ useEffect(() => {
805
+ if (slot !== null) {
806
+ slot.setVisible(isVisible);
807
+ }
808
+ }, [slot, isVisible]);
809
+
810
+ // Render ad when visible
811
+ useEffect(() => {
812
+ const slotReady = slot !== null && !slot.isDefined();
813
+ if (adsReady && slotReady && (alwaysRender || isVisible)) {
814
+ adsManager.defineSlot(slot);
815
+ adsManager.displaySlot(slot);
816
+ track('Init', slot);
817
+ }
818
+ }, [adsManager, adsReady, slot, alwaysRender, isIntersecting, track]);
819
+
820
+ // Refresh ads slot
821
+ useEffect(() => {
822
+ let interval = null;
823
+ const slotReady = slot !== null && slot.isDefined();
824
+ if (adsReady && slotReady && isVisible && refreshInterval !== null) {
825
+ interval = setInterval(() => {
826
+ adsManager.refreshSlot(slot);
827
+ track('Refresh', slot);
828
+ }, refreshInterval);
829
+ }
830
+ return () => {
831
+ if (interval !== null) {
832
+ clearInterval(interval);
833
+ }
834
+ };
835
+ }, [adsManager, adsReady, slot, isVisible, refreshInterval, track]);
836
+
837
+ // Listen to render event
838
+ useEffect(() => {
839
+ if (slot === null) {
840
+ return () => {};
841
+ }
842
+ const onSlotRender = event => {
843
+ setRenderEvent(event);
844
+ if (onRender !== null) {
845
+ onRender(event);
846
+ }
847
+ const {
848
+ isEmpty = true
849
+ } = event || {};
850
+ if (isEmpty) {
851
+ track('Empty', slot);
852
+ } else {
853
+ track('Render', slot, event);
854
+ }
855
+ };
856
+ slot.on('render', onSlotRender);
857
+ return () => slot.off('render', onSlotRender);
858
+ }, [slot, disabled, setRenderEvent, onRender, track]);
859
+
860
+ // Destroy slot
861
+ useEffect(() => () => {
862
+ if (slot !== null) {
863
+ currentSlot.current = null;
864
+ adsManager.destroySlot(slot);
865
+ }
866
+ }, []);
867
+ return {
868
+ refObserver,
869
+ disabled: adsManager.isDisabled(),
870
+ id: slot !== null ? slot.getElementId() : null,
871
+ isRendered: slot !== null && slot.isRendered(),
872
+ isEmpty: slot !== null ? slot.isEmpty() : true,
873
+ isVisible: slot !== null ? slot.isVisible() : true,
874
+ width: null,
875
+ height: null,
876
+ renderEvent,
877
+ ...(slot !== null ? slot.getRenderedSize() : null)
878
+ };
879
+ }
880
+
881
+ const propTypes = {
882
+ position: PropTypes.string.isRequired,
883
+ slot: PropTypes.string,
884
+ path: adPath,
885
+ size: adSize,
886
+ sizeMapping: adSizeMapping,
887
+ targeting: adTargeting,
888
+ refreshInterval: PropTypes.number,
889
+ alwaysRender: PropTypes.bool,
890
+ disabled: PropTypes.bool,
891
+ className: PropTypes.string,
892
+ emptyClassName: PropTypes.string,
893
+ adClassName: PropTypes.string,
894
+ onRender: PropTypes.func
895
+ };
896
+ const defaultProps = {
897
+ path: null,
898
+ slot: 'default',
899
+ size: null,
900
+ sizeMapping: null,
901
+ targeting: null,
902
+ refreshInterval: null,
903
+ alwaysRender: true,
904
+ disabled: false,
905
+ className: null,
906
+ emptyClassName: null,
907
+ adClassName: null,
908
+ onRender: null
909
+ };
910
+ function Ad(_ref) {
911
+ let {
912
+ position: positionName,
913
+ slot: slotName,
914
+ path,
915
+ size,
916
+ sizeMapping,
917
+ targeting,
918
+ refreshInterval,
919
+ alwaysRender,
920
+ disabled,
921
+ className,
922
+ emptyClassName,
923
+ adClassName,
924
+ onRender
925
+ } = _ref;
926
+ const {
927
+ viewports,
928
+ positions,
929
+ slotsPath
930
+ } = useAdsContext();
931
+ const position = positionName !== null ? positions[positionName] || null : null;
932
+ const finalPath = path || (slotName !== null ? slotsPath[slotName] : null) || (positionName !== null ? slotsPath[positionName] : null);
933
+ const finalSize = size !== null ? size : position.size;
934
+
935
+ // Targeting
936
+ const contextTargeting = useAdsTargeting();
937
+ const finalSizeMapping = useMemo(() => sizeMapping !== null ? sizeMapping : getSizeMappingFromPosition(position, viewports), [sizeMapping, position, viewports]);
938
+ const allTargeting = useMemo(() => contextTargeting !== null || targeting !== null || positionName !== null ? {
939
+ position: positionName,
940
+ ...contextTargeting,
941
+ ...targeting
942
+ } : null, [contextTargeting, targeting, positionName]);
943
+ const finalAdTargeting = useMemo(() => {
944
+ const {
945
+ refreshAds = null,
946
+ ...otherProps
947
+ } = allTargeting || {};
948
+ return {
949
+ refreshInterval: refreshAds !== null && refreshAds === 'inactive' ? null : refreshInterval,
950
+ targeting: otherProps || {}
951
+ };
952
+ }, [allTargeting, refreshInterval]);
953
+
954
+ // Create ad
955
+ const {
956
+ disabled: adsDisabled,
957
+ id,
958
+ width,
959
+ height,
960
+ isEmpty,
961
+ isRendered,
962
+ refObserver
963
+ } = useAd(finalPath, finalSize, {
964
+ sizeMapping: finalSizeMapping,
965
+ targeting: finalAdTargeting.targeting,
966
+ refreshInterval: finalAdTargeting.refreshInterval,
967
+ alwaysRender,
968
+ onRender,
969
+ disabled
970
+ });
971
+
972
+ // Get size
973
+ const lastRenderedSize = useRef(null);
974
+ const wasDisabled = useRef(disabled);
975
+ const waitingNextRender = useMemo(() => {
976
+ if (disabled) {
977
+ wasDisabled.current = true;
978
+ } else if (!disabled && isRendered) {
979
+ wasDisabled.current = false;
980
+ }
981
+ return wasDisabled.current && !isRendered;
982
+ }, [isRendered]);
983
+ const sizeStyle = useMemo(() => {
984
+ if (disabled || waitingNextRender) {
985
+ return lastRenderedSize.current;
986
+ }
987
+ const {
988
+ width: minimumWidth,
989
+ height: minimumHeight
990
+ } = getMinimumAdSize(finalSizeMapping !== null ? finalSizeMapping.reduce((allSizes, sizeMap) => [...allSizes, sizeMap[1]], [finalSize]) : finalSize);
991
+ if (isRendered) {
992
+ lastRenderedSize.current = !isEmpty ? {
993
+ width,
994
+ height
995
+ } : null;
996
+ }
997
+ return {
998
+ width: isRendered ? width : minimumWidth,
999
+ height: isRendered ? height : minimumHeight
1000
+ };
1001
+ }, [id, disabled, finalSize, finalSizeMapping, width, height, isRendered, isEmpty]);
1002
+ const keepSize = (disabled || waitingNextRender) && lastRenderedSize.current !== null;
1003
+ if (id === null && !keepSize) {
1004
+ return null;
1005
+ }
1006
+ return /*#__PURE__*/jsx("div", {
1007
+ id: id !== null ? `${id}-container` : null,
1008
+ className: classNames([className, {
1009
+ [emptyClassName]: emptyClassName !== null && isEmpty && !keepSize
1010
+ }]),
1011
+ ref: refObserver,
1012
+ style: isEmpty && !keepSize ? {
1013
+ height: 0,
1014
+ paddingBottom: 0,
1015
+ overflow: 'hidden',
1016
+ opacity: 0
1017
+ } : null,
1018
+ children: /*#__PURE__*/jsx("div", {
1019
+ className: adClassName,
1020
+ style: {
1021
+ ...sizeStyle,
1022
+ margin: 'auto'
1023
+ },
1024
+ children: /*#__PURE__*/jsx("div", {
1025
+ id: id
1026
+ })
1027
+ })
1028
+ });
1029
+ }
1030
+ Ad.propTypes = propTypes;
1031
+ Ad.defaultProps = defaultProps;
644
1032
 
645
- export { AdSlot, AdsManager, AdsProvider, useAdsContext };
1033
+ export { Ad, AdSlot, AdsManager, AdsProvider, AdsTargetingProvider, propTypes$3 as PropTypes, buildSizeMappingFromSizes, buildSizeMappingFromViewports, getAdSizes, getMinimumAdSize, getSizeMappingFromPosition, getSortedViewports, positions, sizeFitsInViewport, useAd, useAdsContext, useAdsTargeting, useTrackAd as useAdsTracking, viewports };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@folklore/ads",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Ads library",
5
5
  "keywords": [
6
6
  "javascript",
@@ -50,7 +50,7 @@
50
50
  "publishConfig": {
51
51
  "access": "public"
52
52
  },
53
- "gitHead": "75940ae5d4cbe7c795e62a4d15aea6c49c782ba6",
53
+ "gitHead": "cbdee31b7e205f95c4ab462380feee74c076daa1",
54
54
  "dependencies": {
55
55
  "@folklore/hooks": "^0.0.41",
56
56
  "@folklore/tracking": "^0.0.16",