From 78468f8a5d2b26b60bddc28613aafab799979e06 Mon Sep 17 00:00:00 2001 From: Kushagra Gour Date: Tue, 23 Oct 2018 19:56:33 +0530 Subject: [PATCH 1/8] Command pallete: first draft! --- src/commandPaletteService.js | 29 +++++++++ src/commands.js | 22 +++++++ src/components/CommandPalette.jsx | 96 +++++++++++++++++++++++++++++ src/components/ContentWrapFiles.jsx | 19 ++++++ src/components/app.jsx | 26 +++++++- src/components/common.jsx | 6 ++ 6 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 src/commandPaletteService.js create mode 100644 src/commands.js create mode 100644 src/components/CommandPalette.jsx diff --git a/src/commandPaletteService.js b/src/commandPaletteService.js new file mode 100644 index 0000000..22607b0 --- /dev/null +++ b/src/commandPaletteService.js @@ -0,0 +1,29 @@ +import { deferred } from './deferred'; +import { log } from 'util'; + +export const SWITCH_FILE_EVENT = 'switchFileEvent'; +export const OPEN_SAVED_CREATIONS_EVENT = 'openSavedCreationsEvent'; +export const SAVE_EVENT = 'saveEvent'; + +export const commandPaletteService = { + subscriptions: {}, + subscribe(eventName, callback) { + console.log('subscribed for ', eventName); + this.subscriptions[eventName] = this.subscriptions[eventName] || []; + this.subscriptions[eventName].push(callback); + return () => { + console.log('Unsubscribing ', eventName); + this.subscriptions[eventName].splice( + this.subscriptions[eventName].indexOf(callback), + 1 + ); + }; + }, + publish(eventName, ...args) { + console.log('published ', eventName, args); + const callbacks = this.subscriptions[eventName] || []; + callbacks.forEach(callback => { + callback.apply(null, args); + }); + } +}; diff --git a/src/commands.js b/src/commands.js new file mode 100644 index 0000000..226be3c --- /dev/null +++ b/src/commands.js @@ -0,0 +1,22 @@ +import { + OPEN_SAVED_CREATIONS_EVENT, + SAVE_EVENT +} from './commandPaletteService'; + +export const commands = [ + { + name: 'Open Creation', + event: OPEN_SAVED_CREATIONS_EVENT, + keyboardShortcut: 'Cmd+O' + }, + { + name: 'Save Creation', + event: SAVE_EVENT, + keyboardShortcut: 'Cmd+S' + }, + { + name: 'Add Library', + run: '', + keyboardShortcut: 'Cmd+F' + } +]; diff --git a/src/components/CommandPalette.jsx b/src/components/CommandPalette.jsx new file mode 100644 index 0000000..f48a9fb --- /dev/null +++ b/src/components/CommandPalette.jsx @@ -0,0 +1,96 @@ +import { h, Component } from 'preact'; +import Modal from './Modal'; +import { AutoFocusInput } from './common'; +import { commands } from '../commands'; +import { + commandPaletteService, + SWITCH_FILE_EVENT +} from '../commandPaletteService'; +import { FileIcon } from './FileIcon'; + +function getFolder(filePath) { + const split = filePath.split('/'); + if (split.length > 1) { + split.length = split.length - 1; + return split.join('/'); + } + return ''; +} +function Row({ item, onClick }) { + return ( +
  • + +
  • + ); +} +export class CommandPalette extends Component { + state = { list: [], search: '' }; + componentDidUpdate(previousProps) { + if (this.props.show && !previousProps.show) { + this.state.search = ''; + + this.isCommandMode = this.props.isCommandMode; + if (this.isCommandMode) { + this.setState({ search: '>' }); + } + + this.setState({ + list: this.getFilteredList() + }); + } + } + + getFilteredList(search = '') { + const list = this.isCommandMode ? commands : this.props.files; + return list.filter( + item => + item.name + .toLowerCase() + .indexOf(this.isCommandMode ? search.substr(1) : search) !== -1 + ); + } + + inputHandler(e) { + const search = e.target.value; + this.setState({ search }); + if (search.indexOf('>') === 0) { + this.isCommandMode = true; + } + this.setState({ + list: this.getFilteredList(search) + }); + } + optionClickHandler(option) { + commandPaletteService.publish( + option.path ? SWITCH_FILE_EVENT : option.event, + option + ); + this.props.closeHandler(); + } + render() { + return ( + + +
      + {this.state.list.map(item => ( + + ))} +
    +
    + ); + } +} diff --git a/src/components/ContentWrapFiles.jsx b/src/components/ContentWrapFiles.jsx index e59d555..06fd13a 100644 --- a/src/components/ContentWrapFiles.jsx +++ b/src/components/ContentWrapFiles.jsx @@ -17,6 +17,10 @@ import 'codemirror/mode/meta'; import { deferred } from '../deferred'; import { SidePane } from './SidePane'; import { Console } from './Console'; +import { + commandPaletteService, + SWITCH_FILE_EVENT +} from '../commandPaletteService'; const minCodeWrapSize = 33; @@ -105,6 +109,21 @@ export default class ContentWrapFiles extends Component { } componentDidMount() { this.props.onRef(this); + this.commandPaletteSubscriptions = []; + this.commandPaletteSubscriptions.push( + commandPaletteService.subscribe(SWITCH_FILE_EVENT, file => { + const targetFile = getFileFromPath( + this.props.currentItem.files, + file.path + ); + if (targetFile.file) { + this.fileSelectHandler(targetFile.file); + } + }) + ); + } + componentWillUnmount() { + this.commandPaletteSubscriptions.forEach(unsubscribeFn => unsubscribeFn()); } getEditorOptions(fileName = '') { diff --git a/src/components/app.jsx b/src/components/app.jsx index ade9d86..b80591c 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -55,6 +55,11 @@ import { Js13KModal } from './Js13KModal'; import { CreateNewModal } from './CreateNewModal'; import { Icons } from './Icons'; import JSZip from 'jszip'; +import { CommandPalette } from './CommandPalette'; +import { + commandPaletteService, + OPEN_SAVED_CREATIONS_EVENT +} from '../commandPaletteService'; if (module.hot) { require('preact/debug'); @@ -84,7 +89,8 @@ export default class App extends Component { isAskToImportModalOpen: false, isOnboardModalOpen: false, isJs13KModalOpen: false, - isCreateNewModalOpen: false + isCreateNewModalOpen: false, + isCommandPaletteOpen: false }; this.state = { isSavedItemPaneOpen: false, @@ -532,6 +538,12 @@ export default class App extends Component { // We might be listening on keydown for some input inside the app. In that case // we don't want this to trigger which in turn focuses back the last editor. this.closeSavedItemsPane(); + } else if ((event.ctrlKey || event.metaKey) && event.keyCode === 80) { + this.setState({ + isCommandPaletteOpen: true, + isCommandPaletteInCommandMode: !!event.shiftKey + }); + event.preventDefault(); } }); @@ -548,6 +560,10 @@ export default class App extends Component { } } }); + + commandPaletteService.subscribe(OPEN_SAVED_CREATIONS_EVENT, () => { + this.openSavedItemsPane(); + }); } closeAllOverlays() { @@ -1553,6 +1569,14 @@ export default class App extends Component { onTemplateSelect={this.templateSelectHandler.bind(this)} /> + this.setState({ isCommandPaletteOpen: false })} + files={linearizeFiles(this.state.currentItem.files || [])} + isCommandMode={this.state.isCommandPaletteInCommandMode} + closeHandler={() => this.setState({ isCommandPaletteOpen: false })} + /> + diff --git a/src/style.css b/src/style.css index 885dc92..a2c99de 100644 --- a/src/style.css +++ b/src/style.css @@ -1,5 +1,6 @@ :root { - --color-text: #d4cde9; + --color-text: #e2daf9; + --color-text-dark-1: #b3aec4; --color-bg: #252637; --color-popup: #3a2b63; --code-font-size: 16px; @@ -74,6 +75,10 @@ button { .d-i { display: inline; } + +.d-b { + display: block; +} .flex { display: flex; } @@ -1688,28 +1693,6 @@ body:not(.is-app) .show-when-app { .is-file-mode .hide-in-file-mode { display: none !important; } -@media screen and (max-width: 600px) { - body { - font-size: 70%; - } - - .main-header { - overflow-x: auto; - } - - .main-header__btn-wrap { - flex-shrink: 0; - } - - .modal__content { - padding: 1em; - } - - .saved-items-pane { - width: 77vw; - padding: 10px 20px; - } -} /* Codemirror themes basic bg styles. This is here so that there is no big FOUC while the theme CSS file is loading */ @@ -1807,3 +1790,41 @@ while the theme CSS file is loading */ .cm-s-midnight .CodeMirror-activeline-background { background: #253540; } +.command-palette__option-row { + padding: 4px 5px; + width: 100%; + text-align: left; + border: 0; + background: transparent; + color: var(--color-text); +} +.command-palette__option-row--selected { + background: rgba(0, 0, 0, 0.2); +} +.command-palette__option-subtitle { + color: var(--color-text-dark-1); + margin-left: 10px; + font-size: 0.8em; +} +@media screen and (max-width: 600px) { + body { + font-size: 70%; + } + + .main-header { + overflow-x: auto; + } + + .main-header__btn-wrap { + flex-shrink: 0; + } + + .modal__content { + padding: 1em; + } + + .saved-items-pane { + width: 77vw; + padding: 10px 20px; + } +} From f4de6572c29fa8deed4872ec2c81e55ab9a7f369 Mon Sep 17 00:00:00 2001 From: Kushagra Gour Date: Sat, 27 Oct 2018 20:24:34 +0530 Subject: [PATCH 3/8] Add some more commands to commands palette. Some refactoring also. Move palette events to commands.js. --- src/commandPaletteService.js | 10 ------ src/commands.js | 20 +++++++----- src/components/CommandPalette.jsx | 8 ++--- src/components/ContentWrapFiles.jsx | 6 ++-- src/components/Modal.jsx | 7 ++++- src/components/app.jsx | 48 +++++++++++++++++++++-------- 6 files changed, 60 insertions(+), 39 deletions(-) diff --git a/src/commandPaletteService.js b/src/commandPaletteService.js index 22607b0..273e170 100644 --- a/src/commandPaletteService.js +++ b/src/commandPaletteService.js @@ -1,18 +1,9 @@ -import { deferred } from './deferred'; -import { log } from 'util'; - -export const SWITCH_FILE_EVENT = 'switchFileEvent'; -export const OPEN_SAVED_CREATIONS_EVENT = 'openSavedCreationsEvent'; -export const SAVE_EVENT = 'saveEvent'; - export const commandPaletteService = { subscriptions: {}, subscribe(eventName, callback) { - console.log('subscribed for ', eventName); this.subscriptions[eventName] = this.subscriptions[eventName] || []; this.subscriptions[eventName].push(callback); return () => { - console.log('Unsubscribing ', eventName); this.subscriptions[eventName].splice( this.subscriptions[eventName].indexOf(callback), 1 @@ -20,7 +11,6 @@ export const commandPaletteService = { }; }, publish(eventName, ...args) { - console.log('published ', eventName, args); const callbacks = this.subscriptions[eventName] || []; callbacks.forEach(callback => { callback.apply(null, args); diff --git a/src/commands.js b/src/commands.js index 226be3c..e886f5f 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,9 +1,15 @@ -import { - OPEN_SAVED_CREATIONS_EVENT, - SAVE_EVENT -} from './commandPaletteService'; +export const SWITCH_FILE_EVENT = 'switchFileEvent'; +export const NEW_CREATION_EVENT = 'newCreationEvent'; +export const OPEN_SAVED_CREATIONS_EVENT = 'openSavedCreationsEvent'; +export const SAVE_EVENT = 'saveEvent'; +export const OPEN_SETTINGS_EVENT = 'openSettingsEvent'; export const commands = [ + { + name: 'Start New Creation', + event: NEW_CREATION_EVENT, + keyboardShortcut: '' + }, { name: 'Open Creation', event: OPEN_SAVED_CREATIONS_EVENT, @@ -15,8 +21,8 @@ export const commands = [ keyboardShortcut: 'Cmd+S' }, { - name: 'Add Library', - run: '', - keyboardShortcut: 'Cmd+F' + name: 'Open Settings', + event: OPEN_SETTINGS_EVENT, + keyboardShortcut: '' } ]; diff --git a/src/components/CommandPalette.jsx b/src/components/CommandPalette.jsx index 63b6ac7..06efbb9 100644 --- a/src/components/CommandPalette.jsx +++ b/src/components/CommandPalette.jsx @@ -1,11 +1,9 @@ import { h, Component } from 'preact'; import Modal from './Modal'; import { AutoFocusInput } from './common'; -import { commands } from '../commands'; -import { - commandPaletteService, - SWITCH_FILE_EVENT -} from '../commandPaletteService'; +import { commands, SWITCH_FILE_EVENT } from '../commands'; + +import { commandPaletteService } from '../commandPaletteService'; import { FileIcon } from './FileIcon'; import { UP_KEY, DOWN_KEY, ENTER_KEY } from '../keyboardKeys'; diff --git a/src/components/ContentWrapFiles.jsx b/src/components/ContentWrapFiles.jsx index 06fd13a..882028f 100644 --- a/src/components/ContentWrapFiles.jsx +++ b/src/components/ContentWrapFiles.jsx @@ -17,10 +17,8 @@ import 'codemirror/mode/meta'; import { deferred } from '../deferred'; import { SidePane } from './SidePane'; import { Console } from './Console'; -import { - commandPaletteService, - SWITCH_FILE_EVENT -} from '../commandPaletteService'; +import { SWITCH_FILE_EVENT } from '../commands'; +import { commandPaletteService } from '../commandPaletteService'; const minCodeWrapSize = 33; diff --git a/src/components/Modal.jsx b/src/components/Modal.jsx index 5f3c4b9..101c431 100644 --- a/src/components/Modal.jsx +++ b/src/components/Modal.jsx @@ -32,7 +32,12 @@ export default class Modal extends Component { if (this.props.show) { // HACK: refs will evaluate on next tick due to portals setTimeout(() => { - this.overlayEl.querySelector('.js-modal__close-btn').focus(); + const closeButton = this.overlayEl.querySelector( + '.js-modal__close-btn' + ); + if (closeButton) { + closeButton.focus(); + } }, 0); /* We insert a dummy hidden input which will take focus as soon as focus diff --git a/src/components/app.jsx b/src/components/app.jsx index b80591c..5590476 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -57,9 +57,12 @@ import { Icons } from './Icons'; import JSZip from 'jszip'; import { CommandPalette } from './CommandPalette'; import { - commandPaletteService, - OPEN_SAVED_CREATIONS_EVENT -} from '../commandPaletteService'; + OPEN_SAVED_CREATIONS_EVENT, + SAVE_EVENT, + OPEN_SETTINGS_EVENT, + NEW_CREATION_EVENT +} from '../commands'; +import { commandPaletteService } from '../commandPaletteService'; if (module.hot) { require('preact/debug'); @@ -497,6 +500,10 @@ export default class App extends Component { this.editorWithFocus.focus(); } } + openSettings() { + this.setState({ isSettingsModalOpen: true }); + } + componentDidMount() { document.body.style.height = `${window.innerHeight}px`; @@ -560,10 +567,26 @@ export default class App extends Component { } } }); - - commandPaletteService.subscribe(OPEN_SAVED_CREATIONS_EVENT, () => { - this.openSavedItemsPane(); - }); + const commandPalleteHooks = { + [NEW_CREATION_EVENT]: () => { + this.openNewCreationModal(); + }, + [OPEN_SAVED_CREATIONS_EVENT]: () => { + this.openSavedItemsPane(); + }, + [SAVE_EVENT]: () => { + this.saveItem(); + }, + [OPEN_SETTINGS_EVENT]: () => { + this.openSettings(); + } + }; + for (let eventName in commandPalleteHooks) { + commandPaletteService.subscribe( + eventName, + commandPalleteHooks[eventName] + ); + } } closeAllOverlays() { @@ -931,8 +954,7 @@ export default class App extends Component { this.forkItem(item); }, 350); } - newBtnClickHandler() { - trackEvent('ui', 'newBtnClick'); + openNewCreationModal() { if (this.state.unsavedEditCount) { var shouldDiscard = confirm( 'You have unsaved changes. Do you still want to create something new?' @@ -948,6 +970,10 @@ export default class App extends Component { }); } } + newBtnClickHandler() { + trackEvent('ui', 'newBtnClick'); + this.openNewCreationModal(); + } openBtnClickHandler() { trackEvent('ui', 'openBtnClick'); this.openSavedItemsPane(); @@ -1412,9 +1438,7 @@ export default class App extends Component { prefs={this.state.prefs} layoutBtnClickHandler={this.layoutBtnClickHandler.bind(this)} helpBtnClickHandler={() => this.setState({ isHelpModalOpen: true })} - settingsBtnClickHandler={() => - this.setState({ isSettingsModalOpen: true }) - } + settingsBtnClickHandler={this.openSettings.bind(this)} notificationsBtnClickHandler={this.notificationsBtnClickHandler.bind( this )} From a25cf1bc316efae9812be3fc9b51272ef544f558 Mon Sep 17 00:00:00 2001 From: Kushagra Gour Date: Sun, 28 Oct 2018 08:51:09 +0530 Subject: [PATCH 4/8] add missing keyboardkeys.js file --- src/keyboardKeys.js | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/keyboardKeys.js diff --git a/src/keyboardKeys.js b/src/keyboardKeys.js new file mode 100644 index 0000000..ad4538c --- /dev/null +++ b/src/keyboardKeys.js @@ -0,0 +1,6 @@ +export const ENTER_KEY = 13; +export const ESCAPE_KEY = 27; +export const LEFT_KEY = 37; +export const UP_KEY = 38; +export const RIGHT_KEY = 39; +export const DOWN_KEY = 40; From 0e289117949944eba3d6ff3f27dca45e703ebeb9 Mon Sep 17 00:00:00 2001 From: Kushagra Gour Date: Mon, 29 Oct 2018 12:24:06 +0530 Subject: [PATCH 5/8] adjust body height when window resizes/zooms --- src/components/app.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/app.jsx b/src/components/app.jsx index 5590476..42ab10c 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -505,7 +505,12 @@ export default class App extends Component { } componentDidMount() { - document.body.style.height = `${window.innerHeight}px`; + function setBodySize() { + document.body.style.height = `${window.innerHeight}px`; + } + window.addEventListener('resize', () => { + setBodySize(); + }); // Editor keyboard shortucuts window.addEventListener('keydown', event => { From ae85bfb4743e014a1b44cf364a3f441b3b79f1b4 Mon Sep 17 00:00:00 2001 From: Kushagra Gour Date: Tue, 30 Oct 2018 00:58:44 +0530 Subject: [PATCH 6/8] add palceholder to search input --- src/components/CommandPalette.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/CommandPalette.jsx b/src/components/CommandPalette.jsx index 06efbb9..92a2eed 100644 --- a/src/components/CommandPalette.jsx +++ b/src/components/CommandPalette.jsx @@ -103,6 +103,8 @@ export class CommandPalette extends Component { hideCloseButton > Date: Tue, 30 Oct 2018 01:05:08 +0530 Subject: [PATCH 7/8] add keyboard sh0rtcuts command --- src/commands.js | 6 ++++++ src/components/app.jsx | 15 +++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/commands.js b/src/commands.js index e886f5f..c0f58ac 100644 --- a/src/commands.js +++ b/src/commands.js @@ -3,6 +3,7 @@ export const NEW_CREATION_EVENT = 'newCreationEvent'; export const OPEN_SAVED_CREATIONS_EVENT = 'openSavedCreationsEvent'; export const SAVE_EVENT = 'saveEvent'; export const OPEN_SETTINGS_EVENT = 'openSettingsEvent'; +export const SHOW_KEYBOARD_SHORTCUTS_EVENT = 'showKeyboardShortcutsEvent'; export const commands = [ { @@ -24,5 +25,10 @@ export const commands = [ name: 'Open Settings', event: OPEN_SETTINGS_EVENT, keyboardShortcut: '' + }, + { + name: 'Show Keyboard Shortcuts', + event: SHOW_KEYBOARD_SHORTCUTS_EVENT, + keyboardShortcut: '' } ]; diff --git a/src/components/app.jsx b/src/components/app.jsx index 42ab10c..183747b 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -60,7 +60,8 @@ import { OPEN_SAVED_CREATIONS_EVENT, SAVE_EVENT, OPEN_SETTINGS_EVENT, - NEW_CREATION_EVENT + NEW_CREATION_EVENT, + SHOW_KEYBOARD_SHORTCUTS_EVENT } from '../commands'; import { commandPaletteService } from '../commandPaletteService'; @@ -503,6 +504,9 @@ export default class App extends Component { openSettings() { this.setState({ isSettingsModalOpen: true }); } + openKeyboardShortcuts() { + this.setState({ isKeyboardShortcutsModalOpen: true }); + } componentDidMount() { function setBodySize() { @@ -584,6 +588,9 @@ export default class App extends Component { }, [OPEN_SETTINGS_EVENT]: () => { this.openSettings(); + }, + [SHOW_KEYBOARD_SHORTCUTS_EVENT]: () => { + this.openKeyboardShortcuts(); } }; for (let eventName in commandPalleteHooks) { @@ -1455,9 +1462,9 @@ export default class App extends Component { )} codepenBtnClickHandler={this.codepenBtnClickHandler.bind(this)} saveHtmlBtnClickHandler={this.saveHtmlBtnClickHandler.bind(this)} - keyboardShortcutsBtnClickHandler={() => - this.setState({ isKeyboardShortcutsModalOpen: true }) - } + keyboardShortcutsBtnClickHandler={this.openKeyboardShortcuts.bind( + this + )} screenshotBtnClickHandler={this.screenshotBtnClickHandler.bind( this )} From f964001dd1d30476e20309d25fc132b789137ac0 Mon Sep 17 00:00:00 2001 From: Kushagra Gour Date: Tue, 30 Oct 2018 01:10:08 +0530 Subject: [PATCH 8/8] command palette ui changes --- src/components/CommandPalette.jsx | 2 +- src/style.css | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/CommandPalette.jsx b/src/components/CommandPalette.jsx index 92a2eed..8b7bf72 100644 --- a/src/components/CommandPalette.jsx +++ b/src/components/CommandPalette.jsx @@ -109,7 +109,7 @@ export class CommandPalette extends Component { onInput={this.inputHandler.bind(this)} onKeyUp={this.keyDownHandler.bind(this)} /> -
      +
        {this.state.list.map((item, index) => (