1
0
mirror of https://github.com/chinchang/web-maker.git synced 2025-08-03 20:07:35 +02:00

Merge pull request #338 from chinchang/command-palette-311

Command palette 311
This commit is contained in:
Kushagra Gour
2018-10-30 01:11:48 +05:30
committed by GitHub
9 changed files with 348 additions and 46 deletions

View File

@@ -0,0 +1,19 @@
export const commandPaletteService = {
subscriptions: {},
subscribe(eventName, callback) {
this.subscriptions[eventName] = this.subscriptions[eventName] || [];
this.subscriptions[eventName].push(callback);
return () => {
this.subscriptions[eventName].splice(
this.subscriptions[eventName].indexOf(callback),
1
);
};
},
publish(eventName, ...args) {
const callbacks = this.subscriptions[eventName] || [];
callbacks.forEach(callback => {
callback.apply(null, args);
});
}
};

34
src/commands.js Normal file
View File

@@ -0,0 +1,34 @@
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 SHOW_KEYBOARD_SHORTCUTS_EVENT = 'showKeyboardShortcutsEvent';
export const commands = [
{
name: 'Start New Creation',
event: NEW_CREATION_EVENT,
keyboardShortcut: ''
},
{
name: 'Open Creation',
event: OPEN_SAVED_CREATIONS_EVENT,
keyboardShortcut: 'Cmd+O'
},
{
name: 'Save Creation',
event: SAVE_EVENT,
keyboardShortcut: 'Cmd+S'
},
{
name: 'Open Settings',
event: OPEN_SETTINGS_EVENT,
keyboardShortcut: ''
},
{
name: 'Show Keyboard Shortcuts',
event: SHOW_KEYBOARD_SHORTCUTS_EVENT,
keyboardShortcut: ''
}
];

View File

@@ -0,0 +1,124 @@
import { h, Component } from 'preact';
import Modal from './Modal';
import { AutoFocusInput } from './common';
import { commands, SWITCH_FILE_EVENT } from '../commands';
import { commandPaletteService } from '../commandPaletteService';
import { FileIcon } from './FileIcon';
import { UP_KEY, DOWN_KEY, ENTER_KEY } from '../keyboardKeys';
function getFolder(filePath) {
const split = filePath.split('/');
if (split.length > 1) {
split.length = split.length - 1;
return split.join('/');
}
return '';
}
function Row({ item, onClick, isSelected }) {
return (
<li>
<button
class={`command-palette__option-row ${
isSelected ? 'command-palette__option-row--selected' : ''
}`}
onClick={onClick}
>
{item.path ? <FileIcon file={item} /> : null}
{item.name}
{item.path ? (
<span class="command-palette__option-subtitle">
{getFolder(item.path)}
</span>
) : null}
</button>
</li>
);
}
export class CommandPalette extends Component {
state = { list: [], search: '', selectedIndex: 0 };
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
);
}
keyDownHandler(e) {
const diff = { [UP_KEY]: -1, [DOWN_KEY]: 1 }[e.which];
if (diff) {
this.setState({
selectedIndex:
(this.state.selectedIndex + diff) % this.state.list.length
});
return;
}
if (e.which === ENTER_KEY) {
this.selectOption(this.state.list[this.state.selectedIndex]);
}
}
inputHandler(e) {
const search = e.target.value;
this.setState({ search });
this.isCommandMode = search.indexOf('>') === 0;
this.setState({
list: this.getFilteredList(search),
selectedIndex: 0
});
}
optionClickHandler(option) {
this.selectOption(option);
}
selectOption(option) {
commandPaletteService.publish(
option.path ? SWITCH_FILE_EVENT : option.event,
option
);
this.props.closeHandler();
}
render() {
return (
<Modal
show={this.props.show}
closeHandler={this.props.closeHandler}
noOverlay
hideCloseButton
>
<AutoFocusInput
type="search"
placeholder="Search"
value={this.state.search}
onInput={this.inputHandler.bind(this)}
onKeyUp={this.keyDownHandler.bind(this)}
/>
<ul class="command-palette__option-list">
{this.state.list.map((item, index) => (
<Row
isSelected={this.state.selectedIndex === index}
item={item}
onClick={this.optionClickHandler.bind(this, item)}
/>
))}
</ul>
</Modal>
);
}
}

View File

@@ -17,6 +17,8 @@ import 'codemirror/mode/meta';
import { deferred } from '../deferred';
import { SidePane } from './SidePane';
import { Console } from './Console';
import { SWITCH_FILE_EVENT } from '../commands';
import { commandPaletteService } from '../commandPaletteService';
const minCodeWrapSize = 33;
@@ -105,6 +107,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 = '') {

View File

@@ -24,13 +24,20 @@ export default class Modal extends Component {
}
componentDidUpdate(prevProps) {
if (this.props.show !== prevProps.show) {
document.body.classList[this.props.show ? 'add' : 'remove'](
'overlay-visible'
);
if (!this.props.noOverlay) {
document.body.classList[this.props.show ? 'add' : 'remove'](
'overlay-visible'
);
}
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
@@ -63,15 +70,17 @@ export default class Modal extends Component {
onClick={this.onOverlayClick.bind(this)}
>
<div class="modal__content">
<button
type="button"
onClick={this.props.closeHandler}
aria-label="Close modal"
title="Close"
class="js-modal__close-btn modal__close-btn"
>
Close
</button>
{this.props.hideCloseButton ? null : (
<button
type="button"
onClick={this.props.closeHandler}
aria-label="Close modal"
title="Close"
class="js-modal__close-btn modal__close-btn"
>
Close
</button>
)}
{this.props.children}
</div>
</div>

View File

@@ -55,6 +55,15 @@ import { Js13KModal } from './Js13KModal';
import { CreateNewModal } from './CreateNewModal';
import { Icons } from './Icons';
import JSZip from 'jszip';
import { CommandPalette } from './CommandPalette';
import {
OPEN_SAVED_CREATIONS_EVENT,
SAVE_EVENT,
OPEN_SETTINGS_EVENT,
NEW_CREATION_EVENT,
SHOW_KEYBOARD_SHORTCUTS_EVENT
} from '../commands';
import { commandPaletteService } from '../commandPaletteService';
if (module.hot) {
require('preact/debug');
@@ -84,7 +93,8 @@ export default class App extends Component {
isAskToImportModalOpen: false,
isOnboardModalOpen: false,
isJs13KModalOpen: false,
isCreateNewModalOpen: false
isCreateNewModalOpen: false,
isCommandPaletteOpen: false
};
this.state = {
isSavedItemPaneOpen: false,
@@ -491,8 +501,20 @@ export default class App extends Component {
this.editorWithFocus.focus();
}
}
openSettings() {
this.setState({ isSettingsModalOpen: true });
}
openKeyboardShortcuts() {
this.setState({ isKeyboardShortcutsModalOpen: true });
}
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 => {
@@ -532,6 +554,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 +576,29 @@ export default class App extends Component {
}
}
});
const commandPalleteHooks = {
[NEW_CREATION_EVENT]: () => {
this.openNewCreationModal();
},
[OPEN_SAVED_CREATIONS_EVENT]: () => {
this.openSavedItemsPane();
},
[SAVE_EVENT]: () => {
this.saveItem();
},
[OPEN_SETTINGS_EVENT]: () => {
this.openSettings();
},
[SHOW_KEYBOARD_SHORTCUTS_EVENT]: () => {
this.openKeyboardShortcuts();
}
};
for (let eventName in commandPalleteHooks) {
commandPaletteService.subscribe(
eventName,
commandPalleteHooks[eventName]
);
}
}
closeAllOverlays() {
@@ -915,8 +966,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?'
@@ -932,6 +982,10 @@ export default class App extends Component {
});
}
}
newBtnClickHandler() {
trackEvent('ui', 'newBtnClick');
this.openNewCreationModal();
}
openBtnClickHandler() {
trackEvent('ui', 'openBtnClick');
this.openSavedItemsPane();
@@ -1396,9 +1450,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
)}
@@ -1410,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
)}
@@ -1553,6 +1605,14 @@ export default class App extends Component {
onTemplateSelect={this.templateSelectHandler.bind(this)}
/>
<CommandPalette
show={this.state.isCommandPaletteOpen}
closeHandler={() => this.setState({ isCommandPaletteOpen: false })}
files={linearizeFiles(this.state.currentItem.files || [])}
isCommandMode={this.state.isCommandPaletteInCommandMode}
closeHandler={() => this.setState({ isCommandPaletteOpen: false })}
/>
<Portal into="body">
<div
class="modal-overlay"

View File

@@ -26,3 +26,9 @@ export function A(props) {
export function Button(props) {
return <Clickable Tag={'button'} {...props} />;
}
export function AutoFocusInput(props) {
return (
<input ref={el => el && setTimeout(() => el.focus(), 100)} {...props} />
);
}

6
src/keyboardKeys.js Normal file
View File

@@ -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;

View File

@@ -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,47 @@ while the theme CSS file is loading */
.cm-s-midnight .CodeMirror-activeline-background {
background: #253540;
}
.command-palette__option-list {
margin: 0;
margin-top: 10px;
padding: 0;
list-style: none;
}
.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;
}
}