WEB 適用業務 *SFL 最上部見出しHTML (PYTHON.400/QHTMLSRC.SFLTOP)

最終更新: 2025-11-27
0001.00 <!DOCTYPE html>                                                                      
0002.00 <html lang="ja">                                                                     
0003.00   <head>                                                                             
0004.00     <meta charset="utf-8">                                                           
0005.00           <meta http-equiv="Content-Language" content="ja">                          
0006.00           <meta http-equiv="Content-Type" content="text/html; charset=utf-8">        
0007.00           <meta http-equiv="Pragma" content="no-cache">                              
0008.00           <meta http-equiv="cache-control" content="no-cache">                       
0009.00           <meta http-equiv="Expires" content="-1">                                   
0010.00           <meta http-equiv="X-UA-Compatible" content="IE=edge"/>                     
0011.00           <meta name="viewport" content="width=device-width, initial-scale=1.0"/>    
0012.00     <title>$TITLE</title>                                                      
0013.00     <style>                                                                          
0014.00       html {                                                                               
0015.00         font-family: "BIZ UPDGothic", Meiryo, sans-serif;                                  
0016.00         accent-color: rgb(26, 115, 232);                                                   
0017.00         color: #555;                                                                       
0018.00         width: 100%;                                                                       
0019.00         height: 100%;                                                                      
0020.00         margin: 0;                                                                         
0021.00         padding: 0;                                                                        
0022.00         border: 0;                                                                         
0023.00       }                                                                                    
0024.00       body {                            
0025.00         box-sizing: border-box;         
0026.00         width: 100%;                    
0027.00         height: 100%;                   
0028.00         margin: 0;                      
0029.00         border: 0;                      
0030.00         padding: 1em 4em;               
0031.00         display: flex;                  
0032.00         flex-direction: column;         
0033.00       }                                 
0034.00       form {                            
0035.00         display: contents;              
0036.00       }                                 
0037.00       header {                          
0038.00         flex: 0;                        
0039.00       }                                 
0040.00       main {                            
0041.00         flex: 1;                        
0042.00         min-height: 0;                  
0043.00         display: flex;                  
0044.00         flex-direction: column;         
0045.00         align-items: flex-start;        
0046.00       }                                 
0047.00       footer {                          
0048.00         flex: 0;                     
0049.00       }                              
0050.00       .content {                     
0051.00         flex: 1;                     
0052.00         min-height: 0;               
0053.00         max-width: calc(100%);       
0054.00         overflow: auto;              
0055.00       }                              
0056.00       input {                        
0057.00         font-family: inherit;        
0058.00         font-size: inherit;          
0059.00         color: inherit;              
0060.00       }                              
0061.00       input:read-only {              
0062.00         border-color: transparent;   
0063.00       }                              
0064.00       h1 {                           
0065.00         font-size: 2.4em;            
0066.00         font-weight: normal;         
0067.00         color: #8888ff;              
0068.00       }                              
0069.00       fieldset {                     
0070.00         border: 0;                   
0071.00         margin: 1em 0;               
0072.00       }                                               
0073.00       label {                                         
0074.00         display: flex;                                
0075.00         align-items: baseline;                        
0076.00         gap: 0 0.7em;                                 
0077.00       }                                               
0078.00       .name {                                         
0079.00         flex: none;                                   
0080.00         width: 8em;                                   
0081.00       }                                               
0082.00       #record-table thead tr th {                     
0083.00         color: #333;                                  
0084.00         font-weight: normal;                          
0085.00         text-align: left;                             
0086.00         border-bottom: 2px solid hsl(220, 90%, 56%);  
0087.00         position: sticky;                             
0088.00         top: 0;                                       
0089.00         background-color: white;                      
0090.00       }                                               
0091.00       #record-table {                                 
0092.00         border-collapse: separate;                    
0093.00       }                                               
0094.00       #record-table th {                              
0095.00         padding-bottom: 0.75em;                       
0096.00       }                                     
0097.00       .buttons {                            
0098.00         margin: 1.5em 0;                    
0099.00       }                                     
0100.00       button > img {                     
0101.00         width: 1.2em;                       
0102.00         height: 1.2em;                      
0103.00         object-fit: contain;                
0104.00         vertical-align: text-bottom;        
0105.00         margin-right: 0.3em;                
0106.00       }                                     
0107.00       [data-dspmod="DSPMOD"] .checkbox {    
0108.00         display: none;                      
0109.00       }                                     
0110.00       .dspmod {                             
0111.00         position: absolute;                 
0112.00         top: 1em;                           
0113.00         right: 3em;                         
0114.00         font-size: 2.5em;                   
0115.00         color: #8888ff;                     
0116.00       }                                     
0117.00     </style>                          
0118.00   </head>                             
0119.00   <body data-dspmod="{{dspmod}}">     
0120.00     <form id="record-form" action="/LISTDEF" method="post">                                                       
0121.00       <header>                                                                                                    
0122.00         <h1>&TITLE </h1>                                                                         
0123.00         <div class="buttons">                                                                                     
0124.00           <button type="submit" formaction="/end"><img src="/icon/END.SVG"> 終了 </button>            
0125.00 % if dspmod == "CHGPTN":                                                                                                
0126.00             <button type="submit" formaction="/update"><img src="/icon/UPDATE.SVG"> 更新 </button>    
0127.00             <button type="submit" formaction="/delete"><img src="/icon/DELETE.SVG"> 削除 </button>    
0128.00 % end                                                                                                                   
0129.00           <button type="button" class="default" onclick="location.href='/init';"><img src="/icon/BACK.SVG"> 
0130.00         </div>                                                                                                    
0131.00       </header>                                                                                                   
0132.00       <main>                                                                                                      
0133.00 % if dspmod == 'DSPPTN':                                                                                                
0134.00         <div class="dspmod"> 表示 </div>                                                                    
0135.00 % elif dspmod == 'CHGPTN':                                                                                              
0136.00         <div class="dspmod"> 変更 </div>                                                                    
0137.00 % else:                                                                                                                 
0138.00         <div class="dspmod"> 入力 </div>                                                                    
0139.00 % end                                                                                                                   
0140.00         <div class="content">                                                                                     
0141.00           <table id="record-table">                                                                               
0142.00             <thead>                                                                                               
0143.00               <tr>                                                                                                
0144.00                 <th class="checkbox">                                               
0145.00                 </th>                                                               
0146.00                 <th>                                                                
0147.00                   <span class="name">$COLHDG</span>                           
0148.00                 </th>                                                               
0149.00               </tr>                                                                 
0150.00             </thead>                                                                
0151.00             <tbody>                                                                 
0152.00             </tbody>                                                                
0153.00           </table>                                                                  
0154.00         </div>                                                                      
0155.00       </main>                                                                       
0156.00       <footer>                                                                      
0157.00       </footer>                                                                     
0158.00     </form>                                                                         
0159.00     <form name="key">                                                               
0160.00       <input type="hidden" name="$KEYFLD" value="{{values[0].get('$KEYFLD', '')}}"> 
0161.00     </form>                                                                         
0162.00     <template id="empty-record">                                                    
0163.00     </template>                                                                     
0164.00     <script>                                                                        
0165.00       "use strict";                                                                       
0166.00                                                                                           
0167.00       /**                                                                                 
0168.00        *  要素の一番下までスクロールされたかどうか監視する。    
0169.00        */                                                       
0170.00       class ScrollEndObserver {                                 
0171.00         #callback;                                              
0172.00         #threshold = 0;                                         
0173.00         #targets = new Map();                                   
0174.00         constructor(callback, options) {                        
0175.00           this.#callback = callback;                            
0176.00           const effective_options = {                           
0177.00             threshold: 0,                                       
0178.00             ...options,                                         
0179.00           };                                                    
0180.00           this.#threshold = effective_options.threshold;        
0181.00         }                                                       
0182.00         /**                                                     
0183.00          * container 要素の監視を開始する。                     
0184.00          */                                                     
0185.00         observe(container) {                                    
0186.00           if(!this.#targets.has(container)) {                   
0187.00             const onscroll = this.#onscroll.bind(this);         
0188.00             container.addEventListener("scroll", onscroll, {    
0189.00               passive: true,                                    
0190.00             });                                                 
0191.00             this.#targets.set(container, {                      
0192.00               onscroll,                                              
0193.00               scrollHeight: 0,                                       
0194.00               clientHeight: 0,                                       
0195.00             });                                                      
0196.00           }                                                          
0197.00         }                                                            
0198.00         /**                                                          
0199.00          * container 要素の監視を終了する。                          
0200.00          */                                                          
0201.00         unobserve(container) {                                       
0202.00           const entry = this.#targets.get(container);                
0203.00           if(entry) {                                                
0204.00             container.removeEventListener("scroll", entry.onscroll); 
0205.00             this.#targets.delete(container);                         
0206.00           }                                                          
0207.00         }                                                            
0208.00         /**                                                          
0209.00          *  全ての要素の監視を終了する。                             
0210.00          */                                                          
0211.00         disconnect() {                                               
0212.00           this.#targets.forEach((value, key) => {                 
0213.00             this.unobserve(key);                                     
0214.00           });                                                        
0215.00         }                                                            
0216.00         /**                                                                              
0217.00          * onscroll イベントのリスナー                                                   
0218.00          */                                                                              
0219.00         #onscroll(event) {                                                               
0220.00           const target = event.target;                                                   
0221.00           const entry = this.#targets.get(target);                                       
0222.00                                                                                          
0223.00           //  監視対象でない場合は終了。                                                 
0224.00           if(!entry)                                                                     
0225.00             return;                                                                      
0226.00                                                                                          
0227.00           const scrollHeight = target.scrollHeight;                                      
0228.00           const clientHeight = target.clientHeight;                                      
0229.00                                                                                          
0230.00           //  要素の大きさが以前と変わっていないなら終了。                               
0231.00           // ( 不必要な callback が発生するのを避けるため。 )                            
0232.00           if(scrollHeight === entry.scrollHeight && clientHeight === entry.clientHeight) 
0233.00             return;                                                                      
0234.00                                                                                          
0235.00           //  データの追加が必要なら callback する。                                     
0236.00           if(this.needsMoreContent(target)) {                                            
0237.00             entry.scrollHeight = scrollHeight;                                           
0238.00             entry.clientHeight = clientHeight;                                           
0239.00             this.#callback({                                                             
0240.00               target,                                                                                    
0241.00               scrollHeight,                                                                              
0242.00               clientHeight,                                                                              
0243.00             }, this);                                                                                    
0244.00           }                                                                                              
0245.00         }                                                                                                
0246.00         /**                                                                                              
0247.00          *  指定のコンテナにデータの追加が必要かどうか判定する。                                         
0248.00          */                                                                                              
0249.00         needsMoreContent(target) {                                                                       
0250.00           const scrollHeight = target.scrollHeight;                                                      
0251.00           const clientHeight = target.clientHeight;                                                      
0252.00           const scrollTop = target.scrollTop;                                                            
0253.00                                                                                                          
0254.00           return scrollTop >= scrollHeight - clientHeight - (clientHeight * (1.0 - this.#threshold)); 
0255.00         }                                                                                                
0256.00       }                                                                                                  
0257.00                                                                                                          
0258.00       /**                                                                                                
0259.00        *  この画面特有の処理                                                                             
0260.00        */                                                                                                
0261.00       class App {                                                                                        
0262.00         #endOfData = false;  //  最後のページまでデータを読み込んだ。                                    
0263.00         #scrollEndObserver;                                                                              
0264.00         #container = document.querySelector(".content");                            
0265.00         #tbody = document.querySelector("#record-table tbody");                     
0266.00         #recordForm = document.getElementById("record-form");                       
0267.00         #emptyRecord = document.getElementById("empty-record");                     
0268.00                                                                                     
0269.00         constructor() {                                                             
0270.00           this.#scrollEndObserver = new ScrollEndObserver((entry, observer) => { 
0271.00             this.readPage();                                                        
0272.00           });                                                                       
0273.00         }                                                                           
0274.00                                                                                     
0275.00         /**                                                                         
0276.00          *  ページが読み込まれたときに最初に実行される処理。                        
0277.00          */                                                                         
0278.00         async start() {                                                             
0279.00           //  ユーザーが変更したレコードに印を付ける。                              
0280.00           this.#tbody.addEventListener("input", event => {                       
0281.00             const tr = event.target.closest("tr");                                  
0282.00             if(tr)                                                                  
0283.00               tr.setAttribute("data-modified", "");                                 
0284.00           });                                                                       
0285.00                                                                                     
0286.00           //  ユーザーが変更したレコードのみを submit する。                        
0287.00           this.#recordForm.addEventListener("submit", event => {                 
0288.00             //  変更されていないレコードに一時的に  disabled をつける。                                       
0289.00             this.#tbody.querySelectorAll("tr:not([data-modified])").forEach(tr => {                        
0290.00               tr.querySelectorAll("input:enabled, select:enabled, textarea:enabled").forEach(element => {  
0291.00                 element.setAttribute("data-disabled-temporarily", "");                                        
0292.00                 element.disabled = true;                                                                      
0293.00               });                                                                                             
0294.00             });                                                                                               
0295.00             // submit 後に元に戻す。                                                                          
0296.00             setTimeout(() => {                                                                             
0297.00               this.#tbody.querySelectorAll("[data-disabled-temporarily").forEach(element => {              
0298.00                 element.disabled = false;                                                                     
0299.00                 element.removeAttribute("data-disabled-temporarily");                                         
0300.00               });                                                                                             
0301.00             }, 0);                                                                                            
0302.00           });                                                                                                 
0303.00           //  最初から表示されている部分を読み取る。                                                          
0304.00           await this.readPage();                                                                              
0305.00           //  スクロールの監視を開始する。                                                                    
0306.00           this.#scrollEndObserver.observe(this.#container);                                                   
0307.00                                                                                                               
0308.00           // CHGPTN によって実行されたなら新規入力用の空行を表示する。                                        
0309.00           if(document.body.matches("[data-dspmod='CHGPTN']")) {                                               
0310.00             const text = await this.fetchText("/template", {                                                  
0311.00               method: "GET",                                                                                  
0312.00               cache: "no-cache",                                                                    
0313.00             });                                                                                     
0314.00             this.#emptyRecord.innerHTML = text;                                                     
0315.00             this.appendEmptyRecord();                                                               
0316.00             //  最後の空行に入力されたら新しい空行を追加する。                                      
0317.00             this.#tbody.addEventListener("change", event => {                                    
0318.00               const targetTr = event.target.closest("tr");                                          
0319.00               if(targetTr) {                                                                        
0320.00                 const dspmods = this.#tbody.querySelectorAll("input[name='_DSPMOD']:last-of-type"); 
0321.00                 if(dspmods.length > 0) {                                                         
0322.00                   const previous = dspmods[dspmods.length - 1];                                     
0323.00                   const tr = previous.closest("tr");                                                
0324.00                   if(targetTr === tr) {                                                             
0325.00                     this.appendEmptyRecord();                                                       
0326.00                   }                                                                                 
0327.00                 }                                                                                   
0328.00               }                                                                                     
0329.00             });                                                                                     
0330.00           }                                                                                         
0331.00         }                                                                                           
0332.00         /**                                                                                         
0333.00          *  入力用の空行を一行追加する。                                                            
0334.00          */                                                                                         
0335.00         appendEmptyRecord() {                                                                       
0336.00           const emptyRecord = this.#emptyRecord.content.cloneNode(true);                                  
0337.00           const dspmods = this.#tbody.querySelectorAll("input[name='_DSPMOD']:last-of-type");             
0338.00           if(dspmods.length > 0) {                                                                     
0339.00             const previous = dspmods[dspmods.length - 1];                                                 
0340.00             const tr = previous.closest("tr");                                                            
0341.00             tr.after(emptyRecord);                                                                        
0342.00           } else                                                                                          
0343.00             this.#tbody.prepend(emptyRecord);                                                             
0344.00         }                                                                                                 
0345.00         /**                                                                                               
0346.00          *  非同期通信でテキストを受信する。                                                              
0347.00          */                                                                                               
0348.00         async fetchText(...args) {                                                                        
0349.00           let response;                                                                                   
0350.00           try {                                                                                           
0351.00             response = await fetch(...args);                                                              
0352.00           } catch(error) {                                                                                
0353.00             throw new Error(` 通信エラーが発生しました。 : nn${error}`);                                
0354.00           }                                                                                               
0355.00           if(!response.ok) {                                                                              
0356.00             throw new Error(` サーバーがエラーを返しました : ${response.status} ${response.statusText}`); 
0357.00           }                                                                                               
0358.00           try {                                                                                           
0359.00             return await response.text();                                                                 
0360.00           } catch(error) {                                                           
0361.00             throw new Error(` サーバーからの応答を解釈出来ません : nn${error}`);   
0362.00           }                                                                          
0363.00         }                                                                            
0364.00         /**                                                                          
0365.00          *  指定のレコード番号から始まるデータをサーバーに問い合わせる。             
0366.00          */                                                                          
0367.00         async fetchNext(index) {                                                     
0368.00           const formData = new FormData(document.querySelector("form[name='key']")); 
0369.00           formData.append("_RRN", index)                                             
0370.00           return await this.fetchText("/record", {                                   
0371.00             method: "POST",                                                          
0372.00             body: formData,                                                          
0373.00             cache: "no-store",                                                       
0374.00           });                                                                        
0375.00         }                                                                            
0376.00                                                                                      
0377.00         /**                                                                          
0378.00          *  サーバーから一画面分受け取って画面に表示する。                           
0379.00          */                                                                          
0380.00         async readPage() {                                                           
0381.00           if(this.#endOfData)                                                        
0382.00             return;                                                                  
0383.00                                                                                      
0384.00           const tbody = document.querySelector("tbody");                      
0385.00           do {                                                                
0386.00             const index = tbody.querySelectorAll("tr").length;                
0387.00             let text;                                                         
0388.00             try {                                                             
0389.00               text = await this.fetchNext(index);                             
0390.00             } catch(error) {                                                  
0391.00               alert(error);                                                   
0392.00               break;                                                          
0393.00             }                                                                 
0394.00             if(text.length === 0) {                                           
0395.00               this.#endOfData = true;                                         
0396.00               this.#scrollEndObserver.disconnect();                           
0397.00               break;                                                          
0398.00             }                                                                 
0399.00             tbody.innerHTML += text;                                          
0400.00           } while(this.#scrollEndObserver.needsMoreContent(this.#container)); 
0401.00         }                                                                     
0402.00       }                                                                       
0403.00                                                                               
0404.00       const app = new App();                                                  
0405.00       app.start();                                                            
0406.00     </script>                                                           
0407.00   </body>                                                               
0408.00 </html> 

[解説]

SFLTOP は一覧表形式の最上部として表示されるHTMLテンプケートです。

012.00     <title>$TITLE</title> 

=> $TITLE にはPYTHON適用業務のテキストが埋め込まれます。

0146.00                 <th>                                       
0147.00                   <span class="name">$COLHDG</span>  
0148.00                 </th>                                      

=> $COLHDG には一覧表として表示するフィールドの欄見出しが並べて
置換えられます。

0159.00     <form name="key">                                                                
0160.00       <input type="hidden" name="$KEYFLD" value="{{values[0].get('$KEYFLD', '')}}">  
0161.00     </form>                                                                          

=>&KEYFLDには表示を開始するキー・フィールドの値が表示されます。