電子冒険:エピソード36:ファイルマネージャイベントバス
57687 ワード
それは我々が我々のアプリに学んだことをもたらす時間です.最初のステップはエピソード33からイベント・バスを追加します.
そして、我々がこれをしている間、コードベースを再びリファクタリングするでしょう.
我々は
文脈のために、我々はちょうど2つのものを暴露します-
他の新しいものは
これは、我々が時間の90 %を望むものですが、この場合、要素がどのコンテキストに基づいているかについて、特別なスタイル規則を望みます.
そして今、コード:
注意するマイナーなものは
スクリプトはかなり長いですが、基本的には以前に何をしていたのですか.
結果
(像)
次のステップは、コマンドパレットを追加して、うまくいけば、我々は最後の時間を持っていたよりも少し見ている.
平常通りall the code for the episode is here.
そして、我々がこれをしている間、コードベースを再びリファクタリングするでしょう.
src/EventBus.js
我々は、我々がすでにしたことと同じイベントバスを設定できます.我々は
eventBus.emit("app", "activatePanel", panelId)
objectsを使用してeventBus.app.activatePanel(panelId)
によってProxy
に置き換えることができるようにいくつかの点でいくつかの構文の砂糖サポートを追加することを検討している.これはRubyではとても簡単ですが、JSとはちょっと複雑です.export default class EventBus {
constructor() {
this.callbacks = {}
}
handle(target, map) {
this.callbacks[target] = { ...(this.callbacks[target] || {}), ...map }
}
emit(target, event, ...details) {
let handlers = this.callbacks[target]
if (handlers) {
if (handlers[event]) {
handlers[event](...details)
} else if (handlers["*"]) {
handlers["*"](event, ...details)
}
}
}
}
src/commands.js
以前は、キーボードハンドラ、アプリケーションメニュー、およびコマンドパレットの間に複数回コピーして貼り付けられたコマンドのリストがありました.我々はまだアプリケーションのメニューとコマンドパレットを持っていないが、我々は別のファイルにそれを抽出することによって、この問題を予言することができます.export default [
{key: "Tab", action: ["app", "switchPanel"]},
{key: "F10", action: ["app", "quit"]},
{key: "ArrowDown", action: ["activePanel", "nextItem"]},
{key: "ArrowUp", action: ["activePanel", "previousItem"]},
{key: "PageDown", action: ["activePanel", "pageDown"]},
{key: "PageUp", action: ["activePanel", "pageUp"]},
{key: "Home", action: ["activePanel", "firstItem"]},
{key: "End", action: ["activePanel", "lastItem"]},
{key: " ", action: ["activePanel", "flipItem"]},
{key: "Enter", action: ["activePanel", "activateItem"]},
]
src/Keyboard.svelte
イベントバスとコマンドリストを抽出し、Keyboard
コンポーネントは非常に簡単です.私たちは、cmdのような修飾子キーをサポートするために変更する必要があります.そして、モーダルパネルが開いているときにショートカットを無効にするかもしれません.<script>
import commands from "./commands.js"
import { getContext } from "svelte"
let { eventBus } = getContext("app")
function handleKey(e) {
for (let command of commands) {
if (command.key === e.key) {
e.preventDefault()
eventBus.emit(...command.action)
}
}
}
</script>
<svelte:window on:keydown={handleKey} />
src/Footer.svelte
唯一の変更は、アプリをローカルでの処理の代わりに終了するように指示するeventBus
を使用しています.我々は機能を追加しているとして、我々は他のボタンに似たようなハンドラを追加されます.もちろん、いくつかの時点で我々は空想に行くことができますし、フッターコンテキストを認識する.<script>
import { getContext } from "svelte"
let { eventBus } = getContext("app")
</script>
<footer>
<button>F1 Help</button>
<button>F2 Menu</button>
<button>F3 View</button>
<button>F4 Edit</button>
<button>F5 Copy</button>
<button>F6 Move</button>
<button>F7 Mkdir</button>
<button>F8 Delete</button>
<button on:click={() => eventBus.emit("app", "quit")}>F10 Quit</button>
</footer>
<svelte:window />
<style>
footer {
text-align: center;
grid-area: footer;
}
button {
font-family: inherit;
font-size: inherit;
background-color: #66b;
color: inherit;
}
</style>
src/App.svelte
と主なコンポーネント.最初のテンプレートとスタイリング、非常に少し変更された、我々はちょうどKeyboard
を追加し、いくつかのPanel
小道具を取り除く<div class="ui">
<header>
File Manager
</header>
<Panel initialDirectory={initialDirectoryLeft} id="left" />
<Panel initialDirectory={initialDirectoryRight} id="right" />
<Footer />
</div>
<Keyboard />
<style>
:global(body) {
background-color: #226;
color: #fff;
font-family: monospace;
margin: 0;
font-size: 16px;
}
.ui {
width: 100vw;
height: 100vh;
display: grid;
grid-template-areas:
"header header"
"panel-left panel-right"
"footer footer";
grid-template-columns: 1fr 1fr;
grid-template-rows: auto minmax(0, 1fr) auto;
}
.ui header {
grid-area: header;
}
header {
font-size: 24px;
margin: 4px;
}
</style>
スクリプトの部分はもう少しです.<script>
import { writable } from "svelte/store"
import { setContext } from "svelte"
import Panel from "./Panel.svelte"
import Footer from "./Footer.svelte"
import EventBus from "./EventBus.js"
import Keyboard from "./Keyboard.svelte"
let eventBus = new EventBus()
let activePanel = writable("left")
setContext("app", {eventBus, activePanel})
let initialDirectoryLeft = window.api.currentDirectory()
let initialDirectoryRight = window.api.currentDirectory() + "/node_modules"
function switchPanel() {
if ($activePanel === "left") {
activePanel.set("right")
} else {
activePanel.set("left")
}
}
function activatePanel(panel) {
activePanel.set(panel)
}
function quit() {
window.close()
}
function emitToActivePanel(...args) {
eventBus.emit($activePanel, ...args)
}
eventBus.handle("app", {switchPanel, activatePanel, quit})
eventBus.handle("activePanel", {"*": emitToActivePanel})
</script>
我々は3つのコマンドをswitchPanel
、activatePanel
、quit
を登録します.また、activePanel
またはleft
パネルにright
イベントのセットアップを進めます.文脈のために、我々はちょうど2つのものを暴露します-
activePanel
とeventBus
.そして、私はactivePanel
露出についても確信していません.たった今true
/false
を通過して、ちょうどPanel
はちょうど同様に働きます.後で再会するかもしれない.src/File.svelte
Panel
はすでに非常に複雑になっていたので、私はそれからFile
コンポーネントを抽出することによって始めました.これは、パネル内の単一のエントリを表します.<div
class="file"
class:focused={focused}
class:selected={selected}
on:click|preventDefault={() => onclick()}
on:contextmenu|preventDefault={() => onrightclick()}
on:dblclick|preventDefault={() => ondoubleclick()}
bind:this={node}
>
{filySymbol(file)}{file.name}
</div>
<style>
.file {
cursor: pointer;
}
.file.selected {
color: #ff2;
font-weight: bold;
}
:global(.panel.active) .file.focused {
background-color: #66b;
}
</style>
ここに二つの新しいものがあります.最初はbind:this={node}
です.我々は、node
をBabableプロパティとして公開するので、親はDOMノードにアクセスできます.これは一般的には最良のパターンではないので、おそらく私たちは何かを以下の侵入を把握することができます.他の新しいものは
:global(.panel.active) .file.focused
セレクタです.Svelteセレクタはすべて、現在のコンポーネントによって作成された要素と一致するように自動的に書き直されます-すべてのコンポーネントによって自動的に追加された余分なクラスがあり、.file.selected
は実際に.createdByFileComponent.file.selected
です(ハッシュではなくcreatedByFileComponent
です).これは、我々が時間の90 %を望むものですが、この場合、要素がどのコンテキストに基づいているかについて、特別なスタイル規則を望みます.
.panel.active .file.focused
がここで作成されなかったので、panel
は決して働きません.これを行うには2つの方法があります.コンテキストを記述するコンポーネントに対していくつかの小道具を渡します.または、この1つのセレクタのためにこの規則を無効にするためにexport let inActivePanel
を使用してください.スタイリングの他のすべてはまだコンポーネントスコープです.そして今、コード:
<script>
import { getContext } from "svelte"
export let file
export let idx
export let panelId
export let focused
export let selected
export let node = undefined
let {eventBus} = getContext("app")
function onclick() {
eventBus.emit("app", "activatePanel", panelId)
eventBus.emit(panelId, "focusOn", idx)
}
function onrightclick() {
eventBus.emit("app", "activatePanel", panelId)
eventBus.emit(panelId, "focusOn", idx)
eventBus.emit(panelId, "flipSelected", idx)
}
function ondoubleclick() {
eventBus.emit("app", "activatePanel", panelId)
eventBus.emit(panelId, "focusOn", idx)
eventBus.emit(panelId, "activateItem")
}
function filySymbol(file) {
if (file.type === "directory") {
if (file.linkTarget) {
return "~"
} else {
return "/"
}
} else if (file.type === "special") {
return "-"
} else {
if (file.linkTarget) {
return "@"
} else {
return "\xA0" //
}
}
}
</script>
我々は、:global(selector)
とapp
イベントのシリーズにそれらを翻訳することによって、ローカルのすべてのイベントを処理します.私はいくつかのpanelId
オブジェクトを使用することについて疑問に思っています. function onclick() {
eventBus.app.activatePanel(panelId)
eventBus[panelId].focusOn(idx)
}
function onrightclick() {
eventBus.app.activatePanel(panelId)
eventBus[panelId].focusOn(idx)
eventBus[panelId].flipSelected(idx)
}
function ondoubleclick() {
eventBus.app.activatePanel(panelId)
eventBus[panelId].focusOn(idx)
eventBus[panelId].activateItem()
}
あるいは let app = eventBus.app
let panel = eventBus[panelId]
function onclick() {
app.activatePanel(panelId)
panel.focusOn(idx)
}
function onrightclick() {
app.activatePanel(panelId)
panel.focusOn(idx)
panel.flipSelected(idx)
}
function ondoubleclick() {
app.activatePanel(panelId)
panel.focusOn(idx)
panel.activateItem()
}
それはより良いでしょうか?注意するマイナーなものは
Proxy
です.export let node = undefined
はエクスポートのみのプロパティですので、開発モードで警告を避けるために明示的にマークします.それ以外はnode
を持たないと同じである.= undefined
src/Panel.svelte
Svelteは、Panel
構成要素に移っている若干のコードのおかげですべりました.テンプレートとスタイリングから始めましょう<div class="panel {id}" class:active={active}>
<header>{directory.split("/").slice(-1)[0]}</header>
<div class="file-list" bind:this={fileListNode}>
{#each files as file, idx}
<File
panelId={id}
file={file}
idx={idx}
focused={idx === focusedIdx}
selected={selected.includes(idx)}
bind:node={fileNodes[idx]}
/>
{/each}
</div>
</div>
<style>
.left {
grid-area: panel-left;
}
.right {
grid-area: panel-right;
}
.panel {
background: #338;
margin: 4px;
display: flex;
flex-direction: column;
}
header {
text-align: center;
font-weight: bold;
}
.file-list {
flex: 1;
overflow-y: scroll;
}
</style>
唯一の珍しいことはFile
です.bind:node={fileNodes[idx]}
は、主なDOMノードをFile
インスタンス変数にエクスポートし、それからnode
に格納します.スクリプトはかなり長いですが、基本的には以前に何をしていたのですか.
<script>
import File from "./File.svelte"
import { getContext, tick } from "svelte"
export let initialDirectory
export let id
let directory = initialDirectory
let initialFocus
let files = []
let selected = []
let focusedIdx = 0
let fileNodes = []
let fileListNode
let {eventBus, activePanel} = getContext("app")
$: active = ($activePanel === id)
$: filesPromise = window.api.directoryContents(directory)
$: filesPromise.then(x => {
files = x
selected = []
setInitialFocus()
})
$: filesCount = files.length
$: focused = files[focusedIdx]
let flipSelected = (idx) => {
if (selected.includes(idx)) {
selected = selected.filter(f => f !== idx)
} else {
selected = [...selected, idx]
}
}
let setInitialFocus = async () => {
focusedIdx = 0
if (initialFocus) {
focusedIdx = files.findIndex(x => x.name === initialFocus)
if (focusedIdx === -1) {
focusedIdx = 0
}
} else {
focusedIdx = 0
}
await tick()
scrollFocusedIntoView()
}
let scrollFocusedIntoView = () => {
if (fileNodes[focusedIdx]) {
fileNodes[focusedIdx].scrollIntoViewIfNeeded(true)
}
}
let focusOn = (idx) => {
focusedIdx = idx
if (focusedIdx > filesCount - 1) {
focusedIdx = filesCount - 1
}
if (focusedIdx < 0) {
focusedIdx = 0
}
scrollFocusedIntoView()
}
function pageSize() {
if (!fileNodes[0] || !fileNodes[1] || !fileListNode) {
return 16
}
let y0 = fileNodes[0].getBoundingClientRect().y
let y1 = fileNodes[1].getBoundingClientRect().y
let yh = fileListNode.getBoundingClientRect().height
return Math.floor(yh / (y1 - y0))
}
function activateItem() {
if (focused?.type === "directory") {
if (focused.name === "..") {
initialFocus = directory.split("/").slice(-1)[0]
directory = directory.split("/").slice(0, -1).join("/") || "/"
} else {
initialFocus = null
directory += "/" + focused.name
}
}
}
function nextItem() {
focusOn(focusedIdx + 1)
}
function previousItem() {
focusOn(focusedIdx - 1)
}
function pageDown() {
focusOn(focusedIdx + pageSize())
}
function pageUp() {
focusOn(focusedIdx - pageSize())
}
function firstItem() {
focusOn(0)
}
function lastItem() {
focusOn(filesCount - 1)
}
function flipItem() {
flipSelected(focusedIdx)
nextItem()
}
eventBus.handle(id, {nextItem, previousItem, pageDown, pageUp, firstItem, lastItem, flipItem, activateItem, focusOn, flipSelected, activateItem})
</script>
結果
(像)
次のステップは、コマンドパレットを追加して、うまくいけば、我々は最後の時間を持っていたよりも少し見ている.
平常通りall the code for the episode is here.
Reference
この問題について(電子冒険:エピソード36:ファイルマネージャイベントバス), 我々は、より多くの情報をここで見つけました https://dev.to/taw/electron-adventures-episode-36-file-manager-event-bus-g84テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol