Uygulama yapısı

Pencereler

Bir bileşeni yeni bir pencerede ya da hep üstte duran Picture-in-Picture penceresinde açmak, geri almak; pencere yöneticisinin kuralları, stil aynalama, Electron.

Bir MotifJS bileşeni yeniden kurulmadan başka bir pencereye taşınabilir: window.open ile açılan bir pencereye, tarayıcının hep üstte tuttuğu Picture-in-Picture penceresine ya da Electron'un açtığı çocuk pencereye. Bileşen nesnesi, durumu, bağları, servisleri ve olayları sayfanın JavaScript dünyasında kalır; öbür pencereye yalnız elemanlar gider. Bunu yöneten app.windows'tur; bileşenden this.context.windows, fonksiyon bileşeninde useApplication().windows, dışarıdan Application.main.windows ile erişilir.

Pencere açmak

app.windows.open(bileşen, seçenekler) pencereyi açar, bileşeni oraya taşır ve bir handle döndürür. Tarayıcı çağrısı kullanıcı hareketinin içinde yapılır; bu yüzden open bir tıklama işleyicisinden çağrılır. Dönen söz içerik yerleşince çözülür. Açık notun düzenleyicisini pencerede ya da hep üstte açan araç çubuğu:

import { Component } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";

export class NoteTools extends Component<HTMLDivElement> {
  editor!: NoteEditor;

  override onConfigured() {
    this.class.add("tools");
  }

  override view() {
    return (
      <>
        <button onclick={() => this.context.windows.open(this.editor, { width: 720, height: 560, title: "Not" })}>Pencerede aç</button>
        <button onclick={() => this.context.windows.open(this.editor, { kind: "pip", width: 480, height: 360 })}>Hep üstte</button>
        <NoteEditor ref={(editor) => (this.editor = editor)} />
      </>
    );
  }
}
import { Component, ComponentBase, motifComponent } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";

export class NoteTools extends Component<HTMLDivElement> {
  editor = new NoteEditor();

  override onConfigured() {
    this.class.add("tools");
  }

  override initializeComponent() {
    const popup = motifComponent("button", (button: ComponentBase) => {
      button.setText("Pencerede aç");
      button.motif.on("click", () => this.context.windows.open(this.editor, { width: 720, height: 560, title: "Not" }));
    });
    const pip = motifComponent("button", (button: ComponentBase) => {
      button.setText("Hep üstte");
      button.motif.on("click", () => this.context.windows.open(this.editor, { kind: "pip", width: 480, height: 360 }));
    });
    this.controls.add(popup, pip, this.editor);
  }
}
import { useApplication } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";

export function NoteTools() {
  const { windows } = useApplication();
  let editor!: NoteEditor;

  return (
    <div class="tools">
      <button onclick={() => windows.open(editor, { width: 720, height: 560, title: "Not" })}>Pencerede aç</button>
      <button onclick={() => windows.open(editor, { kind: "pip", width: 480, height: 360 })}>Hep üstte</button>
      <NoteEditor ref={(field) => (editor = field)} />
    </div>
  );
}
import { ComponentBase, motifComponent, useApplication } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";

export function NoteTools() {
  const { windows } = useApplication();
  const editor = new NoteEditor();

  return motifComponent("div", (root: ComponentBase) => {
    root.class.add("tools");
    const popup = motifComponent("button", (button: ComponentBase) => {
      button.setText("Pencerede aç");
      button.motif.on("click", () => windows.open(editor, { width: 720, height: 560, title: "Not" }));
    });
    const pip = motifComponent("button", (button: ComponentBase) => {
      button.setText("Hep üstte");
      button.motif.on("click", () => windows.open(editor, { kind: "pip", width: 480, height: 360 }));
    });
    root.controls.add(popup, pip, editor);
  });
}
import { motifOptions } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";

export const NoteTools = () => motifOptions({
  el: "div",
  data: { editor: null as NoteEditor | null },

  view() {
    this.class.add("tools");
    return (
      <>
        <button onclick={() => this.context.windows.open(this.data.editor!, { width: 720, height: 560, title: "Not" })}>Pencerede aç</button>
        <button onclick={() => this.context.windows.open(this.data.editor!, { kind: "pip", width: 480, height: 360 })}>Hep üstte</button>
        <NoteEditor ref={(editor) => (this.data.editor = editor)} />
      </>
    );
  },
});
import { ComponentBase, motifComponent, motifOptions } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";

export const NoteTools = () => motifOptions({
  el: "div",
  data: { editor: new NoteEditor() },

  view() {
    this.class.add("tools");
    const popup = motifComponent("button", (button: ComponentBase) => {
      button.setText("Pencerede aç");
      button.motif.on("click", () => this.context.windows.open(this.data.editor, { width: 720, height: 560, title: "Not" }));
    });
    const pip = motifComponent("button", (button: ComponentBase) => {
      button.setText("Hep üstte");
      button.motif.on("click", () => this.context.windows.open(this.data.editor, { kind: "pip", width: 480, height: 360 }));
    });
    return [popup, pip, this.data.editor];
  },
});
import { Component } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";

export class NoteTools extends Component {
  static elementTag = "div";
  editor;

  onConfigured() {
    this.class.add("tools");
  }

  view() {
    return (
      <>
        <button onclick={() => this.context.windows.open(this.editor, { width: 720, height: 560, title: "Not" })}>Pencerede aç</button>
        <button onclick={() => this.context.windows.open(this.editor, { kind: "pip", width: 480, height: 360 })}>Hep üstte</button>
        <NoteEditor ref={(editor) => (this.editor = editor)} />
      </>
    );
  }
}
import { Component, motifComponent } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";

export class NoteTools extends Component {
  static elementTag = "div";
  editor = new NoteEditor();

  onConfigured() {
    this.class.add("tools");
  }

  initializeComponent() {
    const popup = motifComponent("button", (button) => {
      button.setText("Pencerede aç");
      button.motif.on("click", () => this.context.windows.open(this.editor, { width: 720, height: 560, title: "Not" }));
    });
    const pip = motifComponent("button", (button) => {
      button.setText("Hep üstte");
      button.motif.on("click", () => this.context.windows.open(this.editor, { kind: "pip", width: 480, height: 360 }));
    });
    this.controls.add(popup, pip, this.editor);
  }
}
import { useApplication } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";

export function NoteTools() {
  const { windows } = useApplication();
  let editor;

  return (
    <div class="tools">
      <button onclick={() => windows.open(editor, { width: 720, height: 560, title: "Not" })}>Pencerede aç</button>
      <button onclick={() => windows.open(editor, { kind: "pip", width: 480, height: 360 })}>Hep üstte</button>
      <NoteEditor ref={(field) => (editor = field)} />
    </div>
  );
}
import { motifComponent, useApplication } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";

export function NoteTools() {
  const { windows } = useApplication();
  const editor = new NoteEditor();

  return motifComponent("div", (root) => {
    root.class.add("tools");
    const popup = motifComponent("button", (button) => {
      button.setText("Pencerede aç");
      button.motif.on("click", () => windows.open(editor, { width: 720, height: 560, title: "Not" }));
    });
    const pip = motifComponent("button", (button) => {
      button.setText("Hep üstte");
      button.motif.on("click", () => windows.open(editor, { kind: "pip", width: 480, height: 360 }));
    });
    root.controls.add(popup, pip, editor);
  });
}
import { motifOptions } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";

export const NoteTools = () => motifOptions({
  el: "div",
  data: { editor: null },

  view() {
    this.class.add("tools");
    return (
      <>
        <button onclick={() => this.context.windows.open(this.data.editor, { width: 720, height: 560, title: "Not" })}>Pencerede aç</button>
        <button onclick={() => this.context.windows.open(this.data.editor, { kind: "pip", width: 480, height: 360 })}>Hep üstte</button>
        <NoteEditor ref={(editor) => (this.data.editor = editor)} />
      </>
    );
  },
});
import { motifComponent, motifOptions } from "@motifx/core";
import { NoteEditor } from "./NoteEditor";

export const NoteTools = () => motifOptions({
  el: "div",
  data: { editor: new NoteEditor() },

  view() {
    this.class.add("tools");
    const popup = motifComponent("button", (button) => {
      button.setText("Pencerede aç");
      button.motif.on("click", () => this.context.windows.open(this.data.editor, { width: 720, height: 560, title: "Not" }));
    });
    const pip = motifComponent("button", (button) => {
      button.setText("Hep üstte");
      button.motif.on("click", () => this.context.windows.open(this.data.editor, { kind: "pip", width: 480, height: 360 }));
    });
    return [popup, pip, this.data.editor];
  },
});

Boş pencere app.windows.create(seçenekler) ile açılır, içerik sonra win.add ile verilir; open bu ikisinin kısa yoludur. Elinizdeki hazır bir pencere, bir <iframe>'in contentWindow'u ya da kendi açtığınız bir pencere app.windows.attach(pencere, seçenekler) ile yönetilen pencere olur.

Seçenek Anlamı
kind "popup" varsayılan, "pip" hep üstte duran Picture-in-Picture penceresi.
fallback "pip" tarayıcıda yoksa ya da istek reddedilirse ne olacağı: "popup" varsayılan, "none" ile söz MJX419 ile reddedilir.
width, height, left, top Pencerenin boyutu ve yeri.
name window.open adı; verilmezse her pencere kendi adını alır. Electron'da ana sürecin gördüğü frameName budur.
features window.open özellik dizesine eklenecek parçalar.
title Pencerenin başlığı; verilmezse sayfanın başlığı.
styles Stil ve tema aynalama: "live" varsayılan, "once", "none" ya da pencerenin belgesini alan bir fonksiyon.
orphans Kapanışta kökeni kalmamış içerik: "dispose" varsayılan, "keep" kopuk bırakır. Yalnız win.add ile verilen bileşenler için geçerlidir.
closeWhenEmpty Son içerik serbest bırakılınca pencere kapanır; varsayılan true. Yalnız win.add ile verilen bileşenler sayılır; host'a doğrudan konan içerik pencereyi açık tutmaz.
closeOnNavigate Rota değişince pencere kapanır; varsayılan false.
returnOnClose Kapanışta içerik kökenine döner; varsayılan true. false ile kökeni olan bileşen de orphans kuralına girer.
owner Pencere bu bileşenin serbest bırakma katmanına bağlanır; bileşen gidince pencere kapanır.
onClosing Pencere kapanmadan hemen önce, içerik hâlâ penceredeyken çağrılır; { window, components, reason } alır. Bkz. Kapanış kuralları.

Handle

open, create ve attach bir AppWindow döndürür.

Üye Anlamı
window, document Pencerenin kendisi ve belgesi.
host Pencerenin gövdesini saran bileşen; içerik onun çocuğudur.
kind Gerçekte açılan tür: "popup", "pip" ya da attach ile gelen için "attached".
components Penceredeki bileşenler, ekleniş sırasıyla.
isOpen Pencere açık mı.
add(...bileşenler) Bileşenleri pencereye taşır.
release(bileşen) Bileşeni kökenine geri koyar, pencere açık kalır.
focus() Pencereyi öne getirir.
close() Pencereyi kapatır; sonucu { returned, disposed, kept } olarak döndürür.
closed Pencere nasıl kapanırsa kapansın aynı sonuçla çözülen söz.
syncStyles() Stil sayfalarını elle yeniler.

Penceredeki bir bileşenin açtığı bildirim ya da iletişim kutusu da o pencerede görünmelidir. app.layerOf(bileşen) bileşenin belgesine bakar: bileşen bu yöneticinin bir penceresindeyse pencerenin host'unu, sayfadaysa app.layer'ı döndürür ve ardından verilen bileşenleri oraya ekler (bkz. Sayfa katmanı). Böyle doğrudan host'a konan içerik pencerenin components listesine girmez; orphans ve returnOnClose ona bakmaz, closeWhenEmpty onu saymaz (win.add ile verilen son bileşen gidince pencere, host'ta hâlâ bir iletişim kutusu olsa da kapanır), pencere kapanınca host ile birlikte serbest bırakılır ve closed sonucunun hiçbir listesinde yer almaz. Pencereyle kalkmaması gereken içerik win.add ile verilir ya da bir Transport yuvasına yazılır.

İçerik, köken ve geri dönüş

Bir bileşen pencereye ilk alındığında yönetici sayfadaki ebeveynini ve o anki sonraki kardeşini not eder. Bileşen eski ebeveyninden ayrılır, onDeactivated alır, pencerenin gövdesine controls.add ile girer ve onActivated alır; onMounted daha önce çalıştıysa bir daha çalışmaz, ilk kez orada yerleşiyorsa o pencerenin sırasında çalışır. Aynı pencereye win.add ile başka bileşenler de konur; bileşen bir pencereden ötekine taşınırsa köken değişmez. Köken yalnız open ya da add ile verilen bileşen için tutulur: uygulamanın attach ile bağladığı pencereye kendi taşıdığı bileşenleri yönetici tanımaz, kapanışta onlar için hiçbir şey geri dönmez ve components listesinde görünmezler.

win.release(bileşen) ve pencerenin kapanışı bileşeni kökenine, eski sırasına koyar: hatırlanan kardeş hâlâ oradaysa onun önüne, o da penceredeyse onun kardeşinin önüne, kimse yoksa sona. Aynı ebeveynden pencereye alınan birden çok bileşen geri döndüğünde controls içindeki eski sırasını bulur. returnOnClose: false bu dönüşü kapatır; kökeni olan bileşen de kökensiz sayılır, release ise yine kökene koyar. Kökeni kalmamışsa, örneğin rota değişip ebeveyn serbest bırakıldıysa, orphans karar verir: varsayılan serbest bırakmak, "keep" ile bileşen ebeveynsiz ama sağ kalır ve closed sonucunun kept listesinde gelir. Köken yokken içeriği korumak isteyen uygulama kapanmadan önce win.release ya da başka bir ebeveyne controls.add ile alır.

Penceredeki bir bileşen gizlenip gösterilince pencerede kalır: iz pencerede bırakılır, motif.show() bileşeni oraya koyar. Bu, uygulamanın elemanını başka bir belgeye kendi taşıdığı bileşen için de geçerlidir. İz oynatılmış ya da tutucusu belgeden çıkmışsa bileşen ebeveyninin altında yeniden kurulur ve MJX106 uyarısı verilir.

Kapanış kuralları

Pencere kullanıcı tarafından kapatıldığında, win.close() çağrıldığında ya da penceresi pagehide aldığında içerik kökenine döner ve closed çözülür. Penceredeki bir bileşen uygulama tarafından serbest bırakılırsa listeden düşer; son bileşen de giderse pencere kapanır, closeWhenEmpty: false açık tutar. owner verilmişse sahip bileşen serbest bırakılınca pencere kapanır; closeOnNavigate rota değişince kapatır. app.dispose() ve sayfanın kendi pagehide'ı bütün pencereleri kapatır, içlerindeki içerik serbest bırakılır; arkada boş pencere kalmaz. app.windows.closeAll() aynı işi istek üzerine yapar.

onClosing verilmişse yönetici içeriğe dokunmadan önce onu çağırır; kullanıcı kapatışında bu pencerenin pagehide anıdır ve belge hâlâ ayaktadır. O anda pencere kapalı sayılır: isOpen false, add MJX420 fırlatır. Kanca { window, components, reason } alır. components o anda penceredeki bileşenlerin kopyasıdır; reason kapanışın nedenidir: "user" kullanıcı kapattı ya da pencere pagehide aldı, "code" win.close() ve closeAll, "owner" sahip bileşen serbest bırakıldı, "empty" son bileşen gitti, "navigate" rota değişti, "page" sayfa kapanıyor ya da app.dispose() çağrıldı. Kancada başka bir ebeveyne controls.add ile alınan bileşen ne geri döner ne serbest bırakılır; kökeni unutulur ve closed sonucunda görünmez. win.release de çağrılabilir. Kanca fırlatırsa hata MJX423 ile raporlanır ve kapanış sürer.

Stil ve tema

Yeni belgede stil sayfası yoktur. Açılışta sayfadaki her <link rel="stylesheet"> ve <style>, nerede olursa olsun, pencerenin head'ine kopyalanır; adoptedStyleSheets ile kurulanlar başka belgeye verilemediği için kurallarının metninden tek bir <style> olarak yeniden kurulur. <html> ve <body>'nin class, lang, dir ve data-* öznitelikleri de pencerenin köküne aynalanır; tema bir data-theme ya da kök sınıfıyla kuruluyorsa pencerede de kurulur.

styles: "live" varsayılanıyla bu açılıştan sonra da sürer: sayfaya eklenen, değişen ya da kaldırılan stil sayfası ve kök özniteliklerindeki her değişiklik açık pencerelere yansır. Bedeli, en az bir pencere açıkken sayfada iki MutationObserver; pencere kalmayınca bırakılır. adoptedStyleSheets'e sonradan eklenen kural gözlenemez; onu win.syncStyles() ile yenilersiniz. "once" yalnız açılışta kopyalar, "none" hiç dokunmaz, fonksiyon verirseniz pencerenin belgesiyle çağrılır ve gerisi sizindir.

Türler

popup window.open("", ad, özellikler) ile açılır; tarayıcı açmazsa, popup engeli ya da kullanıcı hareketi yoksa söz MJX418 ile reddedilir. pip tarayıcının documentPictureInPicture.requestWindow penceresidir: kullanıcı hareketi ister, sayfa başına tek pencere açar ve bugün yalnız Chromium tabanlı tarayıcılarda vardır; Firefox ve Safari'de yalnız video için Picture-in-Picture bulunur. app.windows.supports("pip") önceden söyler; fallback varsayılanı "popup" olduğu için tek satırlık çağrı her tarayıcıda çalışır, win.kind gerçekte açılanı verir.

Ana pencere küçültülse de hep üstte pencere görünür kalır. O sırada uygulamanızın kendi setTimeout ve requestAnimationFrame çağrılarını tarayıcı kısabilir; çekirdeğin geçişleri ve onMounted sırası elemanın penceresinden çalıştığı için kısılmaz.

Electron'da window.open isteğine ana süreç karar verir; pencerenin adı frameName olarak gelir ve pencere seçenekleri orada verilir. Hep üstte pencere Picture-in-Picture'a gerek bırakmaz, alwaysOnTop ile alınır; Electron'un Chromium'u documentPictureInPicture'ı tanısa da isteği reddeder, fallback burada da popup'a düşer. Aynı origin'li çocuk pencere açıcıyla aynı renderer sürecinde yaşar; file:// ile yüklenen uygulamada da böyledir.

win.webContents.setWindowOpenHandler(({ url, frameName }) => {
  if (frameName === "motif-popup") {
    return { action: "allow", overrideBrowserWindowOptions: { width: 560, height: 420, alwaysOnTop: true, frame: false } };
  }
  if (/^https?:\/\//.test(url)) void shell.openExternal(url);
  return { action: "deny" };
});
win.webContents.setWindowOpenHandler(({ url, frameName }) => {
  if (frameName === "motif-popup") {
    return { action: "allow", overrideBrowserWindowOptions: { width: 560, height: 420, alwaysOnTop: true, frame: false } };
  }
  if (/^https?:\/\//.test(url)) void shell.openExternal(url);
  return { action: "deny" };
});

Renderer tarafında pencere adı seçenekle verilir: app.windows.open(editor, { name: "motif-popup" }). Ana süreçten new BrowserWindow ile açılan pencere ayrı bir renderer sürecidir; oraya bileşen taşınamaz, iki pencere IPC ya da MessagePort ile konuşur.

Liste ve olaylar

app.windows.list açık pencerelerin reaktif listesidir; bir bağda okunursa pencere açılıp kapandıkça yenilenir. app.windows.of(bileşen) bileşenin penceresini ya da null verir. Uygulama olayları window-opened ve window-closed handle'ı taşır: this.context.on("window-closed", (e) => …). Pencereye taşınan bileşen router'ı, servisleri ve uygulama olaylarını olduğu gibi görür; penceredeki rel="router" bağlantısı sayfanın router'ını gezdirir, pencereyi yeniden yüklemez.

Hata kodları

Kod Ne zaman
MJX418 Tarayıcı pencereyi açmadı; popup engeli ya da kullanıcı hareketi yok.
MJX419 fallback: "none" iken Picture-in-Picture yok ya da istek reddedildi.
MJX420 Kapalı handle'da add ya da focus.
MJX421 attach'e pencere olmayan bir değer verildi ya da pencere başka bir origin'e ait.
MJX422 Serbest bırakılmış bir bileşen pencereye konmak istendi.
MJX423 onClosing fırlattı; raporlanır, kapanış sürer.

Kodların tamamı Hata ve Uyarı Kodları sayfasındadır.

Sonraki adım

Bileşen ağacını ve içeriği yuvalara taşımayı controls ve Bileşen Ağacı anlatır; pencere açmadan başka bir belgeyi uygulamaya bağlamak için app.connectDocument API Referansı sayfasındadır.