Skip to content

Commit a9c0ff2

Browse files
committed
Merge branch '2.0' of github.com:conestack/cone.app into 2.0
2 parents 63c7b85 + 3f3f92e commit a9c0ff2

7 files changed

Lines changed: 324 additions & 6 deletions

File tree

js/src/referencebrowser.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export class ReferenceHandle {
1313
}
1414
let ol = ol_elem.data('overlay'),
1515
target = ol.ref_target;
16+
// Skip binding if no ref_target is set. This allows other components
17+
// to reuse the referencebrowser overlay with custom selection handling.
18+
if (!target) {
19+
return;
20+
}
1621
$('a.addreference', context).each(function() {
1722
new AddReferenceHandle($(this), target, ol);
1823
});

js/src/scrollbar.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,49 @@ export class Scrollbar extends ts.Motion {
4646
this.scroll_step = 50; // Scroll step in pixels
4747
new ts.Property(this, 'disabled', false);
4848

49-
ts.clock.schedule_frame(() => this.render());
50-
5149
const is_mobile = $(window).width() <= 768; // bs5 small/medium breakpoint
5250
new ts.Property(this, 'is_mobile', is_mobile);
51+
52+
// Persist scroll position
53+
this.persist_scroll = elem.data('persist-scroll') === true;
54+
this._persist_key = elem.data('persist-scroll-key') || elem.attr('id') || null;
55+
if (this.persist_scroll && this.storage_key) {
56+
this._save_position = this._save_position.bind(this);
57+
this.on('on_position', this._save_position);
58+
}
59+
60+
ts.clock.schedule_frame(() => {
61+
// Read saved position BEFORE render() triggers save handler
62+
const saved = this.persist_scroll && this.storage_key
63+
? sessionStorage.getItem(this.storage_key)
64+
: null;
65+
this.render();
66+
if (saved !== null) {
67+
this.position = parseFloat(saved);
68+
}
69+
});
70+
}
71+
72+
/**
73+
* Returns the sessionStorage key for persisting scroll position.
74+
* @returns {string|null}
75+
*/
76+
get storage_key() {
77+
if (!this._persist_key) {
78+
return null;
79+
}
80+
return `cone.app.scroll.${this._persist_key}`;
81+
}
82+
83+
/**
84+
* Saves the current scroll position to sessionStorage.
85+
* @param {Scrollbar} inst - The scrollbar instance (from event)
86+
* @param {number} pos - The scroll position
87+
*/
88+
_save_position(inst, pos) {
89+
if (this.storage_key) {
90+
sessionStorage.setItem(this.storage_key, pos);
91+
}
5392
}
5493

5594
/**
@@ -153,6 +192,9 @@ export class Scrollbar extends ts.Motion {
153192
if (this.fade_out_timeout) {
154193
clearTimeout(this.fade_out_timeout);
155194
}
195+
if (this.persist_scroll && this._save_position) {
196+
this.off('on_position', this._save_position);
197+
}
156198
this.unbind();
157199
this.elem.removeData('scrollbar');
158200
}

js/tests/test_referencebrowser.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ QUnit.module('cone.app.referencebrowser.ReferenceHandle', hooks => {
3535
assert.ok(true, 'no error without modal parent');
3636
});
3737

38+
QUnit.test('ReferenceHandle.initialize returns early without ref_target',
39+
assert => {
40+
// This allows other components (e.g. catalog item picker) to reuse
41+
// the referencebrowser overlay with custom selection handling.
42+
let modal = $('<div class="modal" />').appendTo(container);
43+
let context = $('<div class="modal-body" />').appendTo(modal);
44+
let link = $('<a class="addreference" id="ref-123" />').appendTo(context);
45+
46+
let overlay = {elem: modal}; // no ref_target
47+
modal.data('overlay', overlay);
48+
49+
ReferenceHandle.initialize(context);
50+
51+
let events = $._data(link.get(0), 'events');
52+
assert.notOk(events && events.click,
53+
'no click handler bound when ref_target missing');
54+
});
55+
3856
QUnit.test('ReferenceHandle constructor stores properties', assert => {
3957
let elem = $('<a />');
4058
let target = $('<input />');

js/tests/test_scrollbar.js

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,213 @@ QUnit.module('cone.app.scrollbar.ScrollbarX', hooks => {
386386
scrollbar.destroy();
387387
});
388388
});
389+
390+
QUnit.module('cone.app.scrollbar.persist', hooks => {
391+
392+
let container;
393+
394+
hooks.beforeEach(() => {
395+
container = $('<div />').appendTo('body');
396+
sessionStorage.clear();
397+
});
398+
399+
hooks.afterEach(() => {
400+
container.remove();
401+
sessionStorage.clear();
402+
});
403+
404+
QUnit.test('persist_scroll defaults to false', assert => {
405+
let elem = $(`
406+
<div class="scrollable-y">
407+
<div class="scrollable-content" style="height: 500px;"></div>
408+
</div>
409+
`).css({height: '200px', position: 'relative'}).appendTo(container);
410+
411+
let scrollbar = new ScrollbarY(elem);
412+
413+
assert.strictEqual(scrollbar.persist_scroll, false, 'persist_scroll is false by default');
414+
415+
scrollbar.destroy();
416+
});
417+
418+
QUnit.test('persist_scroll reads from data attribute', assert => {
419+
let elem = $(`
420+
<div class="scrollable-y" data-persist-scroll="true">
421+
<div class="scrollable-content" style="height: 500px;"></div>
422+
</div>
423+
`).css({height: '200px', position: 'relative'}).appendTo(container);
424+
425+
let scrollbar = new ScrollbarY(elem);
426+
427+
assert.strictEqual(scrollbar.persist_scroll, true, 'persist_scroll reads data attribute');
428+
429+
scrollbar.destroy();
430+
});
431+
432+
QUnit.test('storage_key uses element id', assert => {
433+
let elem = $(`
434+
<div id="test_scrollbar" class="scrollable-y" data-persist-scroll="true">
435+
<div class="scrollable-content" style="height: 500px;"></div>
436+
</div>
437+
`).css({height: '200px', position: 'relative'}).appendTo(container);
438+
439+
let scrollbar = new ScrollbarY(elem);
440+
441+
assert.strictEqual(scrollbar.storage_key, 'cone.app.scroll.test_scrollbar', 'storage_key uses id');
442+
443+
scrollbar.destroy();
444+
});
445+
446+
QUnit.test('storage_key uses custom key from data attribute', assert => {
447+
let elem = $(`
448+
<div id="test_scrollbar" class="scrollable-y"
449+
data-persist-scroll="true"
450+
data-persist-scroll-key="custom_key">
451+
<div class="scrollable-content" style="height: 500px;"></div>
452+
</div>
453+
`).css({height: '200px', position: 'relative'}).appendTo(container);
454+
455+
let scrollbar = new ScrollbarY(elem);
456+
457+
assert.strictEqual(scrollbar.storage_key, 'cone.app.scroll.custom_key', 'storage_key uses custom key');
458+
459+
scrollbar.destroy();
460+
});
461+
462+
QUnit.test('storage_key is null without id or custom key', assert => {
463+
let elem = $(`
464+
<div class="scrollable-y" data-persist-scroll="true">
465+
<div class="scrollable-content" style="height: 500px;"></div>
466+
</div>
467+
`).css({height: '200px', position: 'relative'}).appendTo(container);
468+
469+
let scrollbar = new ScrollbarY(elem);
470+
471+
assert.strictEqual(scrollbar.storage_key, null, 'storage_key is null without id');
472+
473+
scrollbar.destroy();
474+
});
475+
476+
QUnit.test('position is saved to sessionStorage on scroll', assert => {
477+
let done = assert.async();
478+
let elem = $(`
479+
<div id="persist_test" class="scrollable-y" data-persist-scroll="true">
480+
<div class="scrollable-content" style="height: 500px;"></div>
481+
</div>
482+
`).css({height: '200px', position: 'relative'}).appendTo(container);
483+
484+
let scrollbar = new ScrollbarY(elem);
485+
scrollbar.position = 100;
486+
487+
// Allow event to fire
488+
setTimeout(() => {
489+
let saved = sessionStorage.getItem('cone.app.scroll.persist_test');
490+
assert.strictEqual(saved, '100', 'position saved to sessionStorage');
491+
scrollbar.destroy();
492+
done();
493+
}, 50);
494+
});
495+
496+
QUnit.test('position is restored from sessionStorage', assert => {
497+
let done = assert.async();
498+
sessionStorage.setItem('cone.app.scroll.restore_test', '150');
499+
500+
let elem = $(`
501+
<div id="restore_test" class="scrollable-y" data-persist-scroll="true">
502+
<div class="scrollable-content" style="height: 500px;"></div>
503+
</div>
504+
`).css({height: '200px', position: 'relative'}).appendTo(container);
505+
506+
let scrollbar = new ScrollbarY(elem);
507+
508+
setTimeout(() => {
509+
assert.strictEqual(scrollbar.position, 150, 'position restored from sessionStorage');
510+
scrollbar.destroy();
511+
done();
512+
}, 100);
513+
});
514+
515+
QUnit.test('restored position is clamped when content shrinks', assert => {
516+
let done = assert.async();
517+
// Save a position that will be too large
518+
sessionStorage.setItem('cone.app.scroll.clamp_test', '500');
519+
520+
let elem = $(`
521+
<div id="clamp_test" class="scrollable-y" data-persist-scroll="true">
522+
<div class="scrollable-content" style="height: 300px;"></div>
523+
</div>
524+
`).css({height: '200px', position: 'relative'}).appendTo(container);
525+
526+
let scrollbar = new ScrollbarY(elem);
527+
528+
// max_pos = 300 - 200 = 100
529+
setTimeout(() => {
530+
assert.strictEqual(scrollbar.position, 100, 'position clamped to max');
531+
scrollbar.destroy();
532+
done();
533+
}, 100);
534+
});
535+
536+
QUnit.test('position not saved when persist_scroll is false', assert => {
537+
let elem = $(`
538+
<div id="no_persist" class="scrollable-y">
539+
<div class="scrollable-content" style="height: 500px;"></div>
540+
</div>
541+
`).css({height: '200px', position: 'relative'}).appendTo(container);
542+
543+
let scrollbar = new ScrollbarY(elem);
544+
scrollbar.position = 100;
545+
546+
let saved = sessionStorage.getItem('cone.app.scroll.no_persist');
547+
assert.strictEqual(saved, null, 'position not saved when persist disabled');
548+
549+
scrollbar.destroy();
550+
});
551+
552+
QUnit.test('position not saved without storage_key', assert => {
553+
let elem = $(`
554+
<div class="scrollable-y" data-persist-scroll="true">
555+
<div class="scrollable-content" style="height: 500px;"></div>
556+
</div>
557+
`).css({height: '200px', position: 'relative'}).appendTo(container);
558+
559+
let scrollbar = new ScrollbarY(elem);
560+
scrollbar.position = 100;
561+
562+
// No key to check, but should not throw
563+
assert.ok(true, 'no error when no storage_key');
564+
565+
scrollbar.destroy();
566+
});
567+
568+
QUnit.test('destroy removes position listener', assert => {
569+
let done = assert.async();
570+
let elem = $(`
571+
<div id="destroy_test" class="scrollable-y" data-persist-scroll="true">
572+
<div class="scrollable-content" style="height: 500px;"></div>
573+
</div>
574+
`).css({height: '200px', position: 'relative'}).appendTo(container);
575+
576+
let scrollbar = new ScrollbarY(elem);
577+
scrollbar.destroy();
578+
579+
sessionStorage.removeItem('cone.app.scroll.destroy_test');
580+
581+
// Create new scrollbar on same elem to test position change
582+
let elem2 = $(`
583+
<div id="destroy_test2" class="scrollable-y">
584+
<div class="scrollable-content" style="height: 500px;"></div>
585+
</div>
586+
`).css({height: '200px', position: 'relative'}).appendTo(container);
587+
588+
let scrollbar2 = new ScrollbarY(elem2);
589+
scrollbar2.position = 200;
590+
591+
setTimeout(() => {
592+
let saved = sessionStorage.getItem('cone.app.scroll.destroy_test');
593+
assert.strictEqual(saved, null, 'destroyed scrollbar does not save');
594+
scrollbar2.destroy();
595+
done();
596+
}, 50);
597+
});
598+
});

src/cone/app/browser/static/cone/cone.app.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,9 @@ var cone = (function (exports, $, ts) {
438438
}
439439
let ol = ol_elem.data('overlay'),
440440
target = ol.ref_target;
441+
if (!target) {
442+
return;
443+
}
441444
$('a.addreference', context).each(function() {
442445
new AddReferenceHandle($(this), target, ol);
443446
});
@@ -768,9 +771,42 @@ var cone = (function (exports, $, ts) {
768771
this.position = 0;
769772
this.scroll_step = 50;
770773
new ts.Property(this, 'disabled', false);
771-
ts.clock.schedule_frame(() => this.render());
772774
const is_mobile = $(window).width() <= 768;
773775
new ts.Property(this, 'is_mobile', is_mobile);
776+
this.persist_scroll = elem.data('persist-scroll') === true;
777+
this._persist_key = elem.data('persist-scroll-key') || elem.attr('id') || null;
778+
if (this.persist_scroll && this.storage_key) {
779+
this._save_position = this._save_position.bind(this);
780+
this.on('on_position', this._save_position);
781+
}
782+
ts.clock.schedule_frame(() => {
783+
const saved_position = this._get_saved_position();
784+
this.render();
785+
this._restore_position(saved_position);
786+
});
787+
}
788+
get storage_key() {
789+
if (!this._persist_key) {
790+
return null;
791+
}
792+
return `cone.app.scroll.${this._persist_key}`;
793+
}
794+
_save_position(inst, pos) {
795+
if (this.storage_key) {
796+
sessionStorage.setItem(this.storage_key, pos);
797+
}
798+
}
799+
_get_saved_position() {
800+
if (!this.persist_scroll || !this.storage_key) {
801+
return null;
802+
}
803+
const saved = sessionStorage.getItem(this.storage_key);
804+
return saved !== null ? parseFloat(saved) : null;
805+
}
806+
_restore_position(saved_position) {
807+
if (saved_position !== null) {
808+
this.position = saved_position;
809+
}
774810
}
775811
on_window_resize(evt) {
776812
this.is_mobile = $(window).innerWidth() <= 768;
@@ -827,6 +863,9 @@ var cone = (function (exports, $, ts) {
827863
if (this.fade_out_timeout) {
828864
clearTimeout(this.fade_out_timeout);
829865
}
866+
if (this.persist_scroll && this._save_position) {
867+
this.off('on_position', this._save_position);
868+
}
830869
this.unbind();
831870
this.elem.removeData('scrollbar');
832871
}

src/cone/app/browser/static/cone/cone.app.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cone/app/browser/templates/layout.pt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@
107107
</tal:controls>
108108
</div>
109109

110-
<div class="sidebar-content scrollable-y scrollbar-left">
110+
<div class="sidebar-content scrollable-y scrollbar-left"
111+
data-persist-scroll="true"
112+
data-persist-scroll-key="sidebar-left">
111113
<div class="scrollable-content px-2">
112114
<ul class="sidebar-tiles nav nav-pills flex-nowrap flex-column mb-auto">
113115

@@ -178,7 +180,9 @@
178180
</tal:controls>
179181
</div>
180182

181-
<div class="sidebar-content scrollable-y scrollbar-right h-100">
183+
<div class="sidebar-content scrollable-y scrollbar-right h-100"
184+
data-persist-scroll="true"
185+
data-persist-scroll-key="sidebar-right">
182186
<div class="sidebar-tiles scrollable-content px-2">
183187
<tal:tiles repeat="tilename config.sidebar_right">
184188
<div class="tile"

0 commit comments

Comments
 (0)