rshow 0.0.5 → 0.0.6
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/dist/main.js +344 -6
- package/package.json +1 -1
- package/skills/rshow/reference/mockups.md +1 -0
- package/skills/rshow-slides/SKILL.md +2 -0
- package/themes/default/deck.css +325 -1
- package/themes/default/deck.js +169 -1
- package/themes/default/mockup.css +105 -2
- package/themes/default/mockup.js +342 -18
- package/themes/default/terminal.css +8 -0
- package/themes/default/terminal.js +70 -13
- package/worker/index.js +15 -15
package/themes/default/mockup.js
CHANGED
|
@@ -55,6 +55,13 @@
|
|
|
55
55
|
* link holds a screen. Escape goes back to the canvas. ?skin= and ?view= and
|
|
56
56
|
* ?screen= set the first frame, which is what a screenshot wants.
|
|
57
57
|
*
|
|
58
|
+
* Keys
|
|
59
|
+
*
|
|
60
|
+
* w a s d and the arrows pan the canvas, + - 0 and z zoom it, [ ] and Tab
|
|
61
|
+
* pick a screen, Enter opens it and Escape goes back to the canvas. 1 to 9
|
|
62
|
+
* pick a state of the screen the keys are on, and ? lists every one of them.
|
|
63
|
+
* A field keeps its own keys and a chord with meta, ctrl or alt is never ours.
|
|
64
|
+
*
|
|
58
65
|
* Comments are read and written at data-store, default ../_data/mockup-comments
|
|
59
66
|
* relative to the page, and the store holds {value: [{id, artboard, text, at}]}.
|
|
60
67
|
* The server records who wrote it: this file never sends an author.
|
|
@@ -464,7 +471,7 @@
|
|
|
464
471
|
modeHost.appendChild(modeButton);
|
|
465
472
|
toolbar.appendChild(modeHost);
|
|
466
473
|
|
|
467
|
-
var hints = el('span', 'mk-hint', 'Drag to pan, Ctrl and wheel to zoom,
|
|
474
|
+
var hints = el('span', 'mk-hint', 'Drag to pan, Ctrl and wheel to zoom, ? for the keys');
|
|
468
475
|
toolbar.appendChild(hints);
|
|
469
476
|
|
|
470
477
|
// The bar that names the screen in focus and in play.
|
|
@@ -529,6 +536,54 @@
|
|
|
529
536
|
main.appendChild(stage);
|
|
530
537
|
main.appendChild(side);
|
|
531
538
|
|
|
539
|
+
// ---------------------------------------------------------------- keys ---
|
|
540
|
+
|
|
541
|
+
var KEYS = [
|
|
542
|
+
['w a s d', 'Pan the canvas'],
|
|
543
|
+
['arrow keys', 'Pan the canvas, or step between screens'],
|
|
544
|
+
['Shift and a key', 'Pan a quarter of the stage'],
|
|
545
|
+
['+ -', 'Zoom in and out'],
|
|
546
|
+
['0', 'Fit every screen'],
|
|
547
|
+
['z', 'Zoom to 100%'],
|
|
548
|
+
['[ ] or Tab', 'Previous and next screen'],
|
|
549
|
+
['Enter', 'Open the selected screen'],
|
|
550
|
+
['1 to 9', 'Pick a state of that screen'],
|
|
551
|
+
['Esc', 'Back to the canvas, or close this list'],
|
|
552
|
+
['?', 'Show or hide this list']
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
var keysOpen = false;
|
|
556
|
+
|
|
557
|
+
var keysPanel = el('div', 'mk-keys');
|
|
558
|
+
keysPanel.setAttribute('role', 'dialog');
|
|
559
|
+
keysPanel.setAttribute('aria-label', 'Keys');
|
|
560
|
+
keysPanel.hidden = true;
|
|
561
|
+
var keysList = el('dl', 'mk-keys-list');
|
|
562
|
+
KEYS.forEach(function (row) {
|
|
563
|
+
keysList.appendChild(el('dt', null, row[0]));
|
|
564
|
+
keysList.appendChild(el('dd', null, row[1]));
|
|
565
|
+
});
|
|
566
|
+
keysPanel.appendChild(keysList);
|
|
567
|
+
stage.appendChild(keysPanel);
|
|
568
|
+
|
|
569
|
+
// The corner button says what it is, so a reader who never guesses ? still
|
|
570
|
+
// finds the list. The pointerdown guard leaves the buttons of the stage alone.
|
|
571
|
+
var keysButton = el('button', 'mk-keys-toggle', '? keys');
|
|
572
|
+
keysButton.type = 'button';
|
|
573
|
+
keysButton.setAttribute('aria-label', 'Keys');
|
|
574
|
+
keysButton.setAttribute('aria-expanded', 'false');
|
|
575
|
+
keysButton.setAttribute('title', 'Every key (?)');
|
|
576
|
+
keysButton.addEventListener('click', function () {
|
|
577
|
+
setKeys(!keysOpen);
|
|
578
|
+
});
|
|
579
|
+
stage.appendChild(keysButton);
|
|
580
|
+
|
|
581
|
+
function setKeys(open) {
|
|
582
|
+
keysOpen = open === true;
|
|
583
|
+
keysPanel.hidden = !keysOpen;
|
|
584
|
+
keysButton.setAttribute('aria-expanded', keysOpen ? 'true' : 'false');
|
|
585
|
+
}
|
|
586
|
+
|
|
532
587
|
// Rows: wide screens and phones pack side by side, everything stays in the
|
|
533
588
|
// same visual order as the markup.
|
|
534
589
|
function layoutRows() {
|
|
@@ -605,6 +660,7 @@
|
|
|
605
660
|
}
|
|
606
661
|
|
|
607
662
|
function fitAll() {
|
|
663
|
+
stopAnimation();
|
|
608
664
|
var box = stageBox();
|
|
609
665
|
var w = world.offsetWidth;
|
|
610
666
|
var h = world.offsetHeight;
|
|
@@ -616,24 +672,33 @@
|
|
|
616
672
|
applyTransform(true);
|
|
617
673
|
}
|
|
618
674
|
|
|
619
|
-
function
|
|
675
|
+
function clampPoint(x, y) {
|
|
620
676
|
var box = stageBox();
|
|
621
677
|
var w = world.offsetWidth * zoom;
|
|
622
678
|
var h = world.offsetHeight * zoom;
|
|
623
679
|
var pad = 80;
|
|
680
|
+
var at = { x: x, y: y };
|
|
624
681
|
if (w <= box.width) {
|
|
625
|
-
|
|
682
|
+
at.x = (box.width - w) / 2;
|
|
626
683
|
} else {
|
|
627
|
-
|
|
684
|
+
at.x = Math.min(pad, Math.max(box.width - w - pad, at.x));
|
|
628
685
|
}
|
|
629
686
|
if (h <= box.height) {
|
|
630
|
-
|
|
687
|
+
at.y = (box.height - h) / 2;
|
|
631
688
|
} else {
|
|
632
|
-
|
|
689
|
+
at.y = Math.min(pad, Math.max(box.height - h - pad, at.y));
|
|
633
690
|
}
|
|
691
|
+
return at;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function clamp() {
|
|
695
|
+
var at = clampPoint(tx, ty);
|
|
696
|
+
tx = at.x;
|
|
697
|
+
ty = at.y;
|
|
634
698
|
}
|
|
635
699
|
|
|
636
700
|
function zoomTo(next, clientX, clientY) {
|
|
701
|
+
stopAnimation();
|
|
637
702
|
var box = stageBox();
|
|
638
703
|
var sx = clientX === undefined ? box.width / 2 : clientX - box.left;
|
|
639
704
|
var sy = clientY === undefined ? box.height / 2 : clientY - box.top;
|
|
@@ -670,6 +735,144 @@
|
|
|
670
735
|
zoomTo(0.1);
|
|
671
736
|
}
|
|
672
737
|
|
|
738
|
+
// ------------------------------------------------------------ selection --
|
|
739
|
+
|
|
740
|
+
var selectedId = null;
|
|
741
|
+
var panAnimation = null;
|
|
742
|
+
|
|
743
|
+
function markSelected() {
|
|
744
|
+
for (var b = 0; b < boards.length; b += 1) {
|
|
745
|
+
boards[b].el.classList.toggle('is-selected', boards[b].id === selectedId);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function selectedBoard() {
|
|
750
|
+
return view === 'canvas' ? boardById(selectedId) : current();
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function pickState(index) {
|
|
754
|
+
var board = selectedBoard();
|
|
755
|
+
if (!board) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
var names = [''].concat(board.states);
|
|
759
|
+
if (index < 0 || index >= names.length) {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
setState(board, names[index]);
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Where a board sits in the stage, measured with the canvas transform out of
|
|
767
|
+
// the way, so a selection lands on it at whatever the zoom is.
|
|
768
|
+
function boardAt(board) {
|
|
769
|
+
var box = stageBox();
|
|
770
|
+
var saved = world.style.transform;
|
|
771
|
+
world.style.transform = 'translate(0px,0px) scale(1)';
|
|
772
|
+
var rect = board.el.getBoundingClientRect();
|
|
773
|
+
world.style.transform = saved;
|
|
774
|
+
return { x: rect.left - box.left, y: rect.top - box.top, w: rect.width, h: rect.height };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function stopAnimation() {
|
|
778
|
+
if (panAnimation !== null) {
|
|
779
|
+
if (typeof window.cancelAnimationFrame === 'function') {
|
|
780
|
+
window.cancelAnimationFrame(panAnimation.frame);
|
|
781
|
+
}
|
|
782
|
+
window.clearTimeout(panAnimation.timer);
|
|
783
|
+
panAnimation = null;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function animateTo(x, y) {
|
|
788
|
+
stopAnimation();
|
|
789
|
+
var run = { frame: null, timer: null };
|
|
790
|
+
panAnimation = run;
|
|
791
|
+
|
|
792
|
+
function finish() {
|
|
793
|
+
if (panAnimation !== run) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
panAnimation = null;
|
|
797
|
+
tx = x;
|
|
798
|
+
ty = y;
|
|
799
|
+
applyTransform(true);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (typeof window.requestAnimationFrame !== 'function') {
|
|
803
|
+
finish();
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
var fromX = tx;
|
|
807
|
+
var fromY = ty;
|
|
808
|
+
var started = null;
|
|
809
|
+
|
|
810
|
+
function frame(now) {
|
|
811
|
+
if (panAnimation !== run) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
var at = typeof now === 'number' ? now : 0;
|
|
815
|
+
if (started === null) {
|
|
816
|
+
started = at;
|
|
817
|
+
}
|
|
818
|
+
var t = Math.min(1, (at - started) / 220);
|
|
819
|
+
var eased = 1 - (1 - t) * (1 - t) * (1 - t);
|
|
820
|
+
tx = fromX + (x - fromX) * eased;
|
|
821
|
+
ty = fromY + (y - fromY) * eased;
|
|
822
|
+
applyTransform(true);
|
|
823
|
+
if (t < 1) {
|
|
824
|
+
run.frame = window.requestAnimationFrame(frame);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
finish();
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
run.frame = window.requestAnimationFrame(frame);
|
|
831
|
+
// A window that draws no frames, such as the headless one behind a
|
|
832
|
+
// screenshot, still lands on the screen the keys picked.
|
|
833
|
+
run.timer = window.setTimeout(finish, 320);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function centreOn(board, animate) {
|
|
837
|
+
var at = boardAt(board);
|
|
838
|
+
var box = stageBox();
|
|
839
|
+
var to = clampPoint(
|
|
840
|
+
(box.width - at.w * zoom) / 2 - at.x * zoom,
|
|
841
|
+
(box.height - at.h * zoom) / 2 - at.y * zoom
|
|
842
|
+
);
|
|
843
|
+
if (animate !== true) {
|
|
844
|
+
tx = to.x;
|
|
845
|
+
ty = to.y;
|
|
846
|
+
applyTransform(true);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
animateTo(to.x, to.y);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function selectBoard(index) {
|
|
853
|
+
var count = boards.length;
|
|
854
|
+
if (count === 0) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
var at = ((index % count) + count) % count;
|
|
858
|
+
selectedId = boards[at].id;
|
|
859
|
+
markSelected();
|
|
860
|
+
if (view === 'canvas') {
|
|
861
|
+
centreOn(boards[at], true);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function stepSelection(delta) {
|
|
866
|
+
if (boards.length === 0) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
var at = selectedId === null ? -1 : boardIndex(selectedId);
|
|
870
|
+
if (at < 0) {
|
|
871
|
+
at = delta < 0 ? boards.length : -1;
|
|
872
|
+
}
|
|
873
|
+
selectBoard(at + delta);
|
|
874
|
+
}
|
|
875
|
+
|
|
673
876
|
function markCurrent() {
|
|
674
877
|
for (var b = 0; b < boards.length; b += 1) {
|
|
675
878
|
boards[b].el.classList.toggle('is-current', boards[b].id === currentId);
|
|
@@ -677,6 +880,7 @@
|
|
|
677
880
|
}
|
|
678
881
|
|
|
679
882
|
function layoutCurrent() {
|
|
883
|
+
stopAnimation();
|
|
680
884
|
var board = current();
|
|
681
885
|
if (!board) {
|
|
682
886
|
return;
|
|
@@ -744,6 +948,8 @@
|
|
|
744
948
|
canvasState = { zoom: zoom, tx: tx, ty: ty };
|
|
745
949
|
}
|
|
746
950
|
currentId = id;
|
|
951
|
+
selectedId = id;
|
|
952
|
+
markSelected();
|
|
747
953
|
setView('focus');
|
|
748
954
|
writeHash(id);
|
|
749
955
|
}
|
|
@@ -756,13 +962,21 @@
|
|
|
756
962
|
canvasState = { zoom: zoom, tx: tx, ty: ty };
|
|
757
963
|
}
|
|
758
964
|
currentId = id;
|
|
965
|
+
selectedId = id;
|
|
966
|
+
markSelected();
|
|
759
967
|
setView('play');
|
|
760
968
|
writeHash(id);
|
|
761
969
|
}
|
|
762
970
|
|
|
763
971
|
function backToCanvas() {
|
|
972
|
+
if (view !== 'canvas' && currentId !== null) {
|
|
973
|
+
// The screen the reader was on stays the selected one, so the keys can
|
|
974
|
+
// open it again or step from it.
|
|
975
|
+
selectedId = currentId;
|
|
976
|
+
}
|
|
764
977
|
currentId = null;
|
|
765
978
|
setView('canvas');
|
|
979
|
+
markSelected();
|
|
766
980
|
writeHash('');
|
|
767
981
|
zoom = canvasState.zoom;
|
|
768
982
|
tx = canvasState.tx;
|
|
@@ -1105,6 +1319,17 @@
|
|
|
1105
1319
|
var flashTimer = null;
|
|
1106
1320
|
var suppressHash = false;
|
|
1107
1321
|
|
|
1322
|
+
// A control placed over the canvas owns its own pointer and its own keys.
|
|
1323
|
+
var CONTROLS =
|
|
1324
|
+
'button, a[href], input, select, textarea, label, summary, [role="button"], [tabindex]:not([tabindex="-1"])';
|
|
1325
|
+
|
|
1326
|
+
function control(node) {
|
|
1327
|
+
if (!node || !node.closest) {
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
return node.closest(CONTROLS);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1108
1333
|
toolbar.addEventListener('click', function (event) {
|
|
1109
1334
|
var button = event.target.closest ? event.target.closest('[data-mk]') : null;
|
|
1110
1335
|
if (!button) {
|
|
@@ -1198,6 +1423,7 @@
|
|
|
1198
1423
|
});
|
|
1199
1424
|
|
|
1200
1425
|
function startPan(event) {
|
|
1426
|
+
stopAnimation();
|
|
1201
1427
|
dragging = { x: event.clientX, y: event.clientY, tx: tx, ty: ty };
|
|
1202
1428
|
stage.classList.add('is-panning');
|
|
1203
1429
|
if (stage.setPointerCapture) {
|
|
@@ -1213,6 +1439,12 @@
|
|
|
1213
1439
|
if (event.button !== 0) {
|
|
1214
1440
|
return;
|
|
1215
1441
|
}
|
|
1442
|
+
// A control above the screen keeps its own click: capturing the pointer
|
|
1443
|
+
// here would retarget that click to the stage.
|
|
1444
|
+
var taken = control(event.target);
|
|
1445
|
+
if (!spaceDown && taken !== null && taken !== stage) {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1216
1448
|
var inScreen = event.target.closest ? event.target.closest('.mk-screen') : null;
|
|
1217
1449
|
if (!spaceDown && inScreen) {
|
|
1218
1450
|
return;
|
|
@@ -1254,6 +1486,7 @@
|
|
|
1254
1486
|
return;
|
|
1255
1487
|
}
|
|
1256
1488
|
event.preventDefault();
|
|
1489
|
+
stopAnimation();
|
|
1257
1490
|
if (event.ctrlKey || event.metaKey) {
|
|
1258
1491
|
zoomTo(zoom * (event.deltaY < 0 ? 1.12 : 1 / 1.12), event.clientX, event.clientY);
|
|
1259
1492
|
return;
|
|
@@ -1274,6 +1507,32 @@
|
|
|
1274
1507
|
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || node.isContentEditable === true;
|
|
1275
1508
|
}
|
|
1276
1509
|
|
|
1510
|
+
// Tab and Enter belong to the page while a reader is in the toolbar, the
|
|
1511
|
+
// panel or a screen: only the stage itself takes them over.
|
|
1512
|
+
function fromStage(node) {
|
|
1513
|
+
return node === document.body || node === document.documentElement || node === stage;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function panSize(event) {
|
|
1517
|
+
if (event.shiftKey === true) {
|
|
1518
|
+
var box = stageBox();
|
|
1519
|
+
return { x: Math.round(box.width / 4), y: Math.round(box.height / 4) };
|
|
1520
|
+
}
|
|
1521
|
+
return { x: 64, y: 64 };
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// A screen taller than the stage scrolls under it. One that fits has nowhere
|
|
1525
|
+
// to go, so up and down leave it alone.
|
|
1526
|
+
function panTall(direction, event) {
|
|
1527
|
+
var board = current();
|
|
1528
|
+
var box = stageBox();
|
|
1529
|
+
if (!board || board.el.offsetHeight * zoom <= box.height) {
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
ty -= direction * panSize(event).y;
|
|
1533
|
+
applyTransform(false);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1277
1536
|
document.addEventListener('keydown', function (event) {
|
|
1278
1537
|
if (event.metaKey || event.ctrlKey || event.altKey || typing(event.target)) {
|
|
1279
1538
|
return;
|
|
@@ -1286,6 +1545,29 @@
|
|
|
1286
1545
|
}
|
|
1287
1546
|
return;
|
|
1288
1547
|
}
|
|
1548
|
+
if (key === '?') {
|
|
1549
|
+
event.preventDefault();
|
|
1550
|
+
setKeys(!keysOpen);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (key === 'Escape') {
|
|
1554
|
+
if (keysOpen) {
|
|
1555
|
+
event.preventDefault();
|
|
1556
|
+
setKeys(false);
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (view !== 'canvas') {
|
|
1560
|
+
event.preventDefault();
|
|
1561
|
+
backToCanvas();
|
|
1562
|
+
}
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
if (key >= '1' && key <= '9' && key.length === 1) {
|
|
1566
|
+
if (pickState(Number(key) - 1)) {
|
|
1567
|
+
event.preventDefault();
|
|
1568
|
+
}
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1289
1571
|
if (key === '+' || key === '=') {
|
|
1290
1572
|
event.preventDefault();
|
|
1291
1573
|
zoomStep(1);
|
|
@@ -1303,33 +1585,67 @@
|
|
|
1303
1585
|
}
|
|
1304
1586
|
return;
|
|
1305
1587
|
}
|
|
1306
|
-
if (key === '
|
|
1307
|
-
if (view
|
|
1588
|
+
if (key === 'z' || key === 'Z') {
|
|
1589
|
+
if (view === 'canvas') {
|
|
1308
1590
|
event.preventDefault();
|
|
1309
|
-
|
|
1591
|
+
zoomTo(1);
|
|
1310
1592
|
}
|
|
1311
1593
|
return;
|
|
1312
1594
|
}
|
|
1313
|
-
if (key === '
|
|
1314
|
-
|
|
1595
|
+
if (key === ']' || key === '[') {
|
|
1596
|
+
event.preventDefault();
|
|
1597
|
+
var order = key === ']' ? 1 : -1;
|
|
1315
1598
|
if (view === 'canvas') {
|
|
1316
|
-
|
|
1317
|
-
clamp();
|
|
1318
|
-
applyTransform(true);
|
|
1599
|
+
stepSelection(order);
|
|
1319
1600
|
} else {
|
|
1601
|
+
step(order);
|
|
1602
|
+
}
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (key === 'Tab') {
|
|
1606
|
+
if (view === 'canvas' && fromStage(event.target) && control(event.target) === null) {
|
|
1320
1607
|
event.preventDefault();
|
|
1321
|
-
|
|
1608
|
+
stepSelection(event.shiftKey === true ? -1 : 1);
|
|
1322
1609
|
}
|
|
1323
1610
|
return;
|
|
1324
1611
|
}
|
|
1325
|
-
if (key === '
|
|
1326
|
-
if (view !== 'canvas') {
|
|
1612
|
+
if (key === 'Enter') {
|
|
1613
|
+
if (view !== 'canvas' || selectedId === null || control(event.target) !== null) {
|
|
1327
1614
|
return;
|
|
1328
1615
|
}
|
|
1329
|
-
|
|
1616
|
+
event.preventDefault();
|
|
1617
|
+
focus(selectedId);
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
var dx = 0;
|
|
1621
|
+
var dy = 0;
|
|
1622
|
+
if (key === 'ArrowRight' || key === 'd' || key === 'D') {
|
|
1623
|
+
dx = 1;
|
|
1624
|
+
} else if (key === 'ArrowLeft' || key === 'a' || key === 'A') {
|
|
1625
|
+
dx = -1;
|
|
1626
|
+
} else if (key === 'ArrowDown' || key === 's' || key === 'S') {
|
|
1627
|
+
dy = 1;
|
|
1628
|
+
} else if (key === 'ArrowUp' || key === 'w' || key === 'W') {
|
|
1629
|
+
dy = -1;
|
|
1630
|
+
}
|
|
1631
|
+
if (dx === 0 && dy === 0) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
event.preventDefault();
|
|
1635
|
+
if (view === 'canvas') {
|
|
1636
|
+
var stepSize = panSize(event);
|
|
1637
|
+
stopAnimation();
|
|
1638
|
+
tx -= dx * stepSize.x;
|
|
1639
|
+
ty -= dy * stepSize.y;
|
|
1330
1640
|
clamp();
|
|
1331
1641
|
applyTransform(true);
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
if (dx !== 0) {
|
|
1645
|
+
step(dx);
|
|
1646
|
+
return;
|
|
1332
1647
|
}
|
|
1648
|
+
panTall(dy, event);
|
|
1333
1649
|
});
|
|
1334
1650
|
|
|
1335
1651
|
document.addEventListener('keyup', function (event) {
|
|
@@ -1412,6 +1728,12 @@
|
|
|
1412
1728
|
|
|
1413
1729
|
var requested = query.get('screen') || window.location.hash.replace(/^#/, '');
|
|
1414
1730
|
var startView = query.get('view');
|
|
1731
|
+
// A phone shows the whole canvas at 12%, which nobody can read. When the stage
|
|
1732
|
+
// is narrower than 700px and the URL asks for no view and no screen, the first
|
|
1733
|
+
// screen opens in focus view, fitted to the width by layoutCurrent. ?view=play,
|
|
1734
|
+
// ?view=canvas and #screen still say what they said.
|
|
1735
|
+
var narrow = stage.getBoundingClientRect().width < 700;
|
|
1736
|
+
var asked = startView !== null && startView !== '';
|
|
1415
1737
|
main.setAttribute('data-view', 'canvas');
|
|
1416
1738
|
fitAll();
|
|
1417
1739
|
if (boards.length > 0) {
|
|
@@ -1425,6 +1747,8 @@
|
|
|
1425
1747
|
play(boards[0].id);
|
|
1426
1748
|
} else if (startView === 'focus') {
|
|
1427
1749
|
focus(boards[0].id);
|
|
1750
|
+
} else if (narrow && !asked) {
|
|
1751
|
+
focus(boards[0].id);
|
|
1428
1752
|
}
|
|
1429
1753
|
}
|
|
1430
1754
|
|
|
@@ -241,6 +241,14 @@
|
|
|
241
241
|
tab-size: 8;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
/* A recording the width cannot hold keeps the grid it was taken on and scrolls
|
|
245
|
+
here, rather than being fitted down to a font nobody can read. The player
|
|
246
|
+
clips its own box, so the scroll has to be said again one class deeper. */
|
|
247
|
+
.terminal .terminal-body .ap-player {
|
|
248
|
+
overflow-x: auto;
|
|
249
|
+
overscroll-behavior-x: contain;
|
|
250
|
+
}
|
|
251
|
+
|
|
244
252
|
/* asciinema-player declares a dark palette of its own on .ap-player, which
|
|
245
253
|
would win over anything inherited from the figure. Hand it the figure's
|
|
246
254
|
palette instead, so a recording and a capture of the same program agree. */
|
|
@@ -228,6 +228,23 @@
|
|
|
228
228
|
return anchor;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// One cell of the theme's monospace font at the size the theme sets, measured
|
|
232
|
+
// on the page. A recording is fitted to the width only while its text stays at
|
|
233
|
+
// MIN_FONT; below that the frame keeps the page width and scrolls instead.
|
|
234
|
+
var MIN_FONT = 9;
|
|
235
|
+
|
|
236
|
+
function cellWidth(host) {
|
|
237
|
+
var probe = element('span', 'terminal-probe');
|
|
238
|
+
probe.style.cssText = 'position: absolute; top: 0; left: 0; visibility: hidden;' +
|
|
239
|
+
'white-space: pre; font-family: ' + property(host, '--font-mono', 'monospace') +
|
|
240
|
+
'; font-size: ' + fontSize(host) + 'px';
|
|
241
|
+
probe.textContent = '0000000000';
|
|
242
|
+
(document.body || document.documentElement).appendChild(probe);
|
|
243
|
+
var width = probe.getBoundingClientRect().width / 10;
|
|
244
|
+
(document.body || document.documentElement).removeChild(probe);
|
|
245
|
+
return width > 0 ? width : 7.8;
|
|
246
|
+
}
|
|
247
|
+
|
|
231
248
|
function build(host) {
|
|
232
249
|
var src = text(host.getAttribute('data-src'));
|
|
233
250
|
var title = text(host.getAttribute('data-title')) || (src === '' ? 'terminal' : fileName(src));
|
|
@@ -264,8 +281,9 @@
|
|
|
264
281
|
|
|
265
282
|
// ----------------------------------------------------------------- ansi ---
|
|
266
283
|
|
|
267
|
-
// A terminal window is as wide as its grid
|
|
268
|
-
//
|
|
284
|
+
// A terminal window is as wide as its grid, and never wider than the page: on a
|
|
285
|
+
// phone the frame keeps the width it is given and the grid scrolls inside the
|
|
286
|
+
// body, which is what terminal.css leaves overflow-x for.
|
|
269
287
|
function hug(shell, body, term) {
|
|
270
288
|
if (!term.element) {
|
|
271
289
|
return;
|
|
@@ -276,7 +294,9 @@
|
|
|
276
294
|
}
|
|
277
295
|
var box = window.getComputedStyle(body);
|
|
278
296
|
var side = (parseFloat(box.paddingLeft) || 0) + (parseFloat(box.paddingRight) || 0);
|
|
279
|
-
|
|
297
|
+
var wanted = Math.ceil(width + side + 2);
|
|
298
|
+
var room = shell.parentNode ? shell.parentNode.clientWidth : 0;
|
|
299
|
+
shell.style.width = (room > 0 && wanted > room ? room : wanted) + 'px';
|
|
280
300
|
}
|
|
281
301
|
|
|
282
302
|
function ansi(host, body, shell, src) {
|
|
@@ -339,7 +359,32 @@
|
|
|
339
359
|
|
|
340
360
|
// ----------------------------------------------------------------- cast ---
|
|
341
361
|
|
|
362
|
+
// The grid a recording ran on: the figure says so, or the first line of the
|
|
363
|
+
// asciicast does, one JSON object with its width and height.
|
|
364
|
+
function castGrid(host, src, done) {
|
|
365
|
+
var cols = count(host.getAttribute('data-cols'), 0);
|
|
366
|
+
var rows = count(host.getAttribute('data-rows'), 0);
|
|
367
|
+
if (cols > 0 && rows > 0) {
|
|
368
|
+
done(cols, rows);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
window.fetch(src, { credentials: 'same-origin' }).then(function (reply) {
|
|
372
|
+
return reply.ok ? reply.text() : '';
|
|
373
|
+
}).then(function (cast) {
|
|
374
|
+
var head = {};
|
|
375
|
+
try {
|
|
376
|
+
head = JSON.parse(cast.split('\n')[0]) || {};
|
|
377
|
+
} catch (err) {
|
|
378
|
+
head = {};
|
|
379
|
+
}
|
|
380
|
+
done(count(head.width, 0), count(head.height, 0));
|
|
381
|
+
}, function () {
|
|
382
|
+
done(0, 0);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
342
386
|
function cast(host, body, src) {
|
|
387
|
+
var size = fontSize(host);
|
|
343
388
|
var settings = {
|
|
344
389
|
fit: 'width',
|
|
345
390
|
controls: true,
|
|
@@ -348,19 +393,12 @@
|
|
|
348
393
|
// rectangle: the prompt and the command being typed.
|
|
349
394
|
poster: 'npt:0:02',
|
|
350
395
|
terminalFontFamily: property(host, '--font-mono', 'monospace'),
|
|
351
|
-
terminalFontSize:
|
|
396
|
+
terminalFontSize: size + 'px',
|
|
352
397
|
terminalLineHeight: 1.4
|
|
353
398
|
};
|
|
354
|
-
var cols = count(host.getAttribute('data-cols'), 0);
|
|
355
|
-
var rows = count(host.getAttribute('data-rows'), 0);
|
|
356
|
-
if (cols > 0) {
|
|
357
|
-
settings.cols = cols;
|
|
358
|
-
}
|
|
359
|
-
if (rows > 0) {
|
|
360
|
-
settings.rows = rows;
|
|
361
|
-
}
|
|
362
399
|
|
|
363
400
|
var player = null;
|
|
401
|
+
var ready = false;
|
|
364
402
|
|
|
365
403
|
function start(at) {
|
|
366
404
|
var options = {};
|
|
@@ -378,11 +416,30 @@
|
|
|
378
416
|
}
|
|
379
417
|
|
|
380
418
|
loadPlayer().then(function () {
|
|
381
|
-
|
|
419
|
+
castGrid(host, src, function (cols, rows) {
|
|
420
|
+
if (cols > 0) {
|
|
421
|
+
settings.cols = cols;
|
|
422
|
+
}
|
|
423
|
+
if (rows > 0) {
|
|
424
|
+
settings.rows = rows;
|
|
425
|
+
}
|
|
426
|
+
// Fitting 100 columns into a phone asks for a 5px font. So the recording
|
|
427
|
+
// is fitted only while the fit keeps its text at MIN_FONT; below that
|
|
428
|
+
// fit is off at the theme's size and the body scrolls sideways.
|
|
429
|
+
if (cols > 0 && body.clientWidth > 0 &&
|
|
430
|
+
body.clientWidth / (cols * cellWidth(host)) * size < MIN_FONT) {
|
|
431
|
+
settings.fit = false;
|
|
432
|
+
}
|
|
433
|
+
ready = true;
|
|
434
|
+
start(0);
|
|
435
|
+
});
|
|
382
436
|
// The player reads the palette once, when it mounts, so a mode change
|
|
383
437
|
// means a new player, put back where the old one was.
|
|
384
438
|
document.addEventListener('rshowmodechange', function () {
|
|
385
439
|
var at = 0;
|
|
440
|
+
if (!ready) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
386
443
|
try {
|
|
387
444
|
at = player && player.getCurrentTime ? player.getCurrentTime() : 0;
|
|
388
445
|
} catch (err) {
|