@tienne/gestalt 0.114.0 → 0.116.0
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.
- package/README.ko.md +1 -0
- package/README.md +1 -0
- package/dist/package.json +1 -1
- package/dist/plugin/role-agents/lens-reader/AGENT.md +22 -1
- package/dist/plugin/skills/_shared/proactive-routing.md +1 -1
- package/dist/plugin/skills/lens/SKILL.md +114 -8
- package/dist/plugin/skills/share/SKILL.md +141 -0
- package/dist/plugin/skills/share/agents/openai.yaml +5 -0
- package/dist/plugin/skills/video-digest/SKILL.md +4 -2
- package/dist/plugin/skills/video-digest/templates/comments.md +1 -0
- package/dist/src/cli/commands/digest.d.ts +1 -0
- package/dist/src/cli/commands/digest.d.ts.map +1 -1
- package/dist/src/cli/commands/digest.js +15 -4
- package/dist/src/cli/commands/digest.js.map +1 -1
- package/dist/src/cli/commands/peer.d.ts +5 -1
- package/dist/src/cli/commands/peer.d.ts.map +1 -1
- package/dist/src/cli/commands/peer.js +74 -13
- package/dist/src/cli/commands/peer.js.map +1 -1
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +7 -1
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/digest/render-blocks.d.ts +5 -0
- package/dist/src/digest/render-blocks.d.ts.map +1 -1
- package/dist/src/digest/render-blocks.js +2 -2
- package/dist/src/digest/render-blocks.js.map +1 -1
- package/dist/src/digest/render-html.d.ts +2 -2
- package/dist/src/digest/render-html.d.ts.map +1 -1
- package/dist/src/digest/render-html.js +20 -3
- package/dist/src/digest/render-html.js.map +1 -1
- package/dist/src/digest/render-md.d.ts.map +1 -1
- package/dist/src/digest/render-md.js +9 -2
- package/dist/src/digest/render-md.js.map +1 -1
- package/dist/src/digest/server.d.ts +46 -1
- package/dist/src/digest/server.d.ts.map +1 -1
- package/dist/src/digest/server.js +213 -10
- package/dist/src/digest/server.js.map +1 -1
- package/dist/src/digest/session-request.d.ts +78 -0
- package/dist/src/digest/session-request.d.ts.map +1 -0
- package/dist/src/digest/session-request.js +105 -0
- package/dist/src/digest/session-request.js.map +1 -0
- package/dist/src/digest/svg.d.ts +5 -0
- package/dist/src/digest/svg.d.ts.map +1 -1
- package/dist/src/digest/svg.js +19 -9
- package/dist/src/digest/svg.js.map +1 -1
- package/dist/src/digest/templates.d.ts +9 -0
- package/dist/src/digest/templates.d.ts.map +1 -1
- package/dist/src/digest/templates.js +12 -0
- package/dist/src/digest/templates.js.map +1 -1
- package/dist/src/digest/viewer-js.d.ts.map +1 -1
- package/dist/src/digest/viewer-js.js +534 -46
- package/dist/src/digest/viewer-js.js.map +1 -1
- package/dist/src/digest/viewer-logic.d.ts +26 -5
- package/dist/src/digest/viewer-logic.d.ts.map +1 -1
- package/dist/src/digest/viewer-logic.js +44 -7
- package/dist/src/digest/viewer-logic.js.map +1 -1
- package/dist/src/mcp/schemas.d.ts +13 -7
- package/dist/src/mcp/schemas.d.ts.map +1 -1
- package/dist/src/mcp/schemas.js +11 -6
- package/dist/src/mcp/schemas.js.map +1 -1
- package/dist/src/mcp/server.js +1 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/peer.d.ts +9 -1
- package/dist/src/mcp/tools/peer.d.ts.map +1 -1
- package/dist/src/mcp/tools/peer.js +133 -17
- package/dist/src/mcp/tools/peer.js.map +1 -1
- package/dist/src/peer/daemon.d.ts +1 -1
- package/dist/src/peer/daemon.d.ts.map +1 -1
- package/dist/src/peer/daemon.js +30 -6
- package/dist/src/peer/daemon.js.map +1 -1
- package/dist/src/peer/locate.d.ts +30 -0
- package/dist/src/peer/locate.d.ts.map +1 -1
- package/dist/src/peer/locate.js +75 -0
- package/dist/src/peer/locate.js.map +1 -1
- package/dist/src/peer/message.d.ts +7 -0
- package/dist/src/peer/message.d.ts.map +1 -1
- package/dist/src/peer/message.js +10 -0
- package/dist/src/peer/message.js.map +1 -1
- package/dist/src/peer/network.d.ts +19 -0
- package/dist/src/peer/network.d.ts.map +1 -1
- package/dist/src/peer/network.js +34 -3
- package/dist/src/peer/network.js.map +1 -1
- package/dist/src/peer/peeks.d.ts +6 -0
- package/dist/src/peer/peeks.d.ts.map +1 -1
- package/dist/src/peer/peeks.js +17 -10
- package/dist/src/peer/peeks.js.map +1 -1
- package/dist/src/peer/protocol.d.ts +38 -2
- package/dist/src/peer/protocol.d.ts.map +1 -1
- package/dist/src/peer/protocol.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/mcp.json +1 -1
- package/plugin/role-agents/lens-reader/AGENT.md +22 -1
- package/plugin/skills/_shared/proactive-routing.md +1 -1
- package/plugin/skills/lens/SKILL.md +114 -8
- package/plugin/skills/share/SKILL.md +141 -0
- package/plugin/skills/share/agents/openai.yaml +5 -0
- package/plugin/skills/video-digest/SKILL.md +4 -2
- package/plugin/skills/video-digest/templates/comments.md +1 -0
|
@@ -15,6 +15,10 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
15
15
|
var starts = data.starts;
|
|
16
16
|
var root = document.documentElement;
|
|
17
17
|
var isFile = location.protocol === 'file:';
|
|
18
|
+
// serve가 내줄 때만 페이지에 끼우는 토큰. 있으면 없는 리포트를 serve를 띄운 세션에 바로 요청하고, 없거나 요청이 실패하면 명령 복사로 돌아간다
|
|
19
|
+
var tokenMeta = document.querySelector('meta[name="gestalt-serve-token"]');
|
|
20
|
+
var serveToken = !isFile && tokenMeta ? tokenMeta.getAttribute('content') : null;
|
|
21
|
+
var SENT_MSG = '세션에 요청을 넣었어요. 다 만들어지면 새로고침하지 않아도 이 화면에 바로 들어와요.';
|
|
18
22
|
var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
19
23
|
var holder = document.getElementById('player');
|
|
20
24
|
var slot = document.getElementById('player-slot');
|
|
@@ -346,10 +350,13 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
346
350
|
pop.tabIndex = -1;
|
|
347
351
|
pop.setAttribute('role', 'dialog');
|
|
348
352
|
document.body.appendChild(pop);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
+
function mark(root){
|
|
354
|
+
root.querySelectorAll('.cite, .kw').forEach(function(b){
|
|
355
|
+
b.setAttribute('aria-expanded', 'false');
|
|
356
|
+
b.setAttribute('aria-controls', 'digest-pop');
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
mark(document);
|
|
353
360
|
var popFor = null;
|
|
354
361
|
function closePop(){
|
|
355
362
|
if (popFor) popFor.setAttribute('aria-expanded', 'false');
|
|
@@ -441,6 +448,7 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
441
448
|
closePop();
|
|
442
449
|
});
|
|
443
450
|
return {
|
|
451
|
+
mark: mark,
|
|
444
452
|
isOpen: function(){ return !pop.hidden; },
|
|
445
453
|
closeAndReturn: function(){
|
|
446
454
|
var back = popFor;
|
|
@@ -482,14 +490,14 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
482
490
|
}, 250);
|
|
483
491
|
}
|
|
484
492
|
|
|
493
|
+
// 리포트를 바꿔 끼우면 목차와 미니맵 링크도 바뀌어서 표시할 때마다 다시 읽는다
|
|
485
494
|
function setupSectionMarks() {
|
|
486
|
-
|
|
487
|
-
if (!('IntersectionObserver' in window) || !marks.length) return;
|
|
495
|
+
if (!('IntersectionObserver' in window)) return { observe: function(){} };
|
|
488
496
|
var io = new IntersectionObserver(function(entries){
|
|
489
497
|
entries.forEach(function(en){
|
|
490
498
|
if (!en.isIntersecting) return;
|
|
491
499
|
var id = en.target.id;
|
|
492
|
-
|
|
500
|
+
document.querySelectorAll('[data-nav]').forEach(function(m){
|
|
493
501
|
var on = m.getAttribute('data-nav') === id;
|
|
494
502
|
m.classList.toggle('is-current', on);
|
|
495
503
|
if (on) m.setAttribute('aria-current', 'location');
|
|
@@ -497,15 +505,16 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
497
505
|
});
|
|
498
506
|
});
|
|
499
507
|
}, { rootMargin: '-15% 0px -70% 0px' });
|
|
500
|
-
|
|
508
|
+
function observe(root){ root.querySelectorAll('section.sec').forEach(function(s){ io.observe(s); }); }
|
|
509
|
+
observe(document);
|
|
510
|
+
return { observe: observe };
|
|
501
511
|
}
|
|
502
512
|
|
|
503
513
|
// 읽는 소섹션이 바뀌면 영상을 그 구간 첫 문장으로 옮긴다. 재생 중이면 요약을 스크롤해도 영상을 옮기지 않고 빠르게 훑을 때는 멈춘 자리만 따른다.
|
|
504
514
|
// 방금 시각을 눌렀으면 유예가 끝난 뒤 영상이 멈춰 있을 때만 따라간다. 기다리는 사이 그 소섹션이 화면에서 빠지면 따라가지 않는다
|
|
505
515
|
// 앞선 이동의 버퍼링이 덜 끝났으면 재생 중으로 읽히므로 300ms 뒤 두 번까지 다시 본다
|
|
506
516
|
function setupSubsectionFollow(player) {
|
|
507
|
-
|
|
508
|
-
if (!player.live || !('IntersectionObserver' in window) || !subs.length) return;
|
|
517
|
+
if (!player.live || !('IntersectionObserver' in window)) return { observe: function(){} };
|
|
509
518
|
var followTimer = null, heldTimer = null, pendingSub = null;
|
|
510
519
|
var currentIdx = function(){
|
|
511
520
|
var t = player.currentTime();
|
|
@@ -547,7 +556,9 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
547
556
|
}, 400);
|
|
548
557
|
});
|
|
549
558
|
}, { rootMargin: '-15% 0px -70% 0px' });
|
|
550
|
-
|
|
559
|
+
function observe(root){ root.querySelectorAll('.sub[data-from]').forEach(function(s){ subIo.observe(s); }); }
|
|
560
|
+
observe(document);
|
|
561
|
+
return { observe: observe };
|
|
551
562
|
}
|
|
552
563
|
|
|
553
564
|
// 플레이어 자리가 화면 밖으로 나가면 미니 플레이어로 띄운다. iframe을 DOM에서 옮기면 영상이 처음부터 다시 로드돼서 자리는 두고 위치만 고정한다
|
|
@@ -596,6 +607,406 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
596
607
|
(ok ? done : fail)();
|
|
597
608
|
}
|
|
598
609
|
}
|
|
610
|
+
// 넣은 요청은 새로고침해도 이어서 기다리게 sessionStorage에 둔다. 세션이 다시 렌더하면 serve가 주는 ETag가 바뀌어서
|
|
611
|
+
// 그때 새 페이지를 받아 새로 생긴 탭과 리포트만 지금 화면에 끼운다. 처음 연 페이지의 ETag를 기준으로 삼으려고 serve로 열었으면 열자마자 한 번 받아 둔다
|
|
612
|
+
var PENDING_KEY = 'gestalt-digest-pending:' + data.slug;
|
|
613
|
+
var WATCH_MS = 3000;
|
|
614
|
+
var WATCH_LIMIT_MS = 30 * 60 * 1000;
|
|
615
|
+
// 다른 탭을 보는 동안은 드물게만 묻는다. 돌아오면 바로 한 번 묻는다
|
|
616
|
+
var WATCH_HIDDEN_MS = 30000;
|
|
617
|
+
function readPending(){
|
|
618
|
+
var raw = null;
|
|
619
|
+
try { raw = sessionStorage.getItem(PENDING_KEY); } catch (e) {}
|
|
620
|
+
return readPendingList(raw, Date.now());
|
|
621
|
+
}
|
|
622
|
+
function writePending(list){
|
|
623
|
+
try {
|
|
624
|
+
if (list.length) sessionStorage.setItem(PENDING_KEY, JSON.stringify(list));
|
|
625
|
+
else sessionStorage.removeItem(PENDING_KEY);
|
|
626
|
+
} catch (e) {}
|
|
627
|
+
}
|
|
628
|
+
function addPending(entry){
|
|
629
|
+
var key = JSON.stringify(entry.b);
|
|
630
|
+
writePending(readPending().filter(function(p){ return JSON.stringify(p.b) !== key; }).concat([entry]));
|
|
631
|
+
}
|
|
632
|
+
function pageTag(){
|
|
633
|
+
return fetch(location.pathname, { method: 'HEAD', cache: 'no-store', credentials: 'same-origin' })
|
|
634
|
+
.then(function(res){ return res.ok ? res.headers.get('ETag') : null; }, function(){ return null; });
|
|
635
|
+
}
|
|
636
|
+
// 렌더가 파일을 쓰는 도중에 받으면 데이터가 잘려 있다. 그때는 null을 돌려주고 다음 차례에 다시 받는다
|
|
637
|
+
function fetchPage(){
|
|
638
|
+
return fetch(location.pathname, { cache: 'no-store', credentials: 'same-origin' }).then(function(res){
|
|
639
|
+
if (!res.ok) return null;
|
|
640
|
+
var tag = res.headers.get('ETag');
|
|
641
|
+
return res.text().then(function(html){
|
|
642
|
+
var doc = new DOMParser().parseFromString(html, 'text/html');
|
|
643
|
+
var el = doc.getElementById('digest-data');
|
|
644
|
+
var next = null;
|
|
645
|
+
try { next = el ? JSON.parse(el.textContent) : null; } catch (e) {}
|
|
646
|
+
return next ? { doc: doc, data: next, tag: tag } : null;
|
|
647
|
+
});
|
|
648
|
+
}).then(null, function(){ return null; });
|
|
649
|
+
}
|
|
650
|
+
var baseTag = serveToken && window.fetch && window.DOMParser ? pageTag() : null;
|
|
651
|
+
var watching = false;
|
|
652
|
+
var watchTimer = null;
|
|
653
|
+
var watchBusy = false;
|
|
654
|
+
var watchNow = null;
|
|
655
|
+
function watchPage(){
|
|
656
|
+
if (watching || !baseTag) return;
|
|
657
|
+
watching = true;
|
|
658
|
+
baseTag.then(function(base){
|
|
659
|
+
function schedule(ms){ watchTimer = setTimeout(tick, ms); }
|
|
660
|
+
function tick(){
|
|
661
|
+
watchTimer = null;
|
|
662
|
+
var list = readPending();
|
|
663
|
+
if (!list.length) { watching = false; return; }
|
|
664
|
+
if (list.every(function(p){ return Date.now() - p.at > WATCH_LIMIT_MS; })) {
|
|
665
|
+
watching = false;
|
|
666
|
+
progress.timeout();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (document.hidden) { schedule(WATCH_HIDDEN_MS); return; }
|
|
670
|
+
watchBusy = true;
|
|
671
|
+
pageTag().then(function(tag){
|
|
672
|
+
if (!tag || !base || tag === base) {
|
|
673
|
+
watchBusy = false;
|
|
674
|
+
if (!base) base = tag;
|
|
675
|
+
schedule(WATCH_MS);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
fetchPage().then(function(page){
|
|
679
|
+
watchBusy = false;
|
|
680
|
+
if (!page) { schedule(WATCH_MS); return; }
|
|
681
|
+
base = page.tag || tag;
|
|
682
|
+
// 끼우다 멈추면 화면이 반쯤 바뀐 채라 다시 읽는다. 남은 요청은 resumePending이 이어 받는다
|
|
683
|
+
try {
|
|
684
|
+
if (!livePatch(page)) { location.reload(); return; }
|
|
685
|
+
progress.settle();
|
|
686
|
+
} catch (e) {
|
|
687
|
+
location.reload();
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
schedule(WATCH_MS);
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
watchNow = function(){
|
|
695
|
+
if (!watching || watchBusy || !watchTimer) return;
|
|
696
|
+
clearTimeout(watchTimer);
|
|
697
|
+
tick();
|
|
698
|
+
};
|
|
699
|
+
schedule(WATCH_MS);
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
document.addEventListener('visibilitychange', function(){ if (!document.hidden && watchNow) watchNow(); });
|
|
703
|
+
// 새로 그려진 페이지에서 만들어진 요청은 그 리포트나 탭을 열고 아직 안 된 요청은 만드는 중 표시를 다시 띄운다.
|
|
704
|
+
// 30분이 지난 요청은 세션이 놓친 것으로 보고 버린다
|
|
705
|
+
function resumePending(reports, tabs){
|
|
706
|
+
var list = readPending().filter(function(p){ return Date.now() - p.at < WATCH_LIMIT_MS; });
|
|
707
|
+
var left = [];
|
|
708
|
+
var ready = null;
|
|
709
|
+
list.forEach(function(p){ if (requestMade(p.b, data.reports, data.extensions)) ready = p.b; else left.push(p); });
|
|
710
|
+
writePending(left);
|
|
711
|
+
left.forEach(function(p){
|
|
712
|
+
requestState[JSON.stringify(p.b)] = 'sent';
|
|
713
|
+
progress.add(p, true);
|
|
714
|
+
});
|
|
715
|
+
if (ready && ready.action === 'template') {
|
|
716
|
+
(data.extensions || []).forEach(function(x){
|
|
717
|
+
if (requestMade(ready, [], [x])) tabs.open(x.panel);
|
|
718
|
+
});
|
|
719
|
+
} else if (ready) {
|
|
720
|
+
tabs.open('panel-summary');
|
|
721
|
+
reports.open(ready.difficulty, ready.lang);
|
|
722
|
+
}
|
|
723
|
+
if (left.length) watchPage();
|
|
724
|
+
}
|
|
725
|
+
// 같은 요청은 한 번만 보낸다. 이미 넣은 요청을 다시 누르면 넣었다는 안내만 다시 보여 주고, 응답을 기다리는 중이면 그 응답에 함께 건다
|
|
726
|
+
var requestState = {};
|
|
727
|
+
function requestToSession(body, title, done, fail) {
|
|
728
|
+
if (!serveToken || !window.fetch) { fail(); return; }
|
|
729
|
+
var key = JSON.stringify(body);
|
|
730
|
+
var state = requestState[key];
|
|
731
|
+
if (state === 'sent') { done(); return; }
|
|
732
|
+
if (state) { state.push([done, fail]); return; }
|
|
733
|
+
var waiting = [[done, fail]];
|
|
734
|
+
requestState[key] = waiting;
|
|
735
|
+
function settle(ok){
|
|
736
|
+
if (ok) {
|
|
737
|
+
requestState[key] = 'sent';
|
|
738
|
+
var entry = { b: body, t: title, at: Date.now() };
|
|
739
|
+
addPending(entry);
|
|
740
|
+
progress.add(entry);
|
|
741
|
+
watchPage();
|
|
742
|
+
} else delete requestState[key];
|
|
743
|
+
waiting.forEach(function(w){ w[ok ? 0 : 1](); });
|
|
744
|
+
}
|
|
745
|
+
fetch('/__request', {
|
|
746
|
+
method: 'POST',
|
|
747
|
+
headers: { 'Content-Type': 'application/json', 'X-Gestalt-Token': serveToken },
|
|
748
|
+
body: key,
|
|
749
|
+
credentials: 'same-origin',
|
|
750
|
+
cache: 'no-store'
|
|
751
|
+
}).then(function(res){
|
|
752
|
+
return res.json().then(function(j){ return res.ok && j && j.ok === true; }, function(){ return false; });
|
|
753
|
+
}).then(settle, function(){ settle(false); });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// 만드는 중인 요청을 화면에 띄운다. 템플릿은 탭 줄에 빈 탭을 세우고 리포트는 쉽게 보기 옆에 칩을 단다. 흐른 시간은 1초마다 고친다
|
|
757
|
+
function setupProgress(tabs, reports) {
|
|
758
|
+
var items = [];
|
|
759
|
+
var timer = null;
|
|
760
|
+
var status = document.createElement('p');
|
|
761
|
+
status.className = 'sr-only';
|
|
762
|
+
status.setAttribute('role', 'status');
|
|
763
|
+
status.setAttribute('aria-live', 'polite');
|
|
764
|
+
document.body.appendChild(status);
|
|
765
|
+
function tick(){
|
|
766
|
+
if (document.hidden && timer) return;
|
|
767
|
+
var els = document.querySelectorAll('[data-pending-at]');
|
|
768
|
+
els.forEach(function(el){ el.textContent = elapsedLabel(Date.now() - Number(el.getAttribute('data-pending-at'))); });
|
|
769
|
+
if (!els.length && timer) { clearInterval(timer); timer = null; }
|
|
770
|
+
}
|
|
771
|
+
function startTimer(){
|
|
772
|
+
tick();
|
|
773
|
+
if (!timer) timer = setInterval(tick, 1000);
|
|
774
|
+
}
|
|
775
|
+
function dot(){
|
|
776
|
+
var s = document.createElement('span');
|
|
777
|
+
s.className = 'busy-dot';
|
|
778
|
+
s.setAttribute('aria-hidden', 'true');
|
|
779
|
+
return s;
|
|
780
|
+
}
|
|
781
|
+
// 매초 바뀌는 글자라 스크린리더에는 숨긴다. 시간 초과 안내로 바뀌면 다시 읽히게 푼다
|
|
782
|
+
function elapsed(p){
|
|
783
|
+
var s = document.createElement('span');
|
|
784
|
+
s.setAttribute('aria-hidden', 'true');
|
|
785
|
+
s.setAttribute('data-pending-at', String(p.at));
|
|
786
|
+
return s;
|
|
787
|
+
}
|
|
788
|
+
function para(cls, text){
|
|
789
|
+
var el = document.createElement('p');
|
|
790
|
+
el.className = cls;
|
|
791
|
+
if (text) el.textContent = text;
|
|
792
|
+
return el;
|
|
793
|
+
}
|
|
794
|
+
function find(p){
|
|
795
|
+
var key = JSON.stringify(p.b);
|
|
796
|
+
var hit = null;
|
|
797
|
+
items.forEach(function(it){ if (it.key === key) hit = it; });
|
|
798
|
+
return hit;
|
|
799
|
+
}
|
|
800
|
+
function addTemplate(p){
|
|
801
|
+
var list = document.querySelector('.tab-row .tabs');
|
|
802
|
+
if (!list) return null;
|
|
803
|
+
var id = 'pending-' + p.b.template;
|
|
804
|
+
var title = p.t || String(p.b.template);
|
|
805
|
+
var tab = document.createElement('button');
|
|
806
|
+
tab.type = 'button';
|
|
807
|
+
tab.className = 'is-pending';
|
|
808
|
+
tab.id = 'tab-' + id;
|
|
809
|
+
tab.setAttribute('role', 'tab');
|
|
810
|
+
tab.setAttribute('data-tab', id);
|
|
811
|
+
tab.setAttribute('aria-controls', id);
|
|
812
|
+
tab.setAttribute('aria-selected', 'false');
|
|
813
|
+
tab.tabIndex = -1;
|
|
814
|
+
tab.appendChild(dot());
|
|
815
|
+
tab.appendChild(document.createTextNode(title));
|
|
816
|
+
var panel = document.createElement('section');
|
|
817
|
+
panel.className = 'ext ext-pending';
|
|
818
|
+
panel.id = id;
|
|
819
|
+
panel.hidden = true;
|
|
820
|
+
panel.tabIndex = 0;
|
|
821
|
+
panel.setAttribute('role', 'tabpanel');
|
|
822
|
+
panel.setAttribute('aria-labelledby', tab.id);
|
|
823
|
+
var h = document.createElement('h2');
|
|
824
|
+
h.textContent = title;
|
|
825
|
+
panel.appendChild(h);
|
|
826
|
+
panel.appendChild(para('', '세션이 만드는 중이에요. 다 만들어지면 새로고침하지 않아도 이 자리에 바로 들어와요.'));
|
|
827
|
+
var meta = para('pending-meta', '');
|
|
828
|
+
meta.appendChild(dot());
|
|
829
|
+
meta.appendChild(elapsed(p));
|
|
830
|
+
panel.appendChild(meta);
|
|
831
|
+
panel.appendChild(para('pending-hint', '세션 화면에 승인 창이 떠 있으면 거기서 먼저 확인해 주세요.'));
|
|
832
|
+
list.appendChild(tab);
|
|
833
|
+
placePanel(panel);
|
|
834
|
+
return { tab: tab, panel: panel, meta: meta };
|
|
835
|
+
}
|
|
836
|
+
function addReport(p){
|
|
837
|
+
var toggle = document.getElementById('easy-toggle');
|
|
838
|
+
if (!toggle) return null;
|
|
839
|
+
var chip = document.createElement('span');
|
|
840
|
+
chip.className = 'busy-chip';
|
|
841
|
+
chip.appendChild(dot());
|
|
842
|
+
chip.appendChild(document.createTextNode((p.b.difficulty === 'easy' ? '쉬운' : '보통') + ' 리포트 만드는 중 · '));
|
|
843
|
+
chip.appendChild(elapsed(p));
|
|
844
|
+
toggle.parentNode.insertBefore(chip, toggle.nextSibling);
|
|
845
|
+
return { chip: chip };
|
|
846
|
+
}
|
|
847
|
+
// 버튼으로 넣은 요청은 넣었다는 안내를 따로 띄워서 announce는 새로고침 뒤 이어 받을 때만 켠다
|
|
848
|
+
function add(p, announce){
|
|
849
|
+
if (find(p)) return;
|
|
850
|
+
var ui = p.b.action === 'template' ? addTemplate(p) : addReport(p);
|
|
851
|
+
if (!ui) return;
|
|
852
|
+
ui.key = JSON.stringify(p.b);
|
|
853
|
+
ui.p = p;
|
|
854
|
+
items.push(ui);
|
|
855
|
+
if (announce) {
|
|
856
|
+
var what = p.b.action === 'template' ? (p.t || String(p.b.template)) : (p.b.difficulty === 'easy' ? '쉬운' : '보통') + ' 리포트';
|
|
857
|
+
status.textContent = what + ' 만드는 중이에요.';
|
|
858
|
+
}
|
|
859
|
+
startTimer();
|
|
860
|
+
return ui;
|
|
861
|
+
}
|
|
862
|
+
function drop(it){ items = items.filter(function(x){ return x !== it; }); }
|
|
863
|
+
// 새로 들어온 확장에 맞는 빈 탭을 넘겨준다. livePatch가 그 자리에 진짜 탭을 끼운다
|
|
864
|
+
function takeTab(x){
|
|
865
|
+
var hit = null;
|
|
866
|
+
items.forEach(function(it){ if (it.tab && requestMade(it.p.b, [], [x])) hit = it; });
|
|
867
|
+
if (hit) drop(hit);
|
|
868
|
+
return hit;
|
|
869
|
+
}
|
|
870
|
+
// 들어온 요청은 목록에서 뺀다. 리포트 칩은 누르면 그 리포트를 여는 버튼으로 바꾼다
|
|
871
|
+
function settle(){
|
|
872
|
+
var left = [];
|
|
873
|
+
var done = [];
|
|
874
|
+
readPending().forEach(function(p){
|
|
875
|
+
if (requestMade(p.b, data.reports, data.extensions)) done.push(p);
|
|
876
|
+
else left.push(p);
|
|
877
|
+
});
|
|
878
|
+
writePending(left);
|
|
879
|
+
done.forEach(function(p){
|
|
880
|
+
delete requestState[JSON.stringify(p.b)];
|
|
881
|
+
var it = find(p);
|
|
882
|
+
if (p.b.action === 'template') {
|
|
883
|
+
if (it) { it.tab.remove(); it.panel.remove(); drop(it); }
|
|
884
|
+
status.textContent = (p.t || '확장') + ' 탭이 다 만들어졌어요.';
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
var label = (p.b.difficulty === 'easy' ? '쉬운' : '보통') + ' 리포트';
|
|
888
|
+
status.textContent = label + '가 다 만들어졌어요.';
|
|
889
|
+
if (!it) return;
|
|
890
|
+
drop(it);
|
|
891
|
+
var btn = document.createElement('button');
|
|
892
|
+
btn.type = 'button';
|
|
893
|
+
btn.className = 'busy-chip is-done';
|
|
894
|
+
btn.textContent = label + ' 보기';
|
|
895
|
+
btn.addEventListener('click', function(){
|
|
896
|
+
btn.remove();
|
|
897
|
+
tabs.open('panel-summary');
|
|
898
|
+
reports.open(p.b.difficulty, p.b.lang);
|
|
899
|
+
});
|
|
900
|
+
it.chip.parentNode.replaceChild(btn, it.chip);
|
|
901
|
+
});
|
|
902
|
+
tick();
|
|
903
|
+
}
|
|
904
|
+
function timeout(){
|
|
905
|
+
items.forEach(function(it){
|
|
906
|
+
var box = it.meta || it.chip;
|
|
907
|
+
box.querySelectorAll('.busy-dot').forEach(function(d){ d.classList.add('is-stopped'); });
|
|
908
|
+
box.querySelectorAll('[data-pending-at]').forEach(function(el){
|
|
909
|
+
el.removeAttribute('data-pending-at');
|
|
910
|
+
el.removeAttribute('aria-hidden');
|
|
911
|
+
el.textContent = '30분이 지나도 안 들어왔어요. 세션 화면을 확인해 주세요.';
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
if (items.length) status.textContent = '요청한 리포트가 30분이 지나도 안 들어왔어요. 세션 화면을 확인해 주세요.';
|
|
915
|
+
tick();
|
|
916
|
+
}
|
|
917
|
+
return { add: add, takeTab: takeTab, settle: settle, timeout: timeout };
|
|
918
|
+
}
|
|
919
|
+
// 확장 패널은 요약 패널과 앞선 확장 패널 뒤에 차례로 붙는다
|
|
920
|
+
function placePanel(panel){
|
|
921
|
+
var all = document.querySelectorAll('section.ext');
|
|
922
|
+
var after = all.length ? all[all.length - 1] : document.getElementById('panel-summary');
|
|
923
|
+
if (after) after.parentNode.insertBefore(panel, after.nextSibling);
|
|
924
|
+
}
|
|
925
|
+
function reportKeys(list){ return (list || []).map(function(r){ return r.key; }).join('|'); }
|
|
926
|
+
// 새 렌더에서 지금 화면에 없는 확장 탭과 바뀐 리포트 묶음만 옮겨 온다. 보던 탭과 리포트, 스크롤은 그대로 둔다.
|
|
927
|
+
// 화면 짜임이 달라 끼울 자리를 못 찾으면 false를 돌려주고 부른 쪽이 새로고침한다
|
|
928
|
+
function livePatch(page){
|
|
929
|
+
var next = page.data;
|
|
930
|
+
var doc = page.doc;
|
|
931
|
+
if (next.slug !== data.slug) return false;
|
|
932
|
+
var list = document.querySelector('.tab-row .tabs');
|
|
933
|
+
var fresh = [];
|
|
934
|
+
var ok = true;
|
|
935
|
+
(next.extensions || []).forEach(function(x){
|
|
936
|
+
if (document.getElementById(x.panel)) return;
|
|
937
|
+
var panel = doc.getElementById(x.panel);
|
|
938
|
+
var tab = doc.getElementById('tab-' + x.panel);
|
|
939
|
+
if (!panel || !tab || !list) ok = false;
|
|
940
|
+
else fresh.push({ x: x, panel: panel, tab: tab });
|
|
941
|
+
});
|
|
942
|
+
var swapReports = reportKeys(next.reports) !== reportKeys(data.reports);
|
|
943
|
+
var oldBoxes = document.querySelectorAll('.report[data-ri]');
|
|
944
|
+
var newBoxes = doc.querySelectorAll('.report[data-ri]');
|
|
945
|
+
var oldSelect = document.getElementById('report-select');
|
|
946
|
+
var newSelect = doc.getElementById('report-select');
|
|
947
|
+
if (swapReports && (!oldBoxes.length || !newBoxes.length || Boolean(oldSelect) !== Boolean(newSelect))) ok = false;
|
|
948
|
+
if (!ok) return false;
|
|
949
|
+
var keep = data.reports[reports.current()].key;
|
|
950
|
+
data.reports = next.reports;
|
|
951
|
+
data.extensions = next.extensions;
|
|
952
|
+
if (swapReports) {
|
|
953
|
+
swapAll(oldBoxes, newBoxes, null);
|
|
954
|
+
swapAll(document.querySelectorAll('.minimap[data-ri]'), doc.querySelectorAll('.minimap[data-ri]'), dataEl);
|
|
955
|
+
if (oldSelect && newSelect) {
|
|
956
|
+
oldSelect.textContent = '';
|
|
957
|
+
Array.prototype.forEach.call(newSelect.children, function(o){ oldSelect.appendChild(document.importNode(o, true)); });
|
|
958
|
+
}
|
|
959
|
+
reports.refresh(keep);
|
|
960
|
+
}
|
|
961
|
+
fresh.forEach(function(f){
|
|
962
|
+
var panel = document.importNode(f.panel, true);
|
|
963
|
+
var tab = document.importNode(f.tab, true);
|
|
964
|
+
var slot = progress.takeTab(f.x);
|
|
965
|
+
var wasOpen = slot && slot.tab.getAttribute('aria-selected') === 'true';
|
|
966
|
+
var active = document.activeElement;
|
|
967
|
+
var hadFocus = slot && (active === slot.tab || slot.panel.contains(active));
|
|
968
|
+
panel.hidden = true;
|
|
969
|
+
tab.setAttribute('aria-selected', 'false');
|
|
970
|
+
tab.tabIndex = -1;
|
|
971
|
+
tab.classList.add('is-new');
|
|
972
|
+
if (slot) {
|
|
973
|
+
list.replaceChild(tab, slot.tab);
|
|
974
|
+
slot.panel.parentNode.replaceChild(panel, slot.panel);
|
|
975
|
+
} else {
|
|
976
|
+
list.appendChild(tab);
|
|
977
|
+
placePanel(panel);
|
|
978
|
+
}
|
|
979
|
+
enhance(panel);
|
|
980
|
+
// 만들어진 템플릿은 더 만들기 메뉴에서 뺀다
|
|
981
|
+
var id = f.x.key.split('.')[0];
|
|
982
|
+
document.querySelectorAll('#more-menu [data-template]').forEach(function(b){
|
|
983
|
+
if (b.getAttribute('data-template') === id && b.parentNode) b.parentNode.remove();
|
|
984
|
+
});
|
|
985
|
+
if (wasOpen) tabs.open(f.x.panel);
|
|
986
|
+
if (hadFocus) tab.focus();
|
|
987
|
+
setTimeout(function(){ tab.classList.remove('is-new'); }, 6000);
|
|
988
|
+
});
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
// 옛 노드 자리에 새 노드를 차례로 끼우고 옛 노드를 지운다. 옛 노드가 없으면 fallback 앞에 붙인다
|
|
992
|
+
function swapAll(olds, news, fallback){
|
|
993
|
+
var at = olds.length ? olds[0] : fallback;
|
|
994
|
+
if (!at) return;
|
|
995
|
+
Array.prototype.forEach.call(news, function(n){
|
|
996
|
+
var el = document.importNode(n, true);
|
|
997
|
+
at.parentNode.insertBefore(el, at);
|
|
998
|
+
enhance(el);
|
|
999
|
+
});
|
|
1000
|
+
Array.prototype.forEach.call(olds, function(o){ o.remove(); });
|
|
1001
|
+
}
|
|
1002
|
+
// 새로 끼운 노드에 처음 로드 때 한 번만 걸던 처리를 다시 건다. 클릭과 키 입력은 document에 걸려 있어 따로 안 건다
|
|
1003
|
+
function enhance(root){
|
|
1004
|
+
popups.mark(root);
|
|
1005
|
+
sectionMarks.observe(root);
|
|
1006
|
+
subFollow.observe(root);
|
|
1007
|
+
zoom.wrap(root);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
599
1010
|
var flash = makeFlash(function(){ return document.getElementById('copy-status'); });
|
|
600
1011
|
function copyWith(btn, label, text) {
|
|
601
1012
|
copyText(text, function(){ flash(btn, label, false); }, function(){ flash(btn, label, true); });
|
|
@@ -610,13 +1021,16 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
610
1021
|
});
|
|
611
1022
|
}
|
|
612
1023
|
|
|
613
|
-
// 요약 탭과 확장 탭을 오간다. 리포트 바와 미니맵은 요약 탭 안이나 body 표시로 같이
|
|
1024
|
+
// 요약 탭과 확장 탭을 오간다. 리포트 바와 미니맵은 요약 탭 안이나 body 표시로 같이 숨는다.
|
|
1025
|
+
// 탭은 나중에 끼워 넣기도 해서 누를 때마다 탭 목록을 다시 읽는다
|
|
614
1026
|
function setupTabs() {
|
|
615
1027
|
var TAB_KEY = 'gestalt-digest-tab:' + data.slug;
|
|
616
1028
|
var SUMMARY = 'panel-summary';
|
|
617
|
-
var
|
|
1029
|
+
var list = document.querySelector('.tab-row .tabs');
|
|
1030
|
+
function all(){ return Array.prototype.slice.call(document.querySelectorAll('.tabs [role="tab"]')); }
|
|
618
1031
|
var current = SUMMARY;
|
|
619
1032
|
function select(id, focus){
|
|
1033
|
+
var tabs = all();
|
|
620
1034
|
if (!tabs.some(function(t){ return t.getAttribute('data-tab') === id; })) id = SUMMARY;
|
|
621
1035
|
current = id;
|
|
622
1036
|
tabs.forEach(function(t){
|
|
@@ -629,18 +1043,27 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
629
1043
|
});
|
|
630
1044
|
if (id === SUMMARY) document.body.removeAttribute('data-tab');
|
|
631
1045
|
else document.body.setAttribute('data-tab', 'ext');
|
|
1046
|
+
// 만드는 중인 빈 탭은 다음에 열 때 없을 수 있어서 기억하지 않는다
|
|
1047
|
+
if (id.indexOf('pending-') === 0) return;
|
|
632
1048
|
try { localStorage.setItem(TAB_KEY, id); } catch (e) {}
|
|
633
1049
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1050
|
+
function tabOf(ev){ return ev.target.closest ? ev.target.closest('[role="tab"]') : null; }
|
|
1051
|
+
if (list) {
|
|
1052
|
+
list.addEventListener('click', function(ev){
|
|
1053
|
+
var t = tabOf(ev);
|
|
1054
|
+
if (t) select(t.getAttribute('data-tab'), false);
|
|
1055
|
+
});
|
|
1056
|
+
list.addEventListener('keydown', function(ev){
|
|
1057
|
+
var tabs = all();
|
|
1058
|
+
var i = tabs.indexOf(tabOf(ev));
|
|
1059
|
+
if (i < 0) return;
|
|
637
1060
|
var j = tabStep(i, tabs.length, ev.key);
|
|
638
1061
|
if (j < 0) return;
|
|
639
1062
|
ev.preventDefault();
|
|
640
1063
|
select(tabs[j].getAttribute('data-tab'), true);
|
|
641
1064
|
});
|
|
642
|
-
}
|
|
643
|
-
if (
|
|
1065
|
+
}
|
|
1066
|
+
if (all().length) {
|
|
644
1067
|
// 공유된 앵커가 있으면 그 앵커가 든 탭을 연다. 저장해 둔 탭보다 먼저다
|
|
645
1068
|
var anchor = safeDecode(location.hash.slice(1));
|
|
646
1069
|
var target = anchor ? document.getElementById(anchor) : null;
|
|
@@ -651,6 +1074,7 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
651
1074
|
else if (!target && saved) select(saved, false);
|
|
652
1075
|
}
|
|
653
1076
|
return {
|
|
1077
|
+
open: function(id){ select(id, false); },
|
|
654
1078
|
markdown: function(){
|
|
655
1079
|
if (current === SUMMARY) return null;
|
|
656
1080
|
var hit = null;
|
|
@@ -660,14 +1084,26 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
660
1084
|
};
|
|
661
1085
|
}
|
|
662
1086
|
|
|
663
|
-
// 없는 템플릿은 뷰어가 만들지
|
|
664
|
-
function setupMore(reports) {
|
|
1087
|
+
// 없는 템플릿은 뷰어가 만들지 않는다. serve로 열었으면 세션에 요청하고 만드는 중 탭을 연다. 아니면 세션에 붙여 넣을 명령만 복사해 준다
|
|
1088
|
+
function setupMore(reports, tabs) {
|
|
665
1089
|
var btn = document.getElementById('more-btn');
|
|
666
1090
|
var menu = document.getElementById('more-menu');
|
|
667
1091
|
var box = document.getElementById('more-cmd-box');
|
|
668
1092
|
var code = document.getElementById('more-cmd');
|
|
669
1093
|
var copy = document.getElementById('more-copy');
|
|
1094
|
+
var hint = document.getElementById('more-hint');
|
|
1095
|
+
var msg = document.getElementById('more-msg');
|
|
1096
|
+
var wrap = document.getElementById('more-cmd-wrap');
|
|
670
1097
|
if (!btn || !menu || !box || !code || !copy) return { close: function(){ return false; } };
|
|
1098
|
+
if (serveToken && hint) hint.textContent = '템플릿을 고르면 serve를 띄운 세션에 만들어 달라고 요청해요.';
|
|
1099
|
+
// 응답이 오기 전에 다른 템플릿을 고르면 앞 응답은 안내를 덮지 않는다
|
|
1100
|
+
var seq = 0;
|
|
1101
|
+
function showCmd(text, message){
|
|
1102
|
+
code.textContent = text;
|
|
1103
|
+
if (msg) msg.textContent = message;
|
|
1104
|
+
if (wrap) wrap.hidden = false;
|
|
1105
|
+
box.hidden = false;
|
|
1106
|
+
}
|
|
671
1107
|
function setOpen(open){
|
|
672
1108
|
menu.hidden = !open;
|
|
673
1109
|
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
@@ -675,9 +1111,33 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
675
1111
|
btn.addEventListener('click', function(){ setOpen(menu.hidden); });
|
|
676
1112
|
menu.querySelectorAll('[data-template]').forEach(function(b){
|
|
677
1113
|
b.addEventListener('click', function(){
|
|
678
|
-
|
|
1114
|
+
var id = b.getAttribute('data-template');
|
|
1115
|
+
var lang = data.reports[reports.current()].lang;
|
|
1116
|
+
var command = templateCommand(data.slug, id, lang, data.lens);
|
|
1117
|
+
var name = b.querySelector('b');
|
|
1118
|
+
var mine = ++seq;
|
|
1119
|
+
if (!serveToken) {
|
|
1120
|
+
showCmd(command, '');
|
|
1121
|
+
copyWith(copy, '명령 복사', command);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (msg) msg.textContent = '세션에 요청하는 중이에요.';
|
|
1125
|
+
if (wrap) wrap.hidden = true;
|
|
679
1126
|
box.hidden = false;
|
|
680
|
-
|
|
1127
|
+
requestToSession(templateRequest(data.slug, id, lang), name ? name.textContent : id, function(){
|
|
1128
|
+
if (mine !== seq) return;
|
|
1129
|
+
// 이 상자는 곧 숨기니 안내는 늘 보이는 status에 쓴다. 메뉴 항목이 사라져 포커스는 만드는 중 탭으로 옮긴다
|
|
1130
|
+
if (msg) msg.textContent = '';
|
|
1131
|
+
var status = document.getElementById('copy-status');
|
|
1132
|
+
if (status) status.textContent = SENT_MSG;
|
|
1133
|
+
box.hidden = true;
|
|
1134
|
+
setOpen(false);
|
|
1135
|
+
tabs.open('pending-' + id);
|
|
1136
|
+
var pendingTab = document.getElementById('tab-pending-' + id);
|
|
1137
|
+
(pendingTab || btn).focus();
|
|
1138
|
+
}, function(){
|
|
1139
|
+
if (mine === seq) showCmd(command, '세션에 요청을 넣지 못했어요. 아래 명령을 세션에 붙여 넣어 주세요.');
|
|
1140
|
+
});
|
|
681
1141
|
});
|
|
682
1142
|
});
|
|
683
1143
|
copy.addEventListener('click', function(){ copyWith(copy, '명령 복사', code.textContent); });
|
|
@@ -691,11 +1151,10 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
691
1151
|
};
|
|
692
1152
|
}
|
|
693
1153
|
|
|
694
|
-
// 툴바는 쉽게 보기 토글 하나로 보통과 쉬운 리포트를 오간다. 없는 리포트는 서버가 만들지 않고 생성 명령만 보여
|
|
1154
|
+
// 툴바는 쉽게 보기 토글 하나로 보통과 쉬운 리포트를 오간다. 없는 리포트는 서버가 만들지 않고 생성 명령만 보여 준다.
|
|
1155
|
+
// 리포트 묶음은 나중에 통째로 바꿔 끼우기도 해서 보여 줄 때마다 다시 읽는다
|
|
695
1156
|
function setupReports() {
|
|
696
1157
|
var REPORT_KEY = 'gestalt-digest-report:' + data.slug;
|
|
697
|
-
var boxes = document.querySelectorAll('.report[data-ri]');
|
|
698
|
-
var maps = document.querySelectorAll('.minimap[data-ri]');
|
|
699
1158
|
var easyToggle = document.getElementById('easy-toggle');
|
|
700
1159
|
var select = document.getElementById('report-select');
|
|
701
1160
|
var missing = document.getElementById('report-missing');
|
|
@@ -706,15 +1165,25 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
706
1165
|
var copyCmd = document.getElementById('copy-cmd');
|
|
707
1166
|
var initial = data.reports[data.initial] ? data.initial : 0;
|
|
708
1167
|
var current = initial;
|
|
1168
|
+
// 안내를 닫거나 다른 리포트로 옮긴 뒤 도착한 요청 응답은 안내를 다시 띄우지 않는다
|
|
1169
|
+
var noticeSeq = 0;
|
|
709
1170
|
function hideMissing(){
|
|
1171
|
+
noticeSeq++;
|
|
710
1172
|
if (missing) missing.hidden = true;
|
|
711
1173
|
if (status) status.textContent = '';
|
|
712
1174
|
}
|
|
1175
|
+
function showNotice(msg, command){
|
|
1176
|
+
if (missingMsg) missingMsg.textContent = msg;
|
|
1177
|
+
if (cmdWrap) cmdWrap.hidden = command === null;
|
|
1178
|
+
if (cmd) cmd.textContent = command || '';
|
|
1179
|
+
if (missing) missing.hidden = false;
|
|
1180
|
+
if (status) status.textContent = msg;
|
|
1181
|
+
}
|
|
713
1182
|
function show(ri){
|
|
714
1183
|
if (!data.reports[ri]) ri = initial;
|
|
715
1184
|
current = ri;
|
|
716
|
-
|
|
717
|
-
|
|
1185
|
+
document.querySelectorAll('.report[data-ri]').forEach(function(b){ b.hidden = Number(b.getAttribute('data-ri')) !== ri; });
|
|
1186
|
+
document.querySelectorAll('.minimap[data-ri]').forEach(function(m){ m.hidden = Number(m.getAttribute('data-ri')) !== ri; });
|
|
718
1187
|
var r = data.reports[ri];
|
|
719
1188
|
if (easyToggle) easyToggle.setAttribute('aria-pressed', r.difficulty === 'easy' ? 'true' : 'false');
|
|
720
1189
|
if (select) select.value = String(ri);
|
|
@@ -727,12 +1196,15 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
727
1196
|
// 짝이 없는 옛 짧게, 기본 리포트는 길게로 간다. 스킬이 길게만 만들어서다. 구간이나 요청이 붙은 리포트는 다른 리포트로 넘기지 않는다
|
|
728
1197
|
if (ri < 0 && r.preset) ri = pickReport(data.reports, 'long', difficulty);
|
|
729
1198
|
if (ri >= 0) { show(ri); return; }
|
|
730
|
-
var notice = missingNotice(r, data.slug, difficulty);
|
|
731
|
-
|
|
732
|
-
if (
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1199
|
+
var notice = missingNotice(r, data.slug, difficulty, data.lens);
|
|
1200
|
+
var mine = ++noticeSeq;
|
|
1201
|
+
if (notice.cmd === null || !serveToken) { showNotice(notice.msg, notice.cmd); return; }
|
|
1202
|
+
showNotice('세션에 만들어 달라고 요청하는 중이에요.', null);
|
|
1203
|
+
requestToSession(reportRequest(data.slug, difficulty, r.lang, r.range), '', function(){
|
|
1204
|
+
if (mine === noticeSeq) showNotice(SENT_MSG, null);
|
|
1205
|
+
}, function(){
|
|
1206
|
+
if (mine === noticeSeq) showNotice('세션에 요청을 넣지 못했어요. 아래 명령을 세션에 붙여 넣으면 만들어요.', notice.cmd);
|
|
1207
|
+
});
|
|
736
1208
|
}
|
|
737
1209
|
// 눌림 상태는 보이는 리포트만 따른다. 안내가 떠 있으면 다시 눌러 닫는다
|
|
738
1210
|
if (easyToggle) easyToggle.addEventListener('click', function(){
|
|
@@ -754,7 +1226,20 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
754
1226
|
if (owner) start = Number(owner.getAttribute('data-ri'));
|
|
755
1227
|
if (start !== initial) show(start);
|
|
756
1228
|
if (owner && target) target.scrollIntoView();
|
|
757
|
-
return {
|
|
1229
|
+
return {
|
|
1230
|
+
current: function(){ return current; },
|
|
1231
|
+
open: function(difficulty, lang){
|
|
1232
|
+
data.reports.forEach(function(r, i){
|
|
1233
|
+
if (r.difficulty === difficulty && (!lang || r.lang === lang) && data.reports[current].difficulty !== difficulty) show(i);
|
|
1234
|
+
});
|
|
1235
|
+
},
|
|
1236
|
+
// 리포트 묶음을 바꿔 끼운 뒤 보던 리포트를 키로 다시 찾는다. 자리 번호는 새 렌더에서 밀릴 수 있다
|
|
1237
|
+
refresh: function(key){
|
|
1238
|
+
var ri = current;
|
|
1239
|
+
data.reports.forEach(function(r, i){ if (r.key === key) ri = i; });
|
|
1240
|
+
show(ri);
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
758
1243
|
}
|
|
759
1244
|
|
|
760
1245
|
// 고른 답을 맞춰 보고 해설을 연다. 아무것도 안 골랐으면 정답과 해설만 연다
|
|
@@ -794,9 +1279,9 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
794
1279
|
}
|
|
795
1280
|
|
|
796
1281
|
// 장면 사진은 확대 창을 열 수 있을 때만 버튼으로 감싼다. 서버가 버튼을 그리면 스크립트가 없을 때 Tab에는 잡히는데 눌러도 반응이 없다
|
|
797
|
-
function wrapShots() {
|
|
1282
|
+
function wrapShots(root) {
|
|
798
1283
|
if (typeof document.createElement('dialog').showModal !== 'function') return;
|
|
799
|
-
var imgs =
|
|
1284
|
+
var imgs = root.querySelectorAll('.shot > img');
|
|
800
1285
|
for (var i = 0; i < imgs.length; i++) {
|
|
801
1286
|
var img = imgs[i];
|
|
802
1287
|
var btn = document.createElement('button');
|
|
@@ -813,10 +1298,10 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
813
1298
|
}
|
|
814
1299
|
}
|
|
815
1300
|
|
|
816
|
-
// 그림과 장면을 화면 크기로 키워 본다. 노드를 눌러 영상으로 가면 창을
|
|
1301
|
+
// 그림과 장면을 화면 크기로 키워 본다. 노드를 눌러 영상으로 가면 창을 닫는다.
|
|
1302
|
+
// 그림이 든 탭이 나중에 들어올 수 있어 그림이 없어도 창은 만들어 둔다
|
|
817
1303
|
function setupZoom() {
|
|
818
|
-
wrapShots();
|
|
819
|
-
if (!document.querySelector('[data-zoom]')) return { isOpen: function(){ return false; } };
|
|
1304
|
+
wrapShots(document);
|
|
820
1305
|
var dialog = document.createElement('dialog');
|
|
821
1306
|
dialog.id = 'zoom-dialog';
|
|
822
1307
|
dialog.setAttribute('aria-label', '그림 크게 보기');
|
|
@@ -833,7 +1318,7 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
833
1318
|
close.addEventListener('click', function(){ dialog.close(); });
|
|
834
1319
|
dialog.addEventListener('click', function(ev){
|
|
835
1320
|
if (ev.target === dialog) dialog.close();
|
|
836
|
-
else if (ev.target.closest && ev.target.closest('[data-sec]')) dialog.close();
|
|
1321
|
+
else if (ev.target.closest && ev.target.closest('[data-sec], [data-unit]')) dialog.close();
|
|
837
1322
|
});
|
|
838
1323
|
dialog.addEventListener('close', function(){
|
|
839
1324
|
body.textContent = '';
|
|
@@ -854,7 +1339,7 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
854
1339
|
opener = btn;
|
|
855
1340
|
dialog.showModal();
|
|
856
1341
|
});
|
|
857
|
-
return { isOpen: function(){ return dialog.open; } };
|
|
1342
|
+
return { isOpen: function(){ return dialog.open; }, wrap: wrapShots };
|
|
858
1343
|
}
|
|
859
1344
|
|
|
860
1345
|
// 인터랙티브 그림은 렌더러가 상태마다 그려 둔 것 가운데 하나만 보인다. 여기서 값을 계산하지 않는다
|
|
@@ -893,22 +1378,25 @@ export const VIEWER_JS = VIEWER_LOGIC_JS +
|
|
|
893
1378
|
// 요약 앵커로 스크롤하려면 요약 패널이 먼저 보여야 해서 탭을 리포트보다 먼저 연다
|
|
894
1379
|
var tabs = setupTabs();
|
|
895
1380
|
var reports = setupReports();
|
|
896
|
-
var more = setupMore(reports);
|
|
1381
|
+
var more = setupMore(reports, tabs);
|
|
1382
|
+
var progress = setupProgress(tabs, reports);
|
|
897
1383
|
var player = createPlayer();
|
|
898
1384
|
setupTimeLinks(player);
|
|
899
1385
|
var docPane = setupDocPane();
|
|
900
1386
|
var popups = setupPopups(docPane);
|
|
901
1387
|
setupScriptFollow(player);
|
|
902
|
-
setupSectionMarks();
|
|
903
|
-
setupSubsectionFollow(player);
|
|
1388
|
+
var sectionMarks = setupSectionMarks();
|
|
1389
|
+
var subFollow = setupSubsectionFollow(player);
|
|
904
1390
|
var mini = setupMiniPlayer(player);
|
|
905
1391
|
setupCopyButton(reports, tabs);
|
|
906
1392
|
setupQuiz();
|
|
907
1393
|
var zoom = setupZoom();
|
|
908
|
-
//
|
|
1394
|
+
// 빈 탭을 다시 세우고 끼울 때 쓰는 처리가 모두 준비된 뒤에 이어서 기다린다
|
|
1395
|
+
resumePending(reports, tabs);
|
|
1396
|
+
// 도식 노드는 Enter와 Space를 클릭으로 넘겨야 시각 이동이나 문단 열기와 확대 창 닫기가 같이 돈다
|
|
909
1397
|
document.addEventListener('keydown', function(ev){
|
|
910
1398
|
if (ev.key !== 'Enter' && ev.key !== ' ') return;
|
|
911
|
-
var el = ev.target.closest ? ev.target.closest('g[data-sec]') : null;
|
|
1399
|
+
var el = ev.target.closest ? ev.target.closest('g[data-sec], g[data-unit]') : null;
|
|
912
1400
|
if (!el) return;
|
|
913
1401
|
ev.preventDefault();
|
|
914
1402
|
el.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|