Skip to content

Commit e688d1b

Browse files
authored
Merge pull request #51 from koudaiii/koudaiii/fix-design
Move updateInfo from footer to header
2 parents 08341a7 + 9daf73a commit e688d1b

File tree

3 files changed

+181
-2
lines changed

3 files changed

+181
-2
lines changed

README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,91 @@ npm run test:unit
9898
npm run test:e2e
9999
```
100100

101+
## Architecture
102+
103+
### DOM Structure and Element Cloning
104+
105+
This extension displays English version update information by cloning the existing `article-metadata-footer` element on Microsoft Learn pages.
106+
107+
#### HTML Structure
108+
109+
The Microsoft Learn page has the following structure:
110+
111+
```html
112+
<div data-main-column="">
113+
<div>
114+
<!-- Page header with breadcrumbs and actions -->
115+
<div id="article-header">...</div>
116+
117+
<!-- Article title -->
118+
<div class="content"><h1>Title</h1></div>
119+
120+
<!-- Top metadata (existing) -->
121+
<div id="article-metadata">
122+
<div id="user-feedback">...</div>
123+
</div>
124+
125+
<!-- Our custom cloned element (inserted here - after article-metadata) -->
126+
<div id="custom-header-from-article-metadata-footer">
127+
<hr class="hr">
128+
<ul class="metadata page-metadata" lang="ja-jp">
129+
<li>
130+
<span class="badge">Last updated on 2025/10/08</span>
131+
<p>英語版の更新日: <a href="...">2025/4/10 (224 日前に更新)</a></p>
132+
</li>
133+
</ul>
134+
</div>
135+
136+
<hr class="hr">
137+
138+
<!-- Article content -->
139+
<div class="content">...</div>
140+
141+
<!-- Feedback section and other components -->
142+
143+
<!-- Bottom metadata (clone source) -->
144+
<div id="article-metadata-footer">
145+
<hr class="hr">
146+
<ul class="metadata page-metadata">
147+
<li>
148+
<span class="badge">Last updated on 2025/10/08</span>
149+
</li>
150+
</ul>
151+
</div>
152+
</div>
153+
</div>
154+
```
155+
156+
#### Cloning Strategy
157+
158+
The extension uses the following approach:
159+
160+
1. **Clone Source**: `article-metadata-footer` element (bottom of the page)
161+
- This element contains the page's metadata structure with proper styling
162+
163+
2. **Clone Process**:
164+
```javascript
165+
customContainer = articleMetadataFooter.cloneNode(true);
166+
customContainer.id = 'custom-header-from-article-metadata-footer';
167+
```
168+
169+
3. **Insertion Point**: Inserted immediately after `article-metadata` (top of the page)
170+
```javascript
171+
articleMetadata.insertAdjacentElement('afterend', customContainer);
172+
```
173+
174+
4. **Customization**:
175+
- Update the `lang` attribute to match the current page language
176+
- Add a new `<p>` element containing the English version update information
177+
- Apply appropriate styling based on whether the English version is newer
178+
179+
#### Why This Approach?
180+
181+
- **Consistency**: By cloning the existing metadata footer, we inherit all the proper CSS classes and structure
182+
- **Maintainability**: If Microsoft changes the metadata structure, our extension adapts automatically
183+
- **Visibility**: Placing the update information near the top of the page ensures users see it immediately
184+
- **Clone-based**: We clone from `article-metadata-footer` at the bottom but display at the top for better UX
185+
101186
## Contribution
102187

103188
Contributions are welcome! Follow these steps to contribute:

src/content.js

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,47 @@ const applyStyles = (element, styles) => {
153153
const timeAgo = calculateTimeAgo(timeDifference, currentLang);
154154
const timeAgoStr = ` (${timeAgo})`;
155155

156-
const updateInfo = document.createElement("p");
157-
dataArticleDateElement.parentElement.appendChild(updateInfo);
156+
// Clone article-metadata-footer and create custom-header-from-article-metadata-footer
157+
let customContainer = document.getElementById('custom-header-from-article-metadata-footer');
158+
const articleMetadata = document.getElementById('article-metadata');
159+
const articleMetadataFooter = document.getElementById('article-metadata-footer');
160+
161+
let updateInfo;
162+
if (!customContainer && articleMetadata && articleMetadataFooter) {
163+
customContainer = articleMetadataFooter.cloneNode(true);
164+
customContainer.id = 'custom-header-from-article-metadata-footer';
165+
customContainer.setAttribute('data-bi-name', 'custom-header-from-article-metadata-footer');
166+
customContainer.setAttribute('data-test-id', 'custom-header-from-article-metadata-footer');
167+
customContainer.className = 'custom-page-metadata-container';
168+
169+
// Update lang attribute
170+
const ul = customContainer.querySelector('ul.metadata.page-metadata');
171+
if (ul) {
172+
ul.setAttribute('lang', currentLang);
173+
}
174+
175+
// Add p tag after span.badge in li
176+
const li = customContainer.querySelector('li.visibility-hidden-visual-diff');
177+
if (li) {
178+
updateInfo = document.createElement('p');
179+
li.appendChild(updateInfo);
180+
}
181+
182+
articleMetadata.insertAdjacentElement('afterend', customContainer);
183+
184+
// Add hr element below custom container
185+
const hr = document.createElement('hr');
186+
hr.className = 'hr';
187+
customContainer.insertAdjacentElement('afterend', hr);
188+
} else if (customContainer) {
189+
updateInfo = customContainer.querySelector('li.visibility-hidden-visual-diff p');
190+
}
191+
192+
// Fallback: if custom container doesn't exist, use original implementation
193+
if (!updateInfo) {
194+
updateInfo = document.createElement("p");
195+
dataArticleDateElement.parentElement.appendChild(updateInfo);
196+
}
158197

159198
const updateClass = () => {
160199
// if theme is selected, apply appropriate text color based on theme

tests/unit/content.test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,58 @@ describe('URL Check in Content Script', () => {
9090
expect(match).toBeNull();
9191
});
9292
});
93+
94+
describe('DOM manipulation in content script', () => {
95+
beforeEach(() => {
96+
// Set up the DOM
97+
document.body.innerHTML = `
98+
<div id="article-metadata">
99+
<local-time datetime="2025-10-08T00:00:00.000Z"></local-time>
100+
</div>
101+
<div id="article-metadata-footer">
102+
<ul class="metadata page-metadata">
103+
<li class="visibility-hidden-visual-diff">
104+
<span class="badge">Last updated on 2025/10/08</span>
105+
</li>
106+
</ul>
107+
</div>
108+
<button data-theme-to="light" aria-pressed="true"></button>
109+
`;
110+
111+
// Mock window.location.href
112+
Object.defineProperty(window, 'location', {
113+
value: {
114+
href: 'https://learn.microsoft.com/ja-jp/test',
115+
},
116+
writable: true
117+
});
118+
119+
// Mock fetch
120+
global.fetch = jest.fn(() =>
121+
Promise.resolve({
122+
text: () => Promise.resolve('<html><body><local-time datetime="2025-11-26T00:00:00.000Z"></local-time></body></html>'),
123+
})
124+
);
125+
});
126+
127+
test('should create and insert the custom header', async () => {
128+
// Run the content script
129+
require('../../src/content.js');
130+
131+
// Wait for the async operations to complete
132+
await new Promise(resolve => setTimeout(resolve, 100));
133+
134+
// Check if the custom header was created
135+
const customHeader = document.getElementById('custom-header-from-article-metadata-footer');
136+
expect(customHeader).not.toBeNull();
137+
138+
// Check if the custom header is in the correct position
139+
const articleMetadata = document.getElementById('article-metadata');
140+
expect(articleMetadata.nextElementSibling).toBe(customHeader);
141+
142+
// Check if the update information is correct
143+
const updateInfo = customHeader.querySelector('p');
144+
expect(updateInfo).not.toBeNull();
145+
expect(updateInfo.innerHTML).toContain('英語版の更新日');
146+
});
147+
});

0 commit comments

Comments
 (0)