Practice makes Perfect !

ExtendedScript素人です。同じ境遇の方、一緒に脱素人を目指しましょう!

実験023_2026.7-1

【InDesign】親ページを一括で指定するスクリプト

昨日このようなnoteを書きました。
note.com
これと関連するというか、この後の処理で親ページを割り当てる作業があると思います。

このときに親ページが5つあったとしたら、5回も"親ページを適用"をしなくてはならず、なかなか面倒です。

今回は、これを一括指定できるスクリプトをGeminiと相談して作りました。
4年前にはこんなものも作っていましたが、使いづらくて使っていませんでしたw
p-makes-p.hatenablog.com

生成AIもだいぶ進化しているのでしょうね。Geminiとのやり取りの回数もだいぶ減ったと思います。

まずスクリプトを実行すると図のような画面が出ます。

画面左側には親ページの名前が表示されます。そしてその横の空欄には適用したいページを入力します。

こんな感じで入力すれば、入力したとおりに親ページを適用してくれます。

入力されなかったページは親ページ[なし]のままです。

あと、親ページが8個以上の場合は右側にスクロールバーが出るようにしました。

また、既に親ページが適用されているドキュメントに対しては、このスクリプトを実行すると、現在どのページにどの親ページが当たっているかが表示されます。

この状態で[適用]を押すと、再度新たに親ページを当て直してしまうのでご注意ください。
確認するだけならそのまま[キャンセル]です。

お~まちさんからご指摘をいただき、ここは修正しました。
すでに設定したい親ページが適用されている場合は、再適用せずに、処理をスキップします。

一部だけ割り当てる親ページを変更したいという場合には、標準機能の[親ページを適用]を使った方がよいと思います。

もしよかったら使ってみてください。
drive.google.com


/*
 * Quick Master Assigner(親ページを一括指定するスクリプト)
 * 制作:制作課長くれそん
 * 制作日:2026年7月29日
 * 
 * 【免責事項】
 * 本スクリプトの使用により生じた損害等について、制作者は一切の責任を負いません。
 * 必ず自己責任においてご使用・テストを行ってください。
 */

#target indesign

  (function() {
    // 1. ドキュメントの存在確認
    if (app.documents.length === 0) {
        alert("ドキュメントが開かれていません。");
        return;
    }

    var doc = app.activeDocument;
    var totalPages = doc.pages.length;
    var masters = doc.masterSpreads;

    if (masters.length === 0) {
        alert("適用できる親ページ(マスタースプレッド)がありません。");
        return;
    }

    var noneMaster = NothingEnum.NOTHING;

    // --- 補助関数: 配列 [1,2,3, 5,6, 8] を "1-3, 5-6, 8" 文字列に変換 ---
    function pagesToRangeString(pageList) {
        if (!pageList || pageList.length === 0) return "";
        pageList.sort(function(a, b) { return a - b; });

        var ranges = [];
        var rangeStart = pageList[0];
        var prev = pageList[0];

        for (var i = 1; i < pageList.length; i++) {
            var curr = pageList[i];
            if (curr === prev + 1) {
                prev = curr;
            } else {
                if (rangeStart === prev) {
                    ranges.push(rangeStart.toString());
                } else {
                    ranges.push(rangeStart + "-" + prev);
                }
                rangeStart = curr;
                prev = curr;
            }
        }
        if (rangeStart === prev) {
            ranges.push(rangeStart.toString());
        } else {
            ranges.push(rangeStart + "-" + prev);
        }
        return ranges.join(", ");
    }

    // --- 補助関数: "1-7, 9-10" 文字列を数値配列 [1,2,3,4,5,6,7, 9,10] に解析 ---
    function parseRangeString(str) {
        var result = [];
        if (!str || str.replace(/^\s+|\s+$/g, "") === "") return result;

        var normalized = str.replace(//g, ",")
                            .replace(/[ー―—–-]/g, "-")
                            .replace(/\s+/g, "");

        var parts = normalized.split(",");
        for (var i = 0; i < parts.length; i++) {
            var part = parts[i];
            if (part === "") continue;

            if (part.indexOf("-") !== -1) {
                var rangeParts = part.split("-");
                if (rangeParts.length !== 2) return null;

                var start = parseInt(rangeParts[0], 10);
                var end = parseInt(rangeParts[1], 10);

                if (isNaN(start) || isNaN(end) || start > end) return null;

                for (var p = start; p <= end; p++) {
                    result.push(p);
                }
            } else {
                var num = parseInt(part, 10);
                if (isNaN(num)) return null;
                result.push(num);
            }
        }
        return result;
    }

    // 2. ドキュメント内の現在の親ページ適用状況を解析・集計
    var masterPagesMap = {};
    for (var mIdx = 0; mIdx < masters.length; mIdx++) {
        masterPagesMap[masters[mIdx].id] = [];
    }

    for (var pIdx = 0; pIdx < doc.pages.length; pIdx++) {
        var page = doc.pages[pIdx];
        var applied = page.appliedMaster;
        if (applied !== null && applied.isValid) {
            if (masterPagesMap[applied.id]) {
                masterPagesMap[applied.id].push(pIdx + 1);
            }
        }
    }

    // 3. UI(ダイアログ)の構築
    var win = new Window("dialog", "親ページの適用範囲");
    win.orientation = "column";
    win.alignChildren = ["fill", "top"];
    win.spacing = 10;
    win.margins = 16;

    // ヘッダー固定エリア
    var headerGroup = win.add("group");
    headerGroup.orientation = "row";
    headerGroup.alignment = ["fill", "top"];
    
    var lblMaster = headerGroup.add("statictext", undefined, "親ページ名");
    lblMaster.preferredSize.width = 160;
    
    var lblRange = headerGroup.add("statictext", undefined, "適用ページ(例: 1-7, 9-12)");
    lblRange.preferredSize.width = 160;

    // 仕切り線1
    var sep1 = win.add("panel", undefined, "");
    sep1.alignment = ["fill", "top"];
    sep1.preferredSize.height = 1;

    // --- スクロールエリアの設定 ---
    var maxVisible = 7;      // 最大7行表示(8行目以降はスクロール)
    var itemHeight = 32;      // 1行あたりの実際の高さ(px)
    var spacingHeight = 4;    // 行間余白(px)
    
    // 実際に表示する行数
    var visibleRows = (masters.length > maxVisible) ? maxVisible : masters.length;
    
    // 全高さを正確に計算
    var viewHeight = (visibleRows * itemHeight) + ((visibleRows - 1) * spacingHeight);
    var needScroll = masters.length > maxVisible;

    // スクロール枠の外側コンテナ
    var scrollContainer = win.add("group");
    scrollContainer.orientation = "row";
    scrollContainer.alignment = ["fill", "top"];
    scrollContainer.alignChildren = ["left", "top"];
    scrollContainer.preferredSize.height = viewHeight;

    // リスト領域(表示ウィンドウ)
    var listViewport = scrollContainer.add("group");
    listViewport.orientation = "column";
    listViewport.alignChildren = ["left", "top"];
    listViewport.spacing = 0;
    listViewport.preferredSize.height = viewHeight;
    listViewport.maximumSize.height = viewHeight;

    // リスト本体(実際にスクロール移動させる枠)
    var contentGroup = listViewport.add("group");
    contentGroup.orientation = "column";
    contentGroup.alignChildren = ["left", "top"];
    contentGroup.spacing = spacingHeight;

    // 各親ページの行を追加
    var rows = [];
    for (var i = 0; i < masters.length; i++) {
        var m = masters[i];
        
        var row = contentGroup.add("group");
        row.orientation = "row";
        row.alignChildren = ["left", "center"];
        row.preferredSize.height = itemHeight;

        var nameText = row.add("statictext", undefined, m.name);
        nameText.preferredSize.width = 160;

        var initString = pagesToRangeString(masterPagesMap[m.id]);

        var rangeEdit = row.add("edittext", undefined, initString);
        rangeEdit.preferredSize.width = 150;

        rows.push({
            master: m,
            input: rangeEdit
        });
    }

    // 7個を超える場合のみ右側にスクロールバーを追加
    if (needScroll) {
        var sb = scrollContainer.add("scrollbar", undefined, 0, 0, 10);
        sb.preferredSize.width = 15;
        sb.preferredSize.height = viewHeight;
        sb.alignment = ["right", "fill"];

        sb.onChanging = function() {
            contentGroup.location.y = -this.value;
        };
    }

    // 仕切り線2
    var sep2 = win.add("panel", undefined, "");
    sep2.alignment = ["fill", "top"];
    sep2.preferredSize.height = 1;

    // ボタンエリア(最下部に固定)
    var btnGroup = win.add("group");
    btnGroup.alignment = ["right", "bottom"];
    
    var btnCancel = btnGroup.add("button", undefined, "キャンセル", { name: "cancel" });
    var btnApply = btnGroup.add("button", undefined, "適用", { name: "ok" });

    // スクロールバーの初期化・上限値セット
    win.onShow = function() {
        if (needScroll) {
            win.layout.layout(true);
            var totalH = contentGroup.size.height;
            var maxScroll = totalH - viewHeight;
            if (maxScroll > 0) {
                sb.maxvalue = maxScroll;
                sb.stepdelta = itemHeight + spacingHeight;
                sb.jumpdelta = (itemHeight + spacingHeight) * 3;
            }
        }
    };

    // 4. 「適用」ボタンクリック時の処理
    btnApply.onClick = function() {
        var pageAssignments = {};
        var usedPages = {};

        for (var k = 0; k < rows.length; k++) {
            var r = rows[k];
            var inputText = r.input.text.replace(/^\s+|\s+$/g, "");

            if (inputText !== "") {
                var pageList = parseRangeString(inputText);

                if (pageList === null) {
                    alert("「" + r.master.name + "」の入力形式が正しくありません。\n半角の数字、ハイフン(-)、カンマ(,)を使用してください。(例: 1-7, 9-12)");
                    return;
                }

                for (var pIdx = 0; pIdx < pageList.length; pIdx++) {
                    var pNum = pageList[pIdx];

                    if (pNum < 1 || pNum > totalPages) {
                        alert("「" + r.master.name + "」に指定されたページ(" + pNum + ")は、ドキュメントの範囲(1 ~ " + totalPages + ")を超えています。");
                        return;
                    }

                    if (usedPages[pNum]) {
                        alert("ページ " + pNum + " が複数の親ページ(" + usedPages[pNum] + " と " + r.master.name + ")で重複指定されています。");
                        return;
                    }

                    usedPages[pNum] = r.master.name;
                    pageAssignments[pNum] = r.master;
                }
            }
        }

       // 5. 実行処理(Undo対応)
        app.doScript(function() {
            for (var idx = 0; idx < doc.pages.length; idx++) {
                var pg = idx + 1;
                var targetMaster = pageAssignments[pg] ? pageAssignments[pg] : noneMaster;

                if (doc.pages[idx].appliedMaster == targetMaster) continue;

                doc.pages[idx].appliedMaster = targetMaster;
            }
        }, ScriptLanguage.JAVASCRIPT, [], UndoModes.ENTIRE_SCRIPT, "親ページの一括適用");

        win.close(1);

        // 6. 親ページが「なし」になっているページを検出して通知
        var unassignedPages = [];
        for (var checkPage = 1; checkPage <= totalPages; checkPage++) {
            if (!usedPages[checkPage]) {
                unassignedPages.push(checkPage);
            }
        }

        if (unassignedPages.length > 0) {
            var unassignedStr = pagesToRangeString(unassignedPages);
            alert("処理が完了しました。\n\n" + unassignedStr + " ページは親ページ[なし]です。");
        } else {
            alert("すべてのページに親ページを適用しました。");
        }
    };

    win.show();
})();

実験022_2026.6-1

【InDesign】大文字と小文字を変更をScriptUIで処理したい

InDesignの書式メニューにある[大文字と小文字の変更]。
ここからさらに4択となっておりまして、これがどうにかならなかとずっと思っていました。


この4択に対してショートカットを4つも当てるのが面倒だし、覚えられないだろうし…


それでスクリプトで解決すればいいと思いました。
AIのおかげで「こういうのを作りたい」というイメージがあれば、そしてきちんと指示を出せば、よほど複雑なものでなければ簡単に作れるようになりましたね。

スクリプトを実行すると、こんなScriptUIが出てくるので希望のボタンをクリックするだけです。


標準機能と同じで、テキストを選択した状態でも、テキストフレームを選択した状態でも動きます。

すべて大文字
すべて小文字
単語の先頭のみ大文字
文頭のみ大文字


ショートカットも1つ当てるだけなので楽になりました。

スクリプトはこんな感じです。もし使ってみたいという方がいましたら、以下をコピーしてテキストエディタにペーストして保存(拡張子は.jsx)してください。

//20260604_大文字と小文字を変更
#target indesign
#targetengine "changeCasePalette"

(function() {
    // パレットウィンドウを作成(作業中も常に前面に表示)
    var win = new Window("palette", "大文字 / 小文字の変更", undefined);
    win.orientation = "column";
    win.alignChildren = ["fill", "top"];
    win.margins = 10;
    win.spacing = 8;

    // ボタンの追加
    var btnUpper = win.add("button", undefined, "すべて大文字");
    var btnLower = win.add("button", undefined, "すべて小文字");
    var btnTitle = win.add("button", undefined, "単語の先頭のみ大文字");
    var btnSentence = win.add("button", undefined, "文頭のみ大文字");

    // テキスト処理用の共通関数
    function changeTextCase(mode) {
        if (app.documents.length === 0 || app.selection.length === 0) {
            return;
        }

        var sel = app.selection[0];
        
        try {
            // 選択範囲がテキスト(文字、単語、段落など)の場合
            if (sel.hasOwnProperty("baseline")) {
                sel.changecase(mode);
            } 
            // 選択範囲がテキストフレームやテキストパスの場合
            else if (sel.constructor.name === "TextFrame" || sel.constructor.name === "TextPath") {
                sel.texts[0].changecase(mode);
            } 
            else {
                alert("テキストまたはテキストフレームを選択してください。");
            }
        } catch (e) {
            alert("エラーが発生しました: " + e);
        }
    }

    // 各ボタンのクリックイベントにInDesignのChangecaseModeを割り当て
    btnUpper.onClick = function() { changeTextCase(ChangecaseMode.UPPERCASE); };
    btnLower.onClick = function() { changeTextCase(ChangecaseMode.LOWERCASE); };
    btnTitle.onClick = function() { changeTextCase(ChangecaseMode.TITLECASE); };
    btnSentence.onClick = function() { changeTextCase(ChangecaseMode.SENTENCECASE); };

    // ウィンドウを表示
    win.show();
})();

実験021_2025.11-1

InDesign】任意の段落スタイル・文字スタイルが当たっている場所を表示するスクリプト

デザイナーから支給されたInDesignデータで、段落スタイルや文字スタイルがたくさん使われてることあるじゃないですか。

どんだけ~

ちゃんとしたデータであれば各種スタイル名が分かりやすくなっていると思うのですが、たまに「こんなにたくさん段落スタイル必要なのかな」って思うこともあるわけです。

当然ながら、段落スタイル・文字スタイルが多いほど、どこにどのスタイルが当たっているか分かりにくくなるわけで「このスタイルはどこに使われているのかな」と思うことがありますよね。

そんなときは[検索と置換]から正規表現

①検索文字列に「.+」と入力
②検索形式で検索したい段落スタイルや文字スタイルを選択

これで選択したスタイルが当たっている場所を表示してくれますよね。(他にいい方法があったら教えてください)

この方法で解決できてしまうのですが、こういうことまでスクリプトでやってみたいなと思ってしまうわけです。

以前にミニ検索ウィンドウ“ミニけん”なんてものも作ってみましたが(結局ほとんど使ってない)ChatGPTを使って自分で作ったものには愛着が湧くものです。
p-makes-p.hatenablog.com

わざわざスクリプトを使うメリットと言えば、操作性が上がるということくらいでしょうか。

スクリプトを実行すると次のようなウィンドウが表示されます。

ドロップダウンリストにドキュメント内の段落スタイルと文字スタイルが表示されます。
検索ボタンを押して検索開始、次へと戻るボタンはその名の通りの動きをします。

たとえば、ドキュメント内の段落スタイル・文字スタイルが次のようになっていたら、


ドロップダウンリストにはこのように表示されます。
段落スタイルと文字スタイルの区切りとしては「============== 文字スタイル ==============」と入れています。またスタイルグループで管理している部分は「--- スタイルグループ名 --------------------)」で区切るようにしています。

下の図が検索ボタンを押した結果。ドキュメント内に選択したスタイルが何件あって、選択中のものが何件目か分かるようになっています。

次へボタンを押すと2件目を表示してくれます。

文字スタイルも段落スタイルと同様に動きます。

選択中のスタイルがドキュメント内のどこにもあたっていない場合は「該当なし」と表示されます。

全体のソースはこんな感じ
【追記】表内テキストに当たっているスタイルが検索に引っかからないので原因解析中です

#target "indesign"
#targetengine "StyleSearch"

(function () {

    if (app.documents.length === 0) {
        alert("ドキュメントを開いてください。");
        return;
    }

    var doc = app.activeDocument;

    // ==============================
    // フォルダ内のスタイルとグループ情報を取得するためのユーティリティ関数
    // ==============================

    function getGroupedStyles(styleCollection, styleGroupCollection, styleType) {
        var styles = [];
        var ddItems = [];
        
        // 1. ルートレベルのスタイルを追加
        for (var i = 0; i < styleCollection.length; i++) {
            var style = styleCollection[i];
            styles.push(style);
            ddItems.push(style.name);
        }

        // 2. スタイルグループを再帰的に処理
        function processGroups(groups) {
            for (var i = 0; i < groups.length; i++) {
                var group = groups[i];
                
                // グループ名を区切り線として追加
                ddItems.push("--- " + group.name + " --------------------");
                
                // グループ内のスタイルを追加
                var groupStyles = (styleType === "paragraph") ? group.paragraphStyles : group.characterStyles;
                for (var j = 0; j < groupStyles.length; j++) {
                    var style = groupStyles[j];
                    styles.push(style);

                    ddItems.push(style.name);
                }
                
                // サブグループを再帰的に処理
                var subGroups = (styleType === "paragraph") ? group.paragraphStyleGroups : group.characterStyleGroups;
                if (subGroups.length > 0) {
                    processGroups(subGroups);
                }
            }
        }

        processGroups(styleGroupCollection);
        return {styles: styles, ddItems: ddItems};
    }


    // ==============================
    // スタイル情報格納
    // ==============================
    var paraData = getGroupedStyles(doc.paragraphStyles, doc.paragraphStyleGroups, "paragraph");
    var charData = getGroupedStyles(doc.characterStyles, doc.characterStyleGroups, "character");
    
    // スタイルオブジェクトのリスト
    var paraStyles = paraData.styles;
    var charStyles = charData.styles;
    
    // ドロップダウンリストの全項目(文字列)のリスト
    var allItems = paraData.ddItems;
    
    // 段落と文字の間の区切り線の位置を記録
    var dividerIndex = allItems.length;
    
    // 全項目に段落と文字の区切り線と文字スタイルを追加
    allItems.push("============== 文字スタイル ==============");
    allItems = allItems.concat(charData.ddItems);
    
    // スタイル検索時に使用する、スタイルオブジェクトへのインデックス
    var styleIndexMap = [];
    var paraStyleCount = 0; // 段落スタイルオブジェクト配列(paraStyles)の現在のインデックス
    var charStyleCount = 0; // 文字スタイルオブジェクト配列(charStyles)の現在のインデックス


    // ==============================
    // UI
    // ==============================
    var win = new Window("palette", "スタイル検索ナビゲーター");
    win.orientation = "column";
    win.alignChildren = "fill";
    win.preferredSize = [360, 170]; 

    var grpStyles = win.add("group");
    grpStyles.orientation = "column";
    grpStyles.alignChildren = "left";

    grpStyles.add("statictext", undefined, "スタイル:");
    // 全項目リスト `allItems` を使用してDDリストを作成
    var ddStyle = grpStyles.add("dropdownlist", undefined, allItems);
    ddStyle.preferredSize = [320, 22];

    // ddStyleの項目を走査し、選択されたインデックスがスタイルオブジェクトのどれに対応するかをマッピング
    for (var i = 0; i < ddStyle.items.length; i++) {
        var itemText = ddStyle.items[i].text;
        
        if (itemText.indexOf("---") === 0 || itemText.indexOf("===") === 0) {
            // グループ区切り線 または 段落/文字の区切り線
            styleIndexMap.push({isStyle: false, isPara: false, index: -1});
        } else if (i < dividerIndex) {
            // 段落スタイル
            styleIndexMap.push({isStyle: true, isPara: true, index: paraStyleCount});
            paraStyleCount++;
        } else if (i > dividerIndex) {
            // 文字スタイル
            styleIndexMap.push({isStyle: true, isPara: false, index: charStyleCount});
            charStyleCount++;
        } else {
            // 段落/文字の区切り線(実際には使わないが念のため)
            styleIndexMap.push({isStyle: false, isPara: false, index: -1});
        }
    }

    // 最初のスタイルを選択状態にする
    ddStyle.selection = 0; 
    
    // 区切り線が選択された場合、直前の有効な項目に戻す処理を強化
    ddStyle.onChange = function() {
        var selectedIndex = ddStyle.selection.index;
        // 選択された項目がスタイルではない場合
        if (!styleIndexMap[selectedIndex].isStyle) {
            // 選択された項目の直前の有効なスタイルに戻す
            for (var i = selectedIndex - 1; i >= 0; i--) {
                if (styleIndexMap[i].isStyle) {
                    ddStyle.selection = i;
                    break;
                }
            }
        }
    };


    var lblCount = win.add("statictext", undefined, "0 / 0 件");
    lblCount.alignment = "left";

    var grpBtn = win.add("group");
    grpBtn.orientation = "row";
    grpBtn.alignment = "center";

    var btnPrev = grpBtn.add("button", undefined, "戻る");
    var btnSearch = grpBtn.add("button", undefined, "検索");
    var btnNext = grpBtn.add("button", undefined, "次へ");

    // ==============================
    // 検索結果保持領域 
    // ==============================
    var foundRanges = [];
    var currentIndex = -1;


    // ==============================
    // 検索メイン(
    // ==============================
    function doSearch() {
        foundRanges = [];
        currentIndex = -1;

        var selectedIndex = ddStyle.selection.index;
        var mapEntry = styleIndexMap[selectedIndex];
        
        // 選択された項目がスタイルではない場合
        if (!mapEntry.isStyle) {
            alert("スタイルを選択してください。");
            lblCount.text = "0 / 0 件";
            return;
        }
        
        var isPara = mapEntry.isPara;
        var targetStyle;
        
        if (isPara) {
            // 段落スタイル
            targetStyle = paraStyles[mapEntry.index];
        } else {
            // 文字スタイル
            targetStyle = charStyles[mapEntry.index];
        }

        var stories = doc.stories;

        for (var s = 0; s < stories.length; s++) {
            var st = stories[s];

            if (isPara) {
                // 段落スタイル検索
                var paras = st.paragraphs;
                for (var p = 0; p < paras.length; p++) {
                    var para = paras[p];
                    if (!isValidText(para)) continue;

                    if (para.appliedParagraphStyle == targetStyle) {
                        foundRanges.push(para);
                    }
                }
            } else {
                // 文字スタイル検索
                var ranges = st.textStyleRanges;
                for (var r = 0; r < ranges.length; r++) {
                    var rg = ranges[r];
                    if (!isValidText(rg)) continue;

                    if (rg.appliedCharacterStyle == targetStyle) {
                        foundRanges.push(rg);
                    }
                }
            }
        }

        if (foundRanges.length === 0) {
            lblCount.text = "0 / 0 件";
            alert("該当なし");
            return;
        }

        currentIndex = 0;
        selectResult(currentIndex);
        lblCount.text = "1 / " + foundRanges.length + " 件";
    }


    // ==============================
    // その他ユーティリティ関数
    // ==============================
    function isValidText(txt) {
        var frame = null;
        if (txt.parent instanceof TextFrame) {
            frame = txt.parent;
        } else if (txt.parentTextFrames && txt.parentTextFrames.length > 0) {
            frame = txt.parentTextFrames[0];
        }

        if (!frame) return false;
        var layer = frame.itemLayer;
        if (!layer.visible || layer.locked) return false;
        if (frame.locked) return false;
        return true;
    }

    function selectResult(index) {
        var obj = foundRanges[index];
        app.select(obj);

        var page = findPage(obj);
        if (page) {
            app.activeWindow.activePage = page;
            try { app.activeWindow.zoom(ZoomOptions.fitPage); } catch (e) { }
        }

        lblCount.text = (index + 1) + " / " + foundRanges.length + " 件";
    }

    function findPage(obj) {
        try {
            if (obj.parentTextFrames && obj.parentTextFrames.length > 0) {
                var tf = obj.parentTextFrames[0];
                if (tf.parentPage) return tf.parentPage;
            }
        } catch (e) { }
        return null;
    }


    // ==============================
    // ボタン 
    // ==============================
    btnSearch.onClick = doSearch;

    btnNext.onClick = function () {
        if (foundRanges.length === 0) { doSearch(); return; }
        if (currentIndex < foundRanges.length - 1) {
            currentIndex++;
            selectResult(currentIndex);
        } else {
            alert("最後の結果です。");
        }
    };

    btnPrev.onClick = function () {
        if (foundRanges.length === 0) { doSearch(); return; }
        if (currentIndex > 0) {
            currentIndex--;
            selectResult(currentIndex);
        } else {
            alert("最初の結果です。");
        }
    };

    win.show();

})();

検索と置換パネルで事足りてしまうんですが、よかったら使ってみてください。


DTP関連のnoteも書いてます。よかったらご覧ください。
note.com

実験021_2025.10-2

InDesign】レイヤーAのオブジェクトをレイヤーBに移動するスクリプト

いつからかもうスクリプトを作るときはChatGPTを使うのが当たり前になってしまいました…。

今回はInDesignでレイヤーAにあるオブジェクトを、親ページアイテムを含め一括でレイヤーBに移動するスクリプトです。前任者のInDesignファイルを開いてみたら、やたらとレイヤーが使われていて、こんなに細かくレイヤーを分ける必要はないのではと思うようなこともあります。

わたしはあまりレイヤーを細かく分ける方ではありません。レイヤー以外の部分でも、自分が引き継いだ仕事のInDesignファイルは自分仕様に変えてしまいます。

なので、今回のようなスクリプトを作りました。

使い方は簡単です。スクリプトを実行すると、次のようなUIが表示されるので、移動元のレイヤーと移動先のレイヤーを選んでOKを押すだけです。

実行時に非表示やロックされているレイヤーがあると、次のようなアラートが出て、そのあとに出るUIからは非表示レイヤーとロックされたレイヤーは選べなくなります。

移動元レイヤーと移動先レイヤーで、利用可能なレイヤー(非表示・ロックされていないレイヤー)が2つ以上ないと怒られます。

移動し終わるとオブジェクトを何個移動したか教えてくれます。ちなみにロックされているアイテムは、いったんロックを解除したあとにレイヤーを移動し、移動後にまたロックしてくれます。

以下ソース。同じようなスクリプトがあるかもしれませんが、よかったら使ってみてください。

(function () {
    if (app.documents.length === 0) {
        alert("ドキュメントを開いてください。");
        return;
    }

    var doc = app.activeDocument;
    var layers = doc.layers;

    if (layers.length < 2) {
        alert("2つ以上のレイヤーが必要です。");
        return;
    }

    // ==== レイヤーのうち、表示中かつロックされていないもののみを抽出 ====
    var availableLayers = [];
    var availableLayerNames = [];
    var excludedLayers = [];

    for (var i = 0; i < layers.length; i++) {
        var lyr = layers[i];
        if (!lyr.locked && lyr.visible) {
            availableLayers.push(lyr);
            availableLayerNames.push(lyr.name);
        } else {
            excludedLayers.push(lyr.name);
        }
    }

    if (availableLayers.length < 2) {
        alert("利用可能なレイヤーが2つ以上必要です。\n(非表示またはロック中のレイヤーがあるため不足しています)");
        return;
    }

    // 除外されたレイヤーがある場合、注意を表示
    if (excludedLayers.length > 0) {
        alert("次のレイヤーは非表示またはロックされているため、選択できません:\n\n" + excludedLayers.join("\n"));
    }

    // ==== UI作成 ====
    var dialog = new Window("dialog", "レイヤーオブジェクトの移動");
    dialog.orientation = "column";
    dialog.alignChildren = ["left", "top"];
    dialog.spacing = 10;
    dialog.margins = 16;

    // 移動元
    var fromGroup = dialog.add("group");
    fromGroup.add("statictext", undefined, "移動元:");
    var fromDropdown = fromGroup.add("dropdownlist", undefined, availableLayerNames);
    fromDropdown.selection = 0;

    // 移動先
    var toGroup = dialog.add("group");
    toGroup.add("statictext", undefined, "移動先:");
    var toDropdown = toGroup.add("dropdownlist", undefined, availableLayerNames);
    toDropdown.selection = (availableLayerNames.length > 1 ? 1 : 0);

    // ボタン
    var buttonGroup = dialog.add("group");
    buttonGroup.alignment = "right";
    var okButton = buttonGroup.add("button", undefined, "OK");
    var cancelButton = buttonGroup.add("button", undefined, "キャンセル");

    // === OKボタンクリック処理 ===
    okButton.onClick = function () {
        // selection が null の可能性に備えて保険
        if (!fromDropdown.selection || !toDropdown.selection) {
            alert("レイヤーを選択してください。");
            return;
        }

        if (fromDropdown.selection.index === toDropdown.selection.index) {
            alert("移動元と移動先のレイヤーが同じです。別のレイヤーを選択してください。");
            return;
        }

        var fromLayer = availableLayers[fromDropdown.selection.index];
        var toLayer = availableLayers[toDropdown.selection.index];

        // === Undo可能な処理 ===
        app.doScript(function () {
            var allPages = [].concat(
                doc.pages.everyItem().getElements(),
                doc.masterSpreads.everyItem().pages.everyItem().getElements()
            );

            var movedCount = 0;

            for (var p = 0; p < allPages.length; p++) {
                var page = allPages[p];
                var items = page.allPageItems;

                for (var i = items.length - 1; i >= 0; i--) {
                    var item = items[i];
                    try {
                        if (item.itemLayer === fromLayer) {
                            var wasLocked = item.locked;
                            if (wasLocked) item.locked = false;

                            item.itemLayer = toLayer;
                            movedCount++;

                            if (wasLocked) item.locked = true;
                        }
                    } catch (e) {
                        // 無視
                    }
                }
            }

            alert(movedCount + " 個のオブジェクトを移動しました。");

        }, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "レイヤーオブジェクト移動");

        dialog.close();
    };

    cancelButton.onClick = function () {
        dialog.close();
    };

    dialog.show();
})();

実験020_2025.10-1

InDesign】“ミニけん”少し安定?

前回はキーボードイベントで躓いていたミニ検索ウィンドウのミニけん。

p-makes-p.hatenablog.com

① 1度目のEnterでは“検索”を開始してくれるが、2度目のEnterで、ヒットしたテキスト(選択範囲)で改行してしまう
②「次へ」や「前へ」のキーボードショートカットが効かない

といった問題があったのですが、こじらせたクマーさんの次のスクリプトを参考にさせていただいたところ、安定したと思います。

note.com

このスクリプトめちゃくちゃ楽しく使わせていただいてます。

わたしがやりたかったのが、キーボードショートカットを使って「次へ」「前へ」と検索をかけていきたかったのですが、これがどうもできなかった。

そこで、こじらせたクマーさんのスクリプトのキーボードショートカットの部分を参考に(というかコピー)させていただきました。

以下、コピーさせていただいた部分

// キーボードショートカット
     w.addEventListener("keydown", function(k){
        try {
            var isMac = $.os.indexOf("Mac") >= 0;
            var cmdKey = isMac ? k.metaKey : k.ctrlKey;
            
            if (k.keyName === "Enter") {
                // Cmd (Ctrl)+Shift+Enter → 前へ
                if (cmdKey && k.shiftKey) {
                    goPrev();
                    k.preventDefault();
                }
                // Cmd (Ctrl)+Enter → 次へ(適用なし)
                else if (cmdKey) {
                   goNext();
                    k.preventDefault();
                }
                // Enter → 適用 & 次へ
                else {
                   doSearch();
                   k.preventDefault();
                }
            }
        } catch(e){}
    });


全体のソースはこんな感じ

#target indesign
#targetengine "persistentSearch"

(function () {
    if (app.documents.length === 0) {
        alert("ドキュメントを開いてください");
        return;
    }

    var doc = app.activeDocument;
    var hits = [];
    var currentIndex = -1;

    // --- ダイアログ ---
    var w = new Window("palette", "ミニけん");

    // 検索文字入力欄
    var findGroup = w.add("group");
    var findField = findGroup.add("edittext", undefined, "");
    findField.characters = 24;
    findField.active = true;

    // ボタン群
    var buttonGroup = w.add("group");
    var prevBtn = buttonGroup.add("button", undefined, "前へ");
    var findBtn = buttonGroup.add("button", undefined, "検索");
    var nextBtn = buttonGroup.add("button", undefined, "次へ");

    // --- 選択とページ移動 ---
    function selectAndShow(obj) {
        if (!obj) return;
        app.select(obj);
        try {
            var tf = obj.parentTextFrames[0];
            var pg = tf.parentPage;
            if (pg) app.layoutWindows[0].activePage = pg;
        } catch (e) {}
        findField.active = true;
    }

    // --- 検索 ---
    function doSearch() {
        var txt = findField.text;
        if (!txt) return;

        app.findGrepPreferences = NothingEnum.nothing;
        app.findChangeGrepOptions.includeLockedLayersForFind = false;
        app.findChangeGrepOptions.includeHiddenLayers = false;
        app.findChangeGrepOptions.includeMasterPages = false;
        app.findChangeGrepOptions.includeFootnotes = false;

        app.findGrepPreferences.findWhat = txt;
        hits = doc.findGrep();
        currentIndex = -1;

        if (hits.length > 0) {
            goNext();
        } else {
            alert("見つかりませんでした");
        }
    }

    // --- 次へ ---
    function goNext() {
        if (hits.length === 0) {
            alert("検索結果がありません");
            return;
        }
        if (currentIndex + 1 >= hits.length) {
            alert("次は見つかりません");
            currentIndex = hits.length - 1;
            return;
        }
        currentIndex++;
        selectAndShow(hits[currentIndex]);
    }

    // --- 前へ ---
    function goPrev() {
        if (hits.length === 0) {
            alert("検索結果がありません");
            return;
        }
        if (currentIndex - 1 < 0) {
            alert("前は見つかりません");
            currentIndex = 0;
            return;
        }
        currentIndex--;
        selectAndShow(hits[currentIndex]);
    }

    // --- ボタンイベント ---
    findBtn.onClick = doSearch;
    nextBtn.onClick = goNext;
    prevBtn.onClick = goPrev;

// キーボードショートカット
     w.addEventListener("keydown", function(k){
        try {
            var isMac = $.os.indexOf("Mac") >= 0;
            var cmdKey = isMac ? k.metaKey : k.ctrlKey;
            
            if (k.keyName === "Enter") {
                // Cmd (Ctrl)+Shift+Enter → 前へ
                if (cmdKey && k.shiftKey) {
                    goPrev();
                    k.preventDefault();
                }
                // Cmd (Ctrl)+Enter → 次へ(適用なし)
                else if (cmdKey) {
                   goNext();
                    k.preventDefault();
                }
                // Enter → 適用 & 次へ
                else {
                   doSearch();
                   k.preventDefault();
                }
            }
        } catch(e){}
    });
    w.frameLocation = [10, 10];
    w.show();
})();

これでEnterで検索、Ctrl+Enterで次へ、Ctrl+Shift+Enterで前へ移動してくれるようになりました。
(ミニけんがアクティブになっていないとCtrl+Enterでクイックアクセスが開いてしまうw)

とりあえずやりたいことはできたので、自分の中ではここで一区切りですが、ミニけんで次々とワードを検索していくと、ある時点で全く反応しなくなることがあるので、まだまだ配布できるものではないと思いました。その場合はスクリプトを一旦終了して(パレットを閉じる)再度実行すると動いてくれます。

というか、やっぱり標準機能を使った方がいいかも知れません…orz

実験019_2025.9-2

InDesign】ミニ検索ウィンドウ “ミニけん”

InDesignの標準機能の「検索と置換」のウィンドウが大きくて常に表示してるのは邪魔に感じるし、いちいちCtrl+F(Windows)でON/OFFを切り替えるのも手間…
と感じることがあり、シンプルに検索だけできるスクリプトを作りたいと思ったので作りました。(今回もChatGPTを使用)

【仕様】標準機能と差別化するために、極力シンプルにしました

① 検索対象は“ドキュメント”

ドロップダウンリストで「ストーリー」や「選択範囲」を追加することもできると思うのですが、ウィンドウを極力小さくしたかったので「ドキュメント」のみに。

② 検索ウィンドウは常に表示

後述しますが、ここがどうしたらよいのか分からなかったです。やはり勉強不足…

③「前へ」と「次へ」ボタンでヒットした箇所を戻したり進めたりできるようにする

ここまで決めるのにだいぶ時間がかかりました。どうしても、ああしたい、こうしたいと欲が出るのですが、試行錯誤して自分がいちばん使いやすいかたちにしました。

そして出来たのがこのミニ検索ウィンドウ “ミニけん”
(かわいい)

InDesignの「検索と置換」と比べてもだいぶ小さくなりました。これなら常に表示していても邪魔になりません。

使い方は簡単です。
検索したい文字を入力してEnterを押します。するとヒットした箇所を表示してくれます。

検索文字列が存在するのにヒットしないことがありました。
このEnterを使う部分がよくなかったのかなと思い、削除したら安定したと思います。(後述)

そのまま、次へをクリックすると、次にヒットした箇所を表示してくれます。

「前へ」をクリックすれば、ひとつ前に戻してくれます。次へ・前への操作は標準機能より操作性がいいかも知れません。(自画自賛

ちょっと注意が必要で、InDesignの「検索と置換」は2つ目以降を検索するとき、Enterキーを連打で「次を検索」を繰り返してくれますが、ミニけんさんは一度ヒットした後にもう一度Enterを押してしまうと、ヒットしたテキストフレーム内で改行してしまいます。

Enterは検索文字入力後に一回のみ押して、その次を検索したいときは「次へ」をクリックします。

「次へ」をクリックして次がない場合と、「前へ」をクリックして前がない場合はアラートが表示されます。
(この状態でさらに「次へ」や「前へ」を押すともう一度アラートが出てしまいますが…気にしないことにします…)

当たり前ですが、検索文字がドキュメント内にないときは「見つかりませんでした」と言ってくれます。

スクリプト実行後のパレットの表示位置は画面左上にしました。邪魔にならないようにという配慮からですが、邪魔ならソースの以下の箇所を削除または変更してください。

w.frameLocation = [10, 10];//お好みの位置にダイアログを表示(ここを削除で画面センターに表示される)

以下、ソースです。

#target indesign
#targetengine "persistentSearch"

(function () {
    if (app.documents.length === 0) {
        alert("ドキュメントを開いてください");
        return;
    }

    var doc = app.activeDocument;
    var hits = [];
    var currentIndex = -1;

    // --- ダイアログ ---
    var w = new Window("palette", "ミニけん");

    // 検索文字入力欄
    var findGroup = w.add("group");
    var findField = findGroup.add("edittext", undefined, "");
    findField.characters = 24;
    findField.active = true;

    // ボタン群
    var buttonGroup = w.add("group");
    var prevBtn = buttonGroup.add("button", undefined, "前へ");
    var findBtn = buttonGroup.add("button", undefined, "検索");
    var nextBtn = buttonGroup.add("button", undefined, "次へ");

    // --- 選択とページ移動 ---
    function selectAndShow(obj) {
        if (!obj) return;
        app.select(obj);

        try {
            var tf = obj.parentTextFrames[0];
            var pg = tf.parentPage;
            if (pg) app.layoutWindows[0].activePage = pg;
        } catch (e) {}

        findField.active = true; // フォーカスを検索欄に戻す
    }

    // --- 正規表現検索 ---
    function doSearch() {
        var txt = findField.text;
        if (!txt) return;

////////////////////////////////////////検索オプションGrep
app.findGrepPreferences = NothingEnum.nothing;
app.findChangeGrepOptions.includeLockedLayersForFind = false;//ロックされたレイヤーをふくめるかどうか
app.findChangeGrepOptions.includeHiddenLayers = false;//非表示レイヤーを含めるかどうか
app.findChangeGrepOptions.includeMasterPages = false;//マスターページを含めるかどうか
app.findChangeGrepOptions.includeFootnotes = false;//脚注を含めるかどうか
////////////////////////////////////////

        app.findGrepPreferences.findWhat = txt;

        hits = doc.findGrep();
        currentIndex = -1;

        if (hits.length > 0) {
            goNext();
        } else {
            alert("見つかりませんでした");
        }
	
    }

    // --- 次を検索 ---
    function goNext() {
        if (hits.length === 0) {
            alert("検索結果がありません");
            return;
        }

        if (currentIndex + 1 >= hits.length) {
            alert("次は見つかりません");
            currentIndex = hits.length - 1; // 末端に固定
            return;
        }

        currentIndex++;
        selectAndShow(hits[currentIndex]);
    }

    // --- 前に戻る ---
    function goPrev() {
        if (hits.length === 0) {
            alert("検索結果がありません");
            return;
        }

        if (currentIndex - 1 < 0) {
            alert("前は見つかりません");
            currentIndex = 0; // 先頭に固定
            return;
        }

        currentIndex--;
        selectAndShow(hits[currentIndex]);
    }

    // --- ボタンイベント ---
    findBtn.onClick = doSearch;
    nextBtn.onClick = goNext;
    prevBtn.onClick = goPrev;

    w.frameLocation = [10, 10];//お好みの位置にダイアログを表示(ここを削除で画面センターに表示される)
    w.show(); 
})();

【躓きポイント】
① ダイアログを常に表示するやり方が分からなかった
(先述のとおり)
スクリプトを実行したダイアログが一瞬で消えてしまって何もできない

ChatGPTもたまにとんちんかんなことを言ってくるので、結局は自分で調べなくてはならないですね。

①に関しては、dialogやwindowではなく、paletteを使えばよい

②に関しては、「#targetengine ~」を入れておく必要がある

Illustratorスクリプトだともうちょっと複雑なのでしょうか。
sppy.stars.ne.jp
2-hats.hateblo.jp

【別バージョン】
ミニけんをさらにコンパクトにした“ミニけんLite”なるものも作ってみました。

キーボードの「→」「←」で「次へ」と「前へ」ができるようにしました
(関数名が先のと違ってます)

// --- キーイベント ---
findField.addEventListener("keydown", function (k) {
    if (k.keyName === "Enter") {
        doSearchFirst();
        k.preventDefault();
    } else if (k.keyName === "Right") {
        doSearchNext();
        k.preventDefault();
    } else if (k.keyName === "Left") {
        doSearchPrev();
        k.preventDefault();
    }

だけど、
① たまに「→」「←」を押しても反応しなくなる
② paletteではなく、dialogでないとキーボード操作できない?

という疑問が出てきたので、ミニけんLiteの公開は見送りました。

【追記】
打ち消し線の部分、検索したい文字を入力してEnterでヒットとしていた部分ですが、次のようなコードでした。

    // --- Enterキーで最初の検索のみ ---
    findField.addEventListener("keydown", function (k) {
        if (k.keyName === "Enter") {
            doSearch();
            k.preventDefault();
        }
    });

上記の「→」「←」のときもそうですが、キーボードイベントをパレットで捕捉って難しいのかしら

実験018_2025.9-1

InDesign】オブジェクトの間隔を表示・調整するスクリプト

2つのオブジェクトがどれくらい空いているのか知りたくなるときあるじゃないですか。

例えば左右に並んだ2つのオブジェクトの間隔を知りたかったら、間隔ツールを当ててあげればよいのですが、スクリプトで表示させた方が速いですし、さらにそのまま間隔を変更できるようにもしたいと思い、今回もChatGPTとやりとりしながらスクリプトを作成しました。

次のような仕様にしました。

① 選択した2つ以上のオブジェクトの基準を常に、いちばん左にあるオブジェクトとする

② 2つのオブジェクトの間隔をダイアログに表示する
確認のみの場合はそのままキャンセル
変更したい場合は間隔を入力して指定する

3つ以上のオブジェクトが選択されている場合はいちばん左にあるオブジェクトと、そのとなりにあるオブジェクトの間隔を表示する

間隔を変更した場合は左から2番目のオブジェクトと3番目以降のオブジェクトの間隔も同様に変更する

例えば、図のように選択してスクリプトを実行した場合、左から1番目と2番目のオブジェクトの間隔を表示する

ここで指定できるのは左から1番目と2番目のオブジェクトの間隔のみ

それが2番目のオブジェクトと3番目以降のオブジェクトの間隔にも反映される

④ 2つのオブジェクトの間にアキがなく、重なっている場合はマイナスの数値を表示する

オブジェクト間のアキとは言えないので数値を入力できないようにグレーアウトしようかと思いましたが、それなりに使い道があるかと思ってこの仕様にしました。
ただちょっと、混乱しやすいと思います。

⑤ 空欄にした場合は変更なし

例えばX方向の間隔は調整したいけど、Y方向は変更したくない場合は空欄にする

図のような場合で、X方向の間隔のみを調整したい場合は

Y方向の間隔を空欄にすると…

X方向の間隔のみ調整してくれます

以下、ソース

#target indesign

(function () {
    if (app.documents.length === 0 || app.selection.length < 2) {
        alert("2つ以上のオブジェクトを選択してください。");
        return;
    }

    var sel = [];
    for (var i = 0; i < app.selection.length; i++) sel.push(app.selection[i]);

    // --- ヘルパー ---
    function getGB(obj) { return obj.geometricBounds; } // [top, left, bottom, right]

    function formatGap(val) {
        if (!isFinite(val) || Math.abs(val) < 0.0001) return "0";
        var rounded = Math.round(val * 1000) / 1000;
        var s = rounded.toFixed(3);
        return s.replace(/\.?0+$/, "");
    }

    function strTrim(s) { return String(s).replace(/^\s+|\s+$/g, ""); }

    function isStrictNumber(s) {
        s = String(s).replace(/,/g, ".").replace(/^\s+|\s+$/g, "");
        return /^-?\d+(\.\d+)?$/.test(s);
    }

    function parseNumeric(s) { return parseFloat(String(s).replace(/,/g, ".")); }

    // --- X優先→Yでソート ---
    sel.sort(function (a, b) {
        var A = getGB(a), B = getGB(b);
        if (A[1] === B[1]) return A[0] - B[0];
        return A[1] - B[1];
    });

    var firstObj = sel[0];
    var secondObj = sel[1];
    var gb1 = getGB(firstObj), gb2 = getGB(secondObj);

    // gap計算(マイナスも表示)
    var gapX = gb2[1] - gb1[3];
    var gapY = gb2[0] - gb1[2];

    // --- ダイアログ ---
    var w = new Window("dialog", "オブジェクト間隔設定(mm)");
    w.orientation = "column";

    var xGroup = w.add("group");
    xGroup.add("statictext", undefined, "X方向の間隔:");
    var xGapField = xGroup.add("edittext", undefined, formatGap(gapX));
    xGapField.characters = 8;

    var yGroup = w.add("group");
    yGroup.add("statictext", undefined, "Y方向の間隔:");
    var yGapField = yGroup.add("edittext", undefined, formatGap(gapY));
    yGapField.characters = 8;

    var btnGroup = w.add("group");
    btnGroup.alignment = "right";
    btnGroup.add("button", undefined, "OK", { name: "ok" });
    btnGroup.add("button", undefined, "キャンセル", { name: "cancel" });

	app.activate();
    if (w.show() !== 1) return; // キャンセル

    // --- 入力取得とチェック ---
    var rawX = strTrim(xGapField.text);
    var rawY = strTrim(yGapField.text);
    var newGapX = null, newGapY = null;

    if (rawX !== "") {
        if (!isStrictNumber(rawX)) {
            alert("X方向の間隔には有効な数値のみを入力してください。");
            return;
        }
        newGapX = parseNumeric(rawX);
    }
    if (rawY !== "") {
        if (!isStrictNumber(rawY)) {
            alert("Y方向の間隔には有効な数値のみを入力してください。");
            return;
        }
        newGapY = parseNumeric(rawY);
    }

    // --- 移動処理(Undoまとめ) ---
    try {
        app.doScript(function () {
            for (var i = 1; i < sel.length; i++) {
                var prev = sel[i - 1];
                var obj = sel[i];
                var prevGB = getGB(prev);
                var objGB = getGB(obj);
                var dx = 0, dy = 0;

                if (newGapX !== null) {
                    dx = (prevGB[3] + newGapX) - objGB[1];
                }
                if (newGapY !== null) {
                    dy = (prevGB[2] + newGapY) - objGB[0];
                }
                if (dx !== 0 || dy !== 0) {
                    obj.move(undefined, [dx, dy]);
                }
            }
        }, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "オブジェクト間隔調整");
    } catch (e) {
        alert("移動中にエラーが発生しました: " + e.message);
    }

})();


たいしたスクリプトではなくても、自分で(ほぼChatGPTに頼ってしまっているけど)作ったスクリプトが動いてくれると嬉しいものですね。