sharepointブックマークレットはてな記法

1
2

javascript:(function () {
  'use strict';

  const logs = [];
  function logError(tag, err) {
    const msg = `[${tag}] ${err && err.name ? `${err.name}: ${err.message}` : String(err)}`;
    logs.push(msg);
  }

  function escapeHtml(s) {
    return String(s)
      .replaceAll('&', '&')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;');
  }

  function extractItemKey(el) {
    try {
      const da = el.getAttribute('data-actions');
      if (!da) return null;
      const m = da.match(/itemKey[^0-9]*([0-9]+)/);
      return m ? m[1] : null;
    } catch (err) {
      logError('extractItemKey', err);
      return null;
    }
  }

  function getBreadcrumbPath() {
    const parts = [];
    try {
      document.querySelectorAll('div.breadcrumbRoot_b6af7cfe li span').forEach((el) => {
        const txt = (el.textContent || '').trim();
        if (txt) parts.push(txt);
      });
    } catch (err) {
      logError('getBreadcrumbPath-doclib', err);
    }
    try {
      document
        .querySelectorAll('.od-ListForm-breadcrumb nav ul li a, .od-ListForm-breadcrumb nav ul li span')
        .forEach((el) => {
          const txt = (el.textContent || '').trim();
          if (txt && !parts.includes(txt)) parts.push(txt);
        });
    } catch (err) {
      logError('getBreadcrumbPath-lists', err);
    }
    return parts;
  }

  function getOverflowBreadcrumb() {
    const parts = [];
    try {
      document.querySelectorAll('#breadcrumb-menu-id li span').forEach((el) => {
        const txt = (el.textContent || '').trim();
        if (txt) parts.push(txt);
      });
    } catch (err) {
      logError('getOverflowBreadcrumb', err);
    }
    return parts;
  }

  function getListsFieldValue(labelText) {
    try {
      const labels = document.querySelectorAll('label, [class*="fieldLabel" i], [class*="FieldLabel"]');
      for (const label of labels) {
        if (!(label.textContent || '').trim().includes(labelText)) continue;
        let container = label.closest('div');
        let depth = 0;
        while (container && depth < 4) {
          const valueEl = container.querySelector(
            'div.ReactFieldEditor-core--display.ReactFieldEditor-core--display-ReadOnly.ReactFieldEditor-linkFocusIndicator > div'
          );
          if (valueEl) {
            const val = (valueEl.textContent || '').trim();
            if (val) return val;
          }
          container = container.parentElement;
          depth++;
        }
      }
      return null;
    } catch (err) {
      logError(`getListsFieldValue:${labelText}`, err);
      return null;
    }
  }

  function getCheckedRows() {
    const results = [];
    try {
      const rows = document.querySelectorAll('div[aria-selected=true][data-automationid^=row-selection-]');
      rows.forEach((rowEl) => {
        const automationId = rowEl.getAttribute('data-automationid') || '';
        const idMatch = automationId.match(/row-selection-(.+)$/);
        const rawId = idMatch ? idMatch[1] : '';

        const ancestorRow = rowEl.closest('[role=row]');
        let isHeaderLike = false;
        if (ancestorRow && ancestorRow.getAttribute('aria-rowindex') === '1') {
          isHeaderLike = true;
        }
        if (!rawId || /^(all|header|select-?all)$/i.test(rawId)) {
          isHeaderLike = true;
        }
        if (isHeaderLike) return;

        let container = rowEl.parentElement;
        let depth = 0;
        let nameSpan = null;
        while (container && depth < 5 && !nameSpan) {
          nameSpan = container.querySelector('span[role=button][data-id=heroField]');
          if (!nameSpan) container = container.parentElement;
          depth++;
        }
        if (nameSpan) {
          const name = (nameSpan.textContent || '').trim();
          const itemKey = extractItemKey(nameSpan);
          results.push({ name, itemKey });
        } else {
          logError('getCheckedRows-nameNotFound', 'row-selection element found but file/item name span not located nearby');
        }
      });

      const seen = new Set();
      const deduped = [];
      for (const row of results) {
        const key = row.itemKey || `name:${row.name}`;
        if (!seen.has(key)) {
          seen.add(key);
          deduped.push(row);
        } else {
          logError('getCheckedRows-dedup', `重複行を除外しました: ${row.name}`);
        }
      }
      return deduped;
    } catch (err) {
      logError('getCheckedRows', err);
      return results;
    }
  }

  function buildListsItemUrl(itemKey) {
    try {
      const href = location.href;
      const idx = href.indexOf('/Lists/');
      if (idx === -1) return null;
      const m = href.substring(idx).match(/^\/Lists\/([^/]+)\//);
      if (!m) return null;
      return `${href.substring(0, idx)}/Lists/${m[1]}/DispForm.aspx?ID=${itemKey}`;
    } catch (err) {
      logError('buildListsItemUrl', err);
      return null;
    }
  }

  function buildDoclibFileUrl(fileName) {
    try {
      const href = location.href;
      const qIdx = href.indexOf('?');
      let base = qIdx === -1 ? href : href.substring(0, qIdx);
      if (!base.endsWith('/')) base += '/';
      return base + encodeURIComponent(fileName);
    } catch (err) {
      logError('buildDoclibFileUrl', err);
      return null;
    }
  }

  function getAttachments() {
    try {
      const labels = document.querySelectorAll('label, [class*="fieldLabel" i], [class*="FieldLabel"]');
      let container = null;
      for (const label of labels) {
        if ((label.textContent || '').trim().includes('添付ファイル')) {
          container = label.closest('div');
          break;
        }
      }
      if (!container) {
        return { ok: false, items: [], reason: '添付ファイルラベルが見つかりませんでした' };
      }
      let depth = 0;
      const found = [];
      while (container && depth < 4 && found.length === 0) {
        container.querySelectorAll('a[href]').forEach((a) => {
          const name = (a.textContent || '').trim();
          const href = a.getAttribute('href');
          if (name && href && name.length < 150) {
            found.push({ name, url: href });
          }
        });
        container = container.parentElement;
        depth++;
      }
      if (found.length === 0) {
        return { ok: false, items: [], reason: '添付ファイル欄は見つかりましたが、リンク要素(href)を特定できませんでした' };
      }
      return { ok: true, items: found, reason: '' };
    } catch (err) {
      logError('getAttachments', err);
      return { ok: false, items: [], reason: `エラー: ${err.name || ''} ${err.message || ''}` };
    }
  }

  async function writeClipboardHtml(pathText, mainUrl, extraLinks) {
    let htmlBody = `<a href="${escapeHtml(mainUrl)}">${escapeHtml(pathText)}</a>`;
    if (extraLinks.length > 0) {
      htmlBody += '<br>';
      htmlBody += extraLinks
        .map((link) => `<a href="${escapeHtml(link.url)}">${escapeHtml(link.name)}</a>`)
        .join('<br>');
    }

    const plainLines = [`${pathText}  ${mainUrl}`];
    extraLinks.forEach((link) => plainLines.push(`${link.name}  ${link.url}`));
    const plainBody = plainLines.join('\n');

    try {
      const htmlBlob = new Blob([htmlBody], { type: 'text/html' });
      const textBlob = new Blob([plainBody], { type: 'text/plain' });
      await navigator.clipboard.write([new ClipboardItem({ 'text/html': htmlBlob, 'text/plain': textBlob })]);
      return { ok: true, message: 'コピー完了' };
    } catch (err) {
      logError('writeClipboardHtml', err);
      let hint = '';
      if (err && err.message && err.message.includes('not focused')) {
        hint = '(画面がフォーカスされていない可能性があります。パネル内の「コピー」ボタンをもう一度押してください)';
      }
      return { ok: false, message: `コピー失敗: ${err.name || ''} ${err.message || ''}${hint}` };
    }
  }

  async function copyPlainText(text) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (err) {
      return false;
    }
  }

  function renderPanel(mode, pathParts, extraLinks, mainUrl, checkedCount, checkedKeys) {
    document.getElementById('spPathBookmarkletPanel')?.remove();

    const panel = document.createElement('div');
    panel.id = 'spPathBookmarkletPanel';
    Object.assign(panel.style, {
      position: 'fixed',
      top: '20px',
      right: '20px',
      width: '460px',
      maxHeight: '80vh',
      overflow: 'auto',
      background: '#fff',
      border: '2px solid #333',
      borderRadius: '8px',
      padding: '16px',
      zIndex: 999999,
      fontFamily: 'sans-serif',
      fontSize: '13px',
      boxShadow: '0 4px 20px rgba(0,0,0,.3)',
      color: '#000',
    });

    const statusBox = document.createElement('div');
    Object.assign(statusBox.style, { marginBottom: '10px', padding: '6px', background: '#eef' });
    statusBox.textContent = '処理中…';
    panel.appendChild(statusBox);

    const pathLabel = document.createElement('div');
    pathLabel.style.fontWeight = 'bold';
    pathLabel.textContent = '保存先:';

    const pathBox = document.createElement('textarea');
    pathBox.id = 'spPathBookmarkletPathBox';
    Object.assign(pathBox.style, { width: '100%', boxSizing: 'border-box', height: '50px', marginBottom: '10px' });
    pathBox.value = pathParts.join(' > ');

    const dumpBtn = document.createElement('button');
    dumpBtn.textContent = '状況をコピー(報告用・エラーも含む)';
    Object.assign(dumpBtn.style, { marginBottom: '6px', display: 'block' });

    const dumpArea = document.createElement('textarea');
    Object.assign(dumpArea.style, {
      width: '100%',
      boxSizing: 'border-box',
      height: '80px',
      marginBottom: '10px',
      display: 'none',
      fontSize: '11px',
    });
    dumpArea.readOnly = true;
    dumpArea.onclick = () => dumpArea.select();

    dumpBtn.onclick = () => {
      const lines = [];
      lines.push('=== SPパスブックマークレット 状況ダンプ ===');
      lines.push(`日時: ${new Date().toString()}`);
      lines.push(`URL: ${location.href}`);
      lines.push(`画面種別(mode): ${mode}`);
      lines.push(`チェック件数: ${checkedCount}`);
      lines.push(`itemKey一覧: ${checkedKeys.length ? checkedKeys.join(', ') : '(なし)'}`);
      lines.push(`保存先(現在の編集ボックスの値): ${pathBox.value}`);
      lines.push('アイテム/ファイルリンク:');
      if (extraLinks.length === 0) {
        lines.push('  (チェックボックス指定なし)');
      } else {
        extraLinks.forEach((link) => lines.push(`  - ${link.name} -> ${link.url}`));
      }
      lines.push(`メインURL: ${mainUrl}`);
      lines.push('エラー/警告ログ:');
      if (logs.length === 0) {
        lines.push('  (なし)');
      } else {
        logs.forEach((log) => lines.push(`  - ${log}`));
      }
      const dumpText = lines.join('\n');
      copyPlainText(dumpText).then((ok) => {
        dumpBtn.textContent = ok ? '状況をコピーしました' : 'コピー失敗(下のボックスを手動選択してください)';
        if (!ok) {
          dumpArea.style.display = 'block';
          dumpArea.value = dumpText;
          dumpArea.select();
        }
      });
    };
    panel.appendChild(dumpBtn);
    panel.appendChild(dumpArea);

    panel.appendChild(pathLabel);
    panel.appendChild(pathBox);

    const nameLabel = document.createElement('div');
    nameLabel.style.fontWeight = 'bold';
    nameLabel.textContent = 'アイテム名';
    panel.appendChild(nameLabel);

    const nameList = document.createElement('div');
    Object.assign(nameList.style, { marginBottom: '10px', padding: '4px', background: '#f7f7f7' });
    if (extraLinks.length === 0) {
      nameList.textContent = '(チェックボックス指定なし)';
    } else {
      extraLinks.forEach((link) => {
        const row = document.createElement('div');
        row.textContent = `${link.name}${link.url}`;
        nameList.appendChild(row);
      });
    }
    panel.appendChild(nameList);

    const attachCheckboxes = [];
    const attachItems = [];
    if (mode === 'lists-popup' || mode === 'lists-single') {
      const attachResult = getAttachments();
      const attachBox = document.createElement('div');
      attachBox.style.marginBottom = '10px';
      const attachLabel = document.createElement('div');
      attachLabel.style.fontWeight = 'bold';
      attachLabel.textContent = '添付ファイル(任意で個別リンクを追加)';
      attachBox.appendChild(attachLabel);

      if (attachResult.ok) {
        if (attachResult.items.length === 0) {
          const noAttach = document.createElement('div');
          noAttach.style.fontSize = '11px';
          noAttach.textContent = '添付ファイルはありません';
          attachBox.appendChild(noAttach);
        } else {
          attachResult.items.forEach((item, idx) => {
            const itemLabel = document.createElement('label');
            itemLabel.style.display = 'block';
            const cb = document.createElement('input');
            cb.type = 'checkbox';
            cb.dataset.attachIndex = String(idx);
            itemLabel.appendChild(cb);
            itemLabel.appendChild(document.createTextNode(` ${item.name}`));
            attachBox.appendChild(itemLabel);
            attachCheckboxes.push(cb);
            attachItems.push(item);
          });
        }
      } else {
        const attachErr = document.createElement('div');
        Object.assign(attachErr.style, { color: '#c00', fontSize: '11px' });
        attachErr.textContent = `取得できませんでした: ${attachResult.reason}`;
        attachBox.appendChild(attachErr);
      }
      panel.appendChild(attachBox);
    }

    if (logs.length > 0) {
      const warnLabel = document.createElement('div');
      Object.assign(warnLabel.style, { fontWeight: 'bold', color: '#c00' });
      warnLabel.textContent = '警告/エラー(タップで全選択できます):';
      panel.appendChild(warnLabel);

      const warnArea = document.createElement('textarea');
      Object.assign(warnArea.style, {
        width: '100%',
        boxSizing: 'border-box',
        height: '80px',
        marginBottom: '6px',
        background: '#fee',
        border: '1px solid #c00',
        fontSize: '11px',
      });
      warnArea.readOnly = true;
      warnArea.value = logs.join('\n');
      warnArea.onclick = () => warnArea.select();
      panel.appendChild(warnArea);
    }

    const btnRow = document.createElement('div');
    const copyBtn = document.createElement('button');
    copyBtn.textContent = 'コピー';
    copyBtn.style.marginRight = '8px';
    const closeBtn = document.createElement('button');
    closeBtn.textContent = '閉じる';
    closeBtn.onclick = () => panel.remove();
    btnRow.appendChild(copyBtn);
    btnRow.appendChild(closeBtn);
    panel.appendChild(btnRow);

    document.body.appendChild(panel);

    const doCopy = async () => {
      const pathText = pathBox.value;
      const links = [...extraLinks];
      attachCheckboxes.forEach((cb, idx) => {
        if (cb.checked) links.push(attachItems[idx]);
      });
      const result = await writeClipboardHtml(pathText, mainUrl, links);
      statusBox.style.background = result.ok ? '#efe' : '#fee';
      statusBox.textContent = result.message;
    };
    copyBtn.onclick = doCopy;

    statusBox.textContent = '自動コピーを試みています…';
    try {
      window.focus();
    } catch (err) {
      logError('window.focus', err);
    }
    try {
      document.body.focus();
    } catch (err) {
      /* noop */
    }
    setTimeout(() => {
      doCopy().catch((err) => {
        logError('autoCopy-unhandled', err);
        statusBox.style.background = '#fee';
        statusBox.textContent = '自動コピーに失敗しました。お手数ですが「コピー」ボタンを押してください。';
      });
    }, 500);
  }

  function detectMode() {
    try {
      if (document.querySelector('.sp-itemDialog')) return 'lists-popup';
    } catch (err) {
      logError('detectMode-popup', err);
    }
    try {
      if (document.querySelector('.od-ListForm-breadcrumb')) return 'lists-single';
    } catch (err) {
      logError('detectMode-listsingle', err);
    }
    try {
      if (document.querySelector('.breadcrumbRoot_b6af7cfe')) return 'doclib-list';
    } catch (err) {
      logError('detectMode-doclib', err);
    }
    try {
      if (document.querySelector('[id^=virtualized-list_]')) return 'unknown-grid';
    } catch (err) {
      logError('detectMode-grid', err);
    }
    return 'unknown';
  }

  function main() {
    const mode = detectMode();
    let pathParts = [];
    const extraLinks = [];
    let mainUrl = location.href;
    let checkedCount = 0;
    const checkedKeys = [];

    if (mode === 'doclib-list') {
      pathParts = [...getOverflowBreadcrumb(), ...getBreadcrumbPath()];

      const checkedFiles = getCheckedRows();
      checkedCount = checkedFiles.length;
      if (checkedFiles.length > 10) {
        alert(`チェックされたファイルが10件を超えています(${checkedFiles.length}件)。10件以内に絞り込んでから再実行してください。`);
        return;
      }
      checkedFiles.forEach((file) => {
        const fileUrl = buildDoclibFileUrl(file.name);
        if (!fileUrl) logError('buildDoclibFileUrl-null', `file=${file.name}`);
        extraLinks.push({ name: file.name, url: fileUrl || mainUrl });
        if (file.itemKey) checkedKeys.push(file.itemKey);
      });
    } else if (mode === 'lists-single' || mode === 'unknown-grid') {
      pathParts = getBreadcrumbPath();
      const checkedItems = getCheckedRows();
      checkedCount = checkedItems.length;
      if (checkedItems.length > 10) {
        alert(`チェックされたアイテムが10件を超えています(${checkedItems.length}件)。10件以内に絞り込んでから再実行してください。`);
        return;
      }
      checkedItems.forEach((item) => {
        const itemUrl = item.itemKey ? buildListsItemUrl(item.itemKey) : null;
        if (!itemUrl) logError('buildListsItemUrl-null', `item=${item.name} key=${item.itemKey}`);
        extraLinks.push({ name: item.name, url: itemUrl || mainUrl });
        if (item.itemKey) checkedKeys.push(item.itemKey);
      });
    } else if (mode === 'lists-popup') {
      const dept = getListsFieldValue('掲載部署');
      const kind = getListsFieldValue('種別');
      if (dept) pathParts.push(dept);
      if (kind) pathParts.push(kind);
      if (!dept && !kind) logError('lists-popup-path', '掲載部署/種別のいずれも取得できませんでした');

      const heroSpan = document.querySelector('.sp-itemDialog span[role=button][data-id=heroField]');
      if (heroSpan) {
        const itemName = (heroSpan.textContent || '').trim();
        const itemKey = extractItemKey(heroSpan);
        const itemUrl = itemKey ? buildListsItemUrl(itemKey) : null;
        if (!itemUrl) logError('buildListsItemUrl-popup-null', `name=${itemName} key=${itemKey}`);
        extraLinks.push({ name: itemName, url: itemUrl || mainUrl });
        mainUrl = itemUrl || mainUrl;
        checkedCount = 1;
        if (itemKey) checkedKeys.push(itemKey);
      } else {
        logError('lists-popup-itemSpan', 'ポップアップ内のアイテム名要素が見つかりませんでした');
      }
    } else {
      logError('detectMode', `既知の画面パターンに一致しませんでした(mode=${mode})`);
      pathParts = getBreadcrumbPath();
    }

    renderPanel(mode, pathParts, extraLinks, mainUrl, checkedCount, checkedKeys);
  }

  main();
})();


3——————————————————————-

javascript:(function () {
‘use strict’;

const logs = [];
function logError(tag, err) {
const msg = `[${tag}] ${err && err.name ? `${err.name}: ${err.message}` : String(err)}`;
logs.push(msg);
}

function escapeHtml(s) {
return String(s)
.replaceAll(’&’, ‘&’)
.replaceAll(’<’, ‘<’)
.replaceAll(’>’, ‘>’);
}

function extractItemKey(el) {
try {
const da = el.getAttribute(‘data-actions’);
if (!da) {
logError(‘extractItemKey-noDataActions’, ‘data-actions属性が要素に存在しませんでした’);
return null;
}
const m = da.match(/itemKey[^0-9]*([0-9]+)/);
if (!m) {
logError(‘extractItemKey-noMatch’, `data-actionsにitemKeyが見つかりませんでした: ${da.slice(0, 200)}`);
return null;
}
return m[1];
} catch (err) {
logError(‘extractItemKey’, err);
return null;
}
}

function getBreadcrumbPath() {
const parts = [];
try {
document.querySelectorAll(‘div.breadcrumbRoot_b6af7cfe li span’).forEach((el) => {
const txt = (el.textContent || ‘’).trim();
if (txt) parts.push(txt);
});
} catch (err) {
logError(‘getBreadcrumbPath-doclib’, err);
}
try {
document
.querySelectorAll(’.od-ListForm-breadcrumb nav ul li a, .od-ListForm-breadcrumb nav ul li span’)
.forEach((el) => {
const txt = (el.textContent || ‘’).trim();
if (txt && !parts.includes(txt)) parts.push(txt);
});
} catch (err) {
logError(‘getBreadcrumbPath-lists’, err);
}
return parts;
}

function findOverflowTriggerButton() {
// 「…」(パンくずのオーバーフローメニュー)を開くトリガーボタンのセレクタは
// 未確定のため、可能性の高い候補を順番に試す。
// 候補1: aria-controlsで展開先のメニューID(breadcrumb-menu-id)を直接指しているボタン
// 候補2: パンくずのルート内にある、aria-haspopupを持つボタン
// 候補3: パンくずのルート内にある最後から2番目より前の、隠れた省略記号ボタンらしき要素
const candidates = [
‘button[aria-controls=breadcrumb-menu-id],
‘div.breadcrumbRoot_b6af7cfe button[aria-haspopup],
‘div.breadcrumbRoot_b6af7cfe button[aria-expanded],
];
for (const sel of candidates) {
try {
const el = document.querySelector(sel);
if (el) return { el, sel };
} catch (err) {
logError(‘findOverflowTriggerButton-selector’, `${sel}: ${err && err.message ? err.message : err}`);
}
}
return null;
}

async function withExpandedOverflow(fn) {
// オーバーフローメニューが既にDOM上にあるか(=展開済みか)を先に確認する。
// 展開済みでなければ、推測したトリガーボタンをクリックして展開し、
// fn()実行後は自力で開いた場合のみ閉じて元の状態に戻す。
const alreadyExpanded = !!document.querySelector(’#breadcrumb-menu-id’);
let openedByScript = false;

```
if (!alreadyExpanded) {
  const trigger = findOverflowTriggerButton();
  if (trigger) {
    try {
      trigger.el.click();
      openedByScript = true;
      logError('withExpandedOverflow-clicked', `推測セレクタでクリックしました: ${trigger.sel}`);
    } catch (err) {
      logError('withExpandedOverflow-clickFailed', err);
    }
  } else {
    logError('withExpandedOverflow-triggerNotFound', 'オーバーフローボタンの候補セレクタがすべて一致しませんでした(親フォルダ階層が省略されていないか、DOM構造が想定と異なる可能性)');
  }
}

// クリック直後はメニューがまだDOMに反映されていない場合があるため少し待つ
if (openedByScript) {
  await new Promise((resolve) => setTimeout(resolve, 150));
}

let result;
try {
  result = fn();
} finally {
  if (openedByScript) {
    const trigger = findOverflowTriggerButton();
    try {
      if (trigger) trigger.el.click();
      else document.body.click();
      // 元の状態(閉じている状態)に戻す。専用の閉じるボタンが見つからない場合は
      // body側クリックでポップアップ系メニューが閉じることを期待するフォールバック
    } catch (err) {
      logError('withExpandedOverflow-closeFailed', err);
    }
  }
}
return result;
```

}

function getOverflowBreadcrumb() {
const parts = [];
try {
document.querySelectorAll(’#breadcrumb-menu-id li span’).forEach((el) => {
const txt = (el.textContent || ‘’).trim();
if (txt) parts.push(txt);
});
} catch (err) {
logError(‘getOverflowBreadcrumb’, err);
}
return parts;
}

function getListsFieldValue(labelText) {
try {
const labels = document.querySelectorAll(‘label, [class*=“fieldLabel” i], [class*=“FieldLabel”]);
for (const label of labels) {
if (!(label.textContent || ‘’).trim().includes(labelText)) continue;
let container = label.closest(‘div’);
let depth = 0;
while (container && depth < 4) {
const valueEl = container.querySelector(
‘div.ReactFieldEditor-core–display.ReactFieldEditor-core–display-ReadOnly.ReactFieldEditor-linkFocusIndicator > div’
);
if (valueEl) {
const val = (valueEl.textContent || ‘’).trim();
if (val) return val;
}
container = container.parentElement;
depth++;
}
}
return null;
} catch (err) {
logError(`getListsFieldValue:${labelText}`, err);
return null;
}
}

function getCheckedRows() {
const results = [];
try {
const rows = document.querySelectorAll(‘div[aria-selected=true][data-automationid^=row-selection-]);
rows.forEach((rowEl) => {
const automationId = rowEl.getAttribute(‘data-automationid’) || ‘’;
const idMatch = automationId.match(/row-selection-(.+)$/);
const rawId = idMatch ? idMatch[1] : ‘’;

```
    const ancestorRow = rowEl.closest('[role=row]');
    let isHeaderLike = false;
    if (ancestorRow && ancestorRow.getAttribute('aria-rowindex') === '1') {
      isHeaderLike = true;
    }
    if (!rawId || /^(all|header|select-?all)$/i.test(rawId)) {
      isHeaderLike = true;
    }
    if (isHeaderLike) return;

    let container = rowEl.parentElement;
    let depth = 0;
    let nameSpan = null;
    while (container && depth < 5 && !nameSpan) {
      nameSpan = container.querySelector('span[role=button][data-id=heroField]');
      if (!nameSpan) container = container.parentElement;
      depth++;
    }
    if (nameSpan) {
      const name = (nameSpan.textContent || '').trim();
      const itemKey = extractItemKey(nameSpan);
      results.push({ name, itemKey });
    } else {
      logError('getCheckedRows-nameNotFound', 'row-selection element found but file/item name span not located nearby');
    }
  });

  const seen = new Set();
  const deduped = [];
  for (const row of results) {
    const key = row.itemKey || `name:${row.name}`;
    if (!seen.has(key)) {
      seen.add(key);
      deduped.push(row);
    } else {
      logError('getCheckedRows-dedup', `重複行を除外しました: ${row.name}`);
    }
  }
  return deduped;
} catch (err) {
  logError('getCheckedRows', err);
  return results;
}
```

}

function buildListsItemUrl(itemKey) {
try {
const href = location.href;
const idx = href.indexOf(/Lists/);
if (idx === -1) return null;
const m = href.substring(idx).match(/^/Lists/([^/]+)//);
if (!m) return null;
return `${href.substring(0, idx)}/Lists/${m[1]}/DispForm.aspx?ID=${itemKey}`;
} catch (err) {
logError(‘buildListsItemUrl’, err);
return null;
}
}

function buildListsAllItemsUrl() {
// buildListsItemUrlと同じ考え方で、location.hrefから/Lists/リスト名/を抜き出し、
// 一覧画面(AllItems.aspx)のURLを組み立てる。パンくずDOMには依存しない。
try {
const href = location.href;
const idx = href.indexOf(/Lists/);
if (idx === -1) {
logError(‘buildListsAllItemsUrl-noListsSegment’, `URLに/Lists/が含まれていません: ${href}`);
return null;
}
const m = href.substring(idx).match(/^/Lists/([^/]+)//);
if (!m) {
logError(‘buildListsAllItemsUrl-noMatch’, `リスト名を抽出できませんでした: ${href.substring(idx)}`);
return null;
}
return `${href.substring(0, idx)}/Lists/${m[1]}/AllItems.aspx`;
} catch (err) {
logError(‘buildListsAllItemsUrl’, err);
return null;
}
}

function buildDoclibFileUrl(fileName) {
try {
const href = location.href;
const qIdx = href.indexOf(?);
let base = qIdx === -1 ? href : href.substring(0, qIdx);
if (!base.endsWith(’/’)) base += ‘/’;
return base + encodeURIComponent(fileName);
} catch (err) {
logError(‘buildDoclibFileUrl’, err);
return null;
}
}

function getAttachments() {
try {
const labels = document.querySelectorAll(‘label, [class*=“fieldLabel” i], [class*=“FieldLabel”]);
let container = null;
for (const label of labels) {
if ((label.textContent || ‘’).trim().includes(‘添付ファイル’)) {
container = label.closest(‘div’);
break;
}
}
if (!container) {
return { ok: false, items: [], reason: ‘添付ファイルラベルが見つかりませんでした’ };
}
let depth = 0;
const found = [];
while (container && depth < 4 && found.length === 0) {
container.querySelectorAll(‘a[href]).forEach((a) => {
const name = (a.textContent || ‘’).trim();
const href = a.getAttribute(‘href’);
if (name && href && name.length < 150) {
found.push({ name, url: href });
}
});
container = container.parentElement;
depth++;
}
if (found.length === 0) {
return { ok: false, items: [], reason: ‘添付ファイル欄は見つかりましたが、リンク要素(href)を特定できませんでした’ };
}
return { ok: true, items: found, reason: ‘’ };
} catch (err) {
logError(‘getAttachments’, err);
return { ok: false, items: [], reason: `エラー: ${err.name || ''} ${err.message || ''}` };
}
}

async function writeClipboardHtml(pathText, mainUrl, extraLinks) {
let htmlBody = `<a href="${escapeHtml(mainUrl)}">${escapeHtml(pathText)}</a>`;
if (extraLinks.length > 0) {
htmlBody += ‘<br>;
htmlBody += extraLinks
.map((link) => `<a href="${escapeHtml(link.url)}">${escapeHtml(link.name)}</a>`)
.join(’<br>);
}

```
const plainLines = [`${pathText}  ${mainUrl}`];
extraLinks.forEach((link) => plainLines.push(`${link.name}  ${link.url}`));
const plainBody = plainLines.join('\n');

try {
  const htmlBlob = new Blob([htmlBody], { type: 'text/html' });
  const textBlob = new Blob([plainBody], { type: 'text/plain' });
  await navigator.clipboard.write([new ClipboardItem({ 'text/html': htmlBlob, 'text/plain': textBlob })]);
  return { ok: true, message: 'コピー完了' };
} catch (err) {
  logError('writeClipboardHtml', err);
  let hint = '';
  if (err && err.message && err.message.includes('not focused')) {
    hint = '(画面がフォーカスされていない可能性があります。パネル内の「コピー」ボタンをもう一度押してください)';
  }
  return { ok: false, message: `コピー失敗: ${err.name || ''} ${err.message || ''}${hint}` };
}
```

}

async function copyPlainText(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
return false;
}
}

function renderPanel(mode, pathParts, extraLinks, mainUrl, checkedCount, checkedKeys) {
document.getElementById(‘spPathBookmarkletPanel’)?.remove();

```
const panel = document.createElement('div');
panel.id = 'spPathBookmarkletPanel';
Object.assign(panel.style, {
  position: 'fixed',
  top: '20px',
  right: '20px',
  width: '460px',
  maxHeight: '80vh',
  overflow: 'auto',
  background: '#fff',
  border: '2px solid #333',
  borderRadius: '8px',
  padding: '16px',
  zIndex: 999999,
  fontFamily: 'sans-serif',
  fontSize: '13px',
  boxShadow: '0 4px 20px rgba(0,0,0,.3)',
  color: '#000',
});

const statusBox = document.createElement('div');
Object.assign(statusBox.style, { marginBottom: '10px', padding: '6px', background: '#eef' });
statusBox.textContent = '処理中…';
panel.appendChild(statusBox);

const pathLabel = document.createElement('div');
pathLabel.style.fontWeight = 'bold';
pathLabel.textContent = '保存先:';

const pathBox = document.createElement('textarea');
pathBox.id = 'spPathBookmarkletPathBox';
Object.assign(pathBox.style, { width: '100%', boxSizing: 'border-box', height: '50px', marginBottom: '10px' });
pathBox.value = pathParts.join(' > ');

const dumpBtn = document.createElement('button');
dumpBtn.textContent = '状況をコピー(報告用・エラーも含む)';
Object.assign(dumpBtn.style, { marginBottom: '6px', display: 'block' });

const dumpArea = document.createElement('textarea');
Object.assign(dumpArea.style, {
  width: '100%',
  boxSizing: 'border-box',
  height: '80px',
  marginBottom: '10px',
  display: 'none',
  fontSize: '11px',
});
dumpArea.readOnly = true;
dumpArea.onclick = () => dumpArea.select();

dumpBtn.onclick = () => {
  const lines = [];
  lines.push('=== SPパスブックマークレット 状況ダンプ ===');
  lines.push(`日時: ${new Date().toString()}`);
  lines.push(`URL: ${location.href}`);
  lines.push(`画面種別(mode): ${mode}`);
  lines.push(`チェック件数: ${checkedCount}`);
  lines.push(`itemKey一覧: ${checkedKeys.length ? checkedKeys.join(', ') : '(なし)'}`);
  lines.push(`保存先(現在の編集ボックスの値): ${pathBox.value}`);
  lines.push('アイテム/ファイルリンク:');
  if (extraLinks.length === 0) {
    lines.push('  (チェックボックス指定なし)');
  } else {
    extraLinks.forEach((link) => lines.push(`  - ${link.name} -> ${link.url}`));
  }
  lines.push(`メインURL: ${mainUrl}`);
  lines.push('エラー/警告ログ:');
  if (logs.length === 0) {
    lines.push('  (なし)');
  } else {
    logs.forEach((log) => lines.push(`  - ${log}`));
  }
  const dumpText = lines.join('\n');
  copyPlainText(dumpText).then((ok) => {
    dumpBtn.textContent = ok ? '状況をコピーしました' : 'コピー失敗(下のボックスを手動選択してください)';
    if (!ok) {
      dumpArea.style.display = 'block';
      dumpArea.value = dumpText;
      dumpArea.select();
    }
  });
};
panel.appendChild(dumpBtn);
panel.appendChild(dumpArea);

panel.appendChild(pathLabel);
panel.appendChild(pathBox);

const nameLabel = document.createElement('div');
nameLabel.style.fontWeight = 'bold';
nameLabel.textContent = 'アイテム名';
panel.appendChild(nameLabel);

const nameList = document.createElement('div');
Object.assign(nameList.style, { marginBottom: '10px', padding: '4px', background: '#f7f7f7' });
if (extraLinks.length === 0) {
  nameList.textContent = '(チェックボックス指定なし)';
} else {
  extraLinks.forEach((link) => {
    const row = document.createElement('div');
    row.textContent = `${link.name}  →  ${link.url}`;
    nameList.appendChild(row);
  });
}
panel.appendChild(nameList);

const attachCheckboxes = [];
const attachItems = [];
if (mode === 'lists-popup' || mode === 'lists-single') {
  const attachResult = getAttachments();
  const attachBox = document.createElement('div');
  attachBox.style.marginBottom = '10px';
  const attachLabel = document.createElement('div');
  attachLabel.style.fontWeight = 'bold';
  attachLabel.textContent = '添付ファイル(任意で個別リンクを追加)';
  attachBox.appendChild(attachLabel);

  if (attachResult.ok) {
    if (attachResult.items.length === 0) {
      const noAttach = document.createElement('div');
      noAttach.style.fontSize = '11px';
      noAttach.textContent = '添付ファイルはありません';
      attachBox.appendChild(noAttach);
    } else {
      attachResult.items.forEach((item, idx) => {
        const itemLabel = document.createElement('label');
        itemLabel.style.display = 'block';
        const cb = document.createElement('input');
        cb.type = 'checkbox';
        cb.dataset.attachIndex = String(idx);
        itemLabel.appendChild(cb);
        itemLabel.appendChild(document.createTextNode(` ${item.name}`));
        attachBox.appendChild(itemLabel);
        attachCheckboxes.push(cb);
        attachItems.push(item);
      });
    }
  } else {
    const attachErr = document.createElement('div');
    Object.assign(attachErr.style, { color: '#c00', fontSize: '11px' });
    attachErr.textContent = `取得できませんでした: ${attachResult.reason}`;
    attachBox.appendChild(attachErr);
  }
  panel.appendChild(attachBox);
}

if (logs.length > 0) {
  const warnLabel = document.createElement('div');
  Object.assign(warnLabel.style, { fontWeight: 'bold', color: '#c00' });
  warnLabel.textContent = '警告/エラー(タップで全選択できます):';
  panel.appendChild(warnLabel);

  const warnArea = document.createElement('textarea');
  Object.assign(warnArea.style, {
    width: '100%',
    boxSizing: 'border-box',
    height: '80px',
    marginBottom: '6px',
    background: '#fee',
    border: '1px solid #c00',
    fontSize: '11px',
  });
  warnArea.readOnly = true;
  warnArea.value = logs.join('\n');
  warnArea.onclick = () => warnArea.select();
  panel.appendChild(warnArea);
}

const btnRow = document.createElement('div');
const copyBtn = document.createElement('button');
copyBtn.textContent = 'コピー';
copyBtn.style.marginRight = '8px';
const closeBtn = document.createElement('button');
closeBtn.textContent = '閉じる';
closeBtn.onclick = () => panel.remove();
btnRow.appendChild(copyBtn);
btnRow.appendChild(closeBtn);
panel.appendChild(btnRow);

document.body.appendChild(panel);

const doCopy = async () => {
  const pathText = pathBox.value;
  const links = [...extraLinks];
  attachCheckboxes.forEach((cb, idx) => {
    if (cb.checked) links.push(attachItems[idx]);
  });
  const result = await writeClipboardHtml(pathText, mainUrl, links);
  statusBox.style.background = result.ok ? '#efe' : '#fee';
  statusBox.textContent = result.message;
};
copyBtn.onclick = doCopy;

statusBox.textContent = '自動コピーを試みています…';
try {
  window.focus();
} catch (err) {
  logError('window.focus', err);
}
try {
  document.body.focus();
} catch (err) {
  /* noop */
}
setTimeout(() => {
  doCopy().catch((err) => {
    logError('autoCopy-unhandled', err);
    statusBox.style.background = '#fee';
    statusBox.textContent = '自動コピーに失敗しました。お手数ですが「コピー」ボタンを押してください。';
  });
}, 500);
```

}

function detectMode() {
try {
if (document.querySelector(’.sp-itemDialog’)) return ‘lists-popup’;
} catch (err) {
logError(‘detectMode-popup’, err);
}
try {
if (document.querySelector(’.od-ListForm-breadcrumb’)) return ‘lists-single’;
} catch (err) {
logError(‘detectMode-listsingle’, err);
}
try {
if (document.querySelector(’.breadcrumbRoot_b6af7cfe’)) return ‘doclib-list’;
} catch (err) {
logError(‘detectMode-doclib’, err);
}
try {
if (document.querySelector([id^=virtualized-list_])) return ‘unknown-grid’;
} catch (err) {
logError(‘detectMode-grid’, err);
}
return ‘unknown’;
}

async function main() {
const mode = detectMode();
let pathParts = [];
const extraLinks = [];
let mainUrl = location.href;
let checkedCount = 0;
const checkedKeys = [];

```
if (mode === 'doclib-list') {
  const overflowParts = await withExpandedOverflow(() => getOverflowBreadcrumb());
  pathParts = [...overflowParts, ...getBreadcrumbPath()];

  const checkedFiles = getCheckedRows();
  checkedCount = checkedFiles.length;
  if (checkedFiles.length > 10) {
    alert(`チェックされたファイルが10件を超えています(${checkedFiles.length})10件以内に絞り込んでから再実行してください。`);
    return;
  }
  checkedFiles.forEach((file) => {
    const fileUrl = buildDoclibFileUrl(file.name);
    if (!fileUrl) logError('buildDoclibFileUrl-null', `file=${file.name}`);
    extraLinks.push({ name: file.name, url: fileUrl || mainUrl });
    if (file.itemKey) checkedKeys.push(file.itemKey);
  });
} else if (mode === 'lists-single' || mode === 'unknown-grid') {
  pathParts = getBreadcrumbPath();
  if (mode === 'lists-single') {
    // 単独ページでは今見ているアイテム自身のURL(location.href)をアイテム名リンクとして先に積んでおく
    const currentItemName = pathParts.length > 0 ? pathParts[pathParts.length - 1] : document.title;
    extraLinks.push({ name: currentItemName, url: location.href });
    // 疑似パス(一覧への案内)のリンク先は、location.hrefではなくAllItems.aspxへ差し替える
    const allItemsUrl = buildListsAllItemsUrl();
    if (allItemsUrl) mainUrl = allItemsUrl;
    else logError('lists-single-allItemsUrl-null', 'AllItems.aspx URLの組み立てに失敗、疑似パスは自身のURLのままになります');
  }
  const checkedItems = getCheckedRows();
  checkedCount = checkedItems.length;
  if (checkedItems.length > 10) {
    alert(`チェックされたアイテムが10件を超えています(${checkedItems.length})10件以内に絞り込んでから再実行してください。`);
    return;
  }
  checkedItems.forEach((item) => {
    const itemUrl = item.itemKey ? buildListsItemUrl(item.itemKey) : null;
    if (!itemUrl) logError('buildListsItemUrl-null', `item=${item.name} key=${item.itemKey}`);
    extraLinks.push({ name: item.name, url: itemUrl || mainUrl });
    if (item.itemKey) checkedKeys.push(item.itemKey);
  });
} else if (mode === 'lists-popup') {
  const dept = getListsFieldValue('掲載部署');
  const kind = getListsFieldValue('種別');
  if (dept) pathParts.push(dept);
  if (kind) pathParts.push(kind);
  if (!dept && !kind) logError('lists-popup-path', '掲載部署/種別のいずれも取得できませんでした');

  const heroSpan = document.querySelector('.sp-itemDialog span[role=button][data-id=heroField]');
  if (heroSpan) {
    const itemName = (heroSpan.textContent || '').trim();
    const itemKey = extractItemKey(heroSpan);
    const itemUrl = itemKey ? buildListsItemUrl(itemKey) : null;
    if (!itemUrl) logError('buildListsItemUrl-popup-null', `name=${itemName} key=${itemKey}`);
    extraLinks.push({ name: itemName, url: itemUrl || mainUrl });
    mainUrl = itemUrl || mainUrl;
    checkedCount = 1;
    if (itemKey) checkedKeys.push(itemKey);
  } else {
    logError('lists-popup-itemSpan', 'ポップアップ内のアイテム名要素が見つかりませんでした');
  }
} else {
  logError('detectMode', `既知の画面パターンに一致しませんでした(mode=${mode})`);
  pathParts = getBreadcrumbPath();
}

renderPanel(mode, pathParts, extraLinks, mainUrl, checkedCount, checkedKeys);
```

}

main().catch((err) => {
logError(‘main-unhandled’, err);
alert(‘ブックマークレットの実行中にエラーが発生しました。詳細はコンソールを確認してください。’);
});
})();

javascript:(function () {
  'use strict';

  const SCRIPT_VERSION = '20260820';

  const logs = [];
  function logError(tag, err) {
    const msg = `[${tag}] ${err && err.name ? `${err.name}: ${err.message}` : String(err)}`;
    logs.push(msg);
  }

  function escapeHtml(s) {
    return String(s)
      .replaceAll('&', '&amp;')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;');
  }

  function extractItemKey(el) {
    try {
      const da = el.getAttribute('data-actions');
      if (!da) {
        logError('extractItemKey-noDataActions', 'data-actions属性が要素に存在しませんでした');
        return null;
      }
      const m = da.match(/itemKey[^0-9]*([0-9]+)/);
      if (!m) {
        logError('extractItemKey-noMatch', `data-actionsにitemKeyが見つかりませんでした: ${da.slice(0, 200)}`);
        return null;
      }
      return m[1];
    } catch (err) {
      logError('extractItemKey', err);
      return null;
    }
  }

  function getBreadcrumbPath() {
    const parts = [];
    try {
      document.querySelectorAll('div.breadcrumbRoot_b6af7cfe li span').forEach((el) => {
        const txt = (el.textContent || '').trim();
        if (txt) parts.push(txt);
      });
    } catch (err) {
      logError('getBreadcrumbPath-doclib', err);
    }
    try {
      document
        .querySelectorAll('.od-ListForm-breadcrumb nav ul li a, .od-ListForm-breadcrumb nav ul li span')
        .forEach((el) => {
          const txt = (el.textContent || '').trim();
          if (txt && !parts.includes(txt)) parts.push(txt);
        });
    } catch (err) {
      logError('getBreadcrumbPath-lists', err);
    }
    return parts;
  }

  function findOverflowTriggerButton() {
    // 「...」(パンくずのオーバーフローメニュー)を開くトリガーボタンのセレクタは
    // 未確定のため、可能性の高い候補を順番に試す。
    // 候補1: aria-controlsで展開先のメニューID(breadcrumb-menu-id)を直接指しているボタン
    // 候補2: パンくずのルート内にある、aria-haspopupを持つボタン
    // 候補3: パンくずのルート内にある最後から2番目より前の、隠れた省略記号ボタンらしき要素
    const candidates = [
      'button[aria-controls=breadcrumb-menu-id]',
      'div.breadcrumbRoot_b6af7cfe button[aria-haspopup]',
      'div.breadcrumbRoot_b6af7cfe button[aria-expanded]',
    ];
    for (const sel of candidates) {
      try {
        const el = document.querySelector(sel);
        if (el) return { el, sel };
      } catch (err) {
        logError('findOverflowTriggerButton-selector', `${sel}: ${err && err.message ? err.message : err}`);
      }
    }
    return null;
  }

  async function withExpandedOverflow(fn) {
    // オーバーフローメニューが既にDOM上にあるか(=展開済みか)を先に確認する。
    // 展開済みでなければ、推測したトリガーボタンをクリックして展開し、
    // fn()実行後は自力で開いた場合のみ閉じて元の状態に戻す。
    const alreadyExpanded = !!document.querySelector('#breadcrumb-menu-id');
    let openedByScript = false;

    if (!alreadyExpanded) {
      const trigger = findOverflowTriggerButton();
      if (trigger) {
        try {
          trigger.el.click();
          openedByScript = true;
          logError('withExpandedOverflow-clicked', `推測セレクタでクリックしました: ${trigger.sel}`);
        } catch (err) {
          logError('withExpandedOverflow-clickFailed', err);
        }
      } else {
        logError('withExpandedOverflow-triggerNotFound', 'オーバーフローボタンの候補セレクタがすべて一致しませんでした(親フォルダ階層が省略されていないか、DOM構造が想定と異なる可能性)');
      }
    }

    // クリック直後はメニューがまだDOMに反映されていない場合があるため少し待つ
    if (openedByScript) {
      await new Promise((resolve) => setTimeout(resolve, 150));
    }

    let result;
    try {
      result = fn();
    } finally {
      if (openedByScript) {
        const trigger = findOverflowTriggerButton();
        try {
          if (trigger) trigger.el.click();
          else document.body.click();
          // 元の状態(閉じている状態)に戻す。専用の閉じるボタンが見つからない場合は
          // body側クリックでポップアップ系メニューが閉じることを期待するフォールバック
        } catch (err) {
          logError('withExpandedOverflow-closeFailed', err);
        }
      }
    }
    return result;
  }

  function getOverflowBreadcrumb() {
    const parts = [];
    try {
      document.querySelectorAll('#breadcrumb-menu-id li span').forEach((el) => {
        const txt = (el.textContent || '').trim();
        if (txt) parts.push(txt);
      });
    } catch (err) {
      logError('getOverflowBreadcrumb', err);
    }
    return parts;
  }

  function getListsFieldValue(labelText) {
    try {
      const labels = document.querySelectorAll('label, [class*="fieldLabel" i], [class*="FieldLabel"]');
      for (const label of labels) {
        if (!(label.textContent || '').trim().includes(labelText)) continue;
        let container = label.closest('div');
        let depth = 0;
        while (container && depth < 4) {
          const valueEl = container.querySelector(
            'div.ReactFieldEditor-core--display.ReactFieldEditor-core--display-ReadOnly.ReactFieldEditor-linkFocusIndicator > div'
          );
          if (valueEl) {
            const val = (valueEl.textContent || '').trim();
            if (val) return val;
          }
          container = container.parentElement;
          depth++;
        }
      }
      return null;
    } catch (err) {
      logError(`getListsFieldValue:${labelText}`, err);
      return null;
    }
  }

  function getCheckedRows() {
    const results = [];
    try {
      const rows = document.querySelectorAll('div[aria-selected=true][data-automationid^=row-selection-]');
      rows.forEach((rowEl) => {
        const automationId = rowEl.getAttribute('data-automationid') || '';
        const idMatch = automationId.match(/row-selection-(.+)$/);
        const rawId = idMatch ? idMatch[1] : '';

        const ancestorRow = rowEl.closest('[role=row]');
        let isHeaderLike = false;
        if (ancestorRow && ancestorRow.getAttribute('aria-rowindex') === '1') {
          isHeaderLike = true;
        }
        if (!rawId || /^(all|header|select-?all)$/i.test(rawId)) {
          isHeaderLike = true;
        }
        if (isHeaderLike) return;

        let container = rowEl.parentElement;
        let depth = 0;
        let nameSpan = null;
        while (container && depth < 5 && !nameSpan) {
          nameSpan = container.querySelector('span[role=button][data-id=heroField]');
          if (!nameSpan) container = container.parentElement;
          depth++;
        }
        if (nameSpan) {
          const name = (nameSpan.textContent || '').trim();
          const itemKey = extractItemKey(nameSpan);
          results.push({ name, itemKey });
        } else {
          logError('getCheckedRows-nameNotFound', 'row-selection element found but file/item name span not located nearby');
        }
      });

      const seen = new Set();
      const deduped = [];
      for (const row of results) {
        const key = row.itemKey || `name:${row.name}`;
        if (!seen.has(key)) {
          seen.add(key);
          deduped.push(row);
        } else {
          logError('getCheckedRows-dedup', `重複行を除外しました: ${row.name}`);
        }
      }
      return deduped;
    } catch (err) {
      logError('getCheckedRows', err);
      return results;
    }
  }

  function buildListsItemUrl(itemKey) {
    try {
      const href = location.href;
      const idx = href.indexOf('/Lists/');
      if (idx === -1) return null;
      const m = href.substring(idx).match(/^\/Lists\/([^/]+)\//);
      if (!m) return null;
      return `${href.substring(0, idx)}/Lists/${m[1]}/DispForm.aspx?ID=${itemKey}`;
    } catch (err) {
      logError('buildListsItemUrl', err);
      return null;
    }
  }

  function buildListsAllItemsUrl() {
    // buildListsItemUrlと同じ考え方で、location.hrefから/Lists/リスト名/を抜き出し、
    // 一覧画面(AllItems.aspx)のURLを組み立てる。パンくずDOMには依存しない。
    try {
      const href = location.href;
      const idx = href.indexOf('/Lists/');
      if (idx === -1) {
        logError('buildListsAllItemsUrl-noListsSegment', `URLに/Lists/が含まれていません: ${href}`);
        return null;
      }
      const m = href.substring(idx).match(/^\/Lists\/([^/]+)\//);
      if (!m) {
        logError('buildListsAllItemsUrl-noMatch', `リスト名を抽出できませんでした: ${href.substring(idx)}`);
        return null;
      }
      return `${href.substring(0, idx)}/Lists/${m[1]}/AllItems.aspx`;
    } catch (err) {
      logError('buildListsAllItemsUrl', err);
      return null;
    }
  }

  function buildDoclibFileUrl(fileName) {
    try {
      const href = location.href;
      const qIdx = href.indexOf('?');
      let base = qIdx === -1 ? href : href.substring(0, qIdx);
      if (!base.endsWith('/')) base += '/';
      return base + encodeURIComponent(fileName);
    } catch (err) {
      logError('buildDoclibFileUrl', err);
      return null;
    }
  }

  function getAttachments() {
    try {
      const labels = document.querySelectorAll('label, [class*="fieldLabel" i], [class*="FieldLabel"]');
      let container = null;
      for (const label of labels) {
        if ((label.textContent || '').trim().includes('添付ファイル')) {
          container = label.closest('div');
          break;
        }
      }
      if (!container) {
        return { ok: false, items: [], reason: '添付ファイルラベルが見つかりませんでした' };
      }
      let depth = 0;
      const found = [];
      while (container && depth < 4 && found.length === 0) {
        container.querySelectorAll('a[href]').forEach((a) => {
          const name = (a.textContent || '').trim();
          const href = a.getAttribute('href');
          if (name && href && name.length < 150) {
            found.push({ name, url: href });
          }
        });
        container = container.parentElement;
        depth++;
      }
      if (found.length === 0) {
        return { ok: false, items: [], reason: '添付ファイル欄は見つかりましたが、リンク要素(href)を特定できませんでした' };
      }
      return { ok: true, items: found, reason: '' };
    } catch (err) {
      logError('getAttachments', err);
      return { ok: false, items: [], reason: `エラー: ${err.name || ''} ${err.message || ''}` };
    }
  }

  async function writeClipboardHtml(pathText, mainUrl, extraLinks) {
    let htmlBody = `<a href="${escapeHtml(mainUrl)}">${escapeHtml(pathText)}</a>`;
    if (extraLinks.length > 0) {
      htmlBody += '<br>';
      htmlBody += extraLinks
        .map((link) => `<a href="${escapeHtml(link.url)}">${escapeHtml(link.name)}</a>`)
        .join('<br>');
    }

    const plainLines = [`${pathText}  ${mainUrl}`];
    extraLinks.forEach((link) => plainLines.push(`${link.name}  ${link.url}`));
    const plainBody = plainLines.join('\n');

    try {
      const htmlBlob = new Blob([htmlBody], { type: 'text/html' });
      const textBlob = new Blob([plainBody], { type: 'text/plain' });
      await navigator.clipboard.write([new ClipboardItem({ 'text/html': htmlBlob, 'text/plain': textBlob })]);
      return { ok: true, message: 'コピー完了' };
    } catch (err) {
      logError('writeClipboardHtml', err);
      let hint = '';
      if (err && err.message && err.message.includes('not focused')) {
        hint = '(画面がフォーカスされていない可能性があります。パネル内の「コピー」ボタンをもう一度押してください)';
      }
      return { ok: false, message: `コピー失敗: ${err.name || ''} ${err.message || ''}${hint}` };
    }
  }

  async function copyPlainText(text) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (err) {
      return false;
    }
  }

  function renderPanel(mode, pathParts, extraLinks, mainUrl, checkedCount, checkedKeys) {
    document.getElementById('spPathBookmarkletPanel')?.remove();

    const panel = document.createElement('div');
    panel.id = 'spPathBookmarkletPanel';
    Object.assign(panel.style, {
      position: 'fixed',
      top: '20px',
      right: '20px',
      width: '460px',
      maxHeight: '80vh',
      overflow: 'auto',
      background: '#fff',
      border: '2px solid #333',
      borderRadius: '8px',
      padding: '16px',
      zIndex: 999999,
      fontFamily: 'sans-serif',
      fontSize: '13px',
      boxShadow: '0 4px 20px rgba(0,0,0,.3)',
      color: '#000',
    });

    const statusBox = document.createElement('div');
    Object.assign(statusBox.style, { marginBottom: '10px', padding: '6px', background: '#eef' });
    statusBox.textContent = '処理中…';
    panel.appendChild(statusBox);

    const pathLabel = document.createElement('div');
    pathLabel.style.fontWeight = 'bold';
    pathLabel.textContent = '保存先:';

    const pathBox = document.createElement('textarea');
    pathBox.id = 'spPathBookmarkletPathBox';
    Object.assign(pathBox.style, { width: '100%', boxSizing: 'border-box', height: '50px', marginBottom: '10px' });
    pathBox.value = pathParts.join(' > ');

    const dumpBtn = document.createElement('button');
    dumpBtn.textContent = '状況をコピー(報告用・エラーも含む)';
    Object.assign(dumpBtn.style, { marginBottom: '6px', display: 'block' });

    const dumpArea = document.createElement('textarea');
    Object.assign(dumpArea.style, {
      width: '100%',
      boxSizing: 'border-box',
      height: '80px',
      marginBottom: '10px',
      display: 'none',
      fontSize: '11px',
    });
    dumpArea.readOnly = true;
    dumpArea.onclick = () => dumpArea.select();

    dumpBtn.onclick = () => {
      const lines = [];
      lines.push('=== SPパスブックマークレット 状況ダンプ ===');
      lines.push(`バージョン: ${SCRIPT_VERSION}`);
      lines.push(`日時: ${new Date().toString()}`);
      lines.push(`URL: ${location.href}`);
      lines.push(`画面種別(mode): ${mode}`);
      lines.push(`チェック件数: ${checkedCount}`);
      lines.push(`itemKey一覧: ${checkedKeys.length ? checkedKeys.join(', ') : '(なし)'}`);
      lines.push(`保存先(現在の編集ボックスの値): ${pathBox.value}`);
      lines.push('アイテム/ファイルリンク:');
      if (extraLinks.length === 0) {
        lines.push('  (チェックボックス指定なし)');
      } else {
        extraLinks.forEach((link) => lines.push(`  - ${link.name} -> ${link.url}`));
      }
      lines.push(`メインURL: ${mainUrl}`);
      lines.push('エラー/警告ログ:');
      if (logs.length === 0) {
        lines.push('  (なし)');
      } else {
        logs.forEach((log) => lines.push(`  - ${log}`));
      }
      const dumpText = lines.join('\n');
      copyPlainText(dumpText).then((ok) => {
        dumpBtn.textContent = ok ? '状況をコピーしました' : 'コピー失敗(下のボックスを手動選択してください)';
        if (!ok) {
          dumpArea.style.display = 'block';
          dumpArea.value = dumpText;
          dumpArea.select();
        }
      });
    };
    panel.appendChild(dumpBtn);
    panel.appendChild(dumpArea);

    panel.appendChild(pathLabel);
    panel.appendChild(pathBox);

    const nameLabel = document.createElement('div');
    nameLabel.style.fontWeight = 'bold';
    nameLabel.textContent = 'アイテム名';
    panel.appendChild(nameLabel);

    const nameList = document.createElement('div');
    Object.assign(nameList.style, { marginBottom: '10px', padding: '4px', background: '#f7f7f7' });
    if (extraLinks.length === 0) {
      nameList.textContent = '(チェックボックス指定なし)';
    } else {
      extraLinks.forEach((link) => {
        const row = document.createElement('div');
        row.textContent = `${link.name}${link.url}`;
        nameList.appendChild(row);
      });
    }
    panel.appendChild(nameList);

    const attachCheckboxes = [];
    const attachItems = [];
    if (mode === 'lists-popup' || mode === 'lists-single') {
      const attachResult = getAttachments();
      const attachBox = document.createElement('div');
      attachBox.style.marginBottom = '10px';
      const attachLabel = document.createElement('div');
      attachLabel.style.fontWeight = 'bold';
      attachLabel.textContent = '添付ファイル(任意で個別リンクを追加)';
      attachBox.appendChild(attachLabel);

      if (attachResult.ok) {
        if (attachResult.items.length === 0) {
          const noAttach = document.createElement('div');
          noAttach.style.fontSize = '11px';
          noAttach.textContent = '添付ファイルはありません';
          attachBox.appendChild(noAttach);
        } else {
          attachResult.items.forEach((item, idx) => {
            const itemLabel = document.createElement('label');
            itemLabel.style.display = 'block';
            const cb = document.createElement('input');
            cb.type = 'checkbox';
            cb.dataset.attachIndex = String(idx);
            itemLabel.appendChild(cb);
            itemLabel.appendChild(document.createTextNode(` ${item.name}`));
            attachBox.appendChild(itemLabel);
            attachCheckboxes.push(cb);
            attachItems.push(item);
          });
        }
      } else {
        const attachErr = document.createElement('div');
        Object.assign(attachErr.style, { color: '#c00', fontSize: '11px' });
        attachErr.textContent = `取得できませんでした: ${attachResult.reason}`;
        attachBox.appendChild(attachErr);
      }
      panel.appendChild(attachBox);
    }

    if (logs.length > 0) {
      const warnLabel = document.createElement('div');
      Object.assign(warnLabel.style, { fontWeight: 'bold', color: '#c00' });
      warnLabel.textContent = '警告/エラー(タップで全選択できます):';
      panel.appendChild(warnLabel);

      const warnArea = document.createElement('textarea');
      Object.assign(warnArea.style, {
        width: '100%',
        boxSizing: 'border-box',
        height: '80px',
        marginBottom: '6px',
        background: '#fee',
        border: '1px solid #c00',
        fontSize: '11px',
      });
      warnArea.readOnly = true;
      warnArea.value = logs.join('\n');
      warnArea.onclick = () => warnArea.select();
      panel.appendChild(warnArea);
    }

    const btnRow = document.createElement('div');
    const copyBtn = document.createElement('button');
    copyBtn.textContent = 'コピー';
    copyBtn.style.marginRight = '8px';
    const closeBtn = document.createElement('button');
    closeBtn.textContent = '閉じる';
    closeBtn.onclick = () => panel.remove();
    btnRow.appendChild(copyBtn);
    btnRow.appendChild(closeBtn);
    panel.appendChild(btnRow);

    document.body.appendChild(panel);

    const doCopy = async () => {
      const pathText = pathBox.value;
      const links = [...extraLinks];
      attachCheckboxes.forEach((cb, idx) => {
        if (cb.checked) links.push(attachItems[idx]);
      });
      const result = await writeClipboardHtml(pathText, mainUrl, links);
      statusBox.style.background = result.ok ? '#efe' : '#fee';
      statusBox.textContent = result.message;
    };
    copyBtn.onclick = doCopy;

    statusBox.textContent = '自動コピーを試みています…';
    try {
      window.focus();
    } catch (err) {
      logError('window.focus', err);
    }
    try {
      document.body.focus();
    } catch (err) {
      /* noop */
    }
    setTimeout(() => {
      doCopy().catch((err) => {
        logError('autoCopy-unhandled', err);
        statusBox.style.background = '#fee';
        statusBox.textContent = '自動コピーに失敗しました。お手数ですが「コピー」ボタンを押してください。';
      });
    }, 500);
  }

  function detectMode() {
    try {
      if (document.querySelector('.sp-itemDialog')) return 'lists-popup';
    } catch (err) {
      logError('detectMode-popup', err);
    }
    try {
      if (document.querySelector('.od-ListForm-breadcrumb')) return 'lists-single';
    } catch (err) {
      logError('detectMode-listsingle', err);
    }
    try {
      if (document.querySelector('.breadcrumbRoot_b6af7cfe')) return 'doclib-list';
    } catch (err) {
      logError('detectMode-doclib', err);
    }
    try {
      if (document.querySelector('[id^=virtualized-list_]')) return 'unknown-grid';
    } catch (err) {
      logError('detectMode-grid', err);
    }
    return 'unknown';
  }

  async function main() {
    const mode = detectMode();
    let pathParts = [];
    const extraLinks = [];
    let mainUrl = location.href;
    let checkedCount = 0;
    const checkedKeys = [];

    if (mode === 'doclib-list') {
      const overflowParts = await withExpandedOverflow(() => getOverflowBreadcrumb());
      pathParts = [...overflowParts, ...getBreadcrumbPath()];

      const checkedFiles = getCheckedRows();
      checkedCount = checkedFiles.length;
      if (checkedFiles.length > 10) {
        alert(`チェックされたファイルが10件を超えています(${checkedFiles.length}件)。10件以内に絞り込んでから再実行してください。`);
        return;
      }
      checkedFiles.forEach((file) => {
        const fileUrl = buildDoclibFileUrl(file.name);
        if (!fileUrl) logError('buildDoclibFileUrl-null', `file=${file.name}`);
        extraLinks.push({ name: file.name, url: fileUrl || mainUrl });
        if (file.itemKey) checkedKeys.push(file.itemKey);
      });
    } else if (mode === 'lists-single' || mode === 'unknown-grid') {
      pathParts = getBreadcrumbPath();
      if (mode === 'lists-single') {
        // 単独ページでは今見ているアイテム自身のURL(location.href)をアイテム名リンクとして先に積んでおく
        const currentItemName = pathParts.length > 0 ? pathParts[pathParts.length - 1] : document.title;
        extraLinks.push({ name: currentItemName, url: location.href });
        // 疑似パス(一覧への案内)のリンク先は、location.hrefではなくAllItems.aspxへ差し替える
        const allItemsUrl = buildListsAllItemsUrl();
        if (allItemsUrl) mainUrl = allItemsUrl;
        else logError('lists-single-allItemsUrl-null', 'AllItems.aspx URLの組み立てに失敗、疑似パスは自身のURLのままになります');
      }
      const checkedItems = getCheckedRows();
      checkedCount = checkedItems.length;
      if (checkedItems.length > 10) {
        alert(`チェックされたアイテムが10件を超えています(${checkedItems.length}件)。10件以内に絞り込んでから再実行してください。`);
        return;
      }
      checkedItems.forEach((item) => {
        const itemUrl = item.itemKey ? buildListsItemUrl(item.itemKey) : null;
        if (!itemUrl) logError('buildListsItemUrl-null', `item=${item.name} key=${item.itemKey}`);
        extraLinks.push({ name: item.name, url: itemUrl || mainUrl });
        if (item.itemKey) checkedKeys.push(item.itemKey);
      });
    } else if (mode === 'lists-popup') {
      const dept = getListsFieldValue('掲載部署');
      const kind = getListsFieldValue('種別');
      if (dept) pathParts.push(dept);
      if (kind) pathParts.push(kind);
      if (!dept && !kind) logError('lists-popup-path', '掲載部署/種別のいずれも取得できませんでした');

      const heroSpan = document.querySelector('.sp-itemDialog span[role=button][data-id=heroField]');
      if (heroSpan) {
        const itemName = (heroSpan.textContent || '').trim();
        const itemKey = extractItemKey(heroSpan);
        const itemUrl = itemKey ? buildListsItemUrl(itemKey) : null;
        if (!itemUrl) logError('buildListsItemUrl-popup-null', `name=${itemName} key=${itemKey}`);
        extraLinks.push({ name: itemName, url: itemUrl || mainUrl });
        mainUrl = itemUrl || mainUrl;
        checkedCount = 1;
        if (itemKey) checkedKeys.push(itemKey);
      } else {
        logError('lists-popup-itemSpan', 'ポップアップ内のアイテム名要素が見つかりませんでした');
      }
    } else {
      logError('detectMode', `既知の画面パターンに一致しませんでした(mode=${mode})`);
      pathParts = getBreadcrumbPath();
    }

    renderPanel(mode, pathParts, extraLinks, mainUrl, checkedCount, checkedKeys);
  }

  main().catch((err) => {
    logError('main-unhandled', err);
    alert('ブックマークレットの実行中にエラーが発生しました。詳細はコンソールを確認してください。');
  });
})();

javascript:(function () {
  'use strict';

  const SCRIPT_VERSION = '20260820';

  const logs = [];
  function logError(tag, err) {
    const msg = `[${tag}] ${err && err.name ? `${err.name}: ${err.message}` : String(err)}`;
    logs.push(msg);
  }

  function escapeHtml(s) {
    return String(s)
      .replaceAll('&', '&amp;')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;');
  }

  function extractItemKey(el) {
    try {
      const da = el.getAttribute('data-actions');
      if (!da) {
        logError('extractItemKey-noDataActions', 'data-actions属性が要素に存在しませんでした');
        return null;
      }
      const m = da.match(/itemKey[^0-9]*([0-9]+)/);
      if (!m) {
        logError('extractItemKey-noMatch', `data-actionsにitemKeyが見つかりませんでした: ${da.slice(0, 200)}`);
        return null;
      }
      return m[1];
    } catch (err) {
      logError('extractItemKey', err);
      return null;
    }
  }

  function getBreadcrumbPath() {
    const parts = [];
    try {
      document.querySelectorAll('div.breadcrumbRoot_b6af7cfe li span').forEach((el) => {
        const txt = (el.textContent || '').trim();
        if (txt) parts.push(txt);
      });
    } catch (err) {
      logError('getBreadcrumbPath-doclib', err);
    }
    try {
      document
        .querySelectorAll('.od-ListForm-breadcrumb nav ul li a, .od-ListForm-breadcrumb nav ul li span')
        .forEach((el) => {
          const txt = (el.textContent || '').trim();
          if (txt && !parts.includes(txt)) parts.push(txt);
        });
    } catch (err) {
      logError('getBreadcrumbPath-lists', err);
    }
    return parts;
  }

  function findOverflowTriggerButton() {
    const candidates = [
      'button[aria-controls=breadcrumb-menu-id]',
      'div.breadcrumbRoot_b6af7cfe button[aria-haspopup]',
      'div.breadcrumbRoot_b6af7cfe button[aria-expanded]',
    ];
    for (const sel of candidates) {
      try {
        const el = document.querySelector(sel);
        if (el) return { el, sel };
      } catch (err) {
        logError('findOverflowTriggerButton-selector', `${sel}: ${err && err.message ? err.message : err}`);
      }
    }
    return null;
  }

  async function withExpandedOverflow(fn) {
    const alreadyExpanded = !!document.querySelector('#breadcrumb-menu-id');
    let openedByScript = false;

    if (!alreadyExpanded) {
      const trigger = findOverflowTriggerButton();
      if (trigger) {
        try {
          trigger.el.click();
          openedByScript = true;
          logError('withExpandedOverflow-clicked', `推測セレクタでクリックしました: ${trigger.sel}`);
        } catch (err) {
          logError('withExpandedOverflow-clickFailed', err);
        }
      } else {
        logError('withExpandedOverflow-triggerNotFound', 'オーバーフローボタンの候補セレクタがすべて一致しませんでした(親フォルダ階層が省略されていないか、DOM構造が想定と異なる可能性)');
      }
    }

    if (openedByScript) {
      await new Promise((resolve) => setTimeout(resolve, 150));
    }

    let result;
    try {
      result = fn();
    } finally {
      if (openedByScript) {
        const trigger = findOverflowTriggerButton();
        try {
          if (trigger) trigger.el.click();
          else document.body.click();
        } catch (err) {
          logError('withExpandedOverflow-closeFailed', err);
        }
      }
    }
    return result;
  }

  function getOverflowBreadcrumb() {
    const parts = [];
    try {
      document.querySelectorAll('#breadcrumb-menu-id li span').forEach((el) => {
        const txt = (el.textContent || '').trim();
        if (txt) parts.push(txt);
      });
    } catch (err) {
      logError('getOverflowBreadcrumb', err);
    }
    return parts;
  }

  function getListsFieldValue(labelText) {
    try {
      const labels = document.querySelectorAll('label, [class*="fieldLabel" i], [class*="FieldLabel"]');
      for (const label of labels) {
        if (!(label.textContent || '').trim().includes(labelText)) continue;
        let container = label.closest('div');
        let depth = 0;
        while (container && depth < 4) {
          const valueEl = container.querySelector(
            'div.ReactFieldEditor-core--display.ReactFieldEditor-core--display-ReadOnly.ReactFieldEditor-linkFocusIndicator > div'
          );
          if (valueEl) {
            const val = (valueEl.textContent || '').trim();
            if (val) return val;
          }
          container = container.parentElement;
          depth++;
        }
      }
      return null;
    } catch (err) {
      logError(`getListsFieldValue:${labelText}`, err);
      return null;
    }
  }

  function getCheckedRows() {
    const results = [];
    try {
      const rows = document.querySelectorAll('div[aria-selected=true][data-automationid^=row-selection-]');
      rows.forEach((rowEl) => {
        const automationId = rowEl.getAttribute('data-automationid') || '';
        const idMatch = automationId.match(/row-selection-(.+)$/);
        const rawId = idMatch ? idMatch[1] : '';

        const ancestorRow = rowEl.closest('[role=row]');
        let isHeaderLike = false;
        if (ancestorRow && ancestorRow.getAttribute('aria-rowindex') === '1') {
          isHeaderLike = true;
        }
        if (!rawId || /^(all|header|select-?all)$/i.test(rawId)) {
          isHeaderLike = true;
        }
        if (isHeaderLike) return;

        let container = rowEl.parentElement;
        let depth = 0;
        let nameSpan = null;
        while (container && depth < 5 && !nameSpan) {
          nameSpan = container.querySelector('span[role=button][data-id=heroField]');
          if (!nameSpan) container = container.parentElement;
          depth++;
        }
        if (nameSpan) {
          const name = (nameSpan.textContent || '').trim();
          const itemKey = extractItemKey(nameSpan);
          results.push({ name, itemKey });
        } else {
          logError('getCheckedRows-nameNotFound', 'row-selection element found but file/item name span not located nearby');
        }
      });

      const seen = new Set();
      const deduped = [];
      for (const row of results) {
        const key = row.itemKey || `name:${row.name}`;
        if (!seen.has(key)) {
          seen.add(key);
          deduped.push(row);
        } else {
          logError('getCheckedRows-dedup', `重複行を除外しました: ${row.name}`);
        }
      }
      return deduped;
    } catch (err) {
      logError('getCheckedRows', err);
      return results;
    }
  }

  function buildListsItemUrl(itemKey) {
    try {
      const href = location.href;
      const idx = href.indexOf('/Lists/');
      if (idx === -1) return null;
      const m = href.substring(idx).match(/^\/Lists\/([^/]+)\//);
      if (!m) return null;
      return `${href.substring(0, idx)}/Lists/${m[1]}/DispForm.aspx?ID=${itemKey}`;
    } catch (err) {
      logError('buildListsItemUrl', err);
      return null;
    }
  }

  function buildListsAllItemsUrl() {
    try {
      const href = location.href;
      const idx = href.indexOf('/Lists/');
      if (idx === -1) {
        logError('buildListsAllItemsUrl-noListsSegment', `URLに/Lists/が含まれていません: ${href}`);
        return null;
      }
      const m = href.substring(idx).match(/^\/Lists\/([^/]+)\//);
      if (!m) {
        logError('buildListsAllItemsUrl-noMatch', `リスト名を抽出できませんでした: ${href.substring(idx)}`);
        return null;
      }
      return `${href.substring(0, idx)}/Lists/${m[1]}/AllItems.aspx`;
    } catch (err) {
      logError('buildListsAllItemsUrl', err);
      return null;
    }
  }

  function buildDoclibFileUrl(fileName) {
    try {
      const href = location.href;
      const qIdx = href.indexOf('?');
      let base = qIdx === -1 ? href : href.substring(0, qIdx);
      if (!base.endsWith('/')) base += '/';
      return base + encodeURIComponent(fileName);
    } catch (err) {
      logError('buildDoclibFileUrl', err);
      return null;
    }
  }

  function getAttachments() {
    try {
      const labels = document.querySelectorAll('label, [class*="fieldLabel" i], [class*="FieldLabel"]');
      let container = null;
      for (const label of labels) {
        if ((label.textContent || '').trim().includes('添付ファイル')) {
          container = label.closest('div');
          break;
        }
      }
      if (!container) {
        return { ok: false, items: [], reason: '添付ファイルラベルが見つかりませんでした' };
      }
      let depth = 0;
      const found = [];
      while (container && depth < 4 && found.length === 0) {
        container.querySelectorAll('a[href]').forEach((a) => {
          const name = (a.textContent || '').trim();
          const href = a.getAttribute('href');
          if (name && href && name.length < 150) {
            found.push({ name, url: href });
          }
        });
        container = container.parentElement;
        depth++;
      }
      if (found.length === 0) {
        return { ok: false, items: [], reason: '添付ファイル欄は見つかりましたが、リンク要素(href)を特定できませんでした' };
      }
      return { ok: true, items: found, reason: '' };
    } catch (err) {
      logError('getAttachments', err);
      return { ok: false, items: [], reason: `エラー: ${err.name || ''} ${err.message || ''}` };
    }
  }

  async function writeClipboardHtml(pathText, mainUrl, extraLinks) {
    let htmlBody = `<a href="${escapeHtml(mainUrl)}">${escapeHtml(pathText)}</a>`;
    if (extraLinks.length > 0) {
      htmlBody += '<br>';
      htmlBody += extraLinks
        .map((link) => `<a href="${escapeHtml(link.url)}">${escapeHtml(link.name)}</a>`)
        .join('<br>');
    }

    const plainLines = [`${pathText}  ${mainUrl}`];
    extraLinks.forEach((link) => plainLines.push(`${link.name}  ${link.url}`));
    const plainBody = plainLines.join('\n');

    try {
      const htmlBlob = new Blob([htmlBody], { type: 'text/html' });
      const textBlob = new Blob([plainBody], { type: 'text/plain' });
      await navigator.clipboard.write([new ClipboardItem({ 'text/html': htmlBlob, 'text/plain': textBlob })]);
      return { ok: true, message: 'コピー完了' };
    } catch (err) {
      logError('writeClipboardHtml', err);
      let hint = '';
      if (err && err.message && err.message.includes('not focused')) {
        hint = '(画面がフォーカスされていない可能性があります。パネル内の「コピー」ボタンをもう一度押してください)';
      }
      return { ok: false, message: `コピー失敗: ${err.name || ''} ${err.message || ''}${hint}` };
    }
  }

  async function copyPlainText(text) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (err) {
      return false;
    }
  }

  function renderPanel(mode, pathParts, extraLinks, mainUrl, checkedCount, checkedKeys) {
    document.getElementById('spPathBookmarkletPanel')?.remove();

    const panel = document.createElement('div');
    panel.id = 'spPathBookmarkletPanel';
    Object.assign(panel.style, {
      position: 'fixed',
      top: '20px',
      right: '20px',
      width: '460px',
      maxHeight: '80vh',
      overflow: 'auto',
      background: '#fff',
      border: '2px solid #333',
      borderRadius: '8px',
      padding: '16px',
      zIndex: 999999,
      fontFamily: 'sans-serif',
      fontSize: '13px',
      boxShadow: '0 4px 20px rgba(0,0,0,.3)',
      color: '#000',
    });

    const statusBox = document.createElement('div');
    Object.assign(statusBox.style, { marginBottom: '10px', padding: '6px', background: '#eef' });
    statusBox.textContent = '処理中…';
    panel.appendChild(statusBox);

    const pathLabel = document.createElement('div');
    pathLabel.style.fontWeight = 'bold';
    pathLabel.textContent = '保存先:';

    const pathBox = document.createElement('textarea');
    pathBox.id = 'spPathBookmarkletPathBox';
    Object.assign(pathBox.style, { width: '100%', boxSizing: 'border-box', height: '50px', marginBottom: '10px' });
    pathBox.value = pathParts.join(' > ');

    const dumpBtn = document.createElement('button');
    dumpBtn.textContent = '状況をコピー(報告用・エラーも含む)';
    Object.assign(dumpBtn.style, { marginBottom: '6px', display: 'block' });

    const dumpArea = document.createElement('textarea');
    Object.assign(dumpArea.style, {
      width: '100%',
      boxSizing: 'border-box',
      height: '80px',
      marginBottom: '10px',
      display: 'none',
      fontSize: '11px',
    });
    dumpArea.readOnly = true;
    dumpArea.onclick = () => dumpArea.select();

    dumpBtn.onclick = () => {
      const lines = [];
      lines.push('=== SPパスブックマークレット 状況ダンプ ===');
      lines.push(`バージョン: ${SCRIPT_VERSION}`);
      lines.push(`日時: ${new Date().toString()}`);
      lines.push(`URL: ${location.href}`);
      lines.push(`画面種別(mode): ${mode}`);
      lines.push(`チェック件数: ${checkedCount}`);
      lines.push(`itemKey一覧: ${checkedKeys.length ? checkedKeys.join(', ') : '(なし)'}`);
      lines.push(`保存先(現在の編集ボックスの値): ${pathBox.value}`);
      lines.push('アイテム/ファイルリンク:');
      if (extraLinks.length === 0) {
        lines.push('  (チェックボックス指定なし)');
      } else {
        extraLinks.forEach((link) => lines.push(`  - ${link.name} -> ${link.url}`));
      }
      lines.push(`メインURL: ${mainUrl}`);
      lines.push('エラー/警告ログ:');
      if (logs.length === 0) {
        lines.push('  (なし)');
      } else {
        logs.forEach((log) => lines.push(`  - ${log}`));
      }
      const dumpText = lines.join('\n');
      copyPlainText(dumpText).then((ok) => {
        dumpBtn.textContent = ok ? '状況をコピーしました' : 'コピー失敗(下のボックスを手動選択してください)';
        if (!ok) {
          dumpArea.style.display = 'block';
          dumpArea.value = dumpText;
          dumpArea.select();
        }
      });
    };
    panel.appendChild(dumpBtn);
    panel.appendChild(dumpArea);

    panel.appendChild(pathLabel);
    panel.appendChild(pathBox);

    const nameLabel = document.createElement('div');
    nameLabel.style.fontWeight = 'bold';
    nameLabel.textContent = 'アイテム名';
    panel.appendChild(nameLabel);

    const nameList = document.createElement('div');
    Object.assign(nameList.style, { marginBottom: '10px', padding: '4px', background: '#f7f7f7' });
    if (extraLinks.length === 0) {
      nameList.textContent = '(チェックボックス指定なし)';
    } else {
      extraLinks.forEach((link) => {
        const row = document.createElement('div');
        row.textContent = `${link.name}${link.url}`;
        nameList.appendChild(row);
      });
    }
    panel.appendChild(nameList);

    const attachCheckboxes = [];
    const attachItems = [];
    if (mode === 'lists-popup' || mode === 'lists-single') {
      const attachResult = getAttachments();
      const attachBox = document.createElement('div');
      attachBox.style.marginBottom = '10px';
      const attachLabel = document.createElement('div');
      attachLabel.style.fontWeight = 'bold';
      attachLabel.textContent = '添付ファイル(任意で個別リンクを追加)';
      attachBox.appendChild(attachLabel);

      if (attachResult.ok) {
        if (attachResult.items.length === 0) {
          const noAttach = document.createElement('div');
          noAttach.style.fontSize = '11px';
          noAttach.textContent = '添付ファイルはありません';
          attachBox.appendChild(noAttach);
        } else {
          attachResult.items.forEach((item, idx) => {
            const itemLabel = document.createElement('label');
            itemLabel.style.display = 'block';
            const cb = document.createElement('input');
            cb.type = 'checkbox';
            cb.dataset.attachIndex = String(idx);
            itemLabel.appendChild(cb);
            itemLabel.appendChild(document.createTextNode(` ${item.name}`));
            attachBox.appendChild(itemLabel);
            attachCheckboxes.push(cb);
            attachItems.push(item);
          });
        }
      } else {
        const attachErr = document.createElement('div');
        Object.assign(attachErr.style, { color: '#c00', fontSize: '11px' });
        attachErr.textContent = `取得できませんでした: ${attachResult.reason}`;
        attachBox.appendChild(attachErr);
      }
      panel.appendChild(attachBox);
    }

    if (logs.length > 0) {
      const warnLabel = document.createElement('div');
      Object.assign(warnLabel.style, { fontWeight: 'bold', color: '#c00' });
      warnLabel.textContent = '警告/エラー(タップで全選択できます):';
      panel.appendChild(warnLabel);

      const warnArea = document.createElement('textarea');
      Object.assign(warnArea.style, {
        width: '100%',
        boxSizing: 'border-box',
        height: '80px',
        marginBottom: '6px',
        background: '#fee',
        border: '1px solid #c00',
        fontSize: '11px',
      });
      warnArea.readOnly = true;
      warnArea.value = logs.join('\n');
      warnArea.onclick = () => warnArea.select();
      panel.appendChild(warnArea);
    }

    const btnRow = document.createElement('div');
    const copyBtn = document.createElement('button');
    copyBtn.textContent = 'コピー';
    copyBtn.style.marginRight = '8px';
    const closeBtn = document.createElement('button');
    closeBtn.textContent = '閉じる';
    closeBtn.onclick = () => panel.remove();
    btnRow.appendChild(copyBtn);
    btnRow.appendChild(closeBtn);
    panel.appendChild(btnRow);

    document.body.appendChild(panel);

    const doCopy = async () => {
      const pathText = pathBox.value;
      const links = [...extraLinks];
      attachCheckboxes.forEach((cb, idx) => {
        if (cb.checked) links.push(attachItems[idx]);
      });
      const result = await writeClipboardHtml(pathText, mainUrl, links);
      statusBox.style.background = result.ok ? '#efe' : '#fee';
      statusBox.textContent = result.message;
    };
    copyBtn.onclick = doCopy;

    statusBox.textContent = '自動コピーを試みています…';
    try {
      window.focus();
    } catch (err) {
      logError('window.focus', err);
    }
    try {
      document.body.focus();
    } catch (err) {
      /* noop */
    }
    setTimeout(() => {
      doCopy().catch((err) => {
        logError('autoCopy-unhandled', err);
        statusBox.style.background = '#fee';
        statusBox.textContent = '自動コピーに失敗しました。お手数ですが「コピー」ボタンを押してください。';
      });
    }, 500);
  }

  function detectMode() {
    try {
      if (document.querySelector('.sp-itemDialog')) return 'lists-popup';
    } catch (err) {
      logError('detectMode-popup', err);
    }
    try {
      if (document.querySelector('.od-ListForm-breadcrumb')) return 'lists-single';
    } catch (err) {
      logError('detectMode-listsingle', err);
    }
    try {
      if (document.querySelector('.breadcrumbRoot_b6af7cfe')) return 'doclib-list';
    } catch (err) {
      logError('detectMode-doclib', err);
    }
    try {
      if (document.querySelector('[id^=virtualized-list_]')) return 'unknown-grid';
    } catch (err) {
      logError('detectMode-grid', err);
    }
    return 'unknown';
  }

  async function main() {
    const mode = detectMode();
    let pathParts = [];
    const extraLinks = [];
    let mainUrl = location.href;
    let checkedCount = 0;
    const checkedKeys = [];

    if (mode === 'doclib-list') {
      const overflowParts = await withExpandedOverflow(() => getOverflowBreadcrumb());
      pathParts = [...overflowParts, ...getBreadcrumbPath()];

      const checkedFiles = getCheckedRows();
      checkedCount = checkedFiles.length;
      if (checkedFiles.length > 10) {
        alert(`チェックされたファイルが10件を超えています(${checkedFiles.length}件)。10件以内に絞り込んでから再実行してください。`);
        return;
      }
      checkedFiles.forEach((file) => {
        const fileUrl = buildDoclibFileUrl(file.name);
        if (!fileUrl) logError('buildDoclibFileUrl-null', `file=${file.name}`);
        extraLinks.push({ name: file.name, url: fileUrl || mainUrl });
        if (file.itemKey) checkedKeys.push(file.itemKey);
      });
    } else if (mode === 'lists-single' || mode === 'unknown-grid') {
      pathParts = getBreadcrumbPath();
      if (mode === 'lists-single') {
        const currentItemName = pathParts.length > 0 ? pathParts[pathParts.length - 1] : document.title;
        extraLinks.push({ name: currentItemName, url: location.href });
        const allItemsUrl = buildListsAllItemsUrl();
        if (allItemsUrl) mainUrl = allItemsUrl;
        else logError('lists-single-allItemsUrl-null', 'AllItems.aspx URLの組み立てに失敗、疑似パスは自身のURLのままになります');
      }
      const checkedItems = getCheckedRows();
      checkedCount = checkedItems.length;
      if (checkedItems.length > 10) {
        alert(`チェックされたアイテムが10件を超えています(${checkedItems.length}件)。10件以内に絞り込んでから再実行してください。`);
        return;
      }
      checkedItems.forEach((item) => {
        const itemUrl = item.itemKey ? buildListsItemUrl(item.itemKey) : null;
        if (!itemUrl) logError('buildListsItemUrl-null', `item=${item.name} key=${item.itemKey}`);
        extraLinks.push({ name: item.name, url: itemUrl || mainUrl });
        if (item.itemKey) checkedKeys.push(item.itemKey);
      });
    } else if (mode === 'lists-popup') {
      const dept = getListsFieldValue('掲載部署');
      const kind = getListsFieldValue('種別');
      if (dept) pathParts.push(dept);
      if (kind) pathParts.push(kind);
      if (!dept && !kind) logError('lists-popup-path', '掲載部署/種別のいずれも取得できませんでした');

      const heroSpan = document.querySelector('.sp-itemDialog span[role=button][data-id=heroField]');
      if (heroSpan) {
        const itemName = (heroSpan.textContent || '').trim();
        const itemKey = extractItemKey(heroSpan);
        const itemUrl = itemKey ? buildListsItemUrl(itemKey) : null;
        if (!itemUrl) logError('buildListsItemUrl-popup-null', `name=${itemName} key=${itemKey}`);
        extraLinks.push({ name: itemName, url: itemUrl || mainUrl });
        mainUrl = itemUrl || mainUrl;
        checkedCount = 1;
        if (itemKey) checkedKeys.push(itemKey);
      } else {
        logError('lists-popup-itemSpan', 'ポップアップ内のアイテム名要素が見つかりませんでした');
      }
    } else {
      logError('detectMode', `既知の画面パターンに一致しませんでした(mode=${mode})`);
      pathParts = getBreadcrumbPath();
    }

    renderPanel(mode, pathParts, extraLinks, mainUrl, checkedCount, checkedKeys);
  }

  main().catch((err) => {
    logError('main-unhandled', err);
    alert('ブックマークレットの実行中にエラーが発生しました。詳細はコンソールを確認してください。');
  });
})();

————————

javascript:(function () {
  'use strict';

  const SCRIPT_VERSION = '20260825_4';

  const logs = [];
  function logError(tag, err) {
    const msg = `[${tag}] ${err && err.name ? `${err.name}: ${err.message}` : String(err)}`;
    logs.push(msg);
  }

  function escapeHtml(s) {
    return String(s)
      .replaceAll('&', '&amp;')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;');
  }

  function extractItemKey(el) {
    try {
      const da = el.getAttribute('data-actions');
      if (!da) {
        logError('extractItemKey-noDataActions', 'data-actions属性が要素に存在しませんでした');
        return null;
      }
      const m = da.match(/itemKey[^0-9]*([0-9]+)/);
      if (!m) {
        logError('extractItemKey-noMatch', `data-actionsにitemKeyが見つかりませんでした: ${da.slice(0, 200)}`);
        return null;
      }
      return m[1];
    } catch (err) {
      logError('extractItemKey', err);
      return null;
    }
  }

  function diagnoseBreadcrumbCandidates() {
    const hints = [];
    try {
      const found = document.querySelectorAll('[class*=breadcrumb i], [data-automationid*=breadcrumb i], [class*=Breadcrumb]');
      const seen = new Set();
      found.forEach((el) => {
        if (hints.length >= 15) return;
        const tag = el.tagName.toLowerCase();
        const cls = (el.className && typeof el.className === 'string') ? el.className.slice(0, 80) : '';
        const auto = el.getAttribute('data-automationid') || '';
        const key = tag + '|' + cls + '|' + auto;
        if (seen.has(key)) return;
        seen.add(key);
        const txt = (el.textContent || '').trim().slice(0, 20);
        hints.push('<' + tag + '> class=[' + cls + '] data-automationid=[' + auto + '] text=[' + txt + ']');
      });
    } catch (err) {
      logError('diagnoseBreadcrumbCandidates', err);
    }
    return hints;
  }

  function getBreadcrumbPath() {
    const parts = [];
    try {
      document.querySelectorAll('div.breadcrumbRoot_b6af7cfe li span').forEach((el) => {
        const txt = (el.textContent || '').trim();
        if (txt) parts.push(txt);
      });
    } catch (err) {
      logError('getBreadcrumbPath-doclib', err);
    }
    try {
      document
        .querySelectorAll('.od-ListForm-breadcrumb nav ul li a, .od-ListForm-breadcrumb nav ul li span')
        .forEach((el) => {
          const txt = (el.textContent || '').trim();
          if (txt && !parts.includes(txt)) parts.push(txt);
        });
    } catch (err) {
      logError('getBreadcrumbPath-lists', err);
    }
    try {
      document.querySelectorAll('[data-automationid=breadcrumb-crumb]').forEach((el) => {
        const txt = (el.textContent || '').trim();
        if (txt && !parts.includes(txt)) parts.push(txt);
      });
    } catch (err) {
      logError('getBreadcrumbPath-crumb', err);
    }
    return parts;
  }

  function findOverflowTriggerButton() {
    const candidates = [
      'button[data-automationid=breadcrumb-overflow]',
      'button[aria-controls=breadcrumb-menu-id]',
      'div.breadcrumbRoot_b6af7cfe button[aria-haspopup]',
      'div.breadcrumbRoot_b6af7cfe button[aria-expanded]',
    ];
    for (const sel of candidates) {
      try {
        const el = document.querySelector(sel);
        if (el) return { el, sel };
      } catch (err) {
        logError('findOverflowTriggerButton-selector', `${sel}: ${err && err.message ? err.message : err}`);
      }
    }
    return null;
  }

  async function withExpandedOverflow(fn) {
    const alreadyExpanded = !!document.querySelector('#breadcrumb-menu-id');
    let openedByScript = false;

    if (!alreadyExpanded) {
      const trigger = findOverflowTriggerButton();
      if (trigger) {
        try {
          trigger.el.click();
          openedByScript = true;
          logError('withExpandedOverflow-clicked', `推測セレクタでクリックしました: ${trigger.sel}`);
        } catch (err) {
          logError('withExpandedOverflow-clickFailed', err);
        }
      } else {
        logError('withExpandedOverflow-triggerNotFound', 'オーバーフローボタンの候補セレクタがすべて一致しませんでした(親フォルダ階層が省略されていないか、DOM構造が想定と異なる可能性)');
      }
    }

    if (openedByScript) {
      await new Promise((resolve) => setTimeout(resolve, 150));
    }

    let result;
    try {
      result = fn();
    } finally {
      if (openedByScript) {
        const trigger = findOverflowTriggerButton();
        try {
          if (trigger) trigger.el.click();
          else document.body.click();
        } catch (err) {
          logError('withExpandedOverflow-closeFailed', err);
        }
      }
    }
    return result;
  }

  function getOverflowBreadcrumb() {
    const parts = [];
    try {
      document.querySelectorAll('#breadcrumb-menu-id li span').forEach((el) => {
        const txt = (el.textContent || '').trim();
        if (txt) parts.push(txt);
      });
    } catch (err) {
      logError('getOverflowBreadcrumb', err);
    }
    return parts;
  }

  function getListsFieldValue(labelText) {
    try {
      const labels = document.querySelectorAll('label, [class*="fieldLabel" i], [class*="FieldLabel"]');
      for (const label of labels) {
        if (!(label.textContent || '').trim().includes(labelText)) continue;
        let container = label.closest('div');
        let depth = 0;
        while (container && depth < 4) {
          const valueEl = container.querySelector(
            'div.ReactFieldEditor-core--display.ReactFieldEditor-core--display-ReadOnly.ReactFieldEditor-linkFocusIndicator > div'
          );
          if (valueEl) {
            const val = (valueEl.textContent || '').trim();
            if (val) return val;
          }
          container = container.parentElement;
          depth++;
        }
      }
      return null;
    } catch (err) {
      logError(`getListsFieldValue:${labelText}`, err);
      return null;
    }
  }

  function getCheckedRows() {
    const results = [];
    try {
      const checkboxes = document.querySelectorAll('input[type=checkbox][data-automationid=selection-checkbox][aria-checked=true]');
      checkboxes.forEach((cb) => {
        const rowEl = cb.closest('[data-automationid^=row-selection-]');
        if (!rowEl) {
          logError('getCheckedRows-rowNotFound', 'aria-checked=trueのチェックボックスからrow-selection要素が見つかりませんでした');
          return;
        }
        const automationId = rowEl.getAttribute('data-automationid') || '';
        const idMatch = automationId.match(/row-selection-(.+)$/);
        const rawId = idMatch ? idMatch[1] : '';

        const ancestorRow = rowEl.closest('[role=row]');
        let isHeaderLike = false;
        if (ancestorRow && ancestorRow.getAttribute('aria-rowindex') === '1') {
          isHeaderLike = true;
        }
        if (!rawId || /^(all|header|select-?all)$/i.test(rawId)) {
          isHeaderLike = true;
        }
        if (isHeaderLike) return;

        let container = rowEl.parentElement;
        let depth = 0;
        let nameSpan = null;
        while (container && depth < 5 && !nameSpan) {
          nameSpan = container.querySelector('span[role=button][data-id=heroField]');
          if (!nameSpan) container = container.parentElement;
          depth++;
        }
        if (nameSpan) {
          const name = (nameSpan.textContent || '').trim();
          const itemKey = extractItemKey(nameSpan);
          if (!itemKey) {
            try {
              logError('getCheckedRows-itemKeyNull-html', (nameSpan.outerHTML || '').slice(0, 300));
            } catch (err) {
              logError('getCheckedRows-itemKeyNull-htmlFailed', err);
            }
          }
          results.push({ name, itemKey });
        } else {
          logError('getCheckedRows-nameNotFound', 'row-selection element found but file/item name span not located nearby');
        }
      });

      const seen = new Set();
      const deduped = [];
      for (const row of results) {
        const key = row.itemKey || `name:${row.name}`;
        if (!seen.has(key)) {
          seen.add(key);
          deduped.push(row);
        } else {
          logError('getCheckedRows-dedup', `重複行を除外しました: ${row.name}`);
        }
      }
      return deduped;
    } catch (err) {
      logError('getCheckedRows', err);
      return results;
    }
  }

  function buildListsItemUrl(itemKey) {
    try {
      const href = location.href;
      const idx = href.indexOf('/Lists/');
      if (idx === -1) return null;
      const m = href.substring(idx).match(/^\/Lists\/([^/]+)\//);
      if (!m) return null;
      return `${href.substring(0, idx)}/Lists/${m[1]}/DispForm.aspx?ID=${itemKey}`;
    } catch (err) {
      logError('buildListsItemUrl', err);
      return null;
    }
  }

  function buildListsAllItemsUrl() {
    try {
      const href = location.href;
      const idx = href.indexOf('/Lists/');
      if (idx === -1) {
        logError('buildListsAllItemsUrl-noListsSegment', `URLに/Lists/が含まれていません: ${href}`);
        return null;
      }
      const m = href.substring(idx).match(/^\/Lists\/([^/]+)\//);
      if (!m) {
        logError('buildListsAllItemsUrl-noMatch', `リスト名を抽出できませんでした: ${href.substring(idx)}`);
        return null;
      }
      return `${href.substring(0, idx)}/Lists/${m[1]}/AllItems.aspx`;
    } catch (err) {
      logError('buildListsAllItemsUrl', err);
      return null;
    }
  }

  function buildDoclibFileUrl(fileName) {
    try {
      const href = location.href;
      const qIdx = href.indexOf('?');
      let base = qIdx === -1 ? href : href.substring(0, qIdx);
      if (!base.endsWith('/')) base += '/';
      return base + encodeURIComponent(fileName);
    } catch (err) {
      logError('buildDoclibFileUrl', err);
      return null;
    }
  }

  function getAttachments() {
    try {
      const labels = document.querySelectorAll('label, [class*="fieldLabel" i], [class*="FieldLabel"]');
      let container = null;
      for (const label of labels) {
        if ((label.textContent || '').trim().includes('添付ファイル')) {
          container = label.closest('div');
          break;
        }
      }
      if (!container) {
        return { ok: false, items: [], reason: '添付ファイルラベルが見つかりませんでした' };
      }
      let depth = 0;
      const found = [];
      while (container && depth < 4 && found.length === 0) {
        container.querySelectorAll('a[href]').forEach((a) => {
          const name = (a.textContent || '').trim();
          const href = a.getAttribute('href');
          if (name && href && name.length < 150) {
            found.push({ name, url: href });
          }
        });
        container = container.parentElement;
        depth++;
      }
      if (found.length === 0) {
        return { ok: false, items: [], reason: '添付ファイル欄は見つかりましたが、リンク要素(href)を特定できませんでした' };
      }
      return { ok: true, items: found, reason: '' };
    } catch (err) {
      logError('getAttachments', err);
      return { ok: false, items: [], reason: `エラー: ${err.name || ''} ${err.message || ''}` };
    }
  }

  async function writeClipboardHtml(pathText, mainUrl, extraLinks) {
    let htmlBody = `<a href="${escapeHtml(mainUrl)}">${escapeHtml(pathText)}</a>`;
    if (extraLinks.length > 0) {
      htmlBody += '<br>';
      htmlBody += extraLinks
        .map((link) => `<a href="${escapeHtml(link.url)}">${escapeHtml(link.name)}</a>`)
        .join('<br>');
    }

    const plainLines = [`${pathText}  ${mainUrl}`];
    extraLinks.forEach((link) => plainLines.push(`${link.name}  ${link.url}`));
    const plainBody = plainLines.join('\n');

    try {
      const htmlBlob = new Blob([htmlBody], { type: 'text/html' });
      const textBlob = new Blob([plainBody], { type: 'text/plain' });
      await navigator.clipboard.write([new ClipboardItem({ 'text/html': htmlBlob, 'text/plain': textBlob })]);
      return { ok: true, message: 'コピー完了' };
    } catch (err) {
      logError('writeClipboardHtml', err);
      let hint = '';
      if (err && err.message && err.message.includes('not focused')) {
        hint = '(画面がフォーカスされていない可能性があります。パネル内の「コピー」ボタンをもう一度押してください)';
      }
      return { ok: false, message: `コピー失敗: ${err.name || ''} ${err.message || ''}${hint}` };
    }
  }

  async function copyPlainText(text) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (err) {
      return false;
    }
  }

  function renderPanel(mode, pathParts, extraLinks, mainUrl, checkedCount, checkedKeys) {
    document.getElementById('spPathBookmarkletPanel')?.remove();

    const panel = document.createElement('div');
    panel.id = 'spPathBookmarkletPanel';
    Object.assign(panel.style, {
      position: 'fixed',
      top: '20px',
      right: '20px',
      width: '460px',
      maxHeight: '80vh',
      overflow: 'auto',
      background: '#fff',
      border: '2px solid #333',
      borderRadius: '8px',
      padding: '16px',
      zIndex: 999999,
      fontFamily: 'sans-serif',
      fontSize: '13px',
      boxShadow: '0 4px 20px rgba(0,0,0,.3)',
      color: '#000',
    });

    const closeXBtn = document.createElement('button');
    closeXBtn.textContent = '×';
    Object.assign(closeXBtn.style, {
      position: 'absolute',
      top: '8px',
      right: '10px',
      border: 'none',
      background: 'transparent',
      fontSize: '18px',
      lineHeight: '1',
      cursor: 'pointer',
      color: '#333',
      padding: '2px 6px',
    });
    closeXBtn.setAttribute('aria-label', '閉じる');
    closeXBtn.onclick = () => panel.remove();
    panel.appendChild(closeXBtn);

    const statusBox = document.createElement('div');
    Object.assign(statusBox.style, { marginBottom: '10px', padding: '6px', background: '#eef' });
    statusBox.textContent = '処理中…';
    panel.appendChild(statusBox);

    const pathLabel = document.createElement('div');
    pathLabel.style.fontWeight = 'bold';
    pathLabel.textContent = '保存先:';

    const pathBox = document.createElement('textarea');
    pathBox.id = 'spPathBookmarkletPathBox';
    Object.assign(pathBox.style, { width: '100%', boxSizing: 'border-box', height: '50px', marginBottom: '10px' });
    pathBox.value = pathParts.join(' > ');

    const dumpBtn = document.createElement('button');
    dumpBtn.textContent = '状況をコピー(報告用・エラーも含む)';
    Object.assign(dumpBtn.style, { marginBottom: '6px', display: 'block' });

    const dumpArea = document.createElement('textarea');
    Object.assign(dumpArea.style, {
      width: '100%',
      boxSizing: 'border-box',
      height: '80px',
      marginBottom: '10px',
      display: 'none',
      fontSize: '11px',
    });
    dumpArea.readOnly = true;
    dumpArea.onclick = () => dumpArea.select();

    dumpBtn.onclick = () => {
      const lines = [];
      lines.push('=== SPパスブックマークレット 状況ダンプ ===');
      lines.push(`バージョン: ${SCRIPT_VERSION}`);
      lines.push(`日時: ${new Date().toString()}`);
      lines.push(`URL: ${location.href}`);
      lines.push(`画面種別(mode): ${mode}`);
      lines.push(`チェック件数: ${checkedCount}`);
      lines.push(`itemKey一覧: ${checkedKeys.length ? checkedKeys.join(', ') : '(なし)'}`);
      lines.push(`保存先(現在の編集ボックスの値): ${pathBox.value}`);
      lines.push('アイテム/ファイルリンク:');
      if (extraLinks.length === 0) {
        lines.push('  (チェックボックス指定なし)');
      } else {
        extraLinks.forEach((link) => lines.push(`  - ${link.name} -> ${link.url}`));
      }
      lines.push(`メインURL: ${mainUrl}`);
      lines.push('エラー/警告ログ:');
      if (logs.length === 0) {
        lines.push('  (なし)');
      } else {
        logs.forEach((log) => lines.push(`  - ${log}`));
      }
      const dumpText = lines.join('\n');
      copyPlainText(dumpText).then((ok) => {
        dumpBtn.textContent = ok ? '状況をコピーしました' : 'コピー失敗(下のボックスを手動選択してください)';
        if (!ok) {
          dumpArea.style.display = 'block';
          dumpArea.value = dumpText;
          dumpArea.select();
        }
      });
    };
    panel.appendChild(dumpBtn);
    panel.appendChild(dumpArea);

    panel.appendChild(pathLabel);
    panel.appendChild(pathBox);

    const nameLabel = document.createElement('div');
    nameLabel.style.fontWeight = 'bold';
    nameLabel.textContent = 'アイテム名';
    panel.appendChild(nameLabel);

    const nameList = document.createElement('div');
    Object.assign(nameList.style, { marginBottom: '10px', padding: '4px', background: '#f7f7f7' });
    if (extraLinks.length === 0) {
      nameList.textContent = '(チェックボックス指定なし)';
    } else {
      extraLinks.forEach((link) => {
        const row = document.createElement('div');
        row.textContent = `${link.name}${link.url}`;
        nameList.appendChild(row);
      });
    }
    panel.appendChild(nameList);

    const attachCheckboxes = [];
    const attachItems = [];
    if (mode === 'lists-popup' || mode === 'lists-single') {
      const attachResult = getAttachments();
      const attachBox = document.createElement('div');
      attachBox.style.marginBottom = '10px';
      const attachLabel = document.createElement('div');
      attachLabel.style.fontWeight = 'bold';
      attachLabel.textContent = '添付ファイル(任意で個別リンクを追加)';
      attachBox.appendChild(attachLabel);

      if (attachResult.ok) {
        if (attachResult.items.length === 0) {
          const noAttach = document.createElement('div');
          noAttach.style.fontSize = '11px';
          noAttach.textContent = '添付ファイルはありません';
          attachBox.appendChild(noAttach);
        } else {
          attachResult.items.forEach((item, idx) => {
            const itemLabel = document.createElement('label');
            itemLabel.style.display = 'block';
            const cb = document.createElement('input');
            cb.type = 'checkbox';
            cb.dataset.attachIndex = String(idx);
            itemLabel.appendChild(cb);
            itemLabel.appendChild(document.createTextNode(` ${item.name}`));
            attachBox.appendChild(itemLabel);
            attachCheckboxes.push(cb);
            attachItems.push(item);
          });
        }
      } else {
        const attachErr = document.createElement('div');
        Object.assign(attachErr.style, { color: '#c00', fontSize: '11px' });
        attachErr.textContent = `取得できませんでした: ${attachResult.reason}`;
        attachBox.appendChild(attachErr);
      }
      panel.appendChild(attachBox);
    }

    if (logs.length > 0) {
      const warnLabel = document.createElement('div');
      Object.assign(warnLabel.style, { fontWeight: 'bold', color: '#c00' });
      warnLabel.textContent = '警告/エラー(タップで全選択できます):';
      panel.appendChild(warnLabel);

      const warnArea = document.createElement('textarea');
      Object.assign(warnArea.style, {
        width: '100%',
        boxSizing: 'border-box',
        height: '80px',
        marginBottom: '6px',
        background: '#fee',
        border: '1px solid #c00',
        fontSize: '11px',
      });
      warnArea.readOnly = true;
      warnArea.value = logs.join('\n');
      warnArea.onclick = () => warnArea.select();
      panel.appendChild(warnArea);
    }

    const btnRow = document.createElement('div');
    const copyBtn = document.createElement('button');
    copyBtn.textContent = 'コピー';
    copyBtn.style.marginRight = '8px';
    const reRunBtn = document.createElement('button');
    reRunBtn.textContent = '再実行';
    reRunBtn.style.marginRight = '8px';
    reRunBtn.onclick = () => {
      main().catch((err) => {
        logError('main-unhandled-rerun', err);
        alert('再実行中にエラーが発生しました。詳細はコンソールを確認してください。');
      });
    };
    const closeBtn = document.createElement('button');
    closeBtn.textContent = '閉じる';
    closeBtn.onclick = () => panel.remove();
    btnRow.appendChild(copyBtn);
    btnRow.appendChild(reRunBtn);
    btnRow.appendChild(closeBtn);
    panel.appendChild(btnRow);

    document.body.appendChild(panel);

    const doCopy = async () => {
      const pathText = pathBox.value;
      const links = [...extraLinks];
      attachCheckboxes.forEach((cb, idx) => {
        if (cb.checked) links.push(attachItems[idx]);
      });
      const result = await writeClipboardHtml(pathText, mainUrl, links);
      statusBox.style.background = result.ok ? '#efe' : '#fee';
      statusBox.textContent = result.message;
    };
    copyBtn.onclick = doCopy;

    statusBox.textContent = '自動コピーを試みています…';
    try {
      window.focus();
    } catch (err) {
      logError('window.focus', err);
    }
    try {
      document.body.focus();
    } catch (err) {
      /* noop */
    }
    setTimeout(() => {
      doCopy().catch((err) => {
        logError('autoCopy-unhandled', err);
        statusBox.style.background = '#fee';
        statusBox.textContent = '自動コピーに失敗しました。お手数ですが「コピー」ボタンを押してください。';
      });
    }, 500);
  }

  function detectMode() {
    try {
      if (document.querySelector('.sp-itemDialog')) return 'lists-popup';
    } catch (err) {
      logError('detectMode-popup', err);
    }
    try {
      if (document.querySelector('.od-ListForm-breadcrumb')) return 'lists-single';
    } catch (err) {
      logError('detectMode-listsingle', err);
    }
    try {
      if (document.querySelector('.breadcrumbRoot_b6af7cfe')) return 'doclib-list';
    } catch (err) {
      logError('detectMode-doclib', err);
    }
    try {
      if (document.querySelector('[id^=virtualized-list_]')) return 'unknown-grid';
    } catch (err) {
      logError('detectMode-grid', err);
    }
    return 'unknown';
  }

  async function main() {
    const mode = detectMode();
    let pathParts = [];
    const extraLinks = [];
    let mainUrl = location.href;
    let checkedCount = 0;
    const checkedKeys = [];

    if (mode === 'doclib-list') {
      const overflowParts = await withExpandedOverflow(() => getOverflowBreadcrumb());
      pathParts = [...overflowParts, ...getBreadcrumbPath()];

      const checkedFiles = getCheckedRows();
      checkedCount = checkedFiles.length;
      if (checkedFiles.length > 10) {
        alert(`チェックされたファイルが10件を超えています(${checkedFiles.length}件)。10件以内に絞り込んでから再実行してください。`);
        return;
      }
      checkedFiles.forEach((file) => {
        const fileUrl = buildDoclibFileUrl(file.name);
        if (!fileUrl) logError('buildDoclibFileUrl-null', `file=${file.name}`);
        extraLinks.push({ name: file.name, url: fileUrl || mainUrl });
        if (file.itemKey) checkedKeys.push(file.itemKey);
      });
    } else if (mode === 'lists-single' || mode === 'unknown-grid') {
      pathParts = getBreadcrumbPath();
      if (mode === 'lists-single') {
        const currentItemName = pathParts.length > 0 ? pathParts[pathParts.length - 1] : document.title;
        extraLinks.push({ name: currentItemName, url: location.href });
        const allItemsUrl = buildListsAllItemsUrl();
        if (allItemsUrl) mainUrl = allItemsUrl;
        else logError('lists-single-allItemsUrl-null', 'AllItems.aspx URLの組み立てに失敗、疑似パスは自身のURLのままになります');
      }
      const checkedItems = getCheckedRows();
      checkedCount = checkedItems.length;
      if (checkedItems.length > 10) {
        alert(`チェックされたアイテムが10件を超えています(${checkedItems.length}件)。10件以内に絞り込んでから再実行してください。`);
        return;
      }
      checkedItems.forEach((item) => {
        const itemUrl = item.itemKey ? buildListsItemUrl(item.itemKey) : null;
        if (!itemUrl) logError('buildListsItemUrl-null', `item=${item.name} key=${item.itemKey}`);
        extraLinks.push({ name: item.name, url: itemUrl || mainUrl });
        if (item.itemKey) checkedKeys.push(item.itemKey);
      });
    } else if (mode === 'lists-popup') {
      const dept = getListsFieldValue('掲載部署');
      const kind = getListsFieldValue('種別');
      if (dept) pathParts.push(dept);
      if (kind) pathParts.push(kind);
      if (!dept && !kind) logError('lists-popup-path', '掲載部署/種別のいずれも取得できませんでした');

      let heroSpan = document.querySelector('.sp-itemDialog span[role=button][data-id=heroField]');
      if (!heroSpan) {
        heroSpan = document.querySelector('.sp-itemDialog [data-id=heroField]');
        if (heroSpan) logError('lists-popup-itemSpan-fallback', 'span[role=button]では見つからず、[data-id=heroField]の緩い条件で代替検出しました');
      }
      if (heroSpan) {
        const itemName = (heroSpan.textContent || '').trim();
        const itemKey = extractItemKey(heroSpan);
        const itemUrl = itemKey ? buildListsItemUrl(itemKey) : null;
        if (!itemUrl) logError('buildListsItemUrl-popup-null', `name=${itemName} key=${itemKey}`);
        extraLinks.push({ name: itemName, url: itemUrl || mainUrl });
        mainUrl = itemUrl || mainUrl;
        checkedCount = 1;
        if (itemKey) checkedKeys.push(itemKey);
      } else {
        logError('lists-popup-itemSpan', 'ポップアップ内のアイテム名要素が見つかりませんでした');
        try {
          const candidates = document.querySelectorAll('.sp-itemDialog [role=button], .sp-itemDialog [data-id]');
          const hints = [];
          const seen = new Set();
          candidates.forEach((el) => {
            if (hints.length >= 10) return;
            const tag = el.tagName.toLowerCase();
            const role = el.getAttribute('role') || '';
            const dataId = el.getAttribute('data-id') || '';
            const key = tag + '|' + role + '|' + dataId;
            if (seen.has(key)) return;
            seen.add(key);
            const txt = (el.textContent || '').trim().slice(0, 20);
            hints.push('<' + tag + '> role=[' + role + '] data-id=[' + dataId + '] text=[' + txt + ']');
          });
          if (hints.length === 0) {
            logError('lists-popup-itemSpan-diagnose', '.sp-itemDialog内にrole/data-idを持つ候補要素も見つかりませんでした');
          } else {
            hints.forEach((h, i) => logError('lists-popup-itemSpan-diagnose-' + (i + 1), h));
          }
        } catch (err) {
          logError('lists-popup-itemSpan-diagnoseFailed', err);
        }
      }
    } else {
      logError('detectMode', `既知の画面パターンに一致しませんでした(mode=${mode})`);
      pathParts = getBreadcrumbPath();
    }

    if (pathParts.length === 0 && mode !== 'lists-popup') {
      const hints = diagnoseBreadcrumbCandidates();
      if (hints.length === 0) {
        logError('breadcrumb-diagnose', 'パンくず未取得。breadcrumb関連の候補要素も見つかりませんでした');
      } else {
        hints.forEach((h, i) => logError('breadcrumb-diagnose-' + (i + 1), h));
      }
    }

    renderPanel(mode, pathParts, extraLinks, mainUrl, checkedCount, checkedKeys);
  }

  main().catch((err) => {
    logError('main-unhandled', err);
    alert('ブックマークレットの実行中にエラーが発生しました。詳細はコンソールを確認してください。');
  });
})();
/*
【引継ぎメモ】
このコードはSharePoint Online用の擬似パス取得&ハイパーリンク付きコピー用ブックマークレットです。
導入時に改行が消えて1行になるため、コード途中にスラッシュ2つのコメントやブロックコメント記法を入れないこと。コメントは必ずこの位置(IIFE実行後の末尾)にのみ置くこと。

確定している設計・実装判断:
- クリップボードはexecCommandではなくnavigator.clipboard(writeText/write+ClipboardItem)を使用。実行直後に呼ぶとDocument is not focusedエラーになりやすいため、DOM収集後さらに約500ms待ってから書き込む。
- チェック済み行の判定は、チェックボックス本体の`input[data-automationid=selection-checkbox][aria-checked=true]`を正としてそこから行要素をclosest()で辿る方式(旧`div[aria-selected=true]`方式は常に0件だったため廃止)。
- パンくずオーバーフロー(見た目はフォルダアイコン、「...」ではない)の展開ボタンは`button[data-automationid=breadcrumb-overflow]`が実測確定セレクタ。
- 画面種別unknown-gridのパンくずは`div.breadcrumbRoot_b6af7cfe`でも`.od-ListForm-breadcrumb`でもなく、`[data-automationid=breadcrumb-crumb]`という別の安定属性で取得する。
- パネルに「再実行」ボタン(main()を呼び直すだけ。編集内容・チェック状態は全てリフレッシュされ、初回同様に自動コピーまで試みる)と、右上×閉じるボタンを実装済み。
- 想定挙動: 一覧画面(doclib-list/lists-single/unknown-grid)でチェックを入れて実行すると、擬似パス(一覧ページへのリンク)1本+チェックした各アイテム名(それぞれへの直リンク)を列挙する。この設計自体は確定済みで、課題は「チェック行から正しく名前とitemKeyまで辿れるか」という検出精度の部分に絞られている。

診断ログ機能(F12を開かず持ち帰れるようにする仕組み):
- パンくずが1件も取れなかった場合(lists-popup以外)、diagnoseBreadcrumbCandidates()が`breadcrumb`を含むclass/data-automationidを持つ要素を自動収集し、「状況をコピー」のログにタグ名・class・data属性・テキストの一部を出力する
- getCheckedRowsでheroFieldは見つかったがitemKeyがnull(data-actions属性が無い等)の場合、その要素のouterHTML先頭300文字をログに出力する(getCheckedRows-itemKeyNull-html)
- lists-popupでアイテム名要素が完全に見つからない場合、.sp-itemDialog内のrole/data-id属性を持つ要素候補を最大10件ログに出力する(lists-popup-itemSpan-diagnose-N)
- これらのおかげで、未知のレイアウトに遭遇した場合も「状況をコピー」の結果だけを持ち帰れば、次の一手を検討できるはず

未解決の既知課題:
- lists-popupのアイテム名検出フォールバック(role=button条件を外した緩い検索)はまだ実地で有効性未検証
- unknown-gridモードでheroFieldは見つかるがdata-actions属性が無いケースが実地で確認されている。今回追加したouterHTMLログで原因を特定できる見込み
- 全件選択チェックボックスが個別チェックと一緒にオンになってしまう既知の不具合(data-automationidの命名パターンでの判定のみ)が、根本解決されたか未検証。以前は同じアイテムへのリンクが2行できる事象があった
- SCRIPT_VERSIONはYYYYMMDD_連番形式(同日複数回の改修を区別)。更新時はこの変数とこのメモの内容、両方を意識すること

改修するときは、まず「状況をコピー」ダンプ(バージョン・チェック件数・itemKey・エラーログ・診断ログを含む)を実地テストで取ってもらってから着手すると、推測の手戻りが少ない。
*/

————————

javascript:(function () {
  'use strict';

  const SCRIPT_VERSION = '20260825_8';

  const logs = [];
  function logError(tag, err) {
    const msg = `[${tag}] ${err && err.name ? `${err.name}: ${err.message}` : String(err)}`;
    logs.push(msg);
  }

  function escapeHtml(s) {
    return String(s)
      .replaceAll('&', '&amp;')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;');
  }

  function extractItemKey(el) {
    try {
      const da = el.getAttribute('data-actions');
      if (!da) {
        logError('extractItemKey-noDataActions', 'data-actions属性が要素に存在しませんでした');
        return null;
      }
      const m = da.match(/itemKey[^0-9]*([0-9]+)/);
      if (!m) {
        logError('extractItemKey-noMatch', `data-actionsにitemKeyが見つかりませんでした: ${da.slice(0, 200)}`);
        return null;
      }
      return m[1];
    } catch (err) {
      logError('extractItemKey', err);
      return null;
    }
  }

  function diagnoseBreadcrumbCandidates() {
    const hints = [];
    let truncated = false;
    try {
      const found = document.querySelectorAll('[class*=breadcrumb i], [data-automationid*=breadcrumb i], [class*=Breadcrumb]');
      const seen = new Set();
      found.forEach((el) => {
        if (hints.length >= 15) {
          truncated = true;
          return;
        }
        const tag = el.tagName.toLowerCase();
        const cls = (el.className && typeof el.className === 'string') ? el.className.slice(0, 80) : '';
        const auto = el.getAttribute('data-automationid') || '';
        const key = tag + '|' + cls + '|' + auto;
        if (seen.has(key)) return;
        seen.add(key);
        const txt = (el.textContent || '').trim().slice(0, 20);
        hints.push('<' + tag + '> class=[' + cls + '] data-automationid=[' + auto + '] text=[' + txt + ']');
      });
      if (truncated) {
        hints.push('※以降は15件上限のため打ち切り。実際にはさらに候補が存在します');
      }
    } catch (err) {
      logError('diagnoseBreadcrumbCandidates', err);
    }
    return hints;
  }

  function getBreadcrumbPath() {
    const parts = [];
    try {
      document.querySelectorAll('div.breadcrumbRoot_b6af7cfe li span').forEach((el) => {
        const txt = (el.textContent || '').trim();
        if (txt) parts.push(txt);
      });
    } catch (err) {
      logError('getBreadcrumbPath-doclib', err);
    }
    try {
      document
        .querySelectorAll('.od-ListForm-breadcrumb nav ul li a, .od-ListForm-breadcrumb nav ul li span')
        .forEach((el) => {
          const txt = (el.textContent || '').trim();
          if (txt && !parts.includes(txt)) parts.push(txt);
        });
    } catch (err) {
      logError('getBreadcrumbPath-lists', err);
    }
    try {
      document.querySelectorAll('[data-automationid=breadcrumb-crumb]').forEach((el) => {
        const txt = (el.textContent || '').trim();
        if (txt && !parts.includes(txt)) parts.push(txt);
      });
    } catch (err) {
      logError('getBreadcrumbPath-crumb', err);
    }
    return parts;
  }

  function findOverflowTriggerButton() {
    const candidates = [
      'button[data-automationid=breadcrumb-overflow]',
      'button[aria-controls=breadcrumb-menu-id]',
      'div.breadcrumbRoot_b6af7cfe button[aria-haspopup]',
      'div.breadcrumbRoot_b6af7cfe button[aria-expanded]',
    ];
    for (const sel of candidates) {
      try {
        const el = document.querySelector(sel);
        if (el) return { el, sel };
      } catch (err) {
        logError('findOverflowTriggerButton-selector', `${sel}: ${err && err.message ? err.message : err}`);
      }
    }
    return null;
  }

  async function withExpandedOverflow(fn) {
    const alreadyExpanded = !!document.querySelector('#breadcrumb-menu-id');
    let openedByScript = false;

    if (!alreadyExpanded) {
      const trigger = findOverflowTriggerButton();
      if (trigger) {
        try {
          trigger.el.click();
          openedByScript = true;
          logError('withExpandedOverflow-clicked', `推測セレクタでクリックしました: ${trigger.sel}`);
        } catch (err) {
          logError('withExpandedOverflow-clickFailed', err);
        }
      } else {
        logError('withExpandedOverflow-triggerNotFound', 'オーバーフローボタンの候補セレクタがすべて一致しませんでした(親フォルダ階層が省略されていないか、DOM構造が想定と異なる可能性)');
      }
    }

    if (openedByScript) {
      await new Promise((resolve) => setTimeout(resolve, 150));
    }

    let result;
    try {
      result = fn();
    } finally {
      if (openedByScript) {
        const trigger = findOverflowTriggerButton();
        try {
          if (trigger) trigger.el.click();
          else document.body.click();
        } catch (err) {
          logError('withExpandedOverflow-closeFailed', err);
        }
      }
    }
    return result;
  }

  function getOverflowBreadcrumb() {
    const parts = [];
    try {
      document.querySelectorAll('#breadcrumb-menu-id li span').forEach((el) => {
        const txt = (el.textContent || '').trim();
        if (txt) parts.push(txt);
      });
    } catch (err) {
      logError('getOverflowBreadcrumb', err);
    }
    return parts;
  }

  function getListsFieldValue(labelText) {
    try {
      const labels = document.querySelectorAll('label, [class*="fieldLabel" i], [class*="FieldLabel"]');
      for (const label of labels) {
        if (!(label.textContent || '').trim().includes(labelText)) continue;
        let container = label.closest('div');
        let depth = 0;
        while (container && depth < 4) {
          const valueEl = container.querySelector(
            'div.ReactFieldEditor-core--display.ReactFieldEditor-core--display-ReadOnly.ReactFieldEditor-linkFocusIndicator > div'
          );
          if (valueEl) {
            const val = (valueEl.textContent || '').trim();
            if (val) return val;
          }
          container = container.parentElement;
          depth++;
        }
      }
      return null;
    } catch (err) {
      logError(`getListsFieldValue:${labelText}`, err);
      return null;
    }
  }

  function getCheckedRows() {
    const results = [];
    try {
      const checkboxes = document.querySelectorAll('input[type=checkbox][data-automationid=selection-checkbox][aria-checked=true]');
      checkboxes.forEach((cb) => {
        const rowEl = cb.closest('[data-automationid^=row-selection-]');
        if (!rowEl) {
          logError('getCheckedRows-rowNotFound', 'aria-checked=trueのチェックボックスからrow-selection要素が見つかりませんでした');
          return;
        }
        const automationId = rowEl.getAttribute('data-automationid') || '';
        const idMatch = automationId.match(/row-selection-(.+)$/);
        const rawId = idMatch ? idMatch[1] : '';

        const ancestorRow = rowEl.closest('[role=row]');
        let isHeaderLike = false;
        if (ancestorRow && ancestorRow.getAttribute('aria-rowindex') === '1') {
          isHeaderLike = true;
        }
        if (!rawId || /^(all|header|select-?all)$/i.test(rawId)) {
          isHeaderLike = true;
        }
        if (isHeaderLike) return;

        let container = rowEl.parentElement;
        let depth = 0;
        let nameSpan = null;
        while (container && depth < 5 && !nameSpan) {
          nameSpan = container.querySelector('span[role=button][data-id=heroField]');
          if (!nameSpan) container = container.parentElement;
          depth++;
        }
        if (nameSpan) {
          const name = (nameSpan.textContent || '').trim();
          const itemKey = extractItemKey(nameSpan);
          if (!itemKey) {
            try {
              logError('getCheckedRows-itemKeyNull-html', (nameSpan.outerHTML || '').slice(0, 300));
            } catch (err) {
              logError('getCheckedRows-itemKeyNull-htmlFailed', err);
            }
          }
          results.push({ name, itemKey });
        } else {
          logError('getCheckedRows-nameNotFound', 'row-selection element found but file/item name span not located nearby');
        }
      });

      const seen = new Set();
      const deduped = [];
      for (const row of results) {
        const key = row.itemKey || `name:${row.name}`;
        if (!seen.has(key)) {
          seen.add(key);
          deduped.push(row);
        } else {
          logError('getCheckedRows-dedup', `重複行を除外しました: ${row.name}`);
        }
      }
      return deduped;
    } catch (err) {
      logError('getCheckedRows', err);
      return results;
    }
  }

  function buildListsItemUrl(itemKey) {
    try {
      const href = location.href;
      const idx = href.indexOf('/Lists/');
      if (idx === -1) return null;
      const m = href.substring(idx).match(/^\/Lists\/([^/]+)\//);
      if (!m) return null;
      return `${href.substring(0, idx)}/Lists/${m[1]}/DispForm.aspx?ID=${itemKey}`;
    } catch (err) {
      logError('buildListsItemUrl', err);
      return null;
    }
  }

  function buildListsAllItemsUrl() {
    try {
      const href = location.href;
      const idx = href.indexOf('/Lists/');
      if (idx === -1) {
        logError('buildListsAllItemsUrl-noListsSegment', `URLに/Lists/が含まれていません: ${href}`);
        return null;
      }
      const m = href.substring(idx).match(/^\/Lists\/([^/]+)\//);
      if (!m) {
        logError('buildListsAllItemsUrl-noMatch', `リスト名を抽出できませんでした: ${href.substring(idx)}`);
        return null;
      }
      return `${href.substring(0, idx)}/Lists/${m[1]}/AllItems.aspx`;
    } catch (err) {
      logError('buildListsAllItemsUrl', err);
      return null;
    }
  }

  function buildDoclibFileUrl(fileName) {
    try {
      const href = location.href;
      const qIdx = href.indexOf('?');
      let base = qIdx === -1 ? href : href.substring(0, qIdx);
      if (!base.endsWith('/')) base += '/';
      return base + encodeURIComponent(fileName);
    } catch (err) {
      logError('buildDoclibFileUrl', err);
      return null;
    }
  }

  function getAttachments() {
    try {
      const labels = document.querySelectorAll('label, [class*="fieldLabel" i], [class*="FieldLabel"]');
      let container = null;
      for (const label of labels) {
        if ((label.textContent || '').trim().includes('添付ファイル')) {
          container = label.closest('div');
          break;
        }
      }
      if (!container) {
        return { ok: false, items: [], reason: '添付ファイルラベルが見つかりませんでした' };
      }
      let depth = 0;
      const found = [];
      while (container && depth < 4 && found.length === 0) {
        container.querySelectorAll('a[href]').forEach((a) => {
          if (found.length >= 25) return;
          const name = (a.textContent || '').trim();
          const href = a.getAttribute('href');
          if (name && href && name.length < 150) {
            found.push({ name, url: href });
          }
        });
        container = container.parentElement;
        depth++;
      }
      let truncated = false;
      if (found.length >= 25) {
        truncated = true;
        logError('getAttachments-limit', '添付ファイルが25件以上検出されたため、先頭25件のみ表示します');
      }
      if (found.length === 0) {
        return { ok: false, items: [], reason: '添付ファイル欄は見つかりましたが、リンク要素(href)を特定できませんでした' };
      }
      return { ok: true, items: found, reason: '', truncated };
    } catch (err) {
      logError('getAttachments', err);
      return { ok: false, items: [], reason: `エラー: ${err.name || ''} ${err.message || ''}` };
    }
  }

  async function writeClipboardHtml(pathText, mainUrl, extraLinks) {
    let htmlBody = `<a href="${escapeHtml(mainUrl)}">${escapeHtml(pathText)}</a>`;
    if (extraLinks.length > 0) {
      htmlBody += '<br>';
      htmlBody += extraLinks
        .map((link) => `<a href="${escapeHtml(link.url)}">${escapeHtml(link.name)}</a>`)
        .join('<br>');
    }

    const plainLines = [`${pathText}  ${mainUrl}`];
    extraLinks.forEach((link) => plainLines.push(`${link.name}  ${link.url}`));
    const plainBody = plainLines.join('\n');

    try {
      const htmlBlob = new Blob([htmlBody], { type: 'text/html' });
      const textBlob = new Blob([plainBody], { type: 'text/plain' });
      await navigator.clipboard.write([new ClipboardItem({ 'text/html': htmlBlob, 'text/plain': textBlob })]);
      return { ok: true, message: 'コピー完了' };
    } catch (err) {
      logError('writeClipboardHtml', err);
      let hint = '';
      if (err && err.message && err.message.includes('not focused')) {
        hint = '(画面がフォーカスされていない可能性があります。パネル内の「コピー」ボタンをもう一度押してください)';
      }
      return { ok: false, message: `コピー失敗: ${err.name || ''} ${err.message || ''}${hint}` };
    }
  }

  async function copyPlainText(text) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (err) {
      return false;
    }
  }

  function renderPanel(mode, pathParts, extraLinks, mainUrl, checkedCount, checkedKeys) {
    document.getElementById('spPathBookmarkletPanel')?.remove();

    const panel = document.createElement('div');
    panel.id = 'spPathBookmarkletPanel';
    Object.assign(panel.style, {
      position: 'fixed',
      top: '20px',
      right: '20px',
      width: '460px',
      maxHeight: '80vh',
      overflow: 'auto',
      background: '#fff',
      border: '2px solid #333',
      borderRadius: '8px',
      padding: '16px',
      zIndex: 2147483647,
      fontFamily: 'sans-serif',
      fontSize: '13px',
      boxShadow: '0 4px 20px rgba(0,0,0,.3)',
      color: '#000',
    });

    const closeXBtn = document.createElement('button');
    closeXBtn.textContent = '×';
    Object.assign(closeXBtn.style, {
      position: 'absolute',
      top: '8px',
      right: '10px',
      border: 'none',
      background: 'transparent',
      fontSize: '18px',
      lineHeight: '1',
      cursor: 'pointer',
      color: '#333',
      padding: '2px 6px',
    });
    closeXBtn.setAttribute('aria-label', '閉じる');
    closeXBtn.onclick = () => panel.remove();
    panel.appendChild(closeXBtn);

    const statusBox = document.createElement('div');
    Object.assign(statusBox.style, { marginBottom: '10px', padding: '6px', background: '#eef' });
    statusBox.textContent = '処理中…';
    panel.appendChild(statusBox);

    const pathLabel = document.createElement('div');
    pathLabel.style.fontWeight = 'bold';
    pathLabel.textContent = '保存先:';

    const pathBox = document.createElement('textarea');
    pathBox.id = 'spPathBookmarkletPathBox';
    Object.assign(pathBox.style, { width: '100%', boxSizing: 'border-box', height: '50px', marginBottom: '10px' });
    pathBox.value = pathParts.join(' > ');

    const dumpBtn = document.createElement('button');
    dumpBtn.textContent = '状況をコピー(報告用・エラーも含む)';
    Object.assign(dumpBtn.style, { marginBottom: '6px', display: 'block' });

    const dumpArea = document.createElement('textarea');
    Object.assign(dumpArea.style, {
      width: '100%',
      boxSizing: 'border-box',
      height: '80px',
      marginBottom: '10px',
      display: 'none',
      fontSize: '11px',
    });
    dumpArea.readOnly = true;
    dumpArea.onclick = () => dumpArea.select();

    dumpBtn.onclick = () => {
      const lines = [];
      lines.push('=== SPパスブックマークレット 状況ダンプ ===');
      lines.push(`バージョン: ${SCRIPT_VERSION}`);
      lines.push(`日時: ${new Date().toString()}`);
      lines.push(`URL: ${location.href}`);
      lines.push(`画面種別(mode): ${mode}`);
      lines.push(`チェック件数: ${checkedCount}`);
      lines.push(`itemKey一覧: ${checkedKeys.length ? checkedKeys.join(', ') : '(なし)'}`);
      lines.push(`保存先(現在の編集ボックスの値): ${pathBox.value}`);
      lines.push('アイテム/ファイルリンク:');
      if (extraLinks.length === 0) {
        lines.push('  (チェックボックス指定なし)');
      } else {
        extraLinks.forEach((link) => lines.push(`  - ${link.name} -> ${link.url}`));
      }
      lines.push(`メインURL: ${mainUrl}`);
      lines.push('エラー/警告ログ:');
      if (logs.length === 0) {
        lines.push('  (なし)');
      } else {
        logs.forEach((log) => lines.push(`  - ${log}`));
      }
      const dumpText = lines.join('\n');
      copyPlainText(dumpText).then((ok) => {
        dumpBtn.textContent = ok ? '状況をコピーしました' : 'コピー失敗(下のボックスを手動選択してください)';
        if (!ok) {
          dumpArea.style.display = 'block';
          dumpArea.value = dumpText;
          dumpArea.select();
        }
      });
    };
    panel.appendChild(dumpBtn);
    panel.appendChild(dumpArea);

    panel.appendChild(pathLabel);
    panel.appendChild(pathBox);

    const nameLabel = document.createElement('div');
    nameLabel.style.fontWeight = 'bold';
    nameLabel.textContent = 'アイテム名';
    panel.appendChild(nameLabel);

    const nameList = document.createElement('div');
    Object.assign(nameList.style, { marginBottom: '10px', padding: '4px', background: '#f7f7f7' });
    if (extraLinks.length === 0) {
      nameList.textContent = '(チェックボックス指定なし)';
    } else {
      extraLinks.forEach((link) => {
        const row = document.createElement('div');
        row.textContent = `${link.name}${link.url}`;
        nameList.appendChild(row);
      });
    }
    panel.appendChild(nameList);

    const attachCheckboxes = [];
    const attachItems = [];
    if (mode === 'lists-popup' || mode === 'lists-single') {
      const attachResult = getAttachments();
      const attachBox = document.createElement('div');
      attachBox.style.marginBottom = '10px';
      const attachLabel = document.createElement('div');
      attachLabel.style.fontWeight = 'bold';
      attachLabel.textContent = '添付ファイル(任意で個別リンクを追加)';
      attachBox.appendChild(attachLabel);

      if (attachResult.ok) {
        if (attachResult.items.length === 0) {
          const noAttach = document.createElement('div');
          noAttach.style.fontSize = '11px';
          noAttach.textContent = '添付ファイルはありません';
          attachBox.appendChild(noAttach);
        } else {
          attachResult.items.forEach((item, idx) => {
            const itemLabel = document.createElement('label');
            itemLabel.style.display = 'block';
            const cb = document.createElement('input');
            cb.type = 'checkbox';
            cb.dataset.attachIndex = String(idx);
            itemLabel.appendChild(cb);
            itemLabel.appendChild(document.createTextNode(` ${item.name}`));
            attachBox.appendChild(itemLabel);
            attachCheckboxes.push(cb);
            attachItems.push(item);
          });
          if (attachResult.truncated) {
            const truncNote = document.createElement('div');
            Object.assign(truncNote.style, { color: '#c60', fontSize: '11px', marginTop: '4px' });
            truncNote.textContent = '※添付ファイルが25件を超えていたため、先頭25件のみ表示しています';
            attachBox.appendChild(truncNote);
          }
        }
      } else {
        const attachErr = document.createElement('div');
        Object.assign(attachErr.style, { color: '#c00', fontSize: '11px' });
        attachErr.textContent = `取得できませんでした: ${attachResult.reason}`;
        attachBox.appendChild(attachErr);
      }
      panel.appendChild(attachBox);
    }

    if (logs.length > 0) {
      const warnLabel = document.createElement('div');
      Object.assign(warnLabel.style, { fontWeight: 'bold', color: '#c00' });
      warnLabel.textContent = '警告/エラー(タップで全選択できます):';
      panel.appendChild(warnLabel);

      const warnArea = document.createElement('textarea');
      Object.assign(warnArea.style, {
        width: '100%',
        boxSizing: 'border-box',
        height: '80px',
        marginBottom: '6px',
        background: '#fee',
        border: '1px solid #c00',
        fontSize: '11px',
      });
      warnArea.readOnly = true;
      warnArea.value = logs.join('\n');
      warnArea.onclick = () => warnArea.select();
      panel.appendChild(warnArea);
    }

    const btnRow = document.createElement('div');
    const copyBtn = document.createElement('button');
    copyBtn.textContent = 'コピー';
    copyBtn.style.marginRight = '8px';
    const reRunBtn = document.createElement('button');
    reRunBtn.textContent = '再実行';
    reRunBtn.style.marginRight = '8px';
    reRunBtn.onclick = () => {
      main().catch((err) => {
        logError('main-unhandled-rerun', err);
        alert('再実行中にエラーが発生しました。詳細はコンソールを確認してください。');
      });
    };
    const closeBtn = document.createElement('button');
    closeBtn.textContent = '閉じる';
    closeBtn.onclick = () => panel.remove();
    btnRow.appendChild(copyBtn);
    btnRow.appendChild(reRunBtn);
    btnRow.appendChild(closeBtn);
    panel.appendChild(btnRow);

    document.body.appendChild(panel);

    const doCopy = async () => {
      const pathText = pathBox.value;
      const links = [...extraLinks];
      attachCheckboxes.forEach((cb, idx) => {
        if (cb.checked) links.push(attachItems[idx]);
      });
      const result = await writeClipboardHtml(pathText, mainUrl, links);
      statusBox.style.background = result.ok ? '#efe' : '#fee';
      statusBox.textContent = result.message;
    };
    copyBtn.onclick = doCopy;

    statusBox.textContent = '自動コピーを試みています…';
    try {
      window.focus();
    } catch (err) {
      logError('window.focus', err);
    }
    try {
      document.body.focus();
    } catch (err) {
      /* noop */
    }
    setTimeout(() => {
      doCopy().catch((err) => {
        logError('autoCopy-unhandled', err);
        statusBox.style.background = '#fee';
        statusBox.textContent = '自動コピーに失敗しました。お手数ですが「コピー」ボタンを押してください。';
      });
    }, 500);
  }

  function detectMode() {
    try {
      if (document.querySelector('.sp-itemDialog')) return 'lists-popup';
    } catch (err) {
      logError('detectMode-popup', err);
    }
    try {
      if (document.querySelector('.od-ListForm-breadcrumb')) return 'lists-single';
    } catch (err) {
      logError('detectMode-listsingle', err);
    }
    try {
      if (document.querySelector('.breadcrumbRoot_b6af7cfe')) return 'doclib-list';
    } catch (err) {
      logError('detectMode-doclib', err);
    }
    try {
      if (document.querySelector('[id^=virtualized-list_]')) return 'unknown-grid';
    } catch (err) {
      logError('detectMode-grid', err);
    }
    return 'unknown';
  }

  async function main() {
    const mode = detectMode();
    let pathParts = [];
    const extraLinks = [];
    let mainUrl = location.href;
    let checkedCount = 0;
    const checkedKeys = [];

    if (mode === 'doclib-list') {
      const overflowParts = await withExpandedOverflow(() => getOverflowBreadcrumb());
      pathParts = [...overflowParts, ...getBreadcrumbPath()];

      const checkedFiles = getCheckedRows();
      checkedCount = checkedFiles.length;
      if (checkedFiles.length > 10) {
        alert(`チェックされたファイルが10件を超えています(${checkedFiles.length}件)。10件以内に絞り込んでから再実行してください。`);
        return;
      }
      checkedFiles.forEach((file) => {
        const fileUrl = buildDoclibFileUrl(file.name);
        if (!fileUrl) logError('buildDoclibFileUrl-null', `file=${file.name}`);
        extraLinks.push({ name: file.name, url: fileUrl || mainUrl });
        if (file.itemKey) checkedKeys.push(file.itemKey);
      });
    } else if (mode === 'lists-single' || mode === 'unknown-grid') {
      pathParts = getBreadcrumbPath();
      if (mode === 'lists-single') {
        const currentItemName = pathParts.length > 0 ? pathParts[pathParts.length - 1] : document.title;
        extraLinks.push({ name: currentItemName, url: location.href });
        const allItemsUrl = buildListsAllItemsUrl();
        if (allItemsUrl) mainUrl = allItemsUrl;
        else logError('lists-single-allItemsUrl-null', 'AllItems.aspx URLの組み立てに失敗、疑似パスは自身のURLのままになります');
      }
      const checkedItems = getCheckedRows();
      checkedCount = checkedItems.length;
      if (checkedItems.length > 10) {
        alert(`チェックされたアイテムが10件を超えています(${checkedItems.length}件)。10件以内に絞り込んでから再実行してください。`);
        return;
      }
      checkedItems.forEach((item) => {
        const itemUrl = item.itemKey ? buildListsItemUrl(item.itemKey) : null;
        if (!itemUrl) logError('buildListsItemUrl-null', `item=${item.name} key=${item.itemKey}`);
        extraLinks.push({ name: item.name, url: itemUrl || mainUrl });
        if (item.itemKey) checkedKeys.push(item.itemKey);
      });
    } else if (mode === 'lists-popup') {
      const dept = getListsFieldValue('掲載部署');
      const kind = getListsFieldValue('種別');
      if (dept) pathParts.push(dept);
      if (kind) pathParts.push(kind);
      if (!dept && !kind) logError('lists-popup-path', '掲載部署/種別のいずれも取得できませんでした');

      let heroSpan = document.querySelector('.sp-itemDialog span[role=button][data-id=heroField]');
      if (!heroSpan) {
        heroSpan = document.querySelector('.sp-itemDialog [data-id=heroField]');
        if (heroSpan) logError('lists-popup-itemSpan-fallback', 'span[role=button]では見つからず、[data-id=heroField]の緩い条件で代替検出しました');
      }
      if (heroSpan) {
        const itemName = (heroSpan.textContent || '').trim();
        const itemKey = extractItemKey(heroSpan);
        const itemUrl = itemKey ? buildListsItemUrl(itemKey) : null;
        if (!itemUrl) logError('buildListsItemUrl-popup-null', `name=${itemName} key=${itemKey}`);
        extraLinks.push({ name: itemName, url: itemUrl || mainUrl });
        mainUrl = itemUrl || mainUrl;
        checkedCount = 1;
        if (itemKey) checkedKeys.push(itemKey);
      } else {
        logError('lists-popup-itemSpan', 'ポップアップ内のアイテム名要素が見つかりませんでした');
        try {
          const candidates = document.querySelectorAll('.sp-itemDialog [role=button], .sp-itemDialog [data-id]');
          const hints = [];
          const seen = new Set();
          let truncated = false;
          candidates.forEach((el) => {
            if (hints.length >= 20) {
              truncated = true;
              return;
            }
            const tag = el.tagName.toLowerCase();
            const role = el.getAttribute('role') || '';
            const dataId = el.getAttribute('data-id') || '';
            const key = tag + '|' + role + '|' + dataId;
            if (seen.has(key)) return;
            seen.add(key);
            const txt = (el.textContent || '').trim().slice(0, 20);
            hints.push('<' + tag + '> role=[' + role + '] data-id=[' + dataId + '] text=[' + txt + ']');
          });
          if (truncated) {
            hints.push('※以降は20件上限のため打ち切り。実際にはさらに候補が存在します');
          }
          if (hints.length === 0) {
            logError('lists-popup-itemSpan-diagnose', '.sp-itemDialog内にrole/data-idを持つ候補要素も見つかりませんでした');
          } else {
            hints.forEach((h, i) => logError('lists-popup-itemSpan-diagnose-' + (i + 1), h));
          }
        } catch (err) {
          logError('lists-popup-itemSpan-diagnoseFailed', err);
        }
      }
    } else {
      logError('detectMode', `既知の画面パターンに一致しませんでした(mode=${mode})`);
      pathParts = getBreadcrumbPath();
    }

    if (pathParts.length === 0 && mode !== 'lists-popup') {
      const hints = diagnoseBreadcrumbCandidates();
      if (hints.length === 0) {
        logError('breadcrumb-diagnose', 'パンくず未取得。breadcrumb関連の候補要素も見つかりませんでした');
      } else {
        hints.forEach((h, i) => logError('breadcrumb-diagnose-' + (i + 1), h));
      }
    }

    renderPanel(mode, pathParts, extraLinks, mainUrl, checkedCount, checkedKeys);
  }

  main().catch((err) => {
    logError('main-unhandled', err);
    alert('ブックマークレットの実行中にエラーが発生しました。詳細はコンソールを確認してください。');
  });
})();
/*
【引継ぎメモ】
このコードはSharePoint Online用の擬似パス取得&ハイパーリンク付きコピー用ブックマークレットです。
導入時に改行が消えて1行になるため、コード途中にスラッシュ2つのコメントやブロックコメント記法を入れないこと。コメントは必ずこの位置(IIFE実行後の末尾)にのみ置くこと。

確定している設計・実装判断:
- クリップボードはexecCommandではなくnavigator.clipboard(writeText/write+ClipboardItem)を使用。実行直後に呼ぶとDocument is not focusedエラーになりやすいため、DOM収集後さらに約500ms待ってから書き込む。
- チェック済み行の判定は、チェックボックス本体の`input[data-automationid=selection-checkbox][aria-checked=true]`を正としてそこから行要素をclosest()で辿る方式(旧`div[aria-selected=true]`方式は常に0件だったため廃止)。
- パンくずオーバーフロー(見た目はフォルダアイコン、「...」ではない)の展開ボタンは`button[data-automationid=breadcrumb-overflow]`が実測確定セレクタ。
- 画面種別unknown-gridのパンくずは`div.breadcrumbRoot_b6af7cfe`でも`.od-ListForm-breadcrumb`でもなく、`[data-automationid=breadcrumb-crumb]`という別の安定属性で取得する。
- パネルに「再実行」ボタン(main()を呼び直すだけ。編集内容・チェック状態は全てリフレッシュされ、初回同様に自動コピーまで試みる)と、右上×閉じるボタンを実装済み。
- パネルのz-indexは2147483647(実質最大値)に設定済み。Listsアイテムのモーダルダイアログ(.sp-itemDialog)を開いたままブックマークレットを実行すると、モーダルが持つ背景クリック防止用の半透明の幕にパネルが隠れて操作不能になっていたための対策。パネルの表示領域(右上460px×最大80vh)外は引き続きモーダルの幕に覆われたままなので、パネル自体の操作性だけが改善される。
- 想定挙動: 一覧画面(doclib-list/lists-single/unknown-grid)でチェックを入れて実行すると、擬似パス(一覧ページへのリンク)1本+チェックした各アイテム名(それぞれへの直リンク)を列挙する。この設計自体は確定済みで、課題は「チェック行から正しく名前とitemKeyまで辿れるか」という検出精度の部分に絞られている。

診断ログ機能(F12を開かず持ち帰れるようにする仕組み):
- パンくずが1件も取れなかった場合(lists-popup以外)、diagnoseBreadcrumbCandidates()が`breadcrumb`を含むclass/data-automationidを持つ要素を自動収集し、「状況をコピー」のログにタグ名・class・data属性・テキストの一部を出力する(上限15件、超過時は打ち切りメッセージを追加)
- getCheckedRowsでheroFieldは見つかったがitemKeyがnull(data-actions属性が無い等)の場合、その要素のouterHTML先頭300文字をログに出力する(getCheckedRows-itemKeyNull-html)
- lists-popupでアイテム名要素が完全に見つからない場合、.sp-itemDialog内のrole/data-id属性を持つ要素候補を最大20件ログに出力する(lists-popup-itemSpan-diagnose-N、超過時は打ち切りメッセージを追加)
- 添付ファイル一覧(getAttachments)は上限25件、超過時はログとパネルUI両方に打ち切り注意書きを表示する
- チェックボックス選択数の上限は10件(既存、超過でアラート中断)。診断系・添付ファイル系の上限を意図的に全て別の数字(10/15/20/25)にすることで、会話や引継ぎの際に「何の上限の話か」を数字だけで区別できるようにしている
- これらのおかげで、未知のレイアウトに遭遇した場合も「状況をコピー」の結果だけを持ち帰れば、次の一手を検討できるはず

未解決の既知課題:
- lists-popupのアイテム名検出フォールバック(role=button条件を外した緩い検索)はまだ実地で有効性未検証
- unknown-gridモードでheroFieldは見つかるがdata-actions属性が無いケースが実地で確認されている。今回追加したouterHTMLログで原因を特定できる見込み
- 全件選択チェックボックスが個別チェックと一緒にオンになってしまう既知の不具合(data-automationidの命名パターンでの判定のみ)が、根本解決されたか未検証。以前は同じアイテムへのリンクが2行できる事象があった
- SCRIPT_VERSIONはYYYYMMDD_連番形式(同日複数回の改修を区別)。更新時はこの変数とこのメモの内容、両方を意識すること

改修するときは、まず「状況をコピー」ダンプ(バージョン・チェック件数・itemKey・エラーログ・診断ログを含む)を実地テストで取ってもらってから着手すると、推測の手戻りが少ない。
*/

————————

————————

————————

————————





3

htmlとjavascriptの動作確認

1 htmlとjsの動作が制限されるかチェック

 

 

 

2 コード

--------------index.html-------------

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>テスト</title>
</head>
<body>
  <h1>HTML表示OK</h1>
  <button onclick="test()">クリック</button>

  <script src="main.js"></script>
</body>
</html>

     

--------------main.js-------------

main.js

function test() {
    alert("JavaScript OK");
}

 

 

3

 

URLをクリップポードにコピーした状態からワンクリックでショートカットを作成

1 目的

シェアポイントのアドレスはコピーすると長過ぎてWindowsのショートカットに収まらず途中で切れてしまう。そこで、バッチファイル からps1ファイルを呼び出して手当てする。

2 コード

batファイル

Run_make_shortcut.bat

powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0make_shortcut.ps1"

ps1ファイル

make_shortcut.ps1

 <#
    URLショートカット作成ツール

    やっていること:
    1. ショートカットの名前をユーザーに入力させる
    2. クリップボードにコピーされているURL(SharePointのリンクなど)を取得する
    3. そのURLをEdgeで開くだけの .bat ファイルを作る
    4. 同名ファイルによる上書きを防ぐため、ファイル名の末尾に作成日時を付加する
    5. 出来上がった .bat ファイルは、この .ps1 ファイルと同じフォルダに保存される

    ファイル名: make_shortcut.ps1
    起動用ランチャー: Run_make_shortcut.bat
      (中身: powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0make_shortcut.ps1")

    使い方:
    - このスクリプトを実行する前に、対象URLをコピー(Ctrl+C)しておく
    - 実行するとショートカット名の入力を求められるので、任意の名前を入力する
    - 完了すると .bat ファイルが作成され、ダブルクリックでそのURLをEdgeで開けるようになる
#>

# ユーザーにショートカット名を入力させ、$name に格納する
$name = Read-Host "ショートカット名"

# ファイル名として使えない禁則文字(\ / : * ? " < > |)を
# アンダースコア "_" に置き換えた安全な名前を作る
$safeName = $name -replace '[\\/:*?"<>|]', '_'

# クリップボードにコピーされている文字列(SharePointのURLなど)を取得
$clip = Get-Clipboard

# 生成する.batファイルの中身を文字列として組み立てる
# `r`n は改行(CRLF)を意味する特殊文字
# `"`" は「空のダブルクォート " " 」を出力するためのエスケープ表現
# 最終的にできる中身のイメージ:
#   @echo off
#   start "" msedge "https://sharepoint.example.com/..."
$content = "@echo off`r`nstart `"`" msedge `"$clip`""

# 同名ファイルによる上書きを防ぐため、現在日時を "yyyyMMdd-HHmmss" 形式で取得し、
# ファイル名の末尾に付加する(例: 会議URL_20260717-143025.bat)
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"

# 保存先のフルパスを作る
# $PSScriptRoot は「このps1ファイルが置かれているフォルダ」を指す
# そこに $safeName + "_" + タイムスタンプ + ".bat" というファイル名を連結する
$path = Join-Path $PSScriptRoot "$safeName`_$timestamp.bat"

# 組み立てた中身($content)を、上で決めたパス($path)に
# UTF-8エンコーディングで書き出す(=新しいbatファイルが作られる)
Set-Content -Path $path -Value $content -Encoding UTF8

# 完了メッセージとして、作成したファイルのパスを画面に表示する
Write-Host "作成しました: $path"

3 claude

 

複数グループから提出されたものに、通し番号を付与して50件ずつ束を作成する。

1目的

 

2コード

 

Option Explicit

'============================================================
' モジュール名
'   mod提出物採番して外注束作成
'
' Version
'   1.1
'
'【目的】
' 読取シートへ入力した会社ごとの提出件数を、
' 50件単位の外注束へ展開して出力シートへ出力する。
'
'【設計方針】
' ・シートレイアウトはExcel側で管理する。
' ・見出し・書式・列幅はVBAで変更しない。
' ・VBAはデータのみ出力する。
' ・読取順に処理する。
' ・変数は初めて意味を持つブロックで宣言する。
' ・コメントは処理の区切りと、コードだけでは伝わらない
'   設計意図のみ記述する。
'============================================================

Private Enum cNoWs読取
    会社名 = 1
    件数 = 2
End Enum

Private Enum cNoWs出力
    ID = 1
    会社名 = 2
    件数 = 3
    束番号 = 4
    開始番号 = 5
    終了番号 = 6
End Enum

Sub 出力作成()

    '======== 定数 ========

    Const 最大束件数 As Long = 50

    Const 読取シート名 As String = "入力"
    Const 出力シート名 As String = "出力"

    Const 読取開始行 As Long = 2
    Const 出力開始行 As Long = 2

    '======== シート取得 ========

    Dim ws読取 As Worksheet
    Dim ws出力 As Worksheet

    Set ws読取 = Worksheets(読取シート名)
    Set ws出力 = Worksheets(出力シート名)

    '======== 出力シート初期化 ========

    ws出力.Rows(出力開始行 & ":" & ws出力.Rows.Count).ClearContents

    '======== 採番初期化 ========

    Dim lng現在束番号 As Long
    Dim lng現在束残件数 As Long
    Dim lng次開始番号 As Long
    Dim lng次行ID As Long
    Dim rNo出力行 As Long

    lng現在束番号 = 1
    lng現在束残件数 = 最大束件数
    lng次開始番号 = 1
    lng次行ID = 1
    rNo出力行 = 出力開始行

    '======== 読取データを順番に処理 ========

    Dim rNo読取最終行 As Long
    Dim rNo読取行 As Long

    rNo読取最終行 = ws読取.Cells(ws読取.Rows.Count, cNoWs読取.会社名).End(xlUp).Row

    For rNo読取行 = 読取開始行 To rNo読取最終行

        Dim str会社名 As String
        Dim lng会社残件数 As Long

        str会社名 = ws読取.Cells(rNo読取行, cNoWs読取.会社名).Value
        lng会社残件数 = ws読取.Cells(rNo読取行, cNoWs読取.件数).Value

        '会社より50件単位を優先して束を構成する
        Do While lng会社残件数 > 0

            '======== 今回出力件数を決定 ========

            Dim lng今回出力件数 As Long

            If lng会社残件数 <= lng現在束残件数 Then
                lng今回出力件数 = lng会社残件数
            Else
                lng今回出力件数 = lng現在束残件数
            End If

            '======== 開始番号・終了番号を決定 ========

            Dim lng終了番号 As Long

            lng終了番号 = lng次開始番号 + lng今回出力件数 - 1

            '======== 出力 ========

            ws出力.Cells(rNo出力行, cNoWs出力.ID).Value = lng次行ID
            ws出力.Cells(rNo出力行, cNoWs出力.会社名).Value = str会社名
            ws出力.Cells(rNo出力行, cNoWs出力.件数).Value = lng今回出力件数
            ws出力.Cells(rNo出力行, cNoWs出力.束番号).Value = lng現在束番号

            ws出力.Cells(rNo出力行, cNoWs出力.開始番号).Value = lng次開始番号
            ws出力.Cells(rNo出力行, cNoWs出力.終了番号).Value = lng終了番号

            '======== 状態更新 ========

            lng会社残件数 = lng会社残件数 - lng今回出力件数
            lng現在束残件数 = lng現在束残件数 - lng今回出力件数

            lng次開始番号 = lng終了番号 + 1

            lng次行ID = lng次行ID + 1
            rNo出力行 = rNo出力行 + 1

            If lng現在束残件数 = 0 Then
                lng現在束番号 = lng現在束番号 + 1
                lng現在束残件数 = 最大束件数
            End If

        Loop

    Next rNo読取行

    '======== 完了 ========

    MsgBox "出力が完了しました。"

End Sub

以下claude版

 

Option Explicit

'============================================================
' モジュール名
'   mod提出物採番して外注束作成
'
' Version
'   2.9
'
'【目的】
' 読取シートへ入力した会社ごとの提出件数を、
' 50件単位の外注束へ展開して出力シートへ出力する。
'
'【設計方針】
' ・シートレイアウトはExcel側で管理する。
' ・見出し・書式・列幅はVBAで変更しない。
' ・VBAはデータのみ出力する。
' ・読取順に処理する。
' ・変数は初めて意味を持つブロックで宣言する。
' ・役割(書込み先/読取り元)が異なる行番号は、たとえ同じシートを
'   指していても変数を使い回さず、別の変数名として書き分ける。
' ・行番号変数は「RNo_シート名_役割」の形式で命名し、
'   どのシートのどの用途の行番号かを本文だけで判別できるようにする。
' ・データ値の変数も同様に「型_ws_シート名_列名(用途)」の形式で
'   命名し、どのシートのどの列に対応する値かを本文だけで
'   判別できるようにする。
'
'【コメント方針】
' ・コメントは処理の区切り(ステップの節目)や、コードだけでは
'   伝わらない設計意図・判断理由を記述する。
' ・特に、複数の判定や処理が連続していて、それぞれが何をしているか
'   一目で分からない箇所(連続するIf文、If-Elseの分岐など)や、
'   ループ・変数宣言・分岐の直前など、コードだけでは目的が
'   伝わりにくい箇所には、一言添える。
' ・変数名や条件式自体が文として読める箇所は、コメントがなくても
'   読み下せることが多いが、他の判断基準を優先してよい。
'
'【出力の粒度について】
' ・「束」は読取順の通し番号に対して50件ごとに機械的に区切られる
'   (会社の境界は一切考慮しない)。
' ・出力シートの1行は「束まるごと」ではなく「束×会社」の単位になる。
'   1つの束に複数社が含まれる場合や、1社が複数束にまたがる場合は、
'   会社が変わる箇所・束が変わる箇所のどちらでも行を分ける。
'
'【採番方式について】
' ・通し番号・束番号・束内番号は、いずれも割り算や逆算によらず、
'   作業シートへの展開と同時にカウンタを1件ずつ加算(必要に応じて
'   リセット)することで採番する。
' ・開始番号・終了番号は、束内番号ではなく全体を通した通し番号を
'   使う。これにより、あるデータが全体の何件目から何件目に
'   あたるかを、束をまたいでも一意に追跡できる。
' ・束枝番は「塊(会社×束の塊)が変わったかどうか」という、
'   塊が確定して初めて定義できる情報である。そのため作業シート
'   (1件単位)の段階では採番せず、出力シート(塊単位)が
'   完成した後にステップ3として改めて上から見て採番する。
'   判定は単純で、出力シートを上から見て、束番号が1行上と同じ
'   なら束枝番を1加算、違えば1にリセットする。
'============================================================

'読取シートの列番号
Private Enum CNoWs読取
    会社名 = 1
    件数 = 2
End Enum

'作業シートの列番号
Private Enum CNoWs作業
    通し番号 = 1
    会社名 = 2
    束番号 = 3
    束内番号 = 4
End Enum

'出力シートの列番号
Private Enum CNoWs出力
    ID = 1
    会社名 = 2
    件数 = 3
    束番号 = 4
    束枝番 = 5
    開始番号 = 6
    終了番号 = 7
End Enum

Sub 出力作成()

    '======== 定数 ========

    '読取シート
    Const str読取シート名 As String = "読取"
    Const RNo_ws読取_開始行 As Long = 2

    '作業シート
    Const str作業シート名 As String = "作業"
    Const RNo_ws作業_開始行 As Long = 2

    '出力シート
    Const str出力シート名 As String = "出力"
    Const RNo_ws出力_開始行 As Long = 2

    Const lng最大束件数 As Long = 50

    '======== シート取得 ========

    Dim ws読取 As Worksheet
    Dim ws作業 As Worksheet
    Dim ws出力 As Worksheet

    Set ws読取 = Worksheets(str読取シート名)
    Set ws作業 = Worksheets(str作業シート名)
    Set ws出力 = Worksheets(str出力シート名)

    ws作業.Rows(RNo_ws作業_開始行 & ":" & ws作業.Rows.Count).ClearContents
    ws出力.Rows(RNo_ws出力_開始行 & ":" & ws出力.Rows.Count).ClearContents

    '======== ステップ1:会社名を件数分だけ作業シートへ展開し、束番号・束内番号も同時に採番 ========
    ' 1行書き込むたびに、通し番号・束内番号を1ずつ進める。
    ' 束内番号が50を超えたら、束番号を1つ進めて束内番号を1に戻す。

    Dim RNo_ws読取_最終行 As Long
    RNo_ws読取_最終行 = ws読取.Cells(ws読取.Rows.Count, CNoWs読取.会社名).End(xlUp).Row

    Dim RNo_ws作業_書込行 As Long
    RNo_ws作業_書込行 = RNo_ws作業_開始行

    '通し番号・束番号・束内番号のカウンタを初期化する
    Dim lng_ws作業_通し番号 As Long
    lng_ws作業_通し番号 = 0

    Dim lng_ws作業_束番号 As Long
    Dim lng_ws作業_束内番号 As Long
    lng_ws作業_束番号 = 1
    lng_ws作業_束内番号 = 0

    '読取シートを1行ずつ、会社の登場順に処理する
    Dim RNo_ws読取_読取行 As Long
    For RNo_ws読取_読取行 = RNo_ws読取_開始行 To RNo_ws読取_最終行

        Dim str_ws読取_会社名 As String
        Dim lng_ws読取_件数 As Long
        str_ws読取_会社名 = ws読取.Cells(RNo_ws読取_読取行, CNoWs読取.会社名).Value
        lng_ws読取_件数 = ws読取.Cells(RNo_ws読取_読取行, CNoWs読取.件数).Value

        '1社分の件数だけ、作業シートへ1件ずつ展開する
        Dim i As Long
        For i = 1 To lng_ws読取_件数

            lng_ws作業_通し番号 = lng_ws作業_通し番号 + 1

            lng_ws作業_束内番号 = lng_ws作業_束内番号 + 1
            If lng_ws作業_束内番号 > lng最大束件数 Then
                lng_ws作業_束番号 = lng_ws作業_束番号 + 1
                lng_ws作業_束内番号 = 1
            End If

            ws作業.Cells(RNo_ws作業_書込行, CNoWs作業.通し番号).Value = lng_ws作業_通し番号
            ws作業.Cells(RNo_ws作業_書込行, CNoWs作業.会社名).Value = str_ws読取_会社名
            ws作業.Cells(RNo_ws作業_書込行, CNoWs作業.束番号).Value = lng_ws作業_束番号
            ws作業.Cells(RNo_ws作業_書込行, CNoWs作業.束内番号).Value = lng_ws作業_束内番号

            RNo_ws作業_書込行 = RNo_ws作業_書込行 + 1

        Next i

    Next RNo_ws読取_読取行

    Dim lng総件数 As Long
    lng総件数 = lng_ws作業_通し番号

    '======== ステップ2:作業シートを上から転記し、会社名か束番号が変わったら出力行を改める ========
    ' 出力1行=「束×会社」の単位。会社の境界・束の境界のどちらでも区切る。
    ' 束枝番はこの時点ではまだ振らない(ステップ3でまとめて振る)。

    '出力行番号・出力IDのカウンタを初期化する
    Dim RNo_ws出力_書込行 As Long: RNo_ws出力_書込行 = RNo_ws出力_開始行
    Dim lng_ws出力_ID As Long: lng_ws出力_ID = 1

    '「同じ会社」かつ「同じ束番号」が連続している塊の先頭行。
    '塊の終わりは読み進めれば都度わかるが、始まりは
    '前の区切りの次まで遡らないとわからないため保持しておく。
    Dim RNo_ws作業_区間開始行 As Long: RNo_ws作業_区間開始行 = RNo_ws作業_開始行

    Dim RNo_ws作業_読取行 As Long
    For RNo_ws作業_読取行 = RNo_ws作業_開始行 To RNo_ws作業_開始行 + lng総件数 - 1

        Dim is改行予定 As Boolean
        is改行予定 = False

        '最終行に達したら、無条件で塊の区切りとする
        If RNo_ws作業_読取行 = RNo_ws作業_開始行 + lng総件数 - 1 Then is改行予定 = True
        '次の行で会社が変わるなら、そこで塊を区切る
        If ws作業.Cells(RNo_ws作業_読取行 + 1, CNoWs作業.会社名).Value <> ws作業.Cells(RNo_ws作業_区間開始行, CNoWs作業.会社名).Value Then is改行予定 = True
        '次の行で束番号が変わるなら、そこでも塊を区切る
        If ws作業.Cells(RNo_ws作業_読取行 + 1, CNoWs作業.束番号).Value <> ws作業.Cells(RNo_ws作業_区間開始行, CNoWs作業.束番号).Value Then is改行予定 = True

        If is改行予定 Then

            ws出力.Cells(RNo_ws出力_書込行, CNoWs出力.ID).Value = lng_ws出力_ID
            ws出力.Cells(RNo_ws出力_書込行, CNoWs出力.会社名).Value = ws作業.Cells(RNo_ws作業_区間開始行, CNoWs作業.会社名).Value
            ws出力.Cells(RNo_ws出力_書込行, CNoWs出力.件数).Value = RNo_ws作業_読取行 - RNo_ws作業_区間開始行 + 1
            ws出力.Cells(RNo_ws出力_書込行, CNoWs出力.束番号).Value = ws作業.Cells(RNo_ws作業_区間開始行, CNoWs作業.束番号).Value
            ws出力.Cells(RNo_ws出力_書込行, CNoWs出力.開始番号).Value = ws作業.Cells(RNo_ws作業_区間開始行, CNoWs作業.通し番号).Value
            ws出力.Cells(RNo_ws出力_書込行, CNoWs出力.終了番号).Value = ws作業.Cells(RNo_ws作業_読取行, CNoWs作業.通し番号).Value

            lng_ws出力_ID = lng_ws出力_ID + 1
            RNo_ws出力_書込行 = RNo_ws出力_書込行 + 1
            RNo_ws作業_区間開始行 = RNo_ws作業_読取行 + 1

        End If

    Next RNo_ws作業_読取行

    '======== ステップ3:出力シートを上から見て、束番号が1行上と同じなら束枝番を加算 ========
    ' 出力シートは既に「束×会社」の塊単位で1行にまとまっているため、
    ' ここでは束番号の変化を見るだけで枝番が決まる。
    ' 直前の束番号の初期値は0(実在しない束番号)にしておくことで、
    ' 1行目は必ず「変わった」と判定され、束枝番が1から始まる。

    Dim RNo_ws出力_最終行 As Long
    RNo_ws出力_最終行 = RNo_ws出力_書込行 - 1

    '束枝番・直前の束番号のカウンタを初期化する
    Dim lng_ws出力_束枝番 As Long
    lng_ws出力_束枝番 = 0

    Dim lng_ws出力_直前の束番号 As Long
    lng_ws出力_直前の束番号 = 0

    Dim RNo_ws出力_採番行 As Long
    For RNo_ws出力_採番行 = RNo_ws出力_開始行 To RNo_ws出力_最終行

        Dim lng_ws出力_現在の束番号 As Long
        lng_ws出力_現在の束番号 = ws出力.Cells(RNo_ws出力_採番行, CNoWs出力.束番号).Value

        If lng_ws出力_現在の束番号 = lng_ws出力_直前の束番号 Then
            '同じ束の中がまだ続いているので、枝番を1つ進める
            lng_ws出力_束枝番 = lng_ws出力_束枝番 + 1
        Else
            '新しい束に入ったので、枝番を1からリセットする
            lng_ws出力_束枝番 = 1
            lng_ws出力_直前の束番号 = lng_ws出力_現在の束番号
        End If

        ws出力.Cells(RNo_ws出力_採番行, CNoWs出力.束枝番).Value = lng_ws出力_束枝番

    Next RNo_ws出力_採番行

    '======== 完了 ========

    MsgBox "出力が完了しました。"

End Sub

3参考

 

chatGPT

 

エクセルを閉じるとき上書き保存してOutlookでメール送信マクロ

1 目的

エクセルを更新して閉じるときに、"送信先リスト"というシートのセル範囲C2:C11に書かれたメールアドレス宛にファイルを更新した旨のメールを自動送信する。

2 コード

注)ブックのイベントなので記載場所はThisworkbook

' Workbook - 改善版
Option Explicit

' ファイルを開いた時に入力シートを表示する
Private Sub Workbook_Open()
    On Error Resume Next
    Worksheets("入力シート").Activate
End Sub

' ファイルを閉じるときにメールを送信する
Private Sub Workbook_BeforeClose(Cancel As Boolean)
    On Error GoTo ErrorHandler
    
    ' 定数定義
    Const WS_送信先リスト As String = "送信先リスト"
    Const COL_EMAIL As Integer = 3
    Const LNG_XLSM拡張子文字数 As Integer = 5 ' ".xlsm"の文字数
    
    Dim m As String
    m = ""
    m = m & "処理担当Gにメールを送信しますか?" & vbCrLf
    m = m & "「はい」なら上書き保存し、メール送信します。" & vbCrLf
    m = m & "「いいえ」なら上書き保存とメール送信を中止します。保存は自分で行なってください。"
    
    ' 早期リターン:「いいえ」を選んだときは終了する
    If MsgBox(m, vbYesNo) = vbNo Then
        MsgBox "保存と送信を中止しました。"
        Exit Sub
    End If
    
    ' ====これ以降はメッセージボックスで「はい」を選んだときの処理====
    
    ' 上書き保存
    ThisWorkbook.Save
    
    ' Outlookオブジェクト作成
    Dim myOL As Object: Set myOL = CreateObject("Outlook.Application")
    Dim myMail As Object: Set myMail = myOL.CreateItem(0)
    
    ' 送信先リストから全てのセルのメールアドレスを指定する
    ' Outlookは末尾や連続したセミコロンを無視するため、整形は不要
    With Worksheets(WS_送信先リスト)
        myMail.To = .Cells(2, COL_EMAIL) & ";" & _
                    .Cells(3, COL_EMAIL) & ";" & _
                    .Cells(4, COL_EMAIL) & ";" & _
                    .Cells(5, COL_EMAIL) & ";" & _
                    .Cells(6, COL_EMAIL) & ";" & _
                    .Cells(7, COL_EMAIL) & ";" & _
                    .Cells(8, COL_EMAIL) & ";" & _
                    .Cells(9, COL_EMAIL) & ";" & _
                    .Cells(10, COL_EMAIL) & ";" & _
                    .Cells(11, COL_EMAIL)
    End With
    
    ' 件名設定
    myMail.Subject = "【自動送信】" & Left(ThisWorkbook.Name, Len(ThisWorkbook.Name) - LNG_XLSM拡張子文字数) & "更新のお知らせ"
    
    ' メール本文作成
    Dim strBody As String
    strBody = ""
    strBody = strBody & "このメールは、リストを更新した際に自動で送信されます。<br>"
    strBody = strBody & "ファイル名:" & ThisWorkbook.Name & " 」 は<br>"
strBody = strBody & "<a href='file://" & ThisWorkbook.Path & "'>" & ThisWorkbook.Path & "</a><br>"
    strBody = strBody & "<br>"
  strBody = strBody & "に保存されています。" ' HTMLメール設定と送信 myMail.HTMLBody = strBody myMail.Send ' オブジェクトの解放(COMオブジェクトのみ) Set myMail = Nothing Set myOL = Nothing Exit Sub ErrorHandler: MsgBox "メール送信時にエラーが発生しました: " & Err.Description Set myMail = Nothing Set myOL = Nothing End Sub

 

3 参考

既存コードをレビューした。

 

複数シートのラベルデータからDB形式へ変換して集約。

1 目的

複数行使って1レコードを表現するラベル型のなんちゃってDBから1レコード1行のDB形式へ変換するコードの、複数シート集約版。ラベルの形は前シートで完全に同じことが前提。

2コード

 

'==============================================================================
' ラベルデータからDBへ変換する(全シート集約版)
'==============================================================================
' 【機能】
'   ワークブック内の全シート(転記先DB以外)のラベル形式データを読み取り、
'   1つのデータベース形式シートに集約する
'
' 【参照の有無】
'   参照なし(標準ライブラリのみ使用)
'
' 【変数の省略記号】
'   Src = Source(読み取り元)
'   Dst = Destination(転記先)
'   ws  = Worksheet
'   col = Column(列)
'   row = Row(行)
'   i   = Index(ループ変数の接頭辞)
'
' 【想定シート構成】
'   - 複数のラベルシート: ラベル形式でデータが配置されたシート群
'   - 転記先DB: 変換後のデータベース形式データを出力するシート
'
' 【想定データレイアウト】
'   各ラベルは指定された幅×高さの矩形領域に配置
'   ラベル内の各項目は相対位置で定義(左上角からのオフセット)
'
' 【使い方】
'   1. 転記先DBシートを作成
'   2. マクロを実行(全シートを自動処理)
'   ※設定値・フィールド定義は「単一シートのラベルを変換」関数内で定義
'==============================================================================

'==============================================================================
' メイン処理:全シートのラベルデータを集約
'==============================================================================
Sub 全シートのラベルデータを集約する()
    Dim wsDst As Worksheet
    Set wsDst = ThisWorkbook.Sheets("転記先DB")
    
    '=== 出力先をクリア ===
    wsDst.Cells.Clear
    
    '=== 見出しを書き出し(フィールド定義は単一シート変換関数内を参照) ===
    wsDst.Cells(1, 1).Value = "氏名"
    wsDst.Cells(1, 2).Value = "生年月日"
    wsDst.Cells(1, 3).Value = "性別"
    wsDst.Cells(1, 4).Value = "住所"
    wsDst.Cells(1, 5).Value = "電話番号"  
    wsDst.Cells(1, 6).Value = "メール"
    
    '=== 各シートを処理 ===
    Dim wsSrc As Worksheet
    For Each wsSrc In ThisWorkbook.Worksheets
        If wsSrc.Name <> wsDst.Name Then
            Call 単一シートのラベルを変換(wsSrc, wsDst)
        End If
    Next
    
    MsgBox "全シート変換完了"
End Sub

'==============================================================================
' 単一シートのラベルデータを変換
'==============================================================================
Sub 単一シートのラベルを変換(wsSrc As Worksheet, wsDst As Worksheet)
    '=== 設定値 ===
    Dim colラベル開始列 As Long: colラベル開始列 = 1          ' 開始列
    Dim rowラベル開始行 As Long: rowラベル開始行 = 2          ' 開始行
    Dim labelWidth As Long: labelWidth = 3        ' ラベル1個の幅(列数)
    Dim labelHeight As Long: labelHeight = 5      ' ラベル1個の高さ(行数)
    Dim colラベル最終列 As String: colラベル最終列 = "I"      ' ラベルが使用する最終列記号

    '=== キーに対する相対位置(行, 列)===
    ' ラベルの左上角(キー位置)からの相対位置で定義
    Dim dicラベル内のoffset As Object
    Set dicラベル内のoffset = CreateObject("Scripting.Dictionary")
    dicラベル内のoffset.Add "氏名", Array(0, 0)      ' キー位置そのもの
    dicラベル内のoffset.Add "生年月日", Array(0, 1)  ' キー位置から右に1列
    dicラベル内のoffset.Add "性別", Array(0, 2)      ' キー位置から右に2列
    dicラベル内のoffset.Add "住所", Array(1, 0)      ' キー位置から下に1行
    dicラベル内のoffset.Add "電話番号", Array(1, 1)  ' キー位置から下に1行、右に1列
    dicラベル内のoffset.Add "メール", Array(1, 2)    ' キー位置から下に1行、右に2列

    Dim lastRow As Long: lastRow = wsSrc.UsedRange.Rows.Count
    Dim lastCol As Long: lastCol = wsSrc.Range(colラベル最終列 & "1").Column
    
    ' 転記先の次の空行を取得
    Dim iRowDst As Long: iRowDst = wsDst.Cells(wsDst.Rows.Count, 1).End(xlUp).Row + 1

    '=== 縦方向のラベルを処理 ===
    Dim iRowSrc As Long: iRowSrc = rowラベル開始行
    Do While iRowSrc <= lastRow
        
        '=== 横方向のラベルを処理 ===
        Dim iColSrc As Long: iColSrc = colラベル開始列
        Do While iColSrc <= lastCol
            
            Dim pos As Variant: pos = dicラベル内のoffset("氏名")
            Dim is氏名セルに値がある As Boolean
            is氏名セルに値がある = (Trim(CStr(wsSrc.Cells(iRowSrc, iColSrc).Offset(pos(0), pos(1)).Value)) <> "")
            
            ' 氏名が空でない場合のみ処理
            If is氏名セルに値がある Then
                Dim iColDst As Long: iColDst = 1
                Dim key As Variant
                For Each key In dicラベル内のoffset.Keys
                    pos = dicラベル内のoffset(key)
                    wsDst.Cells(iRowDst, iColDst).Value = wsSrc.Cells(iRowSrc, iColSrc).Offset(pos(0), pos(1)).Value
                    iColDst = iColDst + 1
                Next
                iRowDst = iRowDst + 1
            End If
            
            iColSrc = iColSrc + labelWidth ' 次のラベル列(横方向)
        Loop
        
        iRowSrc = iRowSrc + labelHeight ' 次のラベル行(縦方向)
    Loop
End Sub

3参考

claude