/
fragment.js
1883 lines (1417 loc) · 62.4 KB
/
fragment.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
require('./fragment.sass')
const u = up.util
const e = up.element
/*-
Fragment API
===========
The `up.fragment` module offers a high-level JavaScript API to work with DOM elements.
A fragment is an element with some additional properties that are useful in the context of
a server-rendered web application:
- Fragments are [identified by a CSS selector](/up.fragment.toTarget), like a `.class` or `#id`.
- Fragments are usually updated by a [link](/a-up-follow) for [form](/form-up-submit) that targets their selector.
When the server renders HTML with a matching element, the fragment is swapped with a new version.
- As fragments enter the page they are automatically [compiled](/up.compiler) to activate JavaScript behavior.
- Fragment changes may be [animated](/up.motion).
- Fragments are placed on a [layer](/up.layer) that is isolated from other layers.
Unpoly features will only see or change fragments from the [current layer](/up.layer.current)
unless you [explicitly target another layer](/layer-option).
- Fragments [know the URL from where they were loaded](/up.fragment.source).
They can be [reloaded](/up.reload) or [polled periodically](/up-poll).
For low-level DOM utilities that complement the browser's native API, see `up.element`.
@see navigation
@see focus-option
@see csp
@see up.render
@see up.navigate
@see up.destroy
@see up.reload
@see up.fragment.get
@see up.hello
@module up.fragment
*/
up.fragment = (function() {
/*-
Configures defaults for fragment updates.
@property up.fragment.config
@param {Array<string>} [config.mainTargets=['[up-main]', 'main', ':layer']]
An array of CSS selectors matching default render targets.
When no other render target is given, Unpoly will update the first selector matching both
the current page and the server response.
When [navigating](/navigation) to a main target, Unpoly will automatically
[reset scroll positions](/scroll-option) and
[update the browser history](/up.render#options.history).
This property is aliased as [`up.layer.config.any.mainTargets`](/up.layer.config#config.any.mainTargets).
@param {Array<string|RegExp>} [config.badTargetClasses]
An array of class names that should be ignored when
[deriving a target selector from a fragment](/up.fragment.toTarget).
The class names may also be passed as a regular expression.
@param {Object} [config.navigateOptions]
An object of default options to apply when [navigating](/navigation).
@param {boolean} [config.matchAroundOrigin]
Whether to match an existing fragment around the triggered link.
If set to `false` Unpoly will replace the first fragment
matching the given target selector in the link's [layer](/up.layer).
@param {Array<string>} [config.autoHistoryTargets]
When an updated fragments contain an element matching one of the given CSS selectors, history will be updated with `{ history: 'auto' }`.
By default Unpoly will auto-update history when updating a [main target](#config.mainTargets).
@param {boolean|string|Function(Element)} [config.autoScroll]
How to scroll after updating a fragment with `{ scroll: 'auto' }`.
See [scroll option](/scroll-option) for a list of allowed values.
The default configuration tries, in this order:
- If the URL has a `#hash`, scroll to the hash.
- If updating a [main target](/up-main), reset scroll positions.
@param {boolean|string|Function(Element)} [config.autoFocus]
How to focus when updating a fragment with `{ focus: 'auto' }`.
See [focus option](/focus-option) for a list of allowed values.
The default configuration tries, in this order:
- Focus a `#hash` in the URL.
- Focus an `[autofocus]` element in the new fragment.
- If focus was lost with the old fragment, focus the new fragment.
- If updating a [main target](/up-main), focus the new fragment.
@param {boolean} [config.runScripts=false]
Whether to execute `<script>` tags in updated fragments.
Scripts will load asynchronously, with no guarantee of execution order.
If you set this to `true`, mind that the `<body>` element is a default
[main target](/up-main). If you are including your global application scripts
at the end of your `<body>`
for performance reasons, swapping the `<body>` will re-execute these scripts.
In that case you must configure a different main target that does not include
your application scripts.
@stable
*/
const config = new up.Config(() => ({
badTargetClasses: [/^up-/],
// These defaults will be set to both success and fail options
// if { navigate: true } is given.
navigateOptions: {
solo: true, // preflight
feedback: true, // preflight
cache: 'auto', // preflight
fallback: true, // FromContent
focus: 'auto', // UpdateLayer/OpenLayer
scroll: 'auto', // UpdateLayer/OpenLayer
history: 'auto', // UpdateLayer/OpenLayer
peel: true // UpdateLayer/OpenLayer
},
matchAroundOrigin: true,
runScripts: false,
autoHistoryTargets: [':main'],
autoFocus: ['hash', 'autofocus', 'main-if-main', 'target-if-lost'],
autoScroll: ['hash', 'layer-if-main']
}))
// Users who are not using layers will prefer settings default targets
// as up.fragment.config.mainTargets instead of up.layer.config.any.mainTargets.
u.delegate(config, 'mainTargets', () => up.layer.config.any)
function reset() {
config.reset()
}
/*-
Returns the URL the given element was retrieved from.
If the given element was never directly updated, but part of a larger fragment update,
the closest known source of an ancestor element is returned.
### Example
In the HTML below, the element `#one` was loaded from the URL `/foo`:
```html
<div id="one" up-source"/foo">
<div id="two">...</div>
</div>
```
We can now ask for the source of an element:
```javascript
up.fragment.source('#two') // returns '/foo'
```
@function up.fragment.source
@param {Element|string} element
The element or CSS selector for which to look up the source URL.
@return {string|undefined}
@stable
*/
function sourceOf(element, options = {}) {
element = getSmart(element, options)
return e.closestAttr(element, 'up-source')
}
/*-
Returns a timestamp for the last modification of the content in the given element.
@function up.fragment.time
@param {Element} element
@return {string}
@internal
*/
function timeOf(element) {
return e.closestAttr(element, 'up-time') || '0'
}
/*-
Sets the time when the fragment's underlying data was last changed.
This can be used to avoid rendering unchanged HTML when [reloading](/up.reload)
a fragment. This saves <b>CPU time</b> and reduces the <b>bandwidth cost</b> for a
request/response exchange to **~1 KB**.
## Example
Let's say we display a list of recent messages.
We use the `[up-poll]` attribute to reload the `.messages` fragment every 30 seconds:
```html
<div class="messages" up-poll>
...
</div>
```
The list is now always up to date. But most of the time there will not be new messages,
and we waste resources sending the same unchanged HTML from the server.
We can improve this by setting an `[up-time]` attribute and the message list.
The attribute value is the time of the most recent message.
The time is encoded as the number of seconds since [Unix epoch](https://en.wikipedia.org/wiki/Unix_time).
When, for instance, the last message in a list was received from December 24th, 1:51:46 PM UTC,
we use the following HTML:
```html
<div class="messages" up-time="1608730818" up-poll>
...
</div>
```
When reloading Unpoly will echo the `[up-time]` timestamp in an `X-Up-Reload-From-Time` header:
```http
X-Up-Reload-From-Time: 1608730818
```
The server can compare the time from the request with the time of the last data update.
If no more recent data is available, the server can render nothing and respond with
an [`X-Up-Target: :none`](/X-Up-Target) header.
Here is an example with [unpoly-rails](https://unpoly.com/install/ruby):
```ruby
class MessagesController < ApplicationController
def index
if up.reload_from_time == current_user.last_message_at
up.render_nothing
else
@messages = current_user.messages.order(time: :desc).to_a
render 'index'
end
end
end
```
@selector [up-time]
@param {string} up-time
The number of seconds between the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time).
and the time when the element's underlying data was last changed.
@experimental
*/
/*-
Sets this element's source URL for [reloading](/up.reload) and [polling](/up-poll)
When an element is reloaded, Unpoly will make a request from the URL
that originally brought the element into the DOM. You may use `[up-source]` to
use another URL instead.
### Example
Assume an application layout with an unread message counter.
You use `[up-poll]` to refresh the counter every 30 seconds.
By default this would make a request to the URL that originally brought the
counter element into the DOM. To save the server from rendering a lot of
unused HTML, you may poll from a different URL like so:
```html
<div class="unread-count" up-poll up-source="/unread-count">
2 new messages
</div>
```
@selector [up-source]
@param {string} up-source
The URL from which to reload this element.
@stable
*/
/*-
Replaces elements on the current page with matching elements from a server response or HTML string.
The current and new elements must both match the same CSS selector.
The selector is either given as `{ target }` option,
or a [main target](/up-main) is used as default.
See the [fragment placement](/fragment-placement) selector for many examples for how you can target content.
This function has many options to enable scrolling, focus, request cancelation and other side
effects. These options are all disabled by default and must be opted into one-by-one. To enable
defaults that a user would expects for navigation (like clicking a link),
pass [`{ navigate: true }`](#options.navigate) or use `up.navigate()` instead.
### Passing the new fragment
The new fragment content can be passed as one of the following options:
- [`{ url }`](#options.url) fetches and renders content from the server
- [`{ document }`](#options.document) renders content from a given HTML document string or partial document
- [`{ fragment }`](#options.fragment) renders content from a given HTML string that only contains the new fragment
- [`{ content }`](#options.content) replaces the targeted fragment's inner HTML with the given HTML string
### Example
Let's say your current HTML looks like this:
```html
<div class="one">old one</div>
<div class="two">old two</div>
```
We now replace the second `<div>` by targeting its CSS class:
```js
up.render({ target: '.two', url: '/new' })
```
The server renders a response for `/new`:
```html
<div class="one">new one</div>
<div class="two">new two</div>
```
Unpoly looks for the selector `.two` in the response and [implants](/up.extract) it into
the current page. The current page now looks like this:
```html
<div class="one">old one</div>
<div class="two">new two</div>
```
Note how only `.two` has changed. The update for `.one` was
discarded, since it didn't match the selector.
### Events
Unpoly will emit events at various stages of the rendering process:
- `up:fragment:destroyed`
- `up:fragment:loaded`
- `up:fragment:inserted`
@function up.render
@param {string|Element|jQuery|Array<string>} [target]
The CSS selector to update.
If omitted a [main target](/up-main) will be rendered.
You may also pass a DOM element or jQuery element here, in which case a selector
will be [inferred from the element attributes](/up.fragment.toTarget). The given element
will also be used as [`{ origin }`](#options.origin) for the fragment update.
You may also pass an array of selector alternatives. The first selector
matching in both old and new content will be used.
Instead of passing the target as the first argument, you may also pass it as
a [´{ target }`](#options.target) option..
@param {string|Element|jQuery|Array<string>} [options.target]
The CSS selector to update.
See documentation for the [`target`](#target) parameter.
@param {string|boolean} [options.fallback=false]
Specifies behavior if the [target selector](/up.render#options.target) is missing from the current page or the server response.
If set to a CSS selector string, Unpoly will attempt to replace that selector instead.
If set to `true` Unpoly will attempt to replace a [main target](/up-main) instead.
If set to `false` Unpoly will immediately reject the render promise.
@param {boolean} [options.navigate=false]
Whether this fragment update is considered [navigation](/navigation).
@param {string} [options.url]
The URL to fetch from the server.
Instead of making a server request, you may also pass an existing HTML string as
[`{ document }`](#options.document), [`{ fragment }`](#options.fragment) or
[`{ content }`](#options.content) option.
@param {string} [options.method='get']
The HTTP method to use for the request.
Common values are `'get'`, `'post'`, `'put'`, `'patch'` and `'delete`'.
The value is case insensitive.
@param {Object|FormData|string|Array} [options.params]
Additional [parameters](/up.Params) that should be sent as the request's
[query string](https://en.wikipedia.org/wiki/Query_string) or payload.
When making a `GET` request to a URL with a query string, the given `{ params }` will be added
to the query parameters.
@param {Object} [options.headers={}]
An object with additional request headers.
Note that Unpoly will by default send a number of custom request headers.
E.g. the `X-Up-Target` header includes the targeted CSS selector.
See `up.protocol` and `up.network.config.requestMetaKeys` for details.
@param {string|Element} [options.content]
The new [inner HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML)
for the fragment.
@param {string|Element} [options.fragment]
A string of HTML comprising *only* the new fragment's [outer HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML).
The `{ target }` selector will be derived from the root element in the given
HTML:
```js
// This will update .foo
up.render({ fragment: '<div class=".foo">inner</div>' })
```
If your HTML string contains other fragments that will not be rendered, use
the [`{ document }`](#options.document) option instead.
If your HTML string comprises only the new fragment's [inner HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML),
consider the [`{ content }`](#options.content) option.
@param {string|Element|Document} [options.document]
A string of HTML containing the new fragment.
The string may contain other HTML, but only the element matching the
`{ target }` selector will be extracted and placed into the page.
Other elements will be discarded.
If your HTML string comprises only the new fragment, consider the [`{ fragment }`](#options.fragment)
option instead. With `{ fragment }` you don't need to pass a `{ target }`, since
Unpoly can derive it from the root element in the given HTML.
If your HTML string comprises only the new fragment's [inner HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML),
consider the [`{ content }`](#options.content) option.
@param {string} [options.fail='auto']
How to render a server response with an error code.
Any HTTP status code other than 2xx is considered an error code.
See [handling server errors](/server-errors) for details.
@param {boolean|string} [options.history]
Whether the browser URL and window title will be updated.
If set to `true`, the history will always be updated, using the title and URL from
the server response, or from given `{ title }` and `{ location }` options.
If set to `'auto'` history will be updated if the `{ target }` matches
a selector in `up.fragment.config.autoHistoryTargets`. By default this contains all
[main targets](/up-main).
If set to `false`, the history will remain unchanged.
@param {string} [options.title]
An explicit document title to use after rendering.
By default the title is extracted from the response's `<title>` tag.
You may also pass `{ title: false }` to explicitly prevent the title from being updated.
Note that the browser's window title will only be updated it you also
pass a [`{ history }`](#options.history) option.
@param {string} [options.location]
An explicit URL to use after rendering.
By default Unpoly will use the `{ url }` or the final URL after the server redirected.
You may also pass `{ location: false }` to explicitly prevent the URL from being updated.
Note that the browser's URL will only be updated it you also
pass a [`{ history }`](#options.history) option.
@param {string} [options.transition]
The name of an [transition](/up.motion) to morph between the old and few fragment.
If you are [prepending or appending content](/fragment-placement#appending-or-prepending-content),
use the `{ animation }` option instead.
@param {string} [options.animation]
The name of an [animation](/up.motion) to reveal a new fragment when
[prepending or appending content](/fragment-placement#appending-or-prepending-content).
If you are replacing content (the default), use the `{ transition }` option instead.
@param {number} [options.duration]
The duration of the transition or animation (in millisconds).
@param {string} [options.easing]
The timing function that accelerates the transition or animation.
See [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-timing-function)
for a list of available timing functions.
@param {boolean} [options.cache]
Whether to read from and write to the [cache](/up.request#caching).
With `{ cache: true }` Unpoly will try to re-use a cached response before connecting
to the network. If no cached response exists, Unpoly will make a request and cache
the server response.
Also see [`up.request({ cache })`](/up.request#options.cache).
@param {boolean|string} [options.clearCache]
Whether existing [cache](/up.request#caching) entries will be [cleared](/up.cache.clear) with this request.
Defaults to the result of `up.network.config.clearCache`, which
defaults to clearing the entire cache after a non-GET request.
To only uncache some requests, pass an [URL pattern](/url-patterns) that matches requests to uncache.
You may also pass a function that accepts an existing `up.Request` and returns a boolean value.
@param {boolean|string|Function(request): boolean} [options.solo]
With `{ solo: true }` Unpoly will [abort](/up.network.abort) all other requests before laoding the new fragment.
To only abort some requests, pass an [URL pattern](/url-patterns) that matches requests to abort.
You may also pass a function that accepts an existing `up.Request` and returns a boolean value.
@param {Element|jQuery} [options.origin]
The element that triggered the change.
When multiple elements in the current page match the `{ target }`,
Unpoly will replace an element in the [origin's vicinity](/fragment-placement).
The origin's selector will be substituted for `:origin` in a target selector.
@param {string|up.Layer|Element} [options.layer='origin current']
The [layer](/up.layer) in which to match and render the fragment.
See [layer option](/layer-option) for a list of allowed values.
To [open the fragment in a new overlay](/opening-overlays), pass `{ layer: 'new' }`.
In this case options for `up.layer.open()` may also be used.
@param {boolean} [options.peel]
Whether to close overlays obstructing the updated layer when the fragment is updated.
This is only relevant when updating a layer that is not the [frontmost layer](/up.layer.front).
@param {Object} [options.context]
An object that will be merged into the [context](/context) of the current layer once the fragment is rendered.
@param {boolean} [options.keep=true]
Whether [`[up-keep]`](/up-keep) elements will be preserved in the updated fragment.
@param {boolean} [options.hungry=true]
Whether [`[up-hungry]`](/up-hungry) elements outside the updated fragment will also be updated.
@param {boolean|string|Element|Function} [options.scroll]
How to scroll after the new fragment was rendered.
See [scroll option](/scroll-option) for a list of allowed values.
@param {boolean} [options.saveScroll=true]
Whether to save scroll positions before updating the fragment.
Saved scroll positions can later be restored with [`{ scroll: 'restore' }`](/scroll-option#restoring-scroll-positions).
@param {boolean|string|Element|Function} [options.focus]
What to focus after the new fragment was rendered.
See [focus option](/focus-option) for a list of allowed values.
@param {string} [options.confirm]
A message the user needs to confirm before fragments are updated.
The message will be shown as a [native browser prompt](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt).
If the user does not confirm the render promise will reject and no fragments will be updated.
@param {boolean|Element} [options.feedback]
Whether to give the [`{ origin }`](#options.origin) element an `.up-active` class
while loading and rendering content.
May also pass an element which should receive the `.up-active` class.
@param {Function(Event)} [options.onLoaded]
A callback that will be run when when the server responds with new HTML,
but before the HTML is rendered.
The callback argument is a preventable `up:fragment:loaded` event.
@param {Function()} [options.onFinished]
A callback that will be run when all animations have concluded and
elements were removed from the DOM tree.
@return {Promise<up.RenderResult>}
A promise that fulfills when the page has been updated.
If the update is animated, the promise will be resolved *before* the existing element was
removed from the DOM tree. The old element will be marked with the `.up-destroying` class
and removed once the animation finishes. To run code after the old element was removed,
pass an `{ onFinished }` callback.
The promise will fulfill with an `up.RenderResult` that contains
references to the updated fragments and layer.
@stable
*/
const render = up.mockable((...args) => {
// Convert thrown errors into rejected promises.
// Convert non-promise values into a resolved promise.
return u.asyncify(function () {
let options = parseTargetAndOptions(args)
options = up.RenderOptions.preprocess(options)
up.browser.assertConfirmed(options)
let guardEvent = u.pluckKey(options, 'guardEvent')
if (guardEvent) {
// Allow guard event handlers to manipulate render options for the default behavior.
//
// Note that we have removed { guardEvent } from options to not recursively define
// guardEvent.renderOptions.guardEvent. This would cause an infinite loop for event
// listeners that prevent the default and re-render.
guardEvent.renderOptions = options
up.event.assertEmitted(guardEvent, {target: options.origin})
}
up.RenderOptions.assertContentGiven(options)
return (options.url ? renderRemoteContent : renderLocalContent)(options)
})
})
function renderRemoteContent(options) {
// Rendering a remote URL is an async operation.
// We give feedback (.up-active) while the fragment is loading.
let execute = () => new up.Change.FromURL(options).execute()
return up.feedback.aroundForOptions(options, execute)
}
function renderLocalContent(options) {
// When we have a given { url }, the { solo } option is honored by up.request().
// But up.request() is never called when we have local content given as { document },
// { content } or { fragment }. Hence we abort here.
up.network.mimicLocalRequest(options)
// (1) No need to give feedback as local changes are sync.
// (2) Value will be converted to a fulfilled promise by up.util.asyncify() in render().
return new up.Change.FromContent(options).execute()
}
/*-
[Navigates](/navigation) to the given URL by updating a major fragment in the current page.
`up.navigate()` will mimic a click on a vanilla `<a href>` link to satisfy user expectations
regarding scrolling, focus, request cancelation and [many other side effects](/navigation).
Instead of calling `up.navigate()` you may also call `up.render({ navigate: true }`) option
for the same effect.
@function up.navigate
@param {string|Element|jQuery} [target]
The CSS selector to update.
If omitted a [main target](/up-main) will be rendered.
You can also pass a DOM element or jQuery element here, in which case a selector
will be [inferred from the element attributes](/up.fragment.toTarget). The given element
will also be set as the `{ origin }` option.
Instead of passing the target as the first argument, you may also pass it as
[´{ target }` option](/up.render#options.target).
@param {Object} [options]
See options for `up.render()`.
@return {Promise<up.RenderResult>}
A promise that fulfills when the page has been updated.
For details, see return value for `up.render()`.
@stable
*/
const navigate = up.mockable((...args) => {
const options = parseTargetAndOptions(args)
return render({...options, navigate: true})
})
/*-
This event is [emitted](/up.emit) when the server responds with the HTML, before
the HTML is used to [change a fragment](/up.render).
Event listeners may call `event.preventDefault()` on an `up:fragment:loaded` event
to prevent any changes to the DOM and browser history. This is useful to detect
an entirely different page layout (like a maintenance page or fatal server error)
which should be open with a full page load:
```js
up.on('up:fragment:loaded', (event) => {
let isMaintenancePage = event.response.getHeader('X-Maintenance')
if (isMaintenancePage) {
// Prevent the fragment update and don't update browser history
event.preventDefault()
// Make a full page load for the same request.
event.request.loadPage()
}
})
```
Instead of preventing the update, listeners may also access the `event.renderOptions` object
to mutate options to the `up.render()` call that will process the server response.
@event up:fragment:loaded
@param event.preventDefault()
Event listeners may call this method to prevent the fragment change.
@param {up.Request} event.request
The original request to the server.
@param {up.Response} event.response
The server response.
@param {Element} [event.origin]
The link or form element that caused the fragment update.
@param {Object} event.renderOptions
Options for the `up.render()` call that will process the server response.
@stable
*/
/*-
Elements with an `up-keep` attribute will be persisted during
[fragment updates](/up.fragment).
The element you're keeping should have an umambiguous class name, ID or `[up-id]`
attribute so Unpoly can find its new position within the page update.
Emits events [`up:fragment:keep`](/up:fragment:keep) and [`up:fragment:kept`](/up:fragment:kept).
### Example
The following `<audio>` element will be persisted through fragment
updates as long as the responses contain an element matching `#player`:
```html
<audio id="player" up-keep src="song.mp3"></audio>
```
### Controlling if an element will be kept
Unpoly will **only** keep an existing element if:
- The existing element has an `up-keep` attribute
- The response contains an element matching the CSS selector of the existing element
- The matching element *also* has an `up-keep` attribute
- The [`up:fragment:keep`](/up:fragment:keep) event that is [emitted](/up.emit) on the existing element
is not prevented by a event listener.
Let's say we want only keep an `<audio>` element as long as it plays
the same song (as identified by the tag's `src` attribute).
On the client we can achieve this by listening to an `up:keep:fragment` event
and preventing it if the `src` attribute of the old and new element differ:
```js
up.compiler('audio', function(element) {
element.addEventListener('up:fragment:keep', function(event) {
if element.getAttribute('src') !== event.newElement.getAttribute('src') {
event.preventDefault()
}
})
})
```
If we don't want to solve this on the client, we can achieve the same effect
on the server. By setting the value of the `up-keep` attribute we can
define the CSS selector used for matching elements.
```html
<audio up-keep="audio[src='song.mp3']" src="song.mp3"></audio>
```
Now, if a response no longer contains an `<audio src="song.mp3">` tag, the existing
element will be destroyed and replaced by a fragment from the response.
@selector [up-keep]
@param up-on-keep
Code to run before an existing element is kept during a page update.
The code may use the variables `event` (see `up:fragment:keep`),
`this` (the old fragment), `newFragment` and `newData`.
@stable
*/
/*-
This event is [emitted](/up.emit) before an existing element is [kept](/up-keep) during
a page update.
Event listeners can call `event.preventDefault()` on an `up:fragment:keep` event
to prevent the element from being persisted. If the event is prevented, the element
will be replaced by a fragment from the response.
@event up:fragment:keep
@param event.preventDefault()
Event listeners may call this method to prevent the element from being preserved.
@param {Element} event.target
The fragment that will be kept.
@param {Element} event.newFragment
The discarded element.
@param {Object} event.newData
The value of the [`up-data`](/up-data) attribute of the discarded element,
parsed as a JSON object.
@stable
*/
/*-
This event is [emitted](/up.emit) when an existing element has been [kept](/up-keep)
during a page update.
Event listeners can inspect the discarded update through `event.newElement`
and `event.newData` and then modify the preserved element when necessary.
@event up:fragment:kept
@param {Element} event.target
The fragment that has been kept.
@param {Element} event.newFragment
The discarded fragment.
@param {Object} event.newData
The value of the [`up-data`](/up-data) attribute of the discarded fragment,
parsed as a JSON object.
@stable
*/
/*-
Manually compiles a page fragment that has been inserted into the DOM
by external code.
All registered [compilers](/up.compiler) and [macros](/up.macro) will be called
with matches in the given `element`.
**As long as you manipulate the DOM using Unpoly, you will never
need to call `up.hello()`.** You only need to use `up.hello()` if the
DOM is manipulated without Unpoly' involvement, e.g. by setting
the `innerHTML` property:
```html
element = document.createElement('div')
element.innerHTML = '... HTML that needs to be activated ...'
up.hello(element)
```
This function emits the [`up:fragment:inserted`](/up:fragment:inserted)
event.
@function up.hello
@param {Element|jQuery} element
@param {Element|jQuery} [options.origin]
@return {Element}
The compiled element
@stable
*/
function hello(element, options = {}) {
// If passed a selector, up.fragment.get() will prefer a match on the current layer.
element = getSmart(element)
// Callers may pass descriptions of child elements that were [kept](/up-keep)
// as { options.keepPlans }. For these elements up.hello() emits an event
// up:fragment:kept instead of up:fragment:inserted.
//
// We will also pass an array of kept child elements to up.hello() as { skip }
// so they won't be compiled a second time.
const keepPlans = options.keepPlans || []
const skip = keepPlans.map(function (plan) {
emitFragmentKept(plan)
return plan.oldElement // the kept element
})
up.syntax.compile(element, { skip, layer: options.layer })
emitFragmentInserted(element, options)
return element
}
/*-
When any page fragment has been [inserted or updated](/up.replace),
this event is [emitted](/up.emit) on the fragment.
If you're looking to run code when a new fragment matches
a selector, use `up.compiler()` instead.
### Example
```js
up.on('up:fragment:inserted', function(event, fragment) {
console.log("Looks like we have a new %o!", fragment)
})
```
@event up:fragment:inserted
@param {Element} event.target
The fragment that has been inserted or updated.
@stable
*/
function emitFragmentInserted(element, options) {
return up.emit(element, 'up:fragment:inserted', {
log: ['Inserted fragment %o', element],
origin: options.origin
})
}
function emitFragmentKeep(keepPlan) {
const log = ['Keeping fragment %o', keepPlan.oldElement]
const callback = e.callbackAttr(keepPlan.oldElement, 'up-on-keep', ['newFragment', 'newData'])
return emitFromKeepPlan(keepPlan, 'up:fragment:keep', {log, callback})
}
function emitFragmentKept(keepPlan) {
const log = ['Kept fragment %o', keepPlan.oldElement]
return emitFromKeepPlan(keepPlan, 'up:fragment:kept', {log})
}
function emitFromKeepPlan(keepPlan, eventType, emitDetails) {
const keepable = keepPlan.oldElement
const event = up.event.build(eventType, {
newFragment: keepPlan.newElement,
newData: keepPlan.newData
})
return up.emit(keepable, event, emitDetails)
}
function emitFragmentDestroyed(fragment, options) {
const log = options.log ?? ['Destroyed fragment %o', fragment]
const parent = options.parent || document
return up.emit(parent, 'up:fragment:destroyed', {fragment, parent, log})
}
function isDestroying(element) {
return !!e.closest(element, '.up-destroying')
}
const isNotDestroying = u.negate(isDestroying)
/*-
Returns the first fragment matching the given selector.
This function differs from `document.querySelector()` and `up.element.get()`:
- This function only selects elements in the [current layer](/up.layer.current).
Pass a `{ layer }`option to match elements in other layers.
- This function ignores elements that are being [destroyed](/up.destroy) or that are being
removed by a [transition](/up.morph).
- This function prefers to match elements in the vicinity of a given `{ origin }` element (optional).
- This function supports non-standard CSS selectors like `:main` and `:has()`.
If no element matches these conditions, `undefined` is returned.
### Example: Matching a selector in a layer
To select the first element with the selector `.foo` on the [current layer](/up.layer.current):
```js
let foo = up.fragment.get('.foo')
```
You may also pass a `{ layer }` option to match elements within another layer:
```js
let foo = up.fragment.get('.foo', { layer: 'any' })
```
### Example: Matching the descendant of an element
To only select in the descendants of an element, pass a root element as the first argument:
```js
let container = up.fragment.get('.container')
let fooInContainer = up.fragment.get(container, '.foo')
```
### Example: Matching around an origin element
When processing a user interaction, it is often helpful to match elements around the link
that's being clicked or the form field that's being changed. In this case you may pass
the triggering element as `{ origin }` element.
Assume the following HTML:
```html
<div class="element"></div>
<div class="element">
<a href="..."></a>
</div>
```