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('<', '<') .replaceAll('>', '>'); } 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('&', '&') .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(`バージョン: ${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('&', '&') .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() { 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('&', '&') .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 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('&', '&') .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 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