電子冒険:エピソード36:ファイルマネージャイベントバス


それは我々が我々のアプリに学んだことをもたらす時間です.最初のステップはエピソード33からイベント・バスを追加します.
そして、我々がこれをしている間、コードベースを再びリファクタリングするでしょう.
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つのコマンドをswitchPanelactivatePanelquitを登録します.また、activePanelまたはleftパネルにrightイベントのセットアップを進めます.
文脈のために、我々はちょうど2つのものを暴露します- activePaneleventBus.そして、私はactivePanel露出についても確信していません.たった今true/falseを通過して、ちょうどPanelはちょうど同様に働きます.後で再会するかもしれない.
src/File.sveltePanelはすでに非常に複雑になっていたので、私はそれから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" // &nbsp;
      }
    }
  }
</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を持たないと同じである.
= undefinedsrc/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.