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

Highlight CSS and JS selectors #1598

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

wylieconlon
Copy link
Contributor

This continues work from @pwjablonski on #1025

@wylieconlon wylieconlon changed the title WIP Element highlighter Highlight CSS and JS selectors Oct 25, 2018
@wylieconlon
Copy link
Contributor Author

I'm feeling pretty good about this code overall, but I'm not sure what's causing the test failure- it wasn't failing locally for me. I think it's ready for another pass, but it might be better to talk about the overall approach again @outoftime

Copy link
Contributor

@outoftime outoftime left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wylieconlon heyo! not going to be able to do a full review tonight (though i have been looking at the diff in spare moments and broadly it’s looking good) but on the test failure tip I believe that is caused by some ES6+ code not getting compiled to ES5 in the build process. My guess would be the new babel libraries, which (naturally) I’m sure is written in modern ES. So that would just need to get added to the list of libraries that go through the Babel loader in the Webpack config.

Also probably obvious but as written this will be blocked on upgrading us to Babel 7 in general (I’m going to do it I swear!)

@outoftime outoftime mentioned this pull request Oct 26, 2018
@outoftime outoftime self-requested a review November 4, 2018 13:34
@@ -129,9 +129,27 @@ class Editor extends React.Component {

_startNewSession(source) {
const session = createAceSessionWithoutWorker(this.props.language, source);
const cursor = session.selection.lead;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like lead as a property may not be part of the public API—the docs only mention getSelectionLead()

cursor,
this.props.language,
);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I think facts about UI state should either be completely invisible to application (redux) state, or completely controlled by/synchronized with Redux. So in this case, once application state is concerned with the cursor position, it should also control cursor position. Practically I think this just means we should be taking cursor position and focus as props in this component, watching for changes, and updating the editor components if they’re out of sync.

This also means we can replace the never-terribly-appealing REQUEST_FOCUSED_LINE mechanism with a simple action to move the cursor to a particular location at the Redux level.

@@ -47,6 +47,7 @@ class PreviewFrame extends React.Component {
const {consoleEntries, isActive} = this.props;

if (this._channel && isActive) {
this._postFocusedSelectorToFrame(this.props.focusedSelector);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we only do this if the focused selector has in fact changed?


onEditorBlurred() {
dispatch(
editorBlurred(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this still contain a language argument? As it is I think we rely on ACE dispatching the blur event before the focus event in the case where the user clicks from one editor to another.

if (element.offsetParent === null) {
cover.style.position = 'fixed';
} else if (element !== document.body ||
element !== document.documentElement) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like every element is either not <html> or not <body>?

cover.style.top = `${offset.top}px`;
cover.style.width = `${element.offsetWidth}px`;
cover.style.height = `${element.offsetHeight}px`;
cover.classList.add('fade');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a second class here? Can’t we use the __popcode-highlighter class for all the rules we want to apply?


const emptyMap = new Immutable.Map();

export default function reduceProjects(stateIn, action) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reduceSelectorLocations

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, on my list is to refactor all existing reducers to use handleActions, but for now I’d at least like to use it for new reducers (e.g. resizableFlex)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also generally we want to have unit tests for all reducers and sagas


export function* parseCurrentProjectSource() {
const getJsSelectorLocations = yield call(importGetJsSelectorLocations);
const getCssSelectorLocations = yield call(importGetCssSelectorLocations);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use an all effect to do these in parallel

export function* parseCurrentProjectSource() {
const getJsSelectorLocations = yield call(importGetJsSelectorLocations);
const getCssSelectorLocations = yield call(importGetCssSelectorLocations);
const currentProject = yield select(getCurrentProject);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mild preference for destructuring here

const cssSelectorLocations = yield call(
getCssSelectorLocations,
currentProject.sources.css,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An all again would be nice (obviously these are not actually async but they certainly could conceivably be in the future, using a web worker for instance, so best to write the code to do the best thing in all cases).

@@ -131,6 +171,10 @@ export default function* projects() {
'UPDATE_PROJECT_SOURCE',
'UPDATE_PROJECT_INSTRUCTIONS',
], updateProjectSource),
throttle(100, [
'UPDATE_PROJECT_SOURCE',
'PROJECT_RESTORED_FROM_LAST_SESSION',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We’ll want to do this for CHANGE_CURRENT_PROJECT as well, maybe others? And CREATE_PROJECT should clear the selector list.

const selectors = yield select(getSelectorLocationsForLanguage, language);
const selector = yield call(selectorAtCursor, selectors, cursor);
yield put(currentFocusedSelectorChanged(selector));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this doesn’t need to be in a saga, does it? But in the spirit of “selectorAtCursor() could very conceivably be an async function” I’m down to skate to where the puck is headed.

case 'UPDATE_SELECTOR_LOCATIONS':
return state.set(
'selectors',
new SelectorLocations(action.payload.selectors),
Copy link
Contributor

@outoftime outoftime Nov 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although there are some specific exceptions, we usually want to store all non-scalar data in Redux using Immutable data structures. In this case I think we can actually just store the locations using the existing EditorLocation record.

return state.getIn([
'selectorLocations',
'selectors',
])[language] || null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not getIn(['selectorLocations', 'selectors', language])?

switch (action.type) {
case 'UPDATE_SELECTOR_LOCATIONS':
return state.set(
'selectors',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think there’s any need for an additional level of nesting here—isn’t the SelectorLocations record itself sufficient to represent this slice of the state?

@@ -131,6 +171,10 @@ export default function* projects() {
'UPDATE_PROJECT_SOURCE',
'UPDATE_PROJECT_INSTRUCTIONS',
], updateProjectSource),
throttle(100, [
'UPDATE_PROJECT_SOURCE',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For UPDATE_PROJECT_SOURCE, wouldn’t it be more efficient to call a different saga that only re-parses and calculates the selector locations for that language?

});

return selectors;
} catch (e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be looking for a specific error that indicates invalid CSS syntax? I wouldn’t want to inadvertently swallow an error thrown by a bug in our code.

traverse(ast, visitor, null);

return foundMatches;
} catch (e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question about blanket catch here

if (
(
callee === '$' ||
callee.indexOf('querySelector') !== -1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use callee.startsWith (this is a fairly new method but should be polyfilled)

@@ -0,0 +1,21 @@
.__popcode-highlighter {
z-index: 2000000;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not an enormous fan of arbitrarily large z-indexes to accomplish “definitely above everything else”—when we’re constructing the highlighter, can we just find the topmost ancestor element that has a z-index, and set the highighter’s z-index to one plus that?


.__popcode-highlighter.fade {
background-color: #ffffff00;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this effect! Seems like it would be a little more intuitive to describe it using an animation rather than a transition, though? I found it confusing when I initially noticed that the highlighter was being given two class names at the same time…


test('finds all selectors in css', (assert) => {
const selectors = getCssSelectorLocations(source);
assert.deepEqual(selectors.map(s => s.selector), [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do the map a little more cutely with lodash, e.g. map(selectors, 'selector')

@outoftime
Copy link
Contributor

Great stuff! Feels like very strong progress and the structure is mostly there—only major thing would be to make the cursor/focus state fully controlled by Redux rather than just a one-way data flow. Can’t wait to release this, it’s going to be huge!!

Copy link
Contributor

@outoftime outoftime left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops I meant to click “request changes” ; D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants