fix: keep the zones in the right places

The description zone needs fixing, but the hint zone should behave correctly.
This commit is contained in:
Oliver Eyton-Williams
2020-07-09 15:44:58 +02:00
committed by Mrugesh Mohapatra
parent e34bdded7d
commit 120bb342e8

View File

@ -105,6 +105,10 @@ const toStartOfLine = range => {
return range.setStartPosition(range.startLineNumber, 1); return range.setStartPosition(range.startLineNumber, 1);
}; };
const toLastLine = range => {
return range.setStartPosition(range.endLineNumber, 1);
};
class Editor extends Component { class Editor extends Component {
constructor(...props) { constructor(...props) {
super(...props); super(...props);
@ -324,73 +328,43 @@ class Editor extends Component {
const getOutputZoneTop = this.getOutputZoneTop.bind(this); const getOutputZoneTop = this.getOutputZoneTop.bind(this);
const createOutputNode = this.createOutputNode.bind(this); const createOutputNode = this.createOutputNode.bind(this);
// TODO: take care that there's no race/ordering problems, with the const createWidget = (id, domNode, getTop) => {
// placement of domNode (shouldn't be once it's no longer used in the const getId = () => id;
// view zone, but make sure!) const getDomNode = () => domNode;
this._overlayWidget = { const getPosition = () => {
domNode: null, domNode.style.width = editor.getLayoutInfo().contentWidth + 'px';
getId: function() { domNode.style.top = getTop();
return 'my.overlay.widget';
},
getDomNode: function() {
if (!this.domNode) {
this.domNode = createDescription();
// make sure it's hidden from screenreaders. // must return null, so that Monaco knows the widget will position
this.domNode.setAttribute('aria-hidden', true); // itself.
this.domNode.style.background = 'yellow';
this.domNode.style.left = editor.getLayoutInfo().contentLeft + 'px';
this.domNode.style.width =
editor.getLayoutInfo().contentWidth + 'px';
this.domNode.style.top = getViewZoneTop();
}
return this.domNode;
},
getPosition: function() {
if (this.domNode) {
this.domNode.style.width =
editor.getLayoutInfo().contentWidth + 'px';
this.domNode.style.top = getViewZoneTop();
}
return null; return null;
} };
return {
getId,
getDomNode,
getPosition
};
}; };
this._editor.addOverlayWidget(this._overlayWidget); this._domNode = createDescription();
// TODO: create this and overlayWidget from the same factory. this._outputNode = createOutputNode();
this._outputWidget = {
domNode: null,
getId: function() {
return 'my.output.widget';
},
getDomNode: function() {
if (!this.domNode) {
this.domNode = createOutputNode();
// make sure it's hidden from screenreaders. this._overlayWidget = createWidget(
this.domNode.setAttribute('aria-hidden', true); 'my.overlay.widget',
this._domNode,
getViewZoneTop
);
this.domNode.style.background = 'red'; this._outputWidget = createWidget(
this.domNode.style.left = editor.getLayoutInfo().contentLeft + 'px'; 'my.output.widget',
this.domNode.style.width = this._outputNode,
editor.getLayoutInfo().contentWidth + 'px'; getOutputZoneTop
this.domNode.style.top = getOutputZoneTop(); );
}
return this.domNode;
},
getPosition: function() {
if (this.domNode) {
this.domNode.style.width =
editor.getLayoutInfo().contentWidth + 'px';
this.domNode.style.top = getOutputZoneTop();
}
return null;
}
};
this._editor.addOverlayWidget(this._overlayWidget); this._editor.addOverlayWidget(this._overlayWidget);
// TODO: order of insertion into the DOM probably matters, revisit once
// the tabs have been fixed!
this._editor.addOverlayWidget(this._outputWidget); this._editor.addOverlayWidget(this._outputWidget);
// TODO: if we keep using a single editor and switching content (rather // TODO: if we keep using a single editor and switching content (rather
// than having multiple open editors), this view zone needs to be // than having multiple open editors), this view zone needs to be
@ -414,7 +388,6 @@ class Editor extends Component {
// make sure the overlayWidget has resized before using it to set the height // make sure the overlayWidget has resized before using it to set the height
domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px'; domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
domNode.style.top = this.getViewZoneTop();
// TODO: set via onComputedHeight? // TODO: set via onComputedHeight?
this.data[fileKey].viewZoneHeight = domNode.offsetHeight; this.data[fileKey].viewZoneHeight = domNode.offsetHeight;
@ -445,7 +418,6 @@ class Editor extends Component {
// make sure the overlayWidget has resized before using it to set the height // make sure the overlayWidget has resized before using it to set the height
outputNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px'; outputNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
outputNode.style.top = this.getOutputZoneTop();
// TODO: set via onComputedHeight? // TODO: set via onComputedHeight?
this.data[fileKey].outputZoneHeight = outputNode.offsetHeight; this.data[fileKey].outputZoneHeight = outputNode.offsetHeight;
@ -493,8 +465,13 @@ class Editor extends Component {
// The z-index needs increasing as ViewZones default to below the lines. // The z-index needs increasing as ViewZones default to below the lines.
domNode.style.zIndex = '10'; domNode.style.zIndex = '10';
this._domNode = domNode; domNode.setAttribute('aria-hidden', true);
domNode.style.background = 'yellow';
domNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
domNode.style.top = this.getViewZoneTop();
this._domNode = domNode;
return domNode; return domNode;
} }
@ -504,11 +481,16 @@ class Editor extends Component {
outputNode.innerHTML = 'TESTS GO HERE'; outputNode.innerHTML = 'TESTS GO HERE';
outputNode.style.background = 'lightblue';
// The z-index needs increasing as ViewZones default to below the lines. // The z-index needs increasing as ViewZones default to below the lines.
outputNode.style.zIndex = '10'; outputNode.style.zIndex = '10';
outputNode.setAttribute('aria-hidden', true);
outputNode.style.background = 'var(--secondary-background)';
outputNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
outputNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
outputNode.style.top = this.getOutputZoneTop();
this._outputNode = outputNode; this._outputNode = outputNode;
return outputNode; return outputNode;
@ -526,11 +508,6 @@ class Editor extends Component {
onChange = editorValue => { onChange = editorValue => {
const { updateFile } = this.props; const { updateFile } = this.props;
// TODO: widgets can go
const widget = this.data[this.state.fileKey].widget;
if (widget) {
this._editor.layoutContentWidget(widget);
}
updateFile({ key: this.state.fileKey, editorValue }); updateFile({ key: this.state.fileKey, editorValue });
}; };
@ -690,14 +667,22 @@ class Editor extends Component {
return isDeleted ? event.changes[0].range.endLineNumber : 0; return isDeleted ? event.changes[0].range.endLineNumber : 0;
} }
function getNextLine(range) { function getNewLineRanges(event) {
return { const newLines = event.changes.filter(
...range, ({ text }) => text[0] === event.eol
startLineNumber: range.startLineNumber + 1, );
endLineNumber: range.endLineNumber + 1 return newLines.map(({ range }) => range);
};
} }
const translateRange = (range, lineDelta) => {
const iRange = {
...range,
startLineNumber: range.startLineNumber + lineDelta,
endLineNumber: range.endLineNumber + lineDelta
};
return this._monaco.Range.lift(iRange);
};
// TODO refactor this mess // TODO refactor this mess
// TODO this listener needs to be replaced on reset. // TODO this listener needs to be replaced on reset.
model.onDidChangeContent(e => { model.onDidChangeContent(e => {
@ -707,9 +692,10 @@ class Editor extends Component {
// edits. However, what if they made a warned edit, then a normal // edits. However, what if they made a warned edit, then a normal
// edit, then a warned one. Could it track that they need to make 3 // edit, then a warned one. Could it track that they need to make 3
// undos? // undos?
// console.log('content', e); const newLineRanges = getNewLineRanges(e).map(range =>
toStartOfLine(range)
);
const deletedLine = getDeletedLine(e); const deletedLine = getDeletedLine(e);
// console.log(deletedLine);
const deletedRange = { const deletedRange = {
startLineNumber: deletedLine, startLineNumber: deletedLine,
@ -719,6 +705,10 @@ class Editor extends Component {
}; };
if (e.isUndoing) { if (e.isUndoing) {
// TODO: can we be more targeted? Only update when they could get out of
// sync
this.updateViewZone();
this.updateOutputZone();
return; return;
} }
@ -728,27 +718,82 @@ class Editor extends Component {
if ( if (
this._monaco.Range.areIntersectingOrTouching(coveringRange, range) this._monaco.Range.areIntersectingOrTouching(coveringRange, range)
) { ) {
// TODO, this triggers twice
console.log('OVERLAP!'); console.log('OVERLAP!');
} }
}); });
}; };
const handleDecorationChange = id => { // Make sure the zone tracks the decoration (i.e. the region)
const handleHintsZoneChange = id => {
const startOfZone = toStartOfLine(
model.getDecorationRange(id)
).collapseToStart();
// the decoration needs adjusting if the user creates a line immediately
// before the greyed out region...
const lineOneRange = translateRange(startOfZone, -2);
// or immediately after it
const lineTwoRange = translateRange(startOfZone, -1);
for (const lineRange of newLineRanges) {
const shouldMoveZone = this._monaco.Range.areIntersectingOrTouching(
lineRange,
lineOneRange.plusRange(lineTwoRange)
);
if (shouldMoveZone) {
this.updateOutputZone();
}
}
};
// Make sure the zone tracks the decoration (i.e. the region)
const handleDescriptionZoneChange = id => {
const endOfZone = toLastLine(
model.getDecorationRange(id)
).collapseToStart();
// the decoration needs adjusting if the user creates a line immediately
// before the editable region.
const lineOneRange = translateRange(endOfZone, -1);
for (const lineRange of newLineRanges) {
const shouldMoveZone = this._monaco.Range.areIntersectingOrTouching(
lineRange,
lineOneRange
);
if (shouldMoveZone) {
this.updateViewZone();
}
}
};
// Stops the greyed out region from covering the editable region. Does not
// change the font decoration.
const preventOverlap = id => {
// Even though the decoration covers the whole line, it has a // Even though the decoration covers the whole line, it has a
// startColumn that moves. toStartOfLine ensures that the // startColumn that moves. toStartOfLine ensures that the
// comparison detects if any change has occured on that line // comparison detects if any change has occured on that line
// NOTE: any change in the decoration has already happened by this point // NOTE: any change in the decoration has already happened by this point
// so this covers the *new* decoration range. // so this covers the *new* decoration range.
const coveringRange = toStartOfLine(model.getDecorationRange(id)); const coveringRange = toStartOfLine(model.getDecorationRange(id));
const oldStartOfRange = getNextLine(coveringRange.collapseToStart()); const oldStartOfRange = translateRange(
coveringRange.collapseToStart(),
1
);
const newCoveringRange = coveringRange.setStartPosition( const newCoveringRange = coveringRange.setStartPosition(
oldStartOfRange.startLineNumber, oldStartOfRange.startLineNumber,
1 1
); );
// TODO: this triggers both when you delete the first line of the // TODO: this triggers both when you delete the first line of the
// decoration AND the second. Is there a way to tell these cases apart? // decoration AND the second. To see this, consider a region on line 5
// If you delete 5, then the new start is 4 and the computed start is 5
// so they match.
// If you delete 6, then the start of the region stays at 5, so the
// computed start is 6 and they still match.
// Is there a way to tell these cases apart?
// This means that if you delete the second line it actually removes the
// grey background from the first line.
const touchingDeleted = this._monaco.Range.areIntersectingOrTouching( const touchingDeleted = this._monaco.Range.areIntersectingOrTouching(
deletedRange, deletedRange,
oldStartOfRange oldStartOfRange
@ -763,6 +808,8 @@ class Editor extends Component {
newCoveringRange, newCoveringRange,
[id] [id]
); );
this.updateOutputZone();
// when there's a change, decorations will be [oldId, newId] // when there's a change, decorations will be [oldId, newId]
return decorations.slice(-1)[0]; return decorations.slice(-1)[0];
} else { } else {
@ -772,10 +819,13 @@ class Editor extends Component {
// we only need to handle the special case of the second region being // we only need to handle the special case of the second region being
// pulled up, the first region already behaves correctly. // pulled up, the first region already behaves correctly.
this.data[key].endEditDecId = handleDecorationChange( this.data[key].endEditDecId = preventOverlap(this.data[key].endEditDecId);
this.data[key].endEditDecId
);
// TODO: do the same for the description widget
// this has to be handle differently, because we care about the END
// of the zone, not the START
handleDescriptionZoneChange(this.data[key].startEditDecId);
handleHintsZoneChange(this.data[key].endEditDecId);
warnUser(this.data[key].startEditDecId); warnUser(this.data[key].startEditDecId);
warnUser(this.data[key].endEditDecId); warnUser(this.data[key].endEditDecId);
}); });
@ -833,7 +883,6 @@ class Editor extends Component {
// dimensions in order to render correctly. // dimensions in order to render correctly.
this._editor.layout(); this._editor.layout();
if (this.data[fileKey].startEditDecId) { if (this.data[fileKey].startEditDecId) {
console.log('data', this.data[fileKey]);
this.updateViewZone(); this.updateViewZone();
this.updateOutputZone(); this.updateOutputZone();
} }