変奏現実

パソコンやMMORPGのことなどを思いつくまま・・・記載されている会社名・製品名・システム名などは、各社の商標、または登録商標です。

この画面は、簡易表示です

spreadsheet5

[javascript]spreadSheet5 ネームスペース

自前コードなので、関数やクラスをネームスペースにバラ巻いていた。

ただ、このままでは、再利用する度に、困りそうなので、カスタムタグのソースのクラスなどをMYAPPに纏めてみた。

まず、ネームスペースをまとめる関数を追加。※ネットで見つけたコードを少し改良したもの

/**
 *  MYAPP   グローバルオブジェクトのアプリ名
 */
const MYAPP = {};   //  初期設定

/**
 *  指定ネームスペースを作成する
 * @param {string} pNameSpace       作成するネームスペース・パス
 * @param {undefined | JSON} pJson  追記する内容 undefined または JSON
 * @param {boolean} pFreeze         変更不可指定
 */
MYAPP.namespace = (pNameSpace, pJson = undefined, pFreeze = false) => {
    /**
     * 再帰的にオブジェクトを凍結する
     * @param {object} obj  凍結するオブジェクト
     */
    const freezeR = (obj) => {
        if (typeof obj === 'object') {
            // オブジェクトの配下を再帰的に凍結
            for (const key of Object.keys(obj)) {
                freezeR(obj[key]);
            }
            // オブジェクト自体を凍結
            Object.freeze(obj);
        }
    };
    // ネームスペースのテキストを .で区切った配列を作成する。※先頭がMYAPPなら削除する
    const aNameSpace = pNameSpace.split('.').filter((element, index) => index !== 0 || element !== 'MYAPP');
    //  現在のパスをMYAPPへ移動する
    let curPath = MYAPP;
    //  パス最後の要素の有無で処理が分岐する為、配列から最後の要素を分離する。※constでもpopはオブジェクトを差替(代入)えないのでOK(らしい
    const lastElement = aNameSpace.pop();
    //  配列の順にネームスペースの有無をチェックする
    for (const part of aNameSpace) {
        // パスが存在しなければ作成し、パス位置を現在位置へ移動する
        curPath = curPath[part] = curPath[part] ?? {};
    }
    // パス最後の要素が未設定なら空リストを代入
    if (curPath[lastElement]) {
        // pJsonをマージする
        Object.assign(curPath[lastElement], pJson ?? {});
    } else {
        // pJsonに差替える
        curPath[lastElement] = pJson ?? {};
    }
    //  編集不可指定ありの場合
    if (pFreeze) {
        //  作成したネームスペースを再帰的に凍結します
        if (!pJson) {
            freezeR(curPath[lastElement]);
        } else {
            //  JSON配下のオブジェクトを再帰的に凍結します
            for (const key of Object.keys(pJson)) {
                freezeR(curPath[lastElement][key]);
            }
        }
    }
};

aNameSpaceからそのままfor (const part of aNameSpace)を回すと、curPathが最後まで移動し、pJsonをネームスペースに追加しようとすると、

 curPath = pJson ?? {} ;                            // MYAPPの方は書き換わらない

となってしまい、MYAPPの方は書き換わらないので、最終の一歩手前でcurPathを止めて、

curPath[lastElement] = pJson ?? {};                 // curPath[lastElement]はMYAPPの一部なのでOK

としたかったので、aNameSpaceから最後の要素を切り取った。

また、Object.assignの第一パラメータの中身がundefinedの場合は、エラってしまうので、

try {
  let obj = undefined;
  Object.assign(obj, pJson ?? {}); 
} catch(ex) {
  debug.print('Fail. '+ex);
}

⇒ Fail. Uncaught TypeError TypeError: Cannot convert undefined or null to object

undefined判定で空リストに変えてマージ。

    if (!curPath[lastElement]) {
        curPath[lastElement] = {};                      // 空リストにする
    }
    Object.assign(curPath[lastElement], pJson ?? {});   // マージする

さすがに

Object.assign(curPath[lastElement]??{}, pJson);   // マージする

は無理。

また、参照先が同じJSONにある場合は未確定な状態になりやすいので、MYAPP.namespaceの呼び出しを参照元と参照先で分けるのも面倒なので、第一パラメータで非参照先のネームパスを作成した方が楽。

これで完了と思ったら( ^ω^)・・・

MYAPP.namespace('aaa.bbb.ccc',
   (p1, p2, p3) => {
     ...
   }
);

aaa.bbb.ccc(1,2,3);

Uncaught TypeError TypeError: aaa.bbb.ccc is not a function

Object.assignは関数をプロパティっぽく書き込み、参照時に未定義な関数のエラーになるので、

    // パス最後の要素が未設定なら空リストを代入
    if (curPath[lastElement]) {
        // pJsonをマージする
        Object.assign(curPath[lastElement], pJson ?? {});
    } else {
        // pJsonに差替える
        curPath[lastElement] = pJson ?? {};
    }

で落ち着いた。

※まずpJsonがJSONかどうか判定した後の処理にした方が意図が読み取りやすいけど、深く意味を読み取ると無駄だと判るコードになる。

元ネタでは、

/**
 * スプレッドシートクラス
 * @class MYAPP.spreadSheet.SpreadSheetCustomElement
 */
MYAPP.namespace('spreadSheet.SpreadSheetCustomElement'),
MYAPP.spreadSheet.SpreadSheetCustomElement = class extends HTMLElement {
    ...
};

と、ネームスペースを作成し、作成したネームスペースにクラス等を設定する感じになっていた。

これを実際に動かしてみると、クラスの#変数がエラるw(パラメータがクラス宣言部なせいだろう)し、ネームスペースの文字も2度書いてるw(被ってるのは嫌い)なので、ネームスペースはファイルのパスまでとし、クラス名等はJSON形式でキー指定した方に変更して、クラスの定義は、こんな感じになった。

/**
 * スプレッドシートクラス
 * @class MYAPP.spreadSheet.SpreadSheetCustomElement
 */
MYAPP.namespace('spreadSheet', {
  'SpreadSheetCustomElement': class extends HTMLElement {
    ...
  }
});

ゴチャ付いてないから見やすくなった。(と思う

クラスを使用する場合はグローバルにクラスを宣言してないので、フルパス名で指定する。

_spreadSheet._command = new MYAPP.spreadSheet.SpreadSheetCommand(this);

関数も同様で、無名関数「function(…) { } 」でもいい。

MYAPP.namespace('lib', {
  'createHtmlElement': (elementName, options) => {
    ...
    },
});

使う時はフルパス名で指定する。

_layout.divTableN = MYAPP.lib.createHtmlElement('div', ... );

オブジェクトのプロパティはObject.freeze(object)で変更不可にできる。

メソッド名プロパティの追加プロパティの削除プロパティの値の変更
Object.preventExtensions()
Object.seal()
Object.freeze()

Object.freeze(object)以外は値の変更ができるので今回は対象外。

MYAPP.namespace('lib', {
  'char': {
    BR:'<br/>',
    CR: '\n',
    TAB: '\t',
    },
},true);             // 変更不可を指示

関数やクラスの外では意図的にstrictモードにしないとエラー(TypeError)が起きない。

ちなみに

"use strict";    // strictモード全開!

//  const 変数
MYAPP.namespace('lib', {
    'char': {
        BR: '<br/>',
        CR: '\n',
        TAB: '\t',
    },
}, true);
MYAPP.lib.char.BR = 'aaaaa'; ☚ここでType Error

Uncaught TypeError TypeError: Cannot assign to read only property 'BR' of object '#<Object>'

修正した結果

デバッグして止めて、スクリプト・スコープの内容を見ると、修正が漏れ過ぎ( ´∀` )

やっとのことでMYAPPにまとまった。

ネームスペースはすっきりした。

しかし、こんなコードを見た試しが無い。(大笑

パス名が付くのでコードが長くなる上、VSCodeの自動でコメントを生成するプラグインが怪しい動きをする可能性があるが、このクラス、変数はどこ?と探すのは楽かもしれない。

ps.2024/10/8

アップロード処理でvalueの無いセルデータでエラっていたので修正



[javascript]spreadSheet5

長らく放置していたセル幅高編集機能をspreadSheet5として実装した。

spreadSheet4はさっぱり進んでいないマクロ機能の実装用に残した。

操作方法:セルの境界線上にマウスポインタを乗せるとカーソルがリサイズっぽく変わりドラッグするとドットラインも表示するので好みの位置でリリースし確定する。

データ保持:セル幅やセル高を変えるとindex.html内のspread-sheetタグのdatabasename属性で指定したデータベースのcellテーブルに列ヘッダー(r0cNNN)または行ヘッダー(rNNNc0)のセルに変更内容(style:{width:’NNNpx’あるいはheight:’NNNpx’})が保持される。

確認方法:変更内容はCtrl+Sでデータをダウンロードすることを確認でき、更に変更を加えspreadSheet上にドロップすることで結果を確認できる。

永続性:再表示(F5)時も変更内容を参照し画面に反映される。

TODO1:セルのHTMLエレメントを作成する部分が初期のセル幅やセル高を前提に作ってるので、セルを初期より狭くするとセルがうまく表示されない。カーソルキーでセル移動すると補正されるケースがある。実装を見直し中。

TODO2:セル内テキストがセル幅を越える場合は…付きの表示になるが、セル高を複数行分まで広げてもセル内テキストは改行しない。canvas.measureTextを使って適切な行数を計測できそうだけど、複数行表示で自動的に省略文字(…)を書きたい場合に困り、思案中。

TODO3:初期のセル幅やセル高に戻すUIは無いので下限値が16pxになっているが、r0c{セル位置}、r{行位置}c0のデータを削除することで初期に戻すことは可能だから、コンテキストメニュー実装時に考慮。

TODO4:セル境界線上でダブルクリックすると、良い感じのセル幅に変更するUIも実装したいが、まずタグにテキストを埋め込んで実測すると遅いのは明白で、canvasで描画して調べるしかなさそうだ。

const context = document.getElementById("canvas").getContext("2d");
/* 以下必要な分繰り返す*/
context.font = "48px serif";  /* な感じでセルのテキスト属性を指定 */
const text = '適当なテキスト'; /* な感じでセルのテキストを指定 */
const textMetrics = context.measureText(text);
let width = textMetrics.width;
let height = textMetrics.height;

これを画面で見える範囲で繰り返すなら処理時間も短かくて済そう。

しかし、測定値を保持するとページスクロールとダブルクリックを最終セルまで繰り返さないとドコかで不都合な場合もありそうなので、’best-value’:「適切な測定値」とかで保持するといいかな?とか思案中。

TODO5:ドラッグ中の値を表示してない。

TODO6:ドットラインの初期位置が現在のセル幅の位置ではなくカーソルの位置になっている。FIX9/30

予想通り色々と問題が発覚しすぎ。ここまで尾を引くなら、セル描画はcanvasに任せた方が良いかも。HitTestの実装もセル単位で判れば良いし。

ps.2024/9/30 ラバーバンドの位置を調整。横スクロールのY軸調整がうまくいかないので外す。セルサイズの小数部を切り捨て(52.3px⇒52px)。画面構成の計算は単位がpxのみ対処。他単位を指定した場合は×。コードを見返してみると長いし複雑、もっと手短にしたい。

ps.2024/10/8

アップロード処理でvalueの無いセルデータでエラっていたので修正




top