画面のHTML・CSSとレイアウトの構造
プリザンターの画面をカスタマイズするときは、画面全体の枠、フォーム、項目の枠、入力要素を分けて見ると、CSSを当てる場所と影響範囲を判断できます。一覧では、表そのものとスクロール領域にも境界があります。
ここでは通常の記録テーブルの一覧・新規・編集画面を中心に、本体ソースから構造を説明します。テーマ、権限、項目設定、コメントの有無によって要素は増減します。図とツリーは主要部分を整理したもので、生成HTMLの全文ではありません。テーマ改善の動作確認モジュールの構造は別ページで扱います。
画面全体の枠と画面固有の領域
共通部分は HtmlTemplates.MainContainer が作り、Content が #Application の中へ画面固有の処理を差し込みます。新規・編集画面の項目配置や一覧のフォームは、ResultUtilities 側が作ります(共通の枠、画面の差し込み)。
図を読み込み中…
同じ #MainForm でも、編集画面では #Editor の内側、一覧では #Application の画面固有領域に置かれます。#MainForm input のような広い指定は、画面によって別の入力群に当たります。編集画面だけを対象にするなら #Editor を起点にします(一覧のフォーム、編集のフォーム)。
| 要素・領域 | 主な役割 | 調べるソース |
|---|---|---|
#MainContainer・#Application | 共通の画面枠、画面固有の内容の受け皿 | HtmlTemplates.cs |
#Header・#Footer | 共通ヘッダー・フッターの見た目 | style.scss・legacy.scss |
#Editor・#MainForm | 編集画面と入力を送るフォーム | ResultUtilities.cs |
#EditorTabsContainer・#EditorTabs | タブ全体とタブの見出しリスト | ResultUtilities.cs |
#MainCommandsContainer・#MainCommands | 作成・更新などの操作領域 | style.scss、各画面のコマンド生成処理 |
#Message | 操作結果などのメッセージ | HtmlTemplates.cs・style.scss |
CSSはどの順で重なるか
テーマによって読み込む本体CSSが違う
cerulean・green-tea・mandarin・midnight はCSS用のテーマバージョンが2.0、その他は1.0として判定されます(テーマ判定)。
HtmlStyles.LinkedStyles は、Normalize、テーマのjQuery UI、Material Symbolsを読み込んだ後、テーマバージョンに応じた本体CSSを読み込み、テーマの custom.css を読み込みます(読み込みの分岐)。
| 条件 | 本体CSS | 元になるファイル |
|---|---|---|
| テーマバージョンが2.0未満 | legacy.min.css | wwwroot/src/styles/legacy.scss |
| テーマバージョンが2.0以上 | style.min.css | wwwroot/src/styles/style.scss |
| モバイル・レスポンシブなどの条件を満たす | 世代に対応する *-responsive.min.css | legacy-responsive.scss・style-responsive.scss |
| 各テーマ | themes/{テーマ}/custom.css | wwwroot/src/clone/assets/themes/ |
ソースのパスは Implem.PleasanterFrontend からの相対パスです。レスポンシブCSSの条件には Parameters.Mobile.Responsive、context.Mobile、context.Responsive、サイトのResponsive設定も含まれます。画面幅だけで読み込むファイルを決めているわけではありません。
第二世代テーマにも従来のレイアウトが残る
style.scss の先頭は @use 'legacy' as * です。第二世代のCSSは従来CSSから独立した全面的な置き換えではなく、従来の定義を含めて上書きします。 例えばフォーム全体にFlexを使っていても、その中の項目にはfloatによる配置が残ります(従来CSSの読み込み、フォームのFlex、項目のfloat)。
色は var(--base-text)・var(--editor-bg)・var(--primaryColor) などのCSS変数を参照します。ceruleanの custom.css がこれらの値を定義しています。背景色や文字色を揃えるときは、現在のテーマが使う変数と、Computedでの最終値を確認します(変数を使う本体CSS、テーマの変数定義)。
サイトのスタイルと拡張スタイル
headでは本体・テーマCSSに続いて拡張スタイルへのリンクが出力されます。通常のテンプレートでは、画面内容の後にサイトの全画面用スタイルと userStyle がstyle要素として出力されます(headの順序、サイトスタイルの出力位置、style要素の生成)。
後から読み込まれたCSSでも、セレクタの詳細度や !important によっては上書きできません。開発者ツールのStylesで適用・打ち消しを調べ、Computedで最終の値を確認します。
編集画面――フォーム全体とコメント領域
#Editor
└─ form#MainForm.main-form.confirm-unload
├─ #RecordHeader
├─ #EditorComments コメントを表示する場合
├─ #EditorTabsContainer.tab-container
│ ├─ ul#EditorTabs
│ ├─ fieldset#FieldSetGeneral
│ ├─ fieldset#FieldSetTab{ID} 追加タブ
│ ├─ fieldset#FieldSetHistories 履歴の条件を満たす場合
│ ├─ #FieldSetRecordAccessControl
│ └─ コマンド領域
└─ hidden input群根拠は編集領域の組立て、タブのリンク先、追加タブです。コマンド領域は、この編集画面ではタブコンテナを生成する処理の中にあります。CSSファイルのコメントに書かれた親子関係だけでなく、HTMLの生成処理と実際のDOMも確認してください。
従来CSSはタブ領域を73%、コメント領域を27%とし、タブ領域の .max では100%にします。第二世代CSSではMainFormを折り返し可能なFlexにし、コメント領域を order: 1 と position: sticky にします。DOMではコメントがタブより先にあっても、画面上の順序はCSSでも変わります(領域の幅、フォーム、コメントの配置)。
コメント欄の幅だけを変えてタブ領域をそのままにすると、合計幅や折り返しに影響します。フォームにはコメント側の背景を作る幅27%の疑似要素もあるため、左右の領域全体を変えるときは、その疑似要素も含めて確認します(MainFormの疑似要素)。
1項目の構造――FieldとControlを分ける
通常の編集可能なテキスト項目を整理すると、次の構造になります。
<!-- 構造説明用の例。実際には属性や追加要素が入ります。 -->
<div id="Results_TitleField" class="field-normal">
<p class="field-label">
<label for="Results_Title">タイトル</label>
</p>
<div class="field-control">
<div class="container-normal">
<input id="Results_Title" class="control-text" type="text">
</div>
</div>
</div>Field は外側のdiv、Label は p.field-label とlabel、Control は div.field-control と .container-normal を生成します。項目の種類によって入力要素やコンテナのタグは変わります(Field、Label、Control)。
| 狙うもの | タイトル項目での例 | 用途 |
|---|---|---|
| 項目全体 | #Results_TitleField | 項目幅、項目全体の余白・表示切替 |
| ラベルの枠 | #Results_TitleField > .field-label | ラベル列の幅・配置 |
| ラベル文字 | label[for="Results_Title"] | 文字色・太さ |
| 入力側の枠 | #Results_TitleField > .field-control | 入力領域の配置 |
| 入力側の内枠 | .field-control > .container-normal | ラベル幅を避ける余白 |
| 入力要素 | #Results_Title | 値、入力枠、フォーカス |
通常の項目生成では、controlIdに Field を付けて外側のIDを作ります。プレビュー、一覧編集の接尾辞、読取専用などでは同じ入力要素があるとは限りません。入力タグを決めつけず、現在の画面で確認してください(IDの受け渡し、FieldのID)。
項目幅とラベル幅は別の値
従来CSSの通常項目はfloatで並び、幅340pxです。ラベル枠は幅120px、入力側の内枠は左余白120pxで、ラベルと重ならないようにします。field-wide は幅100%かつ clear: both です。第二世代CSSは通常項目の高さを4em、wide項目の最小高さを4emへ上書きします(通常項目、wide項目、高さの上書き)。
入力要素の幅だけを広げても、外側の340pxの枠まで広がるとは限りません。ラベル枠の幅だけを変えると、入力側の左余白120pxとの関係が崩れます。幅を変えるときは、この3層を確認します。
サーバー設定が付けるクラス
FieldCss の生成処理は、項目設定に加えて NoWrap なら both、非表示なら hidden、文字寄せやExtendedFieldCssを追加します。ControlCssは入力側へ、自動ポストバックなら control-auto-postback、文字寄せやExtendedControlCssを追加します(FieldCss、ControlCss)。
both は clear: both です。NoWrap をテキストの折り返し禁止の指定だと解釈して、white-space: nowrap と同一視しないでください。hidden は display: none、hidden is-important は display: none !important です(both、hidden)。
CSSに値を書く設定と、クラス名を追加する設定も区別します。各プロパティの適用先はCSSをコントロールする要素を参照してください。
タブとセクションの内側
タブの見出しは ul#EditorTabs で、基本タブのリンク先は #FieldSetGeneral です。タブパネルには TabsPanelField がfieldsetを出力し、その内側に .tabs-panel-inner を作ります(タブ見出し、パネルの内枠)。
ブラウザ側では、jqueryui.js が .tab-container にjQuery UIの .tabs() を適用します。サーバーが生成したHTMLと、ライブラリ適用後にElementsで見るDOMの両方を確認するのが、タブの表示や選択状態を調べる入口です(タブの初期化)。
セクションは次の2層を持ちます。
#SectionFields1Container.section-fields-container
├─ 見出しを含む領域
│ └─ label.field-section[for="SectionFields1"]
└─ #SectionFields1.section-fields
└─ セクション内の項目外側を隠すと見出しも消え、中身を隠すと見出しを残せます。折りたたみ状態では中身へ hidden が付きます。また Hide がtrueならセクションは生成されません。存在しない要素はCSSやToggleで表示できません(セクションの生成)。
一覧――表とスクロール枠は別の構造
一覧のMainFormは、ビュー選択、フィルタ、集計、#ViewModeContainer、コマンド領域などを含みます。通常の表は table.grid とその外側の grid-container.grid-wrap です(一覧フォーム、表の生成、表の外枠)。
#MainForm
└─ #ViewModeContainer
└─ grid-container.grid-wrap
└─ table#Grid.grid
├─ thead
│ └─ tr > th[data-name="項目物理名"]
└─ tbody
└─ tr.grid-row[data-id="レコードID"] > tdヘッダーには項目物理名の data-name、行にはレコードの data-id と行用クラスが付きます。行CSSとセルCSSは別の適用先です。列ヘッダーに data-name があることから、すべてのtdにも同じ属性があると仮定しないでください(ヘッダー、行の属性、セルCSSの組立て)。列のセルを取得する方法は項目の取得とDOMイベントで扱います。
GridTable はid引数を受け取った場合、外枠へそのIDと Wrap を合わせた名前を付けます。ただし通常の一覧の生成は、属性として表へ Grid のIDを渡しています。外枠に常に #GridWrap があると決めつけず、grid-container.grid-wrap と実際のDOMで確認します(IDの生成、一覧側の引数)。
Shadow DOMの中にあるスクロール領域
スクロールが有効な grid-container はopenなShadow Rootを作り、内側に .app-grid-inner・.app-grid-frame・slotを生成します。表は通常のDOMに残り、slotから表示されます。スクロールする枠の .app-grid-frame はShadow DOM内です(Shadow Rootの生成、slotの構造、スクロールCSS)。
そのため #Grid の文字色などは通常のCSSで指定できますが、通常のサイトCSSに .app-grid-frame { ... } と書いてもShadow DOMの内部を対象にはできません。開発者ツールでShadow Rootを開き、表とスクロール枠を分けて確認します。
第一世代テーマやモバイル・レスポンシブ条件では、サーバーがこの表のスクロールを無効にする分岐があります。Shadow Rootがある前提のコードにせず、その有無を確認してください(スクロールの分岐)。
実例――タイトルの幅とラベルを変える
テーブルのスタイルへ置き、新規・編集画面を対象にします。通常の編集可能なタイトル項目を前提に、外側の項目幅を変え、ラベルの文字を太くします。
#Editor #Results_TitleField {
box-sizing: border-box;
width: min(100%, 680px);
clear: both;
}
#Editor #Results_TitleField > .field-label > label {
font-weight: 600;
}#Results_Title のinputだけではなくFieldの幅を変えます。ラベル列の幅と入力側の左余白は変えません。入力タグが違う読取専用状態や、別のテーブル種類へ使う場合は、IDと適用結果を確認します。
このCSSは通常の記録テーブル用の指定です。テーマや表示幅ごとの最終描画は実機で確認してください。画面全体の .field-normal を変更するより、対象項目と画面を絞って、隣の項目やフィルタへの影響を確認しやすくします。
調査サンプル――適用後のレイアウトを一覧する
対象の画面で、ブラウザの開発者ツールのConsoleに貼ります。DOMとComputedの値を読み、Shadow DOMのスクロール枠も存在する場合だけ調べます。
(function () {
const selectors = [
'#MainContainer', '#Application', '#Editor', '#MainForm',
'#EditorTabsContainer', '#EditorComments', '#Results_TitleField',
'#Results_TitleField > .field-label',
'#Results_TitleField > .field-control',
'#Results_TitleField .container-normal', '#Results_Title', '#Grid'
];
const elements = selectors.flatMap(function (selector) {
return Array.from(document.querySelectorAll(selector)).map(function (element) {
const style = getComputedStyle(element);
return {
selector, tag: element.tagName, id: element.id,
classes: element.className,
parent: element.parentElement?.id || element.parentElement?.tagName,
display: style.display, float: style.cssFloat, width: style.width,
marginLeft: style.marginLeft, clear: style.clear, order: style.order
};
});
});
console.table(elements);
console.table(Array.from(document.querySelectorAll('grid-container')).map(function (host) {
const frame = host.shadowRoot?.querySelector('.app-grid-frame');
return {
id: host.id, classes: host.className,
hasShadowRoot: Boolean(host.shadowRoot),
scrollTop: frame?.scrollTop, scrollLeft: frame?.scrollLeft,
overflow: frame ? getComputedStyle(frame).overflow : undefined
};
}));
})();一覧では編集画面用のセレクタに該当する行が出ません。これは、該当するDOMがない場合を区別するための動作です。ダイアログ編集は別のフォームを使うので、MainFormだけで判断せずフォームと送信バッファも確認します。
再描画で何が失われるか
自動ポストバックの返却では、値だけを変更する応答と、ReplaceAll でフィールドを丸ごと差し替える応答があります。差し替えられる要素へブラウザで直接付けたクラス、追加した説明、直接登録したイベントは、新しい要素には引き継がれません(フィールド応答)。
スタイルシートのID・クラスに対する指定は、新しい要素が同じセレクタに一致すれば再び適用されます。独自要素の作り直しはdispatchの実装サンプル、差し替え後も動くイベントはDOMイベントの接続を参照してください。
関連ページ
- テーマ改善前後のHTML・CSS構造を比較する — CSSレイヤー、配色、レイアウトの箱、項目内のDOMの比較図
- CSSをコントロールする要素
- context.AddResponseの用途別実装例
- スマートデザインの内部実装
- セクションの制御
- 画面カスタマイズ集・編集画面
- テーマ改善への備え