Skip to content

Commit 4406642

Browse files
committed
Call a method when an element becomes visible.
1 parent 018f4a5 commit 4406642

File tree

6 files changed

+162
-49
lines changed

6 files changed

+162
-49
lines changed

django_unicorn/static/unicorn/js/attribute.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export class Attribute {
1515
this.isTarget = false;
1616
this.isPartial = false;
1717
this.isDirty = false;
18+
this.isVisible = false;
1819
this.isKey = false;
1920
this.isError = false;
2021
this.modifiers = {};
@@ -45,6 +46,8 @@ export class Attribute {
4546
this.isPartial = true;
4647
} else if (contains(this.name, ":dirty")) {
4748
this.isDirty = true;
49+
} else if (contains(this.name, ":visible")) {
50+
this.isVisible = true;
4851
} else if (this.name === "unicorn:key" || this.name === "u:key") {
4952
this.isKey = true;
5053
} else if (contains(this.name, ":error:")) {

django_unicorn/static/unicorn/js/component.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export class Component {
3939
this.modelEls = [];
4040
this.loadingEls = [];
4141
this.keyEls = [];
42+
this.visibilityEls = [];
4243
this.errors = {};
4344
this.return = {};
4445
this.poll = {};
@@ -53,6 +54,7 @@ export class Component {
5354

5455
this.init();
5556
this.refreshEventListeners();
57+
this.initVisibility();
5658
this.initPolling();
5759

5860
this.callCalls(args.calls);
@@ -206,6 +208,10 @@ export class Component {
206208
this.keyEls.push(element);
207209
}
208210

211+
if (hasValue(element.visibility)) {
212+
this.visibilityEls.push(element);
213+
}
214+
209215
element.actions.forEach((action) => {
210216
if (this.actionEvents[action.eventType]) {
211217
this.actionEvents[action.eventType].push({ action, element });
@@ -255,6 +261,42 @@ export class Component {
255261
});
256262
}
257263

264+
/**
265+
* Initializes `visible` elements.
266+
*/
267+
initVisibility() {
268+
if (
269+
typeof window !== "undefined" &&
270+
"IntersectionObserver" in window &&
271+
"IntersectionObserverEntry" in window &&
272+
"intersectionRatio" in window.IntersectionObserverEntry.prototype
273+
) {
274+
this.visibilityEls.forEach((element) => {
275+
const observer = new IntersectionObserver(
276+
(entries) => {
277+
const entry = entries[0];
278+
279+
if (entry.isIntersecting) {
280+
this.callMethod(
281+
element.visibility.method,
282+
element.visibility.debounceTime,
283+
element.partials,
284+
(err) => {
285+
if (err) {
286+
console.error(err);
287+
}
288+
}
289+
);
290+
}
291+
},
292+
{ threshold: [element.visibility.threshold] }
293+
);
294+
295+
observer.observe(element.el);
296+
});
297+
}
298+
}
299+
258300
/**
259301
* Handles poll errors.
260302
* @param {Error} err Error.

django_unicorn/static/unicorn/js/element.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class Element {
3131
this.actions = [];
3232
this.partials = [];
3333
this.target = null;
34+
this.visibility = {};
3435
this.key = null;
3536
this.events = [];
3637
this.errors = [];
@@ -97,6 +98,19 @@ export class Element {
9798
} else {
9899
this.partials.push({ target: attribute.value });
99100
}
101+
} else if (attribute.isVisible) {
102+
let threshold = attribute.modifiers.threshold || 0;
103+
104+
if (threshold > 1) {
105+
// Convert the whole number into a percentage
106+
threshold /= 100;
107+
}
108+
109+
this.visibility.method = attribute.value;
110+
this.visibility.threshold = threshold;
111+
this.visibility.debounceTime = attribute.modifiers.debounce
112+
? parseInt(attribute.modifiers.debounce, 10) || 0
113+
: 0;
100114
} else if (attribute.eventType) {
101115
const action = {};
102116
action.name = attribute.value;

example/unicorn/components/js.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class JsView(UnicornView):
1212
)
1313
selected_state = ""
1414
select2_datetime = now()
15+
scroll_counter = 0
1516

1617
def call_javascript(self):
1718
self.call("callAlert", "world")
@@ -30,5 +31,8 @@ def select_state(self, val, idx):
3031
print("select_state called idx", idx)
3132
self.selected_state = val
3233

34+
def increase_counter(self):
35+
self.scroll_counter += 1
36+
3337
class Meta:
3438
javascript_excludes = ("states",)
Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,75 @@
11
<div>
2-
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
3-
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css" rel="stylesheet" />
4-
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>
2+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
3+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css" rel="stylesheet" />
4+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>
55

6-
<script>
7-
function callAlert(name) {
8-
alert("hello, " + name);
9-
}
6+
<script>
7+
function callAlert(name) {
8+
alert("hello, " + name);
9+
}
1010

11-
var HelloJs = (function() {
12-
var self = {};
11+
var HelloJs = (function () {
12+
var self = {};
1313

14-
self.hello = function(name){
15-
alert("Hello " + name)
16-
}
14+
self.hello = function (name) {
15+
alert("Hello " + name)
16+
}
1717

18-
return self;
19-
}());
20-
</script>
18+
return self;
19+
}());
20+
</script>
2121

22+
<h2>
23+
<button unicorn:click="call_javascript">Component view calls JavaScript</button>
24+
<button unicorn:click="call_javascript_module">Component view calls JavaScript module</button>
25+
</h2>
26+
27+
<div>
2228
<h2>
23-
<button unicorn:click="call_javascript">Component view calls JavaScript</button>
24-
<button unicorn:click="call_javascript_module">Component view calls JavaScript module</button>
29+
<code>javascript_exclude states</code>
2530
</h2>
31+
{% for state in states %}
32+
{{ state }}
33+
{% endfor %}
34+
</div>
2635

27-
<div>
28-
<h2>
29-
<code>javascript_exclude states</code>
30-
</h2>
31-
{% for state in states %}
32-
{{ state }}
33-
{% endfor %}
34-
</div>
36+
<h2>Select2</h2>
3537

36-
<h2>Select2</h2>
38+
<div u:ignore>
39+
<select unicorn:model="selected_state" class="form-control" id="select2-example" required
40+
onchange="Unicorn.call('js', 'select_state', this.value, this.selectedIndex);">
41+
{% for state in states %}
42+
<option value="{{ state }}">{{ state }}</option>
43+
{% endfor %}
44+
</select>
3745

38-
<div u:ignore>
39-
<select unicorn:model="selected_state" class="form-control" id="select2-example" required onchange="Unicorn.call('js', 'select_state', this.value, this.selectedIndex);">
40-
{% for state in states %}
41-
<option value="{{ state }}">{{ state }}</option>
42-
{% endfor %}
43-
</select>
46+
States (in ignored div): {{ states }}
47+
</div>
4448

45-
States (in ignored div): {{ states }}
46-
</div>
49+
selected_state: {{ selected_state }}
4750

48-
selected_state: {{ selected_state }}
51+
<script>
52+
$(document).ready(function () {
53+
$('#select2-example').select2();
54+
});
55+
</script>
4956

50-
<script>
51-
$(document).ready(function() {
52-
$('#select2-example').select2();
53-
});
54-
</script>
57+
<div>
58+
States (not in ignored div): {{ states }}<br />
59+
<button type="submit" u:click="change_states">Change states</button>
60+
</div>
5561

62+
<div>
63+
<input type="text" u:model="select2_datetime" />
64+
<button type="submit" u:click="get_now">Get now</button>
5665
<div>
57-
States (not in ignored div): {{ states }}<br />
58-
<button type="submit" u:click="change_states">Change states</button>
66+
select2_datetime: {{ select2_datetime }}
5967
</div>
68+
</div>
6069

61-
<div>
62-
<input type="text" u:model="select2_datetime" />
63-
<button type="submit" u:click="get_now">Get now</button>
64-
<div>
65-
select2_datetime: {{ select2_datetime }}
66-
</div>
67-
</div>
70+
<div unicorn:key="visibility">
71+
<span unicorn:visible.threshold-25.debounce-1000="increase_counter" unicorn:partial="visibility">
72+
Number of times this span was scrolled into view: {{ scroll_counter }}
73+
</span>
74+
</div>
6875
</div>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import test from "ava";
2+
import { getComponent, getElement } from "../utils.js";
3+
4+
test("visible", (t) => {
5+
const html = "<span unicorn:visible='test_function1'></span>";
6+
const element = getElement(html);
7+
8+
const { visibility } = element;
9+
t.is(visibility.method, "test_function1");
10+
t.is(visibility.threshold, 0);
11+
t.is(visibility.debounceTime, 0);
12+
});
13+
14+
test("visible threshold", (t) => {
15+
const html = "<span unicorn:visible.threshold-25='test_function2'></span>";
16+
const element = getElement(html);
17+
18+
const { visibility } = element;
19+
t.is(visibility.method, "test_function2");
20+
t.is(visibility.threshold, 0.25);
21+
t.is(visibility.debounceTime, 0);
22+
});
23+
24+
test("visible debounce", (t) => {
25+
const html = "<span unicorn:visible.debounce-1000='test_function3'></span>";
26+
const element = getElement(html);
27+
28+
const { visibility } = element;
29+
t.is(visibility.method, "test_function3");
30+
t.is(visibility.threshold, 0);
31+
t.is(visibility.debounceTime, 1000);
32+
});
33+
34+
test("visible chained", (t) => {
35+
const html =
36+
"<span unicorn:visible.threshold-50.debounce-2000='test_function4'></span>";
37+
const element = getElement(html);
38+
39+
const { visibility } = element;
40+
t.is(visibility.method, "test_function4");
41+
t.is(visibility.threshold, 0.5);
42+
t.is(visibility.debounceTime, 2000);
43+
});

0 commit comments

Comments
 (0)