分享和讨论Javascript相关的话题
回复
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

將該程式改成js,同時將其對象化:
一,整個Class設為Ejcees,將所有變量和方法都用this.來歸放。

二,style,svg.ejceespb都是動態加入。

三,當頁面上存在div.ejcees[mode=ejceesmain]時,說明它是單頁面,整個body只存在這個元素以及這個js的引用。則表現跟現在完全一樣。

四,當頁面上存在div.ejcees[mode=ejceessingle]時,說明它將插入到頁面上的一個主要的地方,比如文章正文。則它的css最大寬度的計算應跟它所插入的地方的父元素一致。div.ejcees[mode=ejceessingle]在一個頁面上只能存在一個,能使用鍵盤上的快捷鍵。

五,當頁面上存在一個或多個div.ejcees[mode=ejceesmini]時,說明將有ejceesmini插入到頁面上的任意地方,它的css最大寬度的計算應跟它所插入的地方的父元素一致。其css以及顯示元素要經過較大調整,ejceestool和ejceestext將不再顯示。ejceesstep的最左邊將放置ejceesoutput,固定其長寬,並且只顯示當前步數和總步數(如20/80)。ejceesstep的最右邊將顯示btn-autoplay按鈕。當有分支時,它將直接絕對定位背景半透明顯示在ejceessvg的左上角,當有評論時,它將直接絕對定位背景半透明顯示在ejceessvg的最底部。div.ejcees[mode=ejceesmini]將不能使用鍵盤上的快捷鍵。

先給出工作流程,製作方法以及注意事項。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

gemini pro說任務很繁重,你以為我不會交給你來做?我只是分階段交:

代码: 全选

這是一個非常龐大且經典的前端重構任務。將面條式(Spaghetti code)的腳本轉換為物件導向(OOP),並且要支援多實例(Multi-instance)與不同的響應式佈局模式,需要非常嚴謹的架構規劃。

以下為您規劃的**工作流程**、**製作方法**以及**核心注意事項**,並附上重構的**程式碼骨架**。

---

### 一、 工作流程 (Workflow)

1.  **環境與基礎類別建立:**
    * 定義 `class Ejcees`。
    * 在建構子(`constructor`)中接收 DOM 元素和配置參數。
    * 設定靜態方法掃描頁面上的 `div.ejcees` 並自動實例化。
2.  **動態資源注入 (CSS & SVG):**
    * 將原本的 CSS 寫成字串,並在第一個實例創建時,動態插入到 `<head>` 中(全域只需插入一次)。
    * 將 SVG 結構與 UI 結構寫成模板字串,根據不同的 `mode` 動態生成並插入到該實例的容器中。
3.  **狀態與變數封裝:**
    * 將原本全域的 `let` 變數(如 `currentTurn`, `historyFEN`, `isEditMode` 等)全部轉換為實例屬性(`this.currentTurn`, `this.historyFEN` 等)。
4.  **DOM 選擇器作用域限制:**
    * 將所有 `document.getElementById` 或 `document.querySelector` 替換為 `this.container.querySelector`,確保多實例情況下不會互相干擾。
5.  **事件綁定與 `this` 指向修正:**
    * 所有事件監聽器(Event Listeners)必須使用箭頭函數 `() => {}` 或 `.bind(this)`,否則 `this` 會指向 DOM 元素而非 `Ejcees` 實例。
6.  **模式特異化開發 (Mode Specifics):**
    * **ejceesmain:** 預設模式,滿版顯示。
    * **ejceessingle:** 插入文章流中,寬度依賴父元素,需掛載鍵盤快捷鍵(全域唯一)。
    * **ejceesmini:** 極簡模式,移除工具列與紀錄區,重構底部控制條,並將分支與評論改為絕對定位的懸浮層(Overlay)。禁止綁定鍵盤事件。
7.  **測試與除錯:**
    * 先測試單一實例(`ejceesmain`),確保基礎功能(走子、悔棋、動畫)正常。
    * 再測試多實例(多個 `ejceesmini` 同時存在),檢查變數是否污染、SVG 渲染是否衝突。

---

### 二、 製作方法與架構設計

#### 1. 初始化與入口
在檔案底部加入一段自動掃描腳本。當 DOM 載入完成後,尋找所有帶有 `class="ejcees"` 的元素,讀取其 `mode` 屬性,並 `new Ejcees(element, mode)`。

#### 2. CSS 隔離與動態注入
原本的 CSS 是針對全螢幕設計的(`100vw`, `100vh`)。重構時:
* `.ejceesmain` 保持使用 `100vw` / `100vh`。
* `.ejceessingle` 與 `.ejceesmini` 應設定 `width: 100%; height: auto; aspect-ratio: ...` 或依賴父容器。

#### 3. SVG ID 衝突解決方案
這是多實例最容易出錯的地方!原本的 SVG 定義了 `<defs>`,裡面有 `#tile0`, `#tile1` 等。
如果頁面上有三個 `ejceesmini`,就會有三個 `<g id="tile0">`。這在 HTML 標準中是不合法的,且會導致渲染錯誤。
**解法:** 在實例化時,生成一個唯一的 `instanceId`(例如 `ejcees-1234`),將所有的 ID 與 `href` 引用加上後綴(如 `id="tile0-ejcees-1234"`, `href="#tile0-ejcees-1234"`)。

#### 4. UI 渲染邏輯重構 (`ejceesmini` 專屬)
在 `renderUI` 階段判斷:
* 如果是 `mini`,隱藏或不生成 `.ejceestool` 和 `.ejceestext`。
* 修改 `.ejceesstep` 結構:左側放入 `.ejceesoutput`,中間進度條,右側放入 AutoPlay 按鈕。
* 將原本顯示在側邊欄的評論(`this.historyFEN.c`)改寫為監聽步驟變更時,若有評論,則顯示在 `.ejceessvg` 內的絕對定位 `div` 中。

---

### 三、 核心注意事項 (Gotchas)

* **不要再用 ID 綁定 DOM:** 原本的 `id="tool-new"` 在多實例會崩潰。請全部改用 `class="tool-btn tool-new"` 或 `data-action="new"` 來選取。
* **鍵盤事件防護:** `keydown` 事件是綁定在 `document` 上的。
    * 如果頁面上有一個 `ejceessingle` 和三個 `ejceesmini`,只有 `ejceessingle` 應該對按鍵做出反應。
    * 在 `keydown` 處理函數開頭加入判斷:`if(this.mode === 'ejceesmini') return;`
* **動畫效能:** 如果頁面上有 10 個 `ejceesmini` 實例,確保不在畫面內(Viewport)的實例不要自動播放動畫(可使用 `IntersectionObserver` 進行優化,不過這算進階功能,可先放著)。
* **拖曳/點擊座標計算:** 原本的 `pt.matrixTransform(svgElement.getScreenCTM().inverse())` 非常依賴 SVG 的渲染尺寸。當容器大小隨父元素縮放時(`single` / `mini`),這段程式碼依然有效,但必須確保 SVG 的 `viewBox` 設定正確,且不要用寫死的 PX 去推算滑鼠位置。

---

### 四、 程式碼骨架 (Code Skeleton)

您可以依照這個架構,將原本的邏輯填入對應的方法中。

```javascript
class Ejcees {
    // 靜態屬性:確保全域 CSS 只注入一次
    static cssInjected = false;
    // 靜態屬性:實例計數器,用於生成唯一 ID
    static instanceCount = 0;

    constructor(container, mode) {
        this.container = container;
        this.mode = mode || 'ejceesmain'; // 'ejceesmain' | 'ejceessingle' | 'ejceesmini'
        this.instanceId = `ejcees-inst-${Ejcees.instanceCount++}`;

        // === 1. 核心狀態變數 (從原本的 let 搬過來) ===
        this.INITIAL_FEN = 'rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1';
        this.currentTurn = 'w';
        this.halfMoveClock = 0;
        this.fullMoveNumber = 1;
        this.historyFEN = { fen: this.INITIAL_FEN, move: null, lastMove: null, v: [] };
        this.currentBranch = [];
        this.currentStepIndex = 0;
        
        // 棋盤映射
        this.tileMap = new Map();
        this.piecePos = new Map();
        
        // UI 狀態
        this.isEditMode = false;
        this.isAutoPlaying = false;
        // ... 其他原本的 let 變數 ...

        // === 2. 初始化 ===
        this.injectCSS();
        this.renderDOM();
        this.cacheDOM();
        this.bindEvents();
        this.initGame();
    }

    // --- 方法:全域 CSS 注入 ---
    injectCSS() {
        if (Ejcees.cssInjected) return;
        const style = document.createElement('style');
        style.innerHTML = `
            /* 這裡放入原本的 CSS,並根據 mode 加上前綴限制作用域 */
            .ejcees-container[data-mode="ejceesmain"] { width: 100vw; height: 100vh; display: flex; }
            .ejcees-container[data-mode="ejceessingle"] { width: 100%; max-width: 100%; display: flex; }
            .ejcees-container[data-mode="ejceesmini"] { width: 100%; position: relative; display: flex; flex-direction: column; }
            
            /* Mini 模式專屬 Overlay */
            .ejcees-overlay-comment { position: absolute; bottom: 0; left: 0; width: 100%; background: rgba(0,0,0,0.7); color: #fff; padding: 10px; display: none; }
            .ejcees-overlay-branch { position: absolute; top: 0; left: 0; background: rgba(255,255,255,0.8); display: none; }
            /* ... 其餘原本的 CSS ... */
        `;
        document.head.appendChild(style);
        Ejcees.cssInjected = true;
    }

    // --- 方法:動態生成 DOM 結構 ---
    renderDOM() {
        this.container.classList.add('ejcees-container');
        this.container.setAttribute('data-mode', this.mode);

        // 動態替換 SVG 內的 ID,避免多實例衝突
        let svgDefString = `... 這裡放原本 SVG <defs> 的內容,但所有的 id="tile0" 要改成 id="tile0-${this.instanceId}" ...`;

        let htmlString = '';
        if (this.mode === 'ejceesmain' || this.mode === 'ejceessingle') {
            htmlString = `
                <div class="ejceesmain">
                    <div class="ejceessvg-wrapper">
                        </div>
                    <div class="ejceesctrl">
                        <div class="ejceesoutput">...</div>
                        </div>
                </div>
            `;
        } else if (this.mode === 'ejceesmini') {
            htmlString = `
                <div class="ejceesmini-wrapper" style="position: relative;">
                    <div class="ejcees-overlay-branch"></div>
                    <div class="ejcees-overlay-comment"></div>
                </div>
                <div class="ejceesstep mini-step-bar">
                    <div class="ejceesoutput mini-output">0/0</div>
                    <input type="range" class="ejceesstepdrop" min="0" max="0" value="0" />
                    <div class="btn-autoplay">Play</div>
                </div>
            `;
        }
        
        this.container.innerHTML = htmlString;
    }

    // --- 方法:快取 DOM 元素 ---
    cacheDOM() {
        this.svgElement = this.container.querySelector('.ejceespb');
        this.outputElement = this.container.querySelector('.ejceesoutput');
        this.stepSlider = this.container.querySelector('.ejceesstepdrop');
        // ... 取代原本的 document.querySelector ...
    }

    // --- 方法:綁定事件 ---
    bindEvents() {
        // SVG 點擊事件 (注意 bind(this) 或是用箭頭函數)
        if (this.svgElement) {
            this.svgElement.addEventListener('click', (e) => this.handleSvgClick(e));
        }

        // 滑桿事件
        if (this.stepSlider) {
            this.stepSlider.addEventListener('input', (e) => this.handleSlider(e));
        }

        // 全域鍵盤事件 (只有 Single 或 Main 模式掛載)
        if (this.mode !== 'ejceesmini') {
            document.addEventListener('keydown', (e) => this.handleKeydown(e));
        }
    }

    // --- 方法:核心邏輯區 ---
    initGame() {
        this.loadFEN(this.historyFEN.fen);
        this.renderRecordUI();
    }

    loadFEN(fenStr) {
        // 實作原本的 loadFEN,注意裡面創建 <use> 時,href 要加上 this.instanceId
    }

    handleSvgClick(event) {
        // 實作原本的點擊走子邏輯
    }

    handleKeydown(e) {
        // 實作快捷鍵
    }

    // --- 方法:UI 更新 ---
    renderNoteUI() {
        // 針對不同模式處理評論顯示
        const node = this.getNodeAtStep(this.currentStepIndex);
        if (!node) return;

        if (this.mode === 'ejceesmini') {
            const commentOverlay = this.container.querySelector('.ejcees-overlay-comment');
            if (node.c && node.c.trim() !== '') {
                commentOverlay.textContent = node.c;
                commentOverlay.style.display = 'block';
            } else {
                commentOverlay.style.display = 'none';
            }
        } else {
            // 原本 Main / Single 的右下角 UI 更新邏輯
        }
    }

    // ... 其餘幾十個方法 (boardToFEN, isValidMove, etc.) 全部包進來 ...
}

// === 自動初始化腳本 ===
document.addEventListener('DOMContentLoaded', () => {
    // 尋找所有帶有 class="ejcees" 的容器
    const containers = document.querySelectorAll('.ejcees');
    containers.forEach(container => {
        const mode = container.getAttribute('mode');
        // 實例化該組件
        new Ejcees(container, mode);
    });
});
```

根據這個骨架,您就可以開始將那 2000 多行的函數與物件逐步搬移進 `class Ejcees` 裡面。這是一個需要耐心的過程,強烈建議**先從 DOM 的綁定與 SVG 的隔離著手**,確保在一個頁面放兩個 `div.ejcees` 時,它們的版面和 SVG 渲染不會互相干擾,再來搬移核心的走棋邏輯。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

寫出constructor(container, mode) {}內的所有內容,如果太長可以省略表示。

代码: 全选

這是在類別化重構中最重要的部分。在 `constructor` 中,我們需要將所有原本在全域範圍的變數轉化為**實例屬性**,並且處理 **SVG ID 唯一化**以防止多實例衝突。

以下是 `Ejcees` 類別的 `constructor` 實作:

```javascript
class Ejcees {
    constructor(container, mode) {
        // 1. 基本屬性與模式設定
        this.container = container;
        this.mode = mode || 'ejceesmain'; // 'ejceesmain', 'ejceessingle', 'ejceesmini'
        // 生成唯一 ID,用於區分多個實例的 SVG Defs 引用
        this.instanceId = 'ej_' + Math.random().toString(36).substr(2, 9);

        // 2. 棋局核心狀態 (原本的全域 let 變數)
        this.INITIAL_FEN = 'rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1';
        this.currentTurn = 'w';
        this.halfMoveClock = 0;
        this.fullMoveNumber = 1;
        
        // 歷史與分支數據
        this.historyFEN = { 
            fen: this.INITIAL_FEN, 
            move: null, 
            lastMove: null, 
            c: '', // 評論
            v: []  // 分支變體
        };
        this.currentBranch = [];
        this.currentStepIndex = 0;
        this.undoStack = [];

        // 棋盤邏輯映射
        this.tileMap = new Map();  // 座標 -> 元素
        this.piecePos = new Map(); // 元素 -> 座標
        
        // 3. UI 與 動畫狀態
        this.selectedTile = null;
        this.isEditMode = false;
        this.isAutoPlaying = false;
        this.moveInterval = 1000;
        this.moveSpeed = 300;
        this.isRotateEnabled = false;
        this.isFlipEnabled = false;
        this.pgnMetadata = { Event: 'Casual Game', Site: 'Local', Date: '', White: '', Black: '', Result: '*' };

        // 4. 動態資源初始化
        this.injectGlobalStyles(); // 靜態方法,全域只注入一次 CSS
        this.renderLayout();       // 根據 mode 生成 HTML 結構
        this.initSvgDefinitions(); // 初始化並唯一化 SVG <defs>

        // 5. DOM 元素快取 (從 container 內尋找,不可使用 document.getElementById)
        this.svgElement = this.container.querySelector('.ejceespb');
        this.boardGroup = this.container.querySelector('.board-group');
        this.piecesGroup = this.container.querySelector('.pieces-group');
        this.marksGroup = this.container.querySelector('.marks-group');
        this.stepSlider = this.container.querySelector('.ejceesstepdrop');
        this.outputElement = this.container.querySelector('.ejceesoutput');
        this.recordContainer = this.container.querySelector('.ejceestext-content');
        this.commentArea = this.container.querySelector('.ejceestool-comment'); // 或 mini 模式的 overlay

        // 6. 座標系統轉換工具 (SVG Point)
        this.svgPoint = this.svgElement.createSVGPoint();

        // 7. 綁定事件監聽
        this.bindEvents();

        // 8. 載入初始棋局
        this.loadFEN(this.INITIAL_FEN);
        this.updateUI();
    }

    // --- 以下為配套的私有初始化方法簡述 ---

    renderLayout() {
        // 這裡判斷 this.mode
        // 若為 ejceesmini:
        //   - 隱藏 .ejceestool 和 .ejceestext
        //   - 在 .ejceesstep 左邊插入 .ejceesoutput (20/80 格式)
        //   - 在右邊插入 Play 按鈕
        //   - 插入懸浮的 Branch 和 Comment div
        
        const layout = this.mode === 'ejceesmini' 
            ? this.getMiniTemplate() 
            : this.getMainTemplate();
        this.container.innerHTML = layout;
    }

    initSvgDefinitions() {
        // 重點:將原本 SVG 的 <use href="#red_k"> 改成 <use href="#red_k_${this.instanceId}">
        // 並且將 <g id="red_k"> 改成 <g id="red_k_${this.instanceId}">
        // 這樣頁面上同時存在多個棋盤時,圖形才不會互相抓錯。
    }

    bindEvents() {
        // 點擊棋盤
        this.svgElement.addEventListener('click', (e) => this.handleBoardClick(e));
        
        // 滑桿與按鈕
        if(this.stepSlider) {
            this.stepSlider.addEventListener('input', (e) => this.jumpToStep(parseInt(e.target.value)));
        }

        // 快捷鍵:僅在非 mini 模式下掛載於 document
        if (this.mode !== 'ejceesmini') {
            document.addEventListener('keydown', this.handleGlobalKeydown.bind(this));
        }
    }
}
```

### 注意事項提醒:

1.  **ID 唯一化**:在 `constructor` 產生的 `this.instanceId` 必須應用到所有內部 HTML/SVG 的 `id` 屬性上。否則,如果你在同一個頁面放兩個 `ejceesmini`,第一個棋盤的棋子可能會「飛」到第二個棋盤上(因為 CSS 或 SVG 引用了重複的 ID)。
2.  **鍵盤事件 (Mode 四 vs 五)**:`ejceesmini` 明確規定不能使用快捷鍵。所以在 `bindEvents` 中我加了判斷。如果是 `ejceessingle`,雖然可以使用快捷鍵,但要注意如果頁面上有多個 instance(雖你說 single 只有一個),通常會建議檢查 `document.activeElement` 是否在該容器內。
3.  **this 綁定**:所有事件回調(Callbacks)建議使用箭頭函數 `(e) => this.method(e)`,這能確保函數內部的 `this` 永遠指向 `Ejcees` 的實例物件。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

js如何將svg文本放進<object>中,並操作這些svg元素?

代码: 全选

這是一個非常經典的需求。雖然 `<object>` 標籤通常用於加載外部檔案,但如果你手頭上的是 **SVG 字符串(文本)**,直接放入 `<object>` 會稍微繞一點路,因為 `<object>` 的 `data` 屬性預期的是一個 URL。

要實現這個目標,最優雅的方式是利用 **Blob** 或 **Data URI**。

---

## 💡 核心流程

1.  **轉換**:將 SVG 字符串轉換為 `Blob` 對象。
2.  **生成 URL**:使用 `URL.createObjectURL()` 生成一個臨時網址。
3.  **掛載**:將該網址賦值給 `<object>` 的 `data` 屬性。
4.  **操作**:在 `load` 事件觸發後,透過 `contentDocument` 存取內部元素。

---

## 🛠️ 實作程式碼

### 1. HTML 結構
```html
<div id="container">
  <object id="mySvgObject" type="image/svg+xml" width="300" height="300"></object>
</div>
```

### 2. JavaScript 邏輯
```javascript
const svgString = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <circle id="myCircle" cx="50" cy="50" r="40" fill="royalblue" />
</svg>`;

const objElement = document.getElementById('mySvgObject');

// --- 第一步:將字符串轉為 URL ---
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);

// --- 第二步:加載 SVG ---
objElement.data = url;

// --- 第三步:操作內部的 DOM ---
objElement.addEventListener('load', () => {
    // 必須在 load 事件後才能獲取 contentDocument
    const svgDoc = objElement.contentDocument;
    
    // 像操作普通 DOM 一樣操作它
    const circle = svgDoc.getElementById('myCircle');
    
    if (circle) {
        circle.setAttribute('fill', 'orange');
        
        circle.addEventListener('click', () => {
            alert('你點擊了 SVG 內部的圓形!');
        });
    }

    // 記得釋放記憶體(如果不再需要動態更換)
    // URL.revokeObjectURL(url);
});
```

---

## ⚠️ 關鍵注意事項

### 1. 同源策略 (CORS)
雖然你是用 `Blob` 生成的 URL,通常不會有跨域問題,但如果你是加載 **遠端伺服器** 的 SVG 檔案到 `<object>` 中,瀏覽器會限制你存取 `contentDocument`。

### 2. 為什麼不直接用 `innerHTML`?
如果你只是想操作 SVG,將 SVG 字符串直接放入一個 `<div>` 的 `innerHTML` 是最簡單的:
* **優點**:直接渲染,操作方便,不需考慮異步加載。
* **缺點**:SVG 的樣式可能會受全域 CSS 影響(有時這反而是優點)。

> **如果你選擇使用 `<object>`**,通常是因為你希望將 SVG 的作用域「隔離」開來,避免 CSS 或 ID 衝突。

### 3. 操作時機
**這是最常出錯的地方:** 你不能在設置 `objElement.data` 後立即操作元素。因為 `<object>` 加載資源是異步的,必須監聽 `load` 事件,否則 `contentDocument` 將會是 `null`。

---

## 🔍 比較表

| 方法 | 操作複雜度 | 樣式隔離 | 適用場景 |
| :--- | :--- | :--- | :--- |
| **Object + Blob** | 中(需處理異步) | **高**(完全獨立) | 需要封裝感、避免樣式污染。 |
| **innerHTML** | 低(同步操作) | **低**(受全域影響) | 快速開發、需要與頁面 CSS 互動。 |
| **Iframe** | 中 | **極高** | 幾乎不建議僅為了 SVG 使用。 |

你目前的專案需求,是否必須限制在 `<object>` 容器內呢?如果沒有特殊隔離需求,直接把 SVG 文本注入到 `div` 會讓開發輕鬆許多。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

對象化進行中……
附件
ejceesobj20260426.7z
(54.88 KiB) 已下载 44 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

當執行 deleteCurrentMoveAndAfter時,為何會報錯this.getNodeAtStep is not a function
附件
ejceesobj.js
(231.75 KiB) 已下载 43 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

基本搞完對象化,接下來就是ejceesmain、ejceessingle、ejceesmini了。
附件
ejceesobj20260427.js
(231.77 KiB) 已下载 39 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

改進:
1.把所有html元素中的id都改放至class(如把class="tool-btn" id="tool-new"改成class="tool-btn tool-new"),同時把相應的css和js調用一並改掉。不包括「this.svgOutHTML」,只包括「this.container.innerHTML = `...`」內的元素。

2.當mode變化時,將css作相應改變:

當mode===ejceesmain時,跟現在一樣。

當mode===ejceessingle時,去掉body,html。「* {box-sizing: border-box;margin: 0;padding: 0;}」應只用於「.ejcees, .ejcees *」。.ejcees的寬度不能再是100vw,而跟父元素一樣寬。

當mode===ejceesmini時,跟前面一樣,但是.ejcees的高度應為所有元素的自然高度。

3.當mode===ejceesmini時,svg要用以下方式加入:

const blob = new Blob([this.svgOutHTML], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
objElement.data = url;
objElement.addEventListener('load', () => {
this.svgElement = objElement.contentDocument;
}

「div.ejceessvg」 清空後appendChild(objElement)。

4.當mode===ejceesmini時,其顯示元素要經過較大調整,ejceestool和ejceestext將不再顯示。ejceesstep的最左邊將放置ejceesoutput,固定其長寬為36*36,並且只顯示當前步數和總步數(如20/80)。ejceesstep的最右邊將顯示btn-autoplay按鈕。當有分支時,它將直接絕對定位背景半透明顯示在ejceessvg的左上角,點擊有下拉菜單,可選擇切換分支。當有評論時,它將直接絕對定位背景半透明顯示在ejceessvg的最底部。將不能使用鍵盤快捷鍵。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
附件
ejceesobj202604270913.js
(269.7 KiB) 已下载 41 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

gemini pro,前兩點:

代码: 全选

Here are the modifications required to achieve your goals. All relevant HTML elements inside `this.container.innerHTML` have been updated to use classes instead of IDs, and the CSS generation has been made dynamic based on the `mode` parameter. 

### 1. Update Constructor
Store the `mode` parameter so it can be accessed in `injectCSS`.

```javascript
  constructor(container, mode) {
    this.container = container;
    this.mode = mode; // Add this line
    this.INITIAL_FEN = 'rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1';
    // ... (Keep the rest of the constructor unchanged)
```

### 2. Modify `injectCSS()`
Update the CSS injection to conditionally render styles based on `this.mode`.

```javascript
  injectCSS() {
    if (Ejcees.cssInjected) return;
    const style = document.createElement('style');

    let resetAndContainerCSS = `
/* Base Reset */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body,
html {
  width: 100%;
  height: 100%;
  font-family: sans-serif;
}

/* Main Container */
.ejcees {
  display: flex;
  width: 100vw;
  height: 100vh;
  background-color: #f4f4f9;
  overflow: hidden;
}
    `;

    if (this.mode === 'ejceessingle' || this.mode === 'ejceesmini') {
      resetAndContainerCSS = `
/* Scoped Reset */
.ejcees, .ejcees * {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* Main Container */
.ejcees {
  display: flex;
  width: 100%;
  height: ${this.mode === 'ejceesmini' ? 'auto' : '100vh'};
  background-color: #f4f4f9;
  overflow: hidden;
  font-family: sans-serif;
}
      `;
    }

    style.innerHTML = `
${resetAndContainerCSS}

.ejceessvg {
/* ... (Keep all the remaining CSS exactly the same from here downwards) ... */
    `;
    // Note: Append the rest of the original CSS to style.innerHTML here
    document.head.appendChild(style);
    Ejcees.cssInjected = true;
  }
```

### 3. Modify `renderDOM()`
Change IDs to classes inside `this.container.innerHTML`.

```javascript
  renderDOM() {
    this.svgOutHTML = `...`; // (Keep svgOutHTML exactly as it is)

    this.container.innerHTML = `
<div class="ejceessvg">
  ${this.svgOutHTML}
</div>
<div class="ejceesctrl">
  <div class="ejceesoutput">Click on the SVG area to get coordinates.</div>
  <div class="ejceesstep">
    <div class="ejceesstepminus">-</div>
    <input type="range" class="ejceesstepdrop" min="0" max="0" value="0" />
    <div class="ejceesstepplus">+</div>
  </div>

  <div class="ejceestool">
    <div class="tool-btn tool-new" title="New">
      <svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /></svg>
    </div>
    <div class="tool-btn tool-open" title="Open">
      <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M4 5a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v2H4V5zm0 4h16v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9z" /></svg>
    </div>
    <div class="tool-btn tool-edit" title="Edit">
      <svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" /></svg>
    </div>
    <div class="tool-btn tool-save" title="Save">
      <svg viewBox="0 0 24 24"><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z" /></svg>
    </div>
    <div class="tool-btn tool-undo" title="Undo">
      <svg viewBox="0 0 24 24"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z" /></svg>
    </div>
    <div class="tool-btn tool-redo" title="Redo">
      <svg viewBox="0 0 24 24"><path d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z" /></svg>
    </div>
    <div class="tool-btn tool-rotate" title="Rotate">
      <svg viewBox="0 0 24 24">
        <path d="M12 2C7.5 2 3.7 5.2 3 9.5l2 .5C5.5 6.5 8.5 4 12 4c2.2 0 4.2 1 5.5 2.5L15 9h7V2l-2.5 2.5C17.5 2.8 14.8 2 12 2z" />
        <path d="M12 22c4.5 0 8.3-3.2 9-7.5l-2-.5c-.5 3.5-3.5 6-7 6-2.2 0-4.2-1-5.5-2.5L9 15H2v7l2.5-2.5C6.5 21.2 9.2 22 12 22z" />
      </svg>
    </div>
    <div class="tool-btn tool-flip" title="Flip">
      <svg viewBox="0 0 24 24"><path d="M15 21h2v-2h-2v2zm4-12h2V7h-2v2zM3 5v14c0 1.1.9 2 2 2h4v-2H5V5h4V3H5c-1.1 0-2 .9-2 2zm16-2v2h2c0-1.1-.9-2-2-2zm-8 20h2V1h-2v22zm8-6h2v-2h-2v2zM15 5h2V3h-2v2zm4 8h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2z" /></svg>
    </div>
    <div class="tool-btn tool-exp-txt" title="Export Text">
      <svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z" /></svg>
    </div>
    <div class="tool-btn tool-exp-svg" title="Export SVG">
      <svg viewBox="0 0 24 24"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" /></svg>
    </div>
  </div>

  <div class="ejceestext">
    <div class="ejceesrecord">
      <div class="ejceesrcdstart record-start">棋局開始,紅方先行</div>
    </div>
    <div class="ejceesnote">
      <div class="ejceescomment"></div>
      <div class="ejceestextbtn"></div>
    </div>
  </div>
</div>
<input type="file" class="file-input" style="display: none" />
    `;
  }
```

### 4. Update References in `bindEvents()`
Find all usages of the old IDs and replace them with class selectors.

```javascript
  bindEvents() {
    // ... (Keep existing code above)

    this.container.querySelector('.ejceesrcdstart').addEventListener('click', () => this.ejceesrcdstartclick());

    // Replace all #tool-xxx and #file-input with .tool-xxx and .file-input
    this.container.querySelector('.tool-new').addEventListener('click', () => { /* ... */ });
    this.container.querySelector('.tool-save').addEventListener('click', () => { /* ... */ });
    this.container.querySelector('.tool-open').addEventListener('click', () => { /* ... */ });
    this.container.querySelector('.file-input').addEventListener('change', e => { /* ... */ });
    this.container.querySelector('.tool-rotate').addEventListener('click', () => { /* ... */ });
    this.container.querySelector('.tool-flip').addEventListener('click', () => { /* ... */ });
    this.container.querySelector('.tool-edit').addEventListener('click', () => { /* ... */ });
    this.container.querySelector('.tool-undo').addEventListener('click', () => { /* ... */ });
    this.container.querySelector('.tool-redo').addEventListener('click', () => { /* ... */ });

    document.addEventListener('keydown', e => {
      // ... (Keep existing code above)
      if (e.ctrlKey) {
        // ... (Keep existing code above)
        if (toolShortcuts[keyLow]) {
          e.preventDefault(); 
          // Change selector here
          const btn = this.container.querySelector('.' + toolShortcuts[keyLow]);
          if (btn && (btn.classList.contains('enable') || keyLow === 'r' || keyLow === 'f')) {
            btn.click();
          }
          return;
        }
        // ... (Keep existing code below)
      }
    });

    this.container.querySelector('.tool-exp-txt').addEventListener('click', () => { /* ... */ });
    this.container.querySelector('.tool-exp-svg').addEventListener('click', () => { /* ... */ });
  }
```

### 5. Update `updateToolHighlights()`
Update the DOM querying logic to match the new class selectors.

```javascript
  updateToolHighlights() {
    if (this.isAutoPlaying) {
      const editHighlights = {
        'tool-new': false,
        'tool-open': false,
        // ...
      };
      for (const [id, shouldEnable] of Object.entries(editHighlights)) {
        const btn = this.container.querySelector('.' + id); // Changed to class selector
        if (btn) btn.classList.toggle('enable', shouldEnable);
      }
      return;
    }
    if (this.isEditMode) {
      const editHighlights = {
        // ...
      };
      for (const [id, shouldEnable] of Object.entries(editHighlights)) {
        const btn = this.container.querySelector('.' + id); // Changed to class selector
        if (btn) btn.classList.toggle('enable', shouldEnable);
      }
      return;
    }
    if (this.isEditingComment) {
      const editHighlights = {
        // ...
      };
      for (const [id, shouldEnable] of Object.entries(editHighlights)) {
        const btn = this.container.querySelector('.' + id); // Changed to class selector
        if (btn) btn.classList.toggle('enable', shouldEnable);
      }
      return;
    }

    const isModified = this.historyFEN.v.length > 0 || !this.ifGameBegin(this.historyFEN.fen);
    const highlights = {
      'tool-new': isModified && !this.isExportTextMode,
      // ...
    };
    for (const [id, shouldEnable] of Object.entries(highlights)) {
      const btn = this.container.querySelector('.' + id); // Changed to class selector
      if (btn) {
        if (shouldEnable) {
          btn.classList.add('enable');
        } else {
          btn.classList.remove('enable');
        }
      }
    }
  }
```

### 6. Update `highlightActiveStep()` and `renderRecordUI()`
Update the query selector for the record start element.

```javascript
  highlightActiveStep(index) {
    // Remove active class from all steps and the start banner
    this.container.querySelectorAll('.ejceesrcdstep').forEach(el => el.classList.remove('active'));
    
    // Change query selector to class
    const startDiv = this.container.querySelector('.record-start');
    if (startDiv) startDiv.classList.remove('active');

    let targetEl = null;
    if (index === 0) {
      if (startDiv) startDiv.classList.add('active');
      targetEl = startDiv;
    } else {
      // ... (Keep existing code)
    }
    // ...
  }

  renderRecordUI() {
    // ... (Keep existing code above)
    const recordContainer = this.container.querySelector('.ejceesrecord');
    recordContainer.innerHTML = '';

    const startDiv = document.createElement('div');
    // Change assignment from id to adding class
    startDiv.classList.add('ejceesrcdstart', 'record-start');
    startDiv.onclick = () => this.ejceesrcdstartclick();
    // ... (Keep existing code below)
  }
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

id不用換了:
附件
ejceesobj202604270950.js
(270.22 KiB) 已下载 40 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

改進:
1.當mode===ejceessingle時,把
@media (max-width: 767px) {...}
@media (min-width: 768px) {...}
換成對this.container的查詢,並寫到 if (this.mode === 'ejceessingle' || this.mode === 'ejceesmini') {resetAndContainerCSS = `...`}的末尾。

2.當mode===ejceesmini時,新建一個objElement = <object width="432" height="480">,用以下方式加入:

const blob = new Blob([this.svgOutHTML], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
objElement.data = url;
objElement.addEventListener('load', () => {
this.svgElement = objElement.contentDocument;
}

「div.ejceessvg」 清空後appendChild(objElement)。

其顯示元素要經過較大調整,ejceestool和ejceestext將不再顯示。ejceesstep的最左邊將放置ejceesoutput,固定其長寬為36*36,並且只顯示當前步數和總步數(如20/80)。ejceesstep的最右邊將顯示btn-autoplay按鈕。當有分支時,它將直接絕對定位背景半透明顯示在ejceessvg的左上角,點擊有下拉菜單,可選擇切換分支。當有評論時,它將直接絕對定位背景半透明顯示在ejceessvg的最底部。將不能使用鍵盤快捷鍵。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

genini pro飽了:

代码: 全选

Here are the specific modifications required to implement your requested features.

### 1. Modify the Constructor
Update the `constructor` initialization phase to handle asynchronous DOM caching for `ejceesmini` mode.

```javascript
    // --- Boot the App --- (Replace the final lines of the constructor)
    this.injectCSS();
    this.renderDOM();
    
    if (this.mode === 'ejceesmini') {
      this.cacheDOM().then(() => {
        this.bindEvents();
        this.initGame();
      });
    } else {
      this.cacheDOM();
      this.bindEvents();
      this.initGame();
    }
  }
```

### 2. Update `injectCSS()`
Replace the CSS string building block handling `ejceessingle` and `ejceesmini` to use CSS Container Queries based on `this.container` instead of Media Queries, and add custom classes for the mini mode.

```javascript
    // Inside injectCSS(), replace the if (this.mode === 'ejceessingle' || this.mode === 'ejceesmini') block with:
    if (this.mode === 'ejceessingle' || this.mode === 'ejceesmini') {
      resetAndContainerCSS = `
/* Scoped Reset */
.ejcees, .ejcees * {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* Container wrapper setup for Container Queries */
.ejcees {
  container-type: inline-size;
  display: flex;
  width: 100%;
  height: ${this.mode === 'ejceesmini' ? 'auto' : '100vh'};
  background-color: #f4f4f9;
  overflow: hidden;
  font-family: sans-serif;
  position: relative;
}

/* Use Container Queries instead of Media Queries */
@container (max-width: 767px) {
  .ejcees { flex-direction: column; }
  .ejceesctrl { flex: 1 0 272px; min-height: 272px; }
  .ejceessvg { width: 100%; height: auto; max-height: calc(100vw * 480 / 432); }
  .ejceespb { width: 100%; height: auto; max-height: 100%; }
  .ejceestool, .ejceesstep, .ejceesoutput { padding: 0 7px; }
  .ejceesstep { gap: 7px; }
  .ejceesrecord { width: 200px; font-size: 12px; }
  .ejceesrcdstep .ejceesrcdstepcontent { margin-left: 24px; }
  .branch-marker { margin-left: 1px; }
  .exp-fmt-outer { display: grid; grid-template-columns: repeat(2, 1fr); gap: 5px; }
  .exp-fmt-btn { padding: 5px; cursor: pointer; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}

@container (min-width: 768px) {
  .ejcees { flex-direction: row; }
  .ejceesctrl { flex: 1 0 414px; min-width: 414px; }
  .ejceessvg { height: 100%; width: auto; max-width: calc(100vh * 432 / 480); }
  .ejceespb { height: 100%; width: auto; max-width: 100%; }
  .ejceestool, .ejceesstep, .ejceesoutput { padding: 0 12px; }
  .ejceesstep { gap: 12px; }
  .ejceesrecord { width: 268px; font-size: 16px; }
}

/* ejceesmini specific elements */
.ejceesmini-branch-menu {
  position: absolute;
  top: 0;
  left: 0;
  background-color: rgba(0, 0, 0, 0.6);
  color: #fff;
  padding: 4px 8px;
  font-size: 12px;
  cursor: pointer;
  z-index: 20;
}

.ejceesmini-comment {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  background-color: rgba(0, 0, 0, 0.6);
  color: #fff;
  padding: 8px;
  font-size: 14px;
  z-index: 20;
  box-sizing: border-box;
  white-space: pre-wrap;
  word-break: break-all;
}
      `;
    }
```

### 3. Update `renderDOM()`
Modify the method to conditionally inject a stripped-down DOM when in `ejceesmini` mode.

```javascript
  // Replace the second half of renderDOM() after setting this.svgOutHTML:
  renderDOM() {
    // ... [Keep the existing this.svgOutHTML string assignment] ...

    if (this.mode === 'ejceesmini') {
      this.container.innerHTML = `
<div class="ejceessvg" style="position: relative;">
  <div id="mini-branch-container" class="ejceesmini-branch-menu" style="display: none;"></div>
  <div id="mini-comment-container" class="ejceesmini-comment" style="display: none;"></div>
</div>
<div class="ejceesctrl" style="flex: 0 0 auto; min-height: unset; height: 48px;">
  <div class="ejceesstep">
    <div class="ejceesoutput" style="width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; font-size: 12px; padding: 0;">0/0</div>
    <div class="ejceesstepminus">-</div>
    <input type="range" class="ejceesstepdrop" min="0" max="0" value="0" />
    <div class="ejceesstepplus">+</div>
    <div class="note-btn" id="btn-autoplay" title="Auto Play" style="width: 36px; height: 36px; cursor: pointer; display: flex; align-items: center; justify-content: center; background-color: transparent;">
      <svg viewBox="0 0 24 24" style="width: 20px; height: 20px; fill: #ccc;"><path d="M8 5v14l11-7z"/></svg>
    </div>
  </div>
</div>
<input type="file" id="file-input" style="display: none" />
      `;
    } else {
      this.container.innerHTML = `
<div class="ejceessvg">
  ${this.svgOutHTML}
</div>
<div class="ejceesctrl">
  <div class="ejceesoutput">Click on the SVG area to get coordinates.</div>
  <div class="ejceesstep">
    <div class="ejceesstepminus">-</div>
    <input type="range" class="ejceesstepdrop" min="0" max="0" value="0" />
    <div class="ejceesstepplus">+</div>
  </div>

  <div class="ejceestool">
</div>
</div>
<input type="file" id="file-input" style="display: none" />
      `;
    }
  }
```

### 4. Update `cacheDOM()`
Update this method to use the `<object>` tag and Blob for loading the SVG, returning a Promise when in `ejceesmini` mode.

```javascript
  cacheDOM() {
    this.outputElement = this.container.querySelector('.ejceesoutput');
    this.stepSlider = this.container.querySelector('.ejceesstepdrop');

    if (this.mode === 'ejceesmini') {
      const svgContainer = this.container.querySelector('.ejceessvg');
      const objElement = document.createElement('object');
      objElement.width = "432";
      objElement.height = "480";
      
      const blob = new Blob([this.svgOutHTML], { type: 'image/svg+xml' });
      const url = URL.createObjectURL(blob);
      objElement.data = url;
      
      return new Promise((resolve) => {
        objElement.addEventListener('load', () => {
          this.svgElement = objElement.contentDocument.querySelector('svg');
          this.etboard = this.svgElement.querySelector('.etboard');
          this.etdrop = this.svgElement.querySelector('.etdrop');
          this.startDot = this.svgElement.querySelector('#ejceesstartdot');
          if (this.startDot) {
            this.initialDotCx = this.startDot.getAttribute('cx') || '0';
            this.initialDotCy = this.startDot.getAttribute('cy') || '0';
          }
          resolve();
        });
        svgContainer.appendChild(objElement);
      });
    } else {
      this.svgElement = this.container.querySelector('.ejceespb');
      this.etboard = this.container.querySelector('.etboard');
      this.startDot = this.container.querySelector('#ejceesstartdot');
      if (this.startDot) {
        this.initialDotCx = this.startDot.getAttribute('cx') || '0';
        this.initialDotCy = this.startDot.getAttribute('cy') || '0';
      }
      this.etdrop = this.container.querySelector('.etdrop');
      return Promise.resolve();
    }
  }
```

### 5. Update `bindEvents()`
Disable keyboard shortcuts and bind the mini-mode autoplay button.

```javascript
  // Inside bindEvents(), add the return clause at the top of the keydown listener
  document.addEventListener('keydown', e => {
    if (this.mode === 'ejceesmini') return; // Disable keyboard shortcuts for ejceesmini
    
    // ... [Rest of original keydown handling] ...

  // At the end of bindEvents(), add the autoplay listener for mini mode
  if (this.mode === 'ejceesmini') {
    const autoBtn = this.container.querySelector('#btn-autoplay');
    if (autoBtn) {
      autoBtn.addEventListener('click', () => {
        if (this.isAutoPlaying) {
          this.stopAutoPlay();
        } else {
          this.startAutoPlay();
        }
      });
    }
  }
```

### 6. Override Interface Methods
Update the `restoreOutputText`, `renderNoteUI`, and `renderRecordUI` methods to behave correctly when running in `ejceesmini` mode.

```javascript
  // Update restoreOutputText method
  restoreOutputText() {
    if (this.mode === 'ejceesmini') {
      const totalDepth = this.getPathDepth(this.historyFEN, this.currentBranch) - 1;
      this.setOutputText(`${this.currentStepIndex}/${totalDepth}`, false);
      return;
    }
    // ... [Keep original restoreOutputText logic] ...

  // Update renderNoteUI method
  renderNoteUI() {
    if (this.mode === 'ejceesmini') {
      const node = this.getNodeAtStep(this.currentStepIndex);
      const commentContainer = this.container.querySelector('#mini-comment-container');
      if (node && node.c && node.c.trim() !== '') {
        commentContainer.textContent = node.c.trim();
        commentContainer.style.display = 'block';
      } else {
        commentContainer.style.display = 'none';
      }

      const autoBtnSvg = this.container.querySelector('#btn-autoplay svg');
      if (autoBtnSvg) {
        autoBtnSvg.innerHTML = this.isAutoPlaying 
          ? '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>' 
          : '<path d="M8 5v14l11-7z"/>';
        autoBtnSvg.style.fill = this.isAutoPlaying ? '#fff' : '#ccc';
      }
      return;
    }
    // ... [Keep original renderNoteUI logic] ...

  // Update renderRecordUI method
  renderRecordUI() {
    if (this.mode === 'ejceesmini') {
      const totalDepth = this.getPathDepth(this.historyFEN, this.currentBranch);
      this.stepSlider.max = Math.max(0, totalDepth - 1);

      const branchContainer = this.container.querySelector('#mini-branch-container');
      const parentNode = this.getNodeAtStep(this.currentStepIndex - 1);

      if (parentNode && parentNode.v && parentNode.v.length > 1) {
        branchContainer.style.display = 'block';
        
        let forkIndex = -1;
        let tempNode = this.historyFEN;
        for (let j = 0; j < this.currentStepIndex - 1; j++) {
          if (tempNode.v && tempNode.v.length > 1) forkIndex++;
          const choice = tempNode.v && tempNode.v.length > 1 ? this.currentBranch[forkIndex] || 0 : 0;
          tempNode = tempNode.v[choice];
        }
        forkIndex++;

        const currentChoice = this.currentBranch[forkIndex] !== undefined ? this.currentBranch[forkIndex] : 0;
        branchContainer.textContent = `Branch: ${currentChoice + 1}/${parentNode.v.length}`;

        branchContainer.onclick = (e) => {
          e.stopPropagation();
          this.showBranchMenu(branchContainer, parentNode, forkIndex, this.currentStepIndex);
        };
      } else {
        branchContainer.style.display = 'none';
      }
      this.updateToolHighlights();
      return;
    }
    // ... [Keep original renderRecordUI logic] ...
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

因為div.ejcees本身是container,所以當mode=ejceessingle或ejceesmini時,要在div.ejcees內加一層div.ejceesInner,並根據.ejcees寬度,改變div.ejceesInner的樣式。

只需給出所要修改的地方,所有的代碼都要使用英文。



gemin fast:

代码: 全选

這是一個非常準確的觀察。在 CSS Container Queries 中,**一個元素不能根據自己的 `container-type` 來改變自己的樣式**(這會導致死循環)。必須對**子元素**進行查詢。

以下是針對 `renderDOM`、`injectCSS` 以及 `cacheDOM` 的修改方案:

### 1. 修改 `renderDOM()`
在 `.ejcees` 內部包裝一層 `.ejcees-inner`,並將所有原本直接放在 `.ejcees` 下的內容移入其中。

```javascript
  renderDOM() {
    // ... [Existing svgOutHTML logic] ...

    // Use a wrapper div.ejcees-inner to allow Container Queries to work
    if (this.mode === 'ejceesmini') {
      this.container.innerHTML = `
<div class="ejcees-inner">
  <div class="ejceessvg" style="position: relative;">
    <div id="mini-branch-container" class="ejceesmini-branch-menu" style="display: none;"></div>
    <div id="mini-comment-container" class="ejceesmini-comment" style="display: none;"></div>
  </div>
  <div class="ejceesctrl" style="flex: 0 0 auto; height: 48px;">
    <div class="ejceesstep">
      <div class="ejceesoutput" style="width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; font-size: 12px; padding: 0;">0/0</div>
      <div class="ejceesstepminus">-</div>
      <input type="range" class="ejceesstepdrop" min="0" max="0" value="0" />
      <div class="ejceesstepplus">+</div>
      <div class="note-btn" id="btn-autoplay" title="Auto Play" style="width: 36px; height: 36px; cursor: pointer; display: flex; align-items: center; justify-content: center;">
        <svg viewBox="0 0 24 24" style="width: 20px; height: 20px; fill: #ccc;"><path d="M8 5v14l11-7z"/></svg>
      </div>
    </div>
  </div>
</div>
<input type="file" id="file-input" style="display: none" />`;
    } else {
      this.container.innerHTML = `
<div class="ejcees-inner">
  <div class="ejceessvg">${this.svgOutHTML}</div>
  <div class="ejceesctrl">
    <div class="ejceesoutput">Click on the SVG area to get coordinates.</div>
    <div class="ejceesstep">
      <div class="ejceesstepminus">-</div>
      <input type="range" class="ejceesstepdrop" min="0" max="0" value="0" />
      <div class="ejceesstepplus">+</div>
    </div>
    <div class="ejceestool"></div>
  </div>
</div>
<input type="file" id="file-input" style="display: none" />`;
    }
  }
```

### 2. 修改 `injectCSS()`
將原本針對 `.ejcees` 的佈局屬性(如 `flex-direction`)轉移到 `.ejcees-inner` 上。

```javascript
    if (this.mode === 'ejceessingle' || this.mode === 'ejceesmini') {
      resetAndContainerCSS = `
.ejcees {
  container-type: inline-size;
  width: 100%;
  height: ${this.mode === 'ejceesmini' ? 'auto' : '100vh'};
  overflow: hidden;
}

/* The inner element responds to the .ejcees container size */
.ejcees-inner {
  display: flex;
  width: 100%;
  height: 100%;
  background-color: #f4f4f9;
}

@container (max-width: 767px) {
  .ejcees-inner { flex-direction: column; }
  .ejceesctrl { flex: 1 0 272px; min-height: 272px; }
  .ejceessvg { width: 100%; height: auto; max-height: calc(100cqw * 480 / 432); }
}

@container (min-width: 768px) {
  .ejcees-inner { flex-direction: row; }
  .ejceesctrl { flex: 1 0 414px; min-width: 414px; }
  .ejceessvg { height: 100%; width: auto; max-width: calc(100cqh * 432 / 480); }
}

/* ... existing ejceesmini-specific classes ... */
`;
    }
```

### 3. 修改 `cacheDOM()`
由於現在多了一層 `ejcees-inner`,原本的選擇器路徑仍然有效(因為 `querySelector` 是深度查找),但為了確保獲取正確的元素,建議保持如下結構:

```javascript
  cacheDOM() {
    // These will search inside .ejcees and find elements within .ejcees-inner
    this.outputElement = this.container.querySelector('.ejceesoutput');
    this.stepSlider = this.container.querySelector('.ejceesstepdrop');

    if (this.mode === 'ejceesmini') {
      const svgContainer = this.container.querySelector('.ejceessvg');
      // ... [Existing <object> creation and Promise logic] ...
      // No change needed here as long as .ejceessvg exists inside .ejcees-inner
    }
    // ...
  }
```

### 關鍵點:
1.  **`100cqw` 代替 `100vw`**:在 `@container` 查詢中,使用 `cqw` 能確保寬度是相對於父容器 `.ejcees` 的,而不是整個屏幕。
2.  **`px` 單位**:在 `@container (min-width: 768px)` 中必須加上 `px`,否則 CSS 會失效。
3.  **層級關係**:`.ejcees` (Container) -> `.ejcees-inner` (Target)。這樣 `.ejcees-inner` 就能根據 `.ejcees` 的寬度切換 `flex-direction`。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

container生效的例子:

gemini fast :

代码: 全选

這裡提供一個完整的 HTML 範例。這個例子展示了為什麼需要「兩層」結構:外層作為 **Container(容器)**,內層作為 **Target(目標)**。

在這個範例中,你可以透過拖動滑塊(Slider)來改變容器的寬度,觀察內部的佈局如何從橫向(Row)切換為縱向(Column),以及字體顏色的變化。

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSS Container Queries Example</title>
    <style>
        /* 1. The Container: This element defines the containment context */
        .ejcees {
            container-type: inline-size;
            container-name: chess-container;
            width: 80%; /* Initial width */
            margin: 20px auto;
            border: 3px dashed #333;
            background-color: #eee;
            overflow: hidden;
            resize: horizontal; /* Allows manual resizing in some browsers */
        }

        /* 2. The Inner Wrapper (Target): This element responds to the container's size */
        .ejcees-inner {
            display: flex;
            gap: 20px;
            padding: 20px;
            background-color: #f4f4f9;
            transition: all 0.3s ease;
        }

        .ejceessvg {
            flex: 1;
            background-color: #2c3e50;
            color: white;
            height: 200px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: bold;
        }

        .ejceesctrl {
            flex: 1;
            background-color: #ecf0f1;
            padding: 15px;
            border: 1px solid #bdc3c7;
        }

        /* 3. Container Queries */
        
        /* Mobile layout: when container width is less than 500px */
        @container chess-container (max-width: 499px) {
            .ejcees-inner {
                flex-direction: column;
                background-color: #fff3e0; /* Orange tint for mobile */
            }
            .ejceessvg {
                height: 100px;
            }
            .ejceessvg::after {
                content: " (Mobile View)";
            }
        }

        /* Desktop layout: when container width is 500px or more */
        @container chess-container (min-width: 500px) {
            .ejcees-inner {
                flex-direction: row;
                background-color: #e3f2fd; /* Blue tint for desktop */
            }
            .ejceessvg::after {
                content: " (Desktop View)";
            }
        }

        /* Control Panel for Demo */
        .demo-controls {
            text-align: center;
            margin-top: 20px;
            font-family: sans-serif;
        }
    </style>
</head>
<body>

    <div class="demo-controls">
        <h2>Container Query Demo</h2>
        <p>Drag the slider to change the <b>Container's</b> width.<br>
           The layout changes based on the container size, NOT the screen size.</p>
        <input type="range" id="widthSlider" min="300" max="800" value="600" style="width: 300px;">
        <span id="widthValue">600px</span>
    </div>

    <div class="ejcees" id="mainContainer" style="width: 600px;">
        <div class="ejcees-inner">
            <div class="ejceessvg">
                SVG AREA
            </div>
            <div class="ejceesctrl">
                <h3>Control Panel</h3>
                <p>This side panel moves below the SVG when the container is narrow.</p>
            </div>
        </div>
    </div>

    <script>
        const slider = document.getElementById('widthSlider');
        const container = document.getElementById('mainContainer');
        const widthDisplay = document.getElementById('widthValue');

        slider.addEventListener('input', (e) => {
            const val = e.target.value;
            container.style.width = val + 'px';
            widthDisplay.textContent = val + 'px';
        });
    </script>
</body>
</html>
```

### 為什麼這個例子會生效?
1.  **層級關係**:`@container` 寫在 `.ejcees-inner` 上,它查詢的是父元素 `.ejcees`。這避開了「自查自糾」的錯誤。
2.  **明確單位**:使用了 `px`(例如 `min-width: 500px`)。
3.  **容器類型**:`.ejcees` 設置了 `container-type: inline-size;`,這告訴瀏覽器該元素需要監聽其寬度變化。
4.  **容器命名(選用)**:使用了 `container-name: chess-container;` 並在查詢中使用 `@container chess-container ...`,這在一個頁面有多個容器時能確保查詢精確。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 6009
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

mini的樣式需要修改:
附件
ejceesobj202604271228.js
(277.54 KiB) 已下载 40 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

正浏览此版面之用户: 没有注册用户 和 0 访客