Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Live Mirror Editor position in Preview pane. #3484

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8ece547
Add live scrolling to keep the preview in sync with editor position.
dbolacksn May 18, 2024
b7dc47f
Add a Live Scroll lock/unlock below Brew/Source Jump buttons.
dbolacksn May 19, 2024
86887b5
Update Jump keys to match parent PR and shift live scrolling to scrol…
dbolacksn May 19, 2024
07f2e8b
Merge branch 'master' into Issue_241_Part_II
dbolacksn May 19, 2024
bacdd65
Add a CR.
dbolacksn May 19, 2024
835ca0d
WIP
dbolacksn May 20, 2024
77450ed
Merge branch 'master' into Issue_241_Part_II
dbolacksn May 21, 2024
e69132b
Constant a lookup.
dbolacksn May 21, 2024
a715c9e
Store livescrolling in local storage
dbolacksn Jun 3, 2024
cca79d4
Merge branch 'master' into Issue_241_Part_II
dbolacksn Jun 3, 2024
4818f70
Add additional visual hinting to liveScroll lock.
dbolacksn Jun 3, 2024
ea9f9a8
Missed a couple of variable repalcements.
dbolacksn Jun 3, 2024
510d8f4
Resolve timing issue with liveScroll on linking.
dbolacksn Jun 4, 2024
7e3f2a3
Merge branch 'master' into Issue_241_Part_II
dbolacksn Jun 27, 2024
19ee3d6
Merge branch 'master' into Issue_241_Part_II
dbolacksn Jul 8, 2024
1844626
Merge branch 'master' into Issue_241_Part_II
dbolacksn Jul 30, 2024
dcdc8b4
Remove Livescrolling toggle hot-key.
dbolacksn Jul 30, 2024
b340276
Move livescrollToggle function out into a class method instead of an …
dbolacksn Jul 30, 2024
f10ef2b
Merge branch 'master' into Issue_241_Part_II
dbolacksn Aug 1, 2024
0057e2b
Merge branch 'master' into Issue_241_Part_II
dbolacksn Aug 2, 2024
2d781f0
Merge branch 'master' into Issue_241_Part_II
dbolacksn Aug 13, 2024
17b22b8
Merge branch 'master' into Issue_241_Part_II
dbolacksn Aug 15, 2024
78c4061
Merge branch 'master' into Issue_241_Part_II
dbolacksn Aug 20, 2024
fa63f1d
Merge branch 'master' into Issue_241_Part_II
dbolacksn Aug 20, 2024
5431d3e
Merge branch 'master' into Issue_241_Part_II
dbolacksn Aug 22, 2024
6952933
Fix merge
dbolacksn Aug 22, 2024
e27e61a
Bind livescrolling when done via scrollbars.
dbolacksn Aug 24, 2024
f24e477
Merge branch 'master' into Issue_241_Part_II
dbolacksn Aug 24, 2024
a4f30d6
Merge branch 'master' into Issue_241_Part_II
dbolacksn Aug 29, 2024
82f2d02
Merge branch 'master' into Issue_241_Part_II
dbolacksn Aug 31, 2024
b124e55
Merge branch 'master' into Issue_241_Part_II
calculuschild Sep 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions client/homebrew/brewRenderer/brewRenderer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const DOMPurify = require('dompurify');
const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false };

const PAGE_HEIGHT = 1056;
let isScrolling;

const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head>
Expand Down Expand Up @@ -87,6 +88,21 @@ const BrewRenderer = (props)=>{
}));
};

const handleScroll = (e)=>{
const target = e.target;
const newPage = Math.floor(target.scrollTop / target.scrollHeight * rawPages.length);
if(newPage != state.viewablePageNumber) {
window.clearTimeout(isScrolling);
isScrolling = setTimeout(function() {
window.parent.document.dispatchEvent(new CustomEvent('renderScrolled', {}));
}, 66);
}
setState((prevState)=>({
...prevState,
viewablePageNumber : newPage
}));
};

const getCurrentPage = (e)=>{
const { scrollTop, clientHeight, scrollHeight } = e.target;
const totalScrollableHeight = scrollHeight - clientHeight;
Expand All @@ -98,6 +114,7 @@ const BrewRenderer = (props)=>{
}));
};


const isInView = (index)=>{
if(!state.isMounted)
return false;
Expand Down
147 changes: 116 additions & 31 deletions client/homebrew/editor/editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ const DEFAULT_STYLE_TEXT = dedent`
color: black;
}`;

let lastPage = 0;
let lockBrewJump = false;
let lockSourceJump = false;
let scrollingJump = false;

const isElementCodeMirror=(element)=>{
let el = element;
while( el.tagName != 'body' ) {
if ( !el?.classList ) return false
if( el?.classList?.contains('CodeMirror-code') || el.classList.contains('CodeMirror-line'))
return true;
el = el.parentNode;
}
return false;
};

const Editor = createClass({
displayName : 'Editor',
Expand Down Expand Up @@ -56,11 +71,22 @@ const Editor = createClass({
isMeta : function() {return this.state.view == 'meta';},

componentDidMount : function() {

this.updateEditorSize();
this.highlightCustomMarkdown();
window.addEventListener('resize', this.updateEditorSize);
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
document.addEventListener('renderScrolled', this.handleBrewScroll);
document.addEventListener('keydown', this.handleControlKeys);
document.addEventListener('click', (e)=>{
if(isElementCodeMirror(e.target) && this.props.liveScroll ) {
const curPage = this.getCurrentPage();
if( curPage != lastPage ) {
this.brewJump();
lastPage = curPage;
}
}
});

const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
if(editorTheme) {
Expand All @@ -75,27 +101,63 @@ const Editor = createClass({
},

componentDidUpdate : function(prevProps, prevState, snapshot) {

this.highlightCustomMarkdown();
if(prevProps.moveBrew !== this.props.moveBrew) {
this.brewJump();
};
if(prevProps.moveSource !== this.props.moveSource) {
this.sourceJump();
};
if(prevProps.liveScroll != this.props.liveScroll) {
if((prevProps.liveScroll != undefined) && (this.props.liveScroll)) this.brewJump();
};
},

handleControlKeys : function(e){
if(!(e.ctrlKey && e.metaKey)) return;
const END_KEY = 35;
const HOME_KEY = 36;
const LEFTARROW_KEY = 37;
const RIGHTARROW_KEY = 39;
if (e.shiftKey && (e.keyCode == RIGHTARROW_KEY)) this.brewJump();
if (e.shiftKey && (e.keyCode == LEFTARROW_KEY)) this.sourceJump();
if ((e.keyCode == LEFTARROW_KEY) || (e.keyCode == RIGHTARROW_KEY)) {
if(!(e.ctrlKey && e.metaKey)) return;

if(!e.ctrlKey) return;

if(this.props.liveScroll) {
const movementKeys = [13, 33, 34, LEFTARROW_KEY, 38, RIGHTARROW_KEY, 40];
if(movementKeys.includes(e.keyCode)) {
const curPage = this.getCurrentPage();
if(curPage != lastPage) {
this.brewJump();
lastPage = curPage;
}
}
}

// Handle CTRL-HOME and CTRL-END
if(((e.keyCode == END_KEY) || (e.keyCode == HOME_KEY)) && this.props.liveScroll) this.brewJump();
if(!e.metaKey) return;
if(e.shiftKey && (e.keyCode == RIGHTARROW_KEY)) this.brewJump();
if(e.shiftKey && (e.keyCode == LEFTARROW_KEY)) this.sourceJump();
if((e.keyCode == LEFTARROW_KEY) || (e.keyCode == RIGHTARROW_KEY)) {
e.stopPropagation();
e.preventDefault();
}
},

handleBrewScroll : function() {
if(!this.props.liveScroll) return;
scrollingJump = true;
this.sourceJump();
scrollingJump = false;
},

handleSourceScroll : function(e) {
if(!this.props.liveScroll) return;
scrollingJump = true;
this.brewJump();
scrollingJump = false;
},

updateEditorSize : function() {
if(this.codeEditor.current) {
Expand Down Expand Up @@ -292,8 +354,11 @@ const Editor = createClass({
},

brewJump : function(targetPage=this.getCurrentPage()){
if(lockBrewJump) return;
if(!window) return;
// console.log(`Scroll to: p${targetPage}`);
lockSourceJump = true;
lockBrewJump = true;
//console.log(`Scroll to: p${targetPage}`);
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
Expand All @@ -302,24 +367,33 @@ const Editor = createClass({
const bounceDelay = 100;
const scrollDelay = 500;

if(!this.throttleBrewMove) {
this.throttleBrewMove = _.throttle((currentPos, interimPos, targetPos)=>{
brewRenderer.scrollTo({ top: currentPos + interimPos, behavior: 'smooth' });
setTimeout(()=>{
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
}, bounceDelay);
}, scrollDelay, { leading: true, trailing: false });
};
this.throttleBrewMove(currentPos, interimPos, targetPos);
if(scrollingJump) {
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'instant', block: 'start' });
} else {
if(!this.throttleBrewMove) {
this.throttleBrewMove = _.throttle((currentPos, interimPos, targetPos)=>{
brewRenderer.scrollTo({ top: currentPos + interimPos, behavior: 'smooth' });
setTimeout(()=>{
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
}, bounceDelay);
}, scrollDelay, { leading: true, trailing: false });
};
this.throttleBrewMove(currentPos, interimPos, targetPos);
}
lockSourceJump = false;
lockBrewJump = false;

// const hashPage = (page != 1) ? `p${page}` : '';
// window.location.hash = hashPage;
},

sourceJump : function(targetLine=null){
if(lockSourceJump) return;
if(this.isText()) {
if(targetLine == null) {
targetLine = 0;
lockBrewJump = true;
lockSourceJump = true;

const pageCollection = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('page');
const brewRendererHeight = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0).getBoundingClientRect().height;
Expand All @@ -342,23 +416,33 @@ const Editor = createClass({
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);

//Scroll 1/10 of the way every 10ms until 1px off.
const incrementalScroll = setInterval(()=>{
currentY += (targetY - currentY) / 10;
this.codeEditor.current.codeMirror.scrollTo(null, currentY);

// Update target: target height is not accurate until within +-10 lines of the visible window
if(Math.abs(targetY - currentY > 100))
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);

// End when close enough
if(Math.abs(targetY - currentY) < 1) {
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
clearInterval(incrementalScroll);
}
}, 10);
if(!scrollingJump) {
//Scroll 1/10 of the way every 10ms until 1px off.
const incrementalScroll = setInterval(()=>{
currentY += (targetY - currentY) / 10;
this.codeEditor.current.codeMirror.scrollTo(null, currentY);

// Update target: target height is not accurate until within +-10 lines of the visible window
if(Math.abs(targetY - currentY > 100))
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);

// End when close enough
if(Math.abs(targetY - currentY) < 1) {
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
clearInterval(incrementalScroll);
lockBrewJump = false;
lockSourceJump = false;
}
}, 10);
} else {
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
lockBrewJump = false;
lockSourceJump = false;
}
}
}
},
Expand Down Expand Up @@ -389,6 +473,7 @@ const Editor = createClass({
view={this.state.view}
value={this.props.brew.text}
onChange={this.props.onTextChange}
onScroll={this.handleSourceScroll}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} />
</>;
Expand Down
9 changes: 9 additions & 0 deletions shared/naturalcrit/codeEditor/codeEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const closeTag = require('./close-tag');
const autoCompleteEmoji = require('./autocompleteEmoji');

let CodeMirror;
let isScrolling;
if(typeof window !== 'undefined'){
CodeMirror = require('codemirror');

Expand Down Expand Up @@ -190,6 +191,14 @@ const CodeEditor = createClass({

// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
this.codeMirror.on('scroll', (cm)=>{
window.clearTimeout(isScrolling);
const props = this.props;
isScrolling = setTimeout(function() {
cm.setCursor({ line: cm.lineAtHeight(cm.getWrapperElement().getBoundingClientRect().top) + 1, ch: 0 });
props.onScroll(cm.lineAtHeight(cm.getWrapperElement().getBoundingClientRect().top));
}, 66);
});
this.updateSize();
},

Expand Down
56 changes: 47 additions & 9 deletions shared/naturalcrit/splitPane/splitPane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ const SplitPane = createClass({

getInitialState : function() {
return {
currentDividerPos : null,
windowWidth : 0,
isDragging : false,
moveSource : false,
moveBrew : false,
showMoveArrows : true
currentDividerPos : null,
windowWidth : 0,
isDragging : false,
moveSource : false,
moveBrew : false,
viewablePageNumber : 0,
showMoveArrows : true
};
},

Expand All @@ -42,6 +43,22 @@ const SplitPane = createClass({
});
}
window.addEventListener('resize', this.handleWindowResize);


// This lives here instead of in the initial render because you cannot touch localStorage until the componant mounts.
const loadLiveScroll = window.localStorage.getItem('liveScroll') === 'true';
this.setState({
liveScroll : loadLiveScroll
});
const toggle = document.getElementById('scrollToggle');
const toggleDiv = document.getElementById('scrollToggleDiv');
if(loadLiveScroll) {
toggle.className = 'fas fa-lock';
toggleDiv.className = 'arrow lock';
} else {
toggle.className = 'fas fa-unlock';
toggleDiv.className = 'arrow unlock';
}
},

componentWillUnmount : function() {
Expand Down Expand Up @@ -89,6 +106,21 @@ const SplitPane = createClass({
userSetDividerPos : newSize
});
},

liveScrollToggle : function(e) {
const flipLiveScroll = !this.state.liveScroll;
const toggle = e.target;
const toggleDiv = toggle.parentElement;
if(flipLiveScroll) {
toggle.className = 'fas fa-lock';
toggleDiv.className = 'arrow lock';
} else {
toggle.className = 'fas fa-unlock';
toggleDiv.className = 'arrow unlock';
}
window.localStorage.setItem('liveScroll', String(flipLiveScroll));
this.setState({ liveScroll: flipLiveScroll });
},
/*
unFocus : function() {
if(document.selection){
Expand Down Expand Up @@ -120,6 +152,11 @@ const SplitPane = createClass({
onClick={()=>this.setState({ moveBrew: !this.state.moveBrew })} >
<i className='fas fa-arrow-right' />
</div>
<div id='scrollToggleDiv' className={`arrow lock`}
style={{ left: this.state.currentDividerPos-4 }}
onClick={this.liveScrollToggle} >
<i id='scrollToggle' className={`fas fa-lock`} />
</div>
</>;
}
},
Expand All @@ -144,9 +181,10 @@ const SplitPane = createClass({
>
{React.cloneElement(this.props.children[0], {
...(this.props.showDividerButtons && {
moveBrew: this.state.moveBrew,
moveSource: this.state.moveSource,
setMoveArrows: this.setMoveArrows,
moveBrew : this.state.moveBrew,
moveSource : this.state.moveSource,
liveScroll : this.state.liveScroll,
setMoveArrows : this.setMoveArrows,
}),
})}
</Pane>
Expand Down
9 changes: 9 additions & 0 deletions shared/naturalcrit/splitPane/splitPane.less
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@
.tooltipRight('Jump to location in Preview');
top : 60px;
}
&.lock{
.tooltipRight('Unlock cursor tracking between windows.');
top : 90px;
background: #666;
}
&.unlock{
.tooltipRight('Lock cursor tracking between windows.');
top : 90px;
}
&:hover{
background-color: #666;
}
Expand Down