-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpopup.js
More file actions
491 lines (417 loc) · 13.9 KB
/
popup.js
File metadata and controls
491 lines (417 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
// グローバル変数
let currentTab = null;
let currentUrl = '';
let editingJumpmark = null;
// DOM要素
const mainView = document.getElementById('mainView');
const formView = document.getElementById('formView');
const currentUrlDiv = document.getElementById('currentUrl');
const jumpmarksContainer = document.getElementById('jumpmarksContainer');
const emptyState = document.getElementById('emptyState');
const addButton = document.getElementById('addButton');
const backButton = document.getElementById('backButton');
const cancelButton = document.getElementById('cancelButton');
const jumpmarkForm = document.getElementById('jumpmarkForm');
const formTitle = document.getElementById('formTitle');
// 初期化
document.addEventListener('DOMContentLoaded', async () => {
try {
await init();
setupThemeDetection();
} catch (error) {
console.error('初期化エラー:', error);
}
});
// メイン初期化関数
async function init() {
currentTab = await getCurrentTab();
if (currentTab) {
currentUrl = normalizeUrl(currentTab.url);
displayCurrentUrl();
await displayJumpmarks();
}
setupEventListeners();
}
// 現在のタブ情報を取得
async function getCurrentTab() {
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
return tab;
} catch (error) {
console.error('タブ情報取得エラー:', error);
return null;
}
}
// URLの正規化
function normalizeUrl(url) {
try {
const urlObj = new URL(url);
let normalized = urlObj.hostname;
// wwwを除去
if (normalized.startsWith('www.')) {
normalized = normalized.substring(4);
}
// パスを追加(ルート以外の場合)
if (urlObj.pathname !== '/') {
normalized += urlObj.pathname;
}
// 末尾のスラッシュを除去
normalized = normalized.replace(/\/$/, '');
return normalized;
} catch (error) {
console.error('URL正規化エラー:', error);
return url;
}
}
// 現在のURLを表示
function displayCurrentUrl() {
if (currentTab) {
const displayUrl = currentTab.url.length > 50
? currentTab.url.substring(0, 47) + '...'
: currentTab.url;
currentUrlDiv.textContent = displayUrl;
}
}
// Jumpmarksを表示
async function displayJumpmarks() {
try {
const jumpmarks = await getJumpmarksForUrl(currentUrl);
if (jumpmarks.length === 0) {
showEmptyState();
} else {
showJumpmarksList(jumpmarks);
}
} catch (error) {
console.error('Jumpmarks表示エラー:', error);
showEmptyState();
}
}
// 指定URLのJumpmarksを取得
async function getJumpmarksForUrl(url) {
try {
const result = await chrome.storage.sync.get(['jumpmarks']);
const allJumpmarks = result.jumpmarks || {};
return allJumpmarks[url] || [];
} catch (error) {
console.error('Jumpmarks取得エラー:', error);
return [];
}
}
// 空状態を表示
function showEmptyState() {
jumpmarksContainer.innerHTML = `
<div class="empty-state">
<div class="empty-icon">⚓</div>
<p>このページにはまだJumpmarkがありません</p>
<p class="empty-subtitle">関連ページへのショートカットを追加しましょう</p>
</div>
`;
}
// Jumpmarksリストを表示
function showJumpmarksList(jumpmarks) {
jumpmarksContainer.innerHTML = '';
jumpmarks.forEach(jumpmark => {
const jumpmarkElement = createJumpmarkElement(jumpmark);
jumpmarksContainer.appendChild(jumpmarkElement);
});
}
// Jumpmark要素を作成
function createJumpmarkElement(jumpmark) {
const div = document.createElement('div');
div.className = 'jumpmark-item';
div.innerHTML = `
<div class="jumpmark-icon">${jumpmark.icon || '🔗'}</div>
<div class="jumpmark-content">
<div class="jumpmark-title">${escapeHtml(jumpmark.title)}</div>
<div class="jumpmark-url">${escapeHtml(jumpmark.url)}</div>
</div>
<div class="jumpmark-actions">
<button class="edit-button" data-id="${jumpmark.id}">編集</button>
<button class="delete-button" data-id="${jumpmark.id}">削除</button>
</div>
`;
// クリックイベント(ボタン以外)
div.addEventListener('click', (e) => {
if (!e.target.classList.contains('delete-button') && !e.target.classList.contains('edit-button')) {
navigateToUrl(jumpmark.url);
}
});
// 編集ボタンのクリックイベント
const editButton = div.querySelector('.edit-button');
editButton.addEventListener('click', (e) => {
e.stopPropagation();
editJumpmark(jumpmark);
});
// 削除ボタンのクリックイベント
const deleteButton = div.querySelector('.delete-button');
deleteButton.addEventListener('click', (e) => {
e.stopPropagation();
deleteJumpmark(jumpmark.id);
});
return div;
}
// HTMLエスケープ
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// URLに移動(同一URLのタブがあればフォーカス、なければ新タブ作成)
async function navigateToUrl(url) {
try {
// 全てのタブを取得
const tabs = await chrome.tabs.query({});
// 完全に同じURLのタブを探す
const exactTab = tabs.find(tab => {
return tab.url === url;
});
if (exactTab) {
// 同じURLのタブがある場合:フォーカスのみ(リロードしない)
await chrome.tabs.update(exactTab.id, { active: true });
await chrome.windows.update(exactTab.windowId, { focused: true });
} else {
// 同じURLのタブがない場合:新しいタブを作成
await chrome.tabs.create({
url: url,
active: true
});
}
window.close();
} catch (error) {
console.error('ナビゲーションエラー:', error);
// エラーが発生した場合は従来通り新しいタブで開く
try {
await chrome.tabs.create({ url: url });
window.close();
} catch (fallbackError) {
console.error('フォールバックナビゲーションエラー:', fallbackError);
}
}
}
// Jumpmarkを削除
async function deleteJumpmark(jumpmarkId) {
try {
const result = await chrome.storage.sync.get(['jumpmarks']);
const allJumpmarks = result.jumpmarks || {};
if (allJumpmarks[currentUrl]) {
allJumpmarks[currentUrl] = allJumpmarks[currentUrl].filter(
jumpmark => jumpmark.id !== jumpmarkId
);
// 空になった場合は配列自体を削除
if (allJumpmarks[currentUrl].length === 0) {
delete allJumpmarks[currentUrl];
}
await chrome.storage.sync.set({ jumpmarks: allJumpmarks });
await displayJumpmarks();
}
} catch (error) {
console.error('Jumpmark削除エラー:', error);
}
}
// Jumpmarkを保存
async function saveJumpmark(jumpmarkData) {
try {
const result = await chrome.storage.sync.get(['jumpmarks']);
const allJumpmarks = result.jumpmarks || {};
// 現在のURLのJumpmarksを取得
if (!allJumpmarks[currentUrl]) {
allJumpmarks[currentUrl] = [];
}
// 新しいJumpmarkを追加
const newJumpmark = {
id: generateId(),
title: jumpmarkData.title,
url: jumpmarkData.url,
icon: jumpmarkData.icon || '🔗',
bidirectional: jumpmarkData.bidirectional,
created: new Date().toISOString()
};
allJumpmarks[currentUrl].push(newJumpmark);
// 双方向リンクの場合、逆方向も作成
if (jumpmarkData.bidirectional) {
const targetUrl = normalizeUrl(jumpmarkData.url);
if (!allJumpmarks[targetUrl]) {
allJumpmarks[targetUrl] = [];
}
const reverseJumpmark = {
id: generateId(),
title: `← ${currentTab.title || 'ページ'}`,
url: currentTab.url,
icon: jumpmarkData.icon || '🔗',
bidirectional: false, // 逆方向はfalse
created: new Date().toISOString()
};
allJumpmarks[targetUrl].push(reverseJumpmark);
}
await chrome.storage.sync.set({ jumpmarks: allJumpmarks });
return true;
} catch (error) {
console.error('Jumpmark保存エラー:', error);
return false;
}
}
// Jumpmarkを更新
async function updateJumpmark(jumpmarkId, jumpmarkData) {
try {
const result = await chrome.storage.sync.get(['jumpmarks']);
const allJumpmarks = result.jumpmarks || {};
// 既存のJumpmarkを検索して更新
let originalJumpmark = null;
let foundUrl = null;
for (const url in allJumpmarks) {
const jumpmark = allJumpmarks[url].find(jm => jm.id === jumpmarkId);
if (jumpmark) {
originalJumpmark = jumpmark;
foundUrl = url;
break;
}
}
if (!originalJumpmark) {
console.error('更新対象のJumpmarkが見つかりません');
return false;
}
// 双方向リンクの処理:元のJumpmarkが双方向だった場合、逆方向も削除
if (originalJumpmark.bidirectional) {
const targetUrl = normalizeUrl(originalJumpmark.url);
if (allJumpmarks[targetUrl]) {
// 逆方向のJumpmarkを探して削除
allJumpmarks[targetUrl] = allJumpmarks[targetUrl].filter(jm =>
!(jm.url === currentTab.url && jm.bidirectional === false)
);
if (allJumpmarks[targetUrl].length === 0) {
delete allJumpmarks[targetUrl];
}
}
}
// Jumpmarkを更新
const jumpmarkIndex = allJumpmarks[foundUrl].findIndex(jm => jm.id === jumpmarkId);
allJumpmarks[foundUrl][jumpmarkIndex] = {
...originalJumpmark,
title: jumpmarkData.title,
url: jumpmarkData.url,
icon: jumpmarkData.icon || '🔗',
bidirectional: jumpmarkData.bidirectional
};
// 新しい双方向リンクの処理
if (jumpmarkData.bidirectional) {
const targetUrl = normalizeUrl(jumpmarkData.url);
if (!allJumpmarks[targetUrl]) {
allJumpmarks[targetUrl] = [];
}
const reverseJumpmark = {
id: generateId(),
title: `← ${currentTab.title || 'ページ'}`,
url: currentTab.url,
icon: jumpmarkData.icon || '🔗',
bidirectional: false,
created: new Date().toISOString()
};
allJumpmarks[targetUrl].push(reverseJumpmark);
}
await chrome.storage.sync.set({ jumpmarks: allJumpmarks });
return true;
} catch (error) {
console.error('Jumpmark更新エラー:', error);
return false;
}
}
// ユニークIDを生成
function generateId() {
return 'jm-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
}
// ビューを切り替え
function showFormView() {
mainView.classList.add('hidden');
formView.classList.remove('hidden');
// フォームタイトルを変更
formTitle.textContent = '新しいJumpmark';
// フォームをリセット
jumpmarkForm.reset();
document.getElementById('bidirectional').checked = true;
// 編集状態をリセット
editingJumpmark = null;
}
function showMainView() {
formView.classList.add('hidden');
mainView.classList.remove('hidden');
// 編集状態をリセット
editingJumpmark = null;
}
// Jumpmarkを編集
function editJumpmark(jumpmark) {
editingJumpmark = jumpmark;
// フォームタイトルを変更
formTitle.textContent = 'Jumpmarkを編集';
// フォームに既存データを入力
document.getElementById('jumpmarkTitle').value = jumpmark.title;
document.getElementById('jumpmarkUrl').value = jumpmark.url;
document.getElementById('jumpmarkIcon').value = jumpmark.icon || '';
document.getElementById('bidirectional').checked = jumpmark.bidirectional;
// フォーム画面を表示
mainView.classList.add('hidden');
formView.classList.remove('hidden');
}
// イベントリスナーの設定
function setupEventListeners() {
// 追加ボタン
addButton.addEventListener('click', showFormView);
// 戻るボタン
backButton.addEventListener('click', showMainView);
// キャンセルボタン
cancelButton.addEventListener('click', showMainView);
// フォーム送信
jumpmarkForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(jumpmarkForm);
const jumpmarkData = {
title: formData.get('jumpmarkTitle') || document.getElementById('jumpmarkTitle').value,
url: formData.get('jumpmarkUrl') || document.getElementById('jumpmarkUrl').value,
icon: formData.get('jumpmarkIcon') || document.getElementById('jumpmarkIcon').value,
bidirectional: document.getElementById('bidirectional').checked
};
// 基本的なバリデーション
if (!jumpmarkData.title || !jumpmarkData.url) {
alert('タイトルとURLは必須です');
return;
}
try {
new URL(jumpmarkData.url);
} catch {
alert('有効なURLを入力してください');
return;
}
let success;
if (editingJumpmark) {
// 編集モード
success = await updateJumpmark(editingJumpmark.id, jumpmarkData);
} else {
// 新規作成モード
success = await saveJumpmark(jumpmarkData);
}
if (success) {
showMainView();
await displayJumpmarks();
} else {
alert(editingJumpmark ? '更新に失敗しました' : '保存に失敗しました');
}
});
}
// テーマ検出とスタイル切り替え
function setupThemeDetection() {
// システムテーマの初期検出
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
applyTheme(darkModeMediaQuery.matches);
// テーマ変更の監視
darkModeMediaQuery.addEventListener('change', (e) => {
applyTheme(e.matches);
});
}
function applyTheme(isDark) {
const body = document.body;
if (isDark) {
body.setAttribute('data-theme', 'dark');
} else {
body.removeAttribute('data-theme');
}
// アイコンの切り替えは不要なので、メッセージ送信は削除
}