lifecycle-timeline 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -28
- package/dist/timeline.js +168 -81
- package/dist/timeline.umd.cjs +9 -9
- package/package.json +16 -4
- package/src/index.d.ts +24 -2
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ A premium, interactive Vanilla JS component for visualizing product lifecycles,
|
|
|
7
7
|
|
|
8
8
|
## ✨ Features
|
|
9
9
|
|
|
10
|
-
- **
|
|
10
|
+
- **Interactive Table**: A clear data table between the filter and tracks for quick reference.
|
|
11
|
+
- **Smart Filtering**: Real-time search to filter versions across both table and timeline.
|
|
11
12
|
- **Dark Mode**: Native support with a persistent toggle.
|
|
12
13
|
- **Rich Legend**: Detailed explanation of support states.
|
|
13
14
|
- **Responsive Design**: Works on all screen sizes with horizontal scroll support.
|
|
@@ -58,47 +59,60 @@ new Timeline('timeline-root', data, { visibleCount: 3 });
|
|
|
58
59
|
</script>
|
|
59
60
|
```
|
|
60
61
|
|
|
61
|
-
## ⚙️ Configuration
|
|
62
|
+
## ⚙️ Configuration & API
|
|
62
63
|
|
|
63
64
|
### Constructor
|
|
64
65
|
`new Timeline(elementId, data, options)`
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
| :--- | :--- | :--- |
|
|
68
|
-
| `elementId` | `string` | The ID of the container element. |
|
|
69
|
-
| `data` | `Array` | List of version objects. |
|
|
70
|
-
| `options` | `Object` | Optional configuration. |
|
|
67
|
+
#### `options` Object
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- `releaseNotesUrl`: (String, Optional) Link to release notes.
|
|
69
|
+
| Property | Type | Default | Description |
|
|
70
|
+
| :--- | :--- | :--- | :--- |
|
|
71
|
+
| `visibleCount` | `number` | `3` | Initial versions shown before "Show More" appears. |
|
|
72
|
+
| `locale` | `string` | `auto` | UI language (`'en'`, `'fr'`). |
|
|
73
|
+
| `i18n` | `object` | `{}` | Custom translations or new languages. |
|
|
74
|
+
| `showTable` | `boolean` | `true` | Shows/hides the summary data table. |
|
|
79
75
|
|
|
80
|
-
###
|
|
81
|
-
- `visibleCount`: (Number, default: `3`) Number of versions to show before the "Show More" button appears.
|
|
82
|
-
- `locale`: (String, default: browser detect) Preferred language for the UI. Supported: `'en'`, `'fr'`.
|
|
76
|
+
### 🌐 Internationalization (i18n)
|
|
83
77
|
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
You can easily override existing labels or add new languages:
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
new Timeline('timeline-root', data, {
|
|
82
|
+
locale: 'fr',
|
|
83
|
+
i18n: {
|
|
84
|
+
fr: {
|
|
85
|
+
filter: "Rechercher une version...",
|
|
86
|
+
more: "Afficher {n} de plus"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 🛡️ Data Validation
|
|
93
|
+
The component automatically validates your data. If required fields (`version`, `ossStart`, `ossEnd`) are missing or if date formats are invalid, a warning is logged in the browser console.
|
|
86
94
|
|
|
95
|
+
## 🎨 Theming
|
|
96
|
+
The component uses CSS variables for easy customization:
|
|
87
97
|
```css
|
|
88
98
|
:root {
|
|
89
|
-
--accent-oss: #99e67d; /*
|
|
90
|
-
--accent-ent: #ffe88e; /*
|
|
91
|
-
--current-date: #086dc3; /*
|
|
92
|
-
--bg-primary: #ffffff; /* Light mode background */
|
|
93
|
-
--text-primary: #1e293b; /* Light mode text */
|
|
99
|
+
--accent-oss: #99e67d; /* Community support color */
|
|
100
|
+
--accent-ent: #ffe88e; /* Enterprise support color */
|
|
101
|
+
--current-date: #086dc3; /* Today's indicator color */
|
|
94
102
|
}
|
|
95
103
|
```
|
|
96
104
|
|
|
97
|
-
##
|
|
105
|
+
## 🧪 Testing
|
|
106
|
+
We use **Vitest** for unit testing.
|
|
107
|
+
```bash
|
|
108
|
+
npm test
|
|
109
|
+
```
|
|
98
110
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
## 🛠 Development
|
|
112
|
+
1. `npm install`
|
|
113
|
+
2. `npm run dev`
|
|
114
|
+
3. `npm run build`
|
|
115
|
+
4. `npm run release` (Build + Publish)
|
|
102
116
|
|
|
103
117
|
## 📄 License
|
|
104
118
|
MIT © Eric REBOISSON
|
package/dist/timeline.js
CHANGED
|
@@ -11,7 +11,11 @@ const u = {
|
|
|
11
11
|
ossDesc: "Free security updates and bugfixes.",
|
|
12
12
|
entDesc: "Expert support during OSS plus extended support after EOL.",
|
|
13
13
|
eolDesc: "End of life. No further updates are provided.",
|
|
14
|
-
today: "Today's date: {date}"
|
|
14
|
+
today: "Today's date: {date}",
|
|
15
|
+
branch: "Branch",
|
|
16
|
+
initial: "Initial Release",
|
|
17
|
+
ossEnd: "End OSS",
|
|
18
|
+
entEnd: "End Enterprise *"
|
|
15
19
|
},
|
|
16
20
|
fr: {
|
|
17
21
|
filter: "Filtrer les versions...",
|
|
@@ -25,10 +29,14 @@ const u = {
|
|
|
25
29
|
ossDesc: "Mises à jour de sécurité et corrections de bugs gratuites.",
|
|
26
30
|
entDesc: "Support pendant la période OSS plus support étendu après.",
|
|
27
31
|
eolDesc: "Version en fin de vie. Plus de mises à jour.",
|
|
28
|
-
today: "Date du jour : {date}"
|
|
32
|
+
today: "Date du jour : {date}",
|
|
33
|
+
branch: "Version",
|
|
34
|
+
initial: "Sortie initiale",
|
|
35
|
+
ossEnd: "Fin OSS",
|
|
36
|
+
entEnd: "Fin Entreprise *"
|
|
29
37
|
}
|
|
30
38
|
};
|
|
31
|
-
class
|
|
39
|
+
class b {
|
|
32
40
|
/**
|
|
33
41
|
* Creates an instance of Timeline.
|
|
34
42
|
* @param {string} elementId - The ID of the root element.
|
|
@@ -38,16 +46,16 @@ class v {
|
|
|
38
46
|
* @param {string} [options.locale] - Language code (e.g., 'en', 'fr').
|
|
39
47
|
* @param {Object} [options.i18n] - Custom translations to merge or override.
|
|
40
48
|
*/
|
|
41
|
-
constructor(
|
|
42
|
-
this.root = document.getElementById(
|
|
49
|
+
constructor(e, i, s = {}) {
|
|
50
|
+
this.root = document.getElementById(e), this.root && (this.options = s, this.visibleCount = s.visibleCount || 3, this.showTable = s.showTable !== !1, this.isExpanded = !1, this.isTableExpanded = !1, this.theme = "light", this.filterText = "", this.translations = { ...u, ...s.i18n || {} }, this.locale = s.locale || this.detectLanguage(), this.setupBaseLayout(), this.updateData(i));
|
|
43
51
|
}
|
|
44
52
|
/**
|
|
45
53
|
* Detects the browser language.
|
|
46
54
|
* @returns {string} The detected language code or 'en'.
|
|
47
55
|
*/
|
|
48
56
|
detectLanguage() {
|
|
49
|
-
const
|
|
50
|
-
return this.translations[
|
|
57
|
+
const e = (navigator.language || "en").split("-")[0];
|
|
58
|
+
return this.translations[e] ? e : "en";
|
|
51
59
|
}
|
|
52
60
|
/**
|
|
53
61
|
* Translates a key based on the current locale.
|
|
@@ -55,22 +63,36 @@ class v {
|
|
|
55
63
|
* @param {Object} [params={}] - Parameters to replace in the translation string.
|
|
56
64
|
* @returns {string} The translated string.
|
|
57
65
|
*/
|
|
58
|
-
t(
|
|
59
|
-
let
|
|
60
|
-
return Object.keys(
|
|
66
|
+
t(e, i = {}) {
|
|
67
|
+
let s = (this.translations[this.locale] || this.translations.en)[e] || e;
|
|
68
|
+
return Object.keys(i).forEach((t) => s = s.replace(`{${t}}`, i[t])), s;
|
|
61
69
|
}
|
|
62
70
|
/**
|
|
63
71
|
* Sets up the initial layout of the timeline.
|
|
64
72
|
*/
|
|
65
73
|
setupBaseLayout() {
|
|
66
|
-
this.root.innerHTML = "", this.root.setAttribute("role", "application"), this.root.setAttribute("aria-label", "Product Lifecycle Timeline"), this.renderToolbar(), this.renderThemeToggle(), this.wrapper = this.el("div", "timeline-wrapper", this.root), this.wrapper.setAttribute("role", "grid"), this.wrapper.setAttribute("aria-readonly", "true"), this.axis = this.el("div", "timeline-axis", this.wrapper), this.axis.setAttribute("role", "row"), this.tracks = this.el("div", "timeline-tracks", this.wrapper), this.legendContainer = this.el("div", "timeline-legend-container", this.wrapper);
|
|
74
|
+
this.root.innerHTML = "", this.root.setAttribute("role", "application"), this.root.setAttribute("aria-label", "Product Lifecycle Timeline"), this.renderToolbar(), this.renderThemeToggle(), this.showTable && (this.tableContainer = this.el("div", "timeline-table-container", this.root)), this.wrapper = this.el("div", "timeline-wrapper", this.root), this.wrapper.setAttribute("role", "grid"), this.wrapper.setAttribute("aria-readonly", "true"), this.axis = this.el("div", "timeline-axis", this.wrapper), this.axis.setAttribute("role", "row"), this.tracks = this.el("div", "timeline-tracks", this.wrapper), this.legendContainer = this.el("div", "timeline-legend-container", this.wrapper);
|
|
67
75
|
}
|
|
68
76
|
/**
|
|
69
77
|
* Updates the timeline data and re-renders.
|
|
70
78
|
* @param {Array<Object>} newData - The new data array.
|
|
71
79
|
*/
|
|
72
|
-
updateData(
|
|
73
|
-
this.data = (
|
|
80
|
+
updateData(e) {
|
|
81
|
+
this.data = this.validateData(e || []), this.calculateTimeRange(), this.render();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validates the input data.
|
|
85
|
+
* @param {Array<Object>} data - The data array to validate.
|
|
86
|
+
* @returns {Array<Object>} The validated and filtered data array.
|
|
87
|
+
*/
|
|
88
|
+
validateData(e) {
|
|
89
|
+
return e.filter((i, s) => {
|
|
90
|
+
const n = ["version", "ossStart", "ossEnd"].filter((l) => !i[l]);
|
|
91
|
+
if (n.length > 0)
|
|
92
|
+
return console.warn(`[Timeline] Missing fields for item at index ${s}: ${n.join(", ")}`), !1;
|
|
93
|
+
const a = ["ossStart", "ossEnd", "enterpriseEnd"].filter((l) => i[l]).filter((l) => isNaN(new Date(i[l]).getTime()));
|
|
94
|
+
return a.length > 0 ? (console.warn(`[Timeline] Invalid date format for item "${i.version}": ${a.join(", ")}`), !1) : !0;
|
|
95
|
+
});
|
|
74
96
|
}
|
|
75
97
|
/**
|
|
76
98
|
* Calculates the min and max years based on the data.
|
|
@@ -80,17 +102,62 @@ class v {
|
|
|
80
102
|
this.minYear = this.currentDate.getFullYear() - 1, this.maxYear = this.currentDate.getFullYear() + 3;
|
|
81
103
|
return;
|
|
82
104
|
}
|
|
83
|
-
const
|
|
84
|
-
new Date(
|
|
85
|
-
new Date(
|
|
105
|
+
const e = this.data.flatMap((i) => [
|
|
106
|
+
new Date(i.ossStart),
|
|
107
|
+
new Date(i.enterpriseEnd || i.ossEnd)
|
|
86
108
|
]);
|
|
87
|
-
this.minYear = new Date(Math.min(...
|
|
109
|
+
this.minYear = new Date(Math.min(...e)).getFullYear(), this.maxYear = new Date(Math.max(...e)).getFullYear();
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Renders the data table.
|
|
113
|
+
*/
|
|
114
|
+
renderTable() {
|
|
115
|
+
const e = this.el("table", "timeline-table", this.tableContainer);
|
|
116
|
+
e.setAttribute("aria-label", "Project support");
|
|
117
|
+
const i = this.el("thead", "", e), s = this.el("tr", "", i);
|
|
118
|
+
[this.t("branch"), this.t("initial"), this.t("ossEnd"), this.t("entEnd")].forEach((t) => {
|
|
119
|
+
this.el("th", "", s).textContent = t;
|
|
120
|
+
}), this.tableBody = this.el("tbody", "", e), this.tableRows = this.data.map((t) => this.createTableRow(t)), this.tableToggleContainer = this.el("div", "timeline-table-toggle", this.tableContainer), this.updateTableVisibility();
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Creates a row for the data table.
|
|
124
|
+
* @param {Object} item - Version data.
|
|
125
|
+
*/
|
|
126
|
+
createTableRow(e) {
|
|
127
|
+
const i = this.el("tr", "", this.tableBody), s = Date.now(), t = new Date(e.ossStart).getTime(), n = new Date(e.ossEnd).getTime(), o = e.enterpriseEnd ? new Date(e.enterpriseEnd).getTime() : n, a = s >= t && s <= n ? "status-oss" : s > n && s <= o ? "status-ent" : s > o ? "status-expired" : "", l = this.el("td", "", i);
|
|
128
|
+
let h = `<span class="table-badge ${a}">${e.versionOriginal || e.version}</span>`;
|
|
129
|
+
e.releaseNotesUrl ? l.innerHTML = `<a href="${e.releaseNotesUrl}" target="_blank" class="table-version-link">${h}</a>` : l.innerHTML = h;
|
|
130
|
+
const r = this.el("td", "", i);
|
|
131
|
+
r.textContent = e.ossStart, s > t && (r.className = "past-date");
|
|
132
|
+
const c = this.el("td", "", i);
|
|
133
|
+
c.textContent = e.ossEnd, s > n && (c.className = "past-date");
|
|
134
|
+
const d = this.el("td", "", i);
|
|
135
|
+
return d.textContent = e.enterpriseEnd || e.ossEnd, s > o && (d.className = "past-date"), { el: i, version: e.version.toLowerCase(), versionOriginal: e.versionOriginal || e.version };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Updates table visibility based on filter and visibleCount.
|
|
139
|
+
*/
|
|
140
|
+
updateTableVisibility() {
|
|
141
|
+
const e = this.tableRows.filter((t) => t.version.includes(this.filterText));
|
|
142
|
+
this.tableRows.forEach((t) => {
|
|
143
|
+
const n = t.el.querySelector(".table-badge");
|
|
144
|
+
n.innerHTML = this.highlight(t.versionOriginal || t.version), t.el.classList.remove("row-visible"), t.el.classList.add("row-hidden");
|
|
145
|
+
});
|
|
146
|
+
const i = this.filterText === "" && e.length > this.visibleCount;
|
|
147
|
+
if ((i && !this.isTableExpanded ? e.slice(0, this.visibleCount) : e).forEach((t) => {
|
|
148
|
+
t.el.classList.remove("row-hidden"), t.el.classList.add("row-visible");
|
|
149
|
+
}), this.tableToggleContainer.innerHTML = "", i) {
|
|
150
|
+
const t = this.el("button", "timeline-toggle-btn table-toggle", this.tableToggleContainer), n = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="${this.isTableExpanded ? "18 15 12 9 6 15" : "6 9 12 15 18 9"}"></polyline></svg>`;
|
|
151
|
+
t.innerHTML = this.isTableExpanded ? `${this.t("less")} ${n}` : `${this.t("more", { n: e.length - this.visibleCount })} ${n}`, t.onclick = () => {
|
|
152
|
+
this.isTableExpanded = !this.isTableExpanded, this.updateTableVisibility();
|
|
153
|
+
};
|
|
154
|
+
}
|
|
88
155
|
}
|
|
89
156
|
/**
|
|
90
157
|
* Renders the entire timeline.
|
|
91
158
|
*/
|
|
92
159
|
render() {
|
|
93
|
-
this.axis.innerHTML = "", this.tracks.innerHTML = "", this.legendContainer.innerHTML = "", this.renderAxis(), this.grid = this.el("div", "grid-lines-container", this.tracks), this.indicators = this.el("div", "indicators-container", this.tracks), this.renderGrid(), this.renderCurrentDateLine(), this.rows = this.data.map((
|
|
160
|
+
this.showTable && (this.tableContainer.innerHTML = ""), this.axis.innerHTML = "", this.tracks.innerHTML = "", this.legendContainer.innerHTML = "", this.showTable && this.renderTable(), this.renderAxis(), this.grid = this.el("div", "grid-lines-container", this.tracks), this.indicators = this.el("div", "indicators-container", this.tracks), this.renderGrid(), this.renderCurrentDateLine(), this.rows = this.data.map((e) => this.createRow(e)), this.toggleContainer = this.el("div", "timeline-more-toggle", this.tracks, {
|
|
94
161
|
paddingTop: "10px",
|
|
95
162
|
display: "flex",
|
|
96
163
|
justifyContent: "center",
|
|
@@ -109,17 +176,17 @@ class v {
|
|
|
109
176
|
* @param {MouseEvent} e - The mouse event.
|
|
110
177
|
* @param {string} content - The HTML content for the tooltip.
|
|
111
178
|
*/
|
|
112
|
-
showTooltip(
|
|
113
|
-
this.tooltip.innerHTML =
|
|
179
|
+
showTooltip(e, i) {
|
|
180
|
+
this.tooltip.innerHTML = i, this.tooltip.style.display = "block", this.updateTooltipPos(e);
|
|
114
181
|
}
|
|
115
182
|
/**
|
|
116
183
|
* Updates tooltip position relative to mouse.
|
|
117
184
|
* @param {MouseEvent} e - The mouse event.
|
|
118
185
|
*/
|
|
119
|
-
updateTooltipPos(
|
|
120
|
-
let
|
|
121
|
-
const
|
|
122
|
-
|
|
186
|
+
updateTooltipPos(e) {
|
|
187
|
+
let s = e.pageX + 12, t = e.pageY + 12;
|
|
188
|
+
const n = this.tooltip.offsetWidth, o = this.tooltip.offsetHeight;
|
|
189
|
+
s + n > window.innerWidth && (s = e.pageX - n - 12), t + o > window.innerHeight && (t = e.pageY - o - 12), this.tooltip.style.left = `${s}px`, this.tooltip.style.top = `${t}px`;
|
|
123
190
|
}
|
|
124
191
|
/**
|
|
125
192
|
* Hides the tooltip.
|
|
@@ -127,6 +194,20 @@ class v {
|
|
|
127
194
|
hideTooltip() {
|
|
128
195
|
this.tooltip && (this.tooltip.style.display = "none");
|
|
129
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Highlights the search term in a given text.
|
|
199
|
+
* @param {string} text - The text to process.
|
|
200
|
+
* @returns {string} The HTML with highlighted segments.
|
|
201
|
+
*/
|
|
202
|
+
highlight(e) {
|
|
203
|
+
if (!this.filterText) return e;
|
|
204
|
+
try {
|
|
205
|
+
const i = new RegExp(`(${this.filterText})`, "gi");
|
|
206
|
+
return e.replace(i, '<mark class="highlight-match">$1</mark>');
|
|
207
|
+
} catch {
|
|
208
|
+
return e;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
130
211
|
/**
|
|
131
212
|
* Helper to create DOM elements with styles and classes.
|
|
132
213
|
* @param {string} tag - HTML tag.
|
|
@@ -135,33 +216,33 @@ class v {
|
|
|
135
216
|
* @param {Object} [styles={}] - Inline styles.
|
|
136
217
|
* @returns {HTMLElement} The created element.
|
|
137
218
|
*/
|
|
138
|
-
el(
|
|
139
|
-
const
|
|
140
|
-
return
|
|
219
|
+
el(e, i, s, t = {}) {
|
|
220
|
+
const n = document.createElement(e);
|
|
221
|
+
return i && (n.className = i), Object.assign(n.style, t), s && s.appendChild(n), n;
|
|
141
222
|
}
|
|
142
223
|
/**
|
|
143
224
|
* Renders the toolbar with the filter input.
|
|
144
225
|
*/
|
|
145
226
|
renderToolbar() {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
this.filterText =
|
|
227
|
+
const e = this.el("div", "timeline-toolbar", this.root), i = this.el("div", "timeline-filter-container", e);
|
|
228
|
+
i.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>';
|
|
229
|
+
const s = this.el("input", "timeline-filter-input", i);
|
|
230
|
+
s.placeholder = this.t("filter"), s.value = this.filterText, s.setAttribute("aria-label", this.t("filter")), s.oninput = (t) => {
|
|
231
|
+
this.filterText = t.target.value.toLowerCase().trim(), this.updateVisibility();
|
|
151
232
|
};
|
|
152
233
|
}
|
|
153
234
|
/**
|
|
154
235
|
* Renders the theme toggle button.
|
|
155
236
|
*/
|
|
156
237
|
renderThemeToggle() {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
const
|
|
238
|
+
const e = this.el("button", "theme-toggle-btn", this.root);
|
|
239
|
+
e.title = this.t("dark"), e.setAttribute("aria-label", this.t("dark"));
|
|
240
|
+
const i = {
|
|
160
241
|
moon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>',
|
|
161
242
|
sun: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>'
|
|
162
243
|
};
|
|
163
|
-
|
|
164
|
-
this.theme = this.theme === "light" ? "dark" : "light", document.documentElement.setAttribute("data-theme", this.theme),
|
|
244
|
+
e.innerHTML = this.theme === "dark" ? i.sun : i.moon, e.onclick = () => {
|
|
245
|
+
this.theme = this.theme === "light" ? "dark" : "light", document.documentElement.setAttribute("data-theme", this.theme), e.innerHTML = this.theme === "dark" ? i.sun : i.moon, e.setAttribute("aria-pressed", this.theme === "dark");
|
|
165
246
|
};
|
|
166
247
|
}
|
|
167
248
|
/**
|
|
@@ -170,10 +251,10 @@ class v {
|
|
|
170
251
|
renderLegend() {
|
|
171
252
|
this.legendContainer.innerHTML = `
|
|
172
253
|
<div class="release-legend" role="complementary" aria-label="Support Legend">
|
|
173
|
-
${["oss", "ent", "eol"].map((
|
|
174
|
-
<div class="legend-block ${
|
|
254
|
+
${["oss", "ent", "eol"].map((e) => `
|
|
255
|
+
<div class="legend-block ${e === "ent" ? "commercial" : e}">
|
|
175
256
|
<div class="legend-icon" aria-hidden="true"></div>
|
|
176
|
-
<div><h3>${this.t(
|
|
257
|
+
<div><h3>${this.t(e)}</h3><p>${this.t(e + "Desc")}</p></div>
|
|
177
258
|
</div>
|
|
178
259
|
`).join("")}
|
|
179
260
|
</div>
|
|
@@ -184,19 +265,19 @@ class v {
|
|
|
184
265
|
*/
|
|
185
266
|
renderAxis() {
|
|
186
267
|
this.el("div", "", this.axis, { width: "96px", flexShrink: "0" }).setAttribute("role", "presentation");
|
|
187
|
-
for (let
|
|
188
|
-
const
|
|
189
|
-
|
|
268
|
+
for (let i = this.minYear; i <= this.maxYear; i++) {
|
|
269
|
+
const s = this.el("div", "timeline-year", this.axis);
|
|
270
|
+
s.textContent = i, s.setAttribute("role", "columnheader");
|
|
190
271
|
}
|
|
191
272
|
}
|
|
192
273
|
/**
|
|
193
274
|
* Renders the vertical grid lines for years.
|
|
194
275
|
*/
|
|
195
276
|
renderGrid() {
|
|
196
|
-
const
|
|
197
|
-
for (let
|
|
198
|
-
const
|
|
199
|
-
|
|
277
|
+
const e = new Date(this.minYear, 0, 1).getTime(), i = new Date(this.maxYear, 11, 31).getTime() - e;
|
|
278
|
+
for (let s = this.minYear; s <= this.maxYear; s++) {
|
|
279
|
+
const t = this.el("div", "year-grid-line", this.grid);
|
|
280
|
+
t.style.left = `${(new Date(s, 0, 1).getTime() - e) / i * 100}%`, t.setAttribute("role", "presentation");
|
|
200
281
|
}
|
|
201
282
|
}
|
|
202
283
|
/**
|
|
@@ -204,18 +285,19 @@ class v {
|
|
|
204
285
|
* @param {Object} item - The version data item.
|
|
205
286
|
* @returns {Object} Metadata about the created row.
|
|
206
287
|
*/
|
|
207
|
-
createRow(
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
if (l &&
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
} else
|
|
217
|
-
|
|
218
|
-
|
|
288
|
+
createRow(e) {
|
|
289
|
+
const i = this.el("div", "timeline-row", this.tracks);
|
|
290
|
+
i.setAttribute("role", "row");
|
|
291
|
+
const s = this.el("div", "version-label", i);
|
|
292
|
+
s.setAttribute("role", "rowheader");
|
|
293
|
+
const t = Date.now(), n = new Date(e.ossStart).getTime(), o = new Date(e.ossEnd).getTime(), a = e.enterpriseEnd ? new Date(e.enterpriseEnd).getTime() : o, l = t >= n && t <= o ? "status-oss" : t > o && t <= a ? "status-ent" : t > a ? "status-expired" : "";
|
|
294
|
+
if (l && s.classList.add(l), e.releaseNotesUrl) {
|
|
295
|
+
const r = this.el("a", "version-link", s);
|
|
296
|
+
r.href = e.releaseNotesUrl, r.target = "_blank", r.innerHTML = this.highlight(e.version), r.title = this.t("notes", { v: e.version }), r.setAttribute("aria-label", this.t("notes", { v: e.version }));
|
|
297
|
+
} else
|
|
298
|
+
s.innerHTML = this.highlight(e.version);
|
|
299
|
+
const h = this.el("div", "track-container", i);
|
|
300
|
+
return h.setAttribute("role", "gridcell"), h.appendChild(this.createBar(e, e.ossStart, e.enterpriseEnd || e.ossEnd, "bar-ent", this.t("ent"))), h.appendChild(this.createBar(e, e.ossStart, e.ossEnd, "bar-oss", this.t("oss"))), { el: i, version: e.version.toLowerCase(), versionOriginal: e.version };
|
|
219
301
|
}
|
|
220
302
|
/**
|
|
221
303
|
* Creates a colored bar segment for the timeline.
|
|
@@ -226,29 +308,34 @@ class v {
|
|
|
226
308
|
* @param {string} prefix - Support type prefix for tooltip.
|
|
227
309
|
* @returns {HTMLElement} The created bar element.
|
|
228
310
|
*/
|
|
229
|
-
createBar(
|
|
230
|
-
const o = new Date(
|
|
231
|
-
|
|
311
|
+
createBar(e, i, s, t, n) {
|
|
312
|
+
const o = new Date(i).getTime(), a = new Date(s).getTime(), l = new Date(this.minYear, 0, 1).getTime(), h = new Date(this.maxYear, 11, 31).getTime() - l, r = this.el("div", `bar-segment ${t}`);
|
|
313
|
+
r.style.left = `${(o - l) / h * 100}%`, r.style.width = `${(a - o) / h * 100}%`, r.setAttribute("role", "img"), r.setAttribute("aria-label", `${e.version} ${n}: ${i} to ${s}`), r.tabIndex = 0;
|
|
232
314
|
const c = `
|
|
233
|
-
<div class="tooltip-header">${
|
|
234
|
-
<div class="tooltip-date"><strong>Du:</strong> ${
|
|
235
|
-
<div class="tooltip-date"><strong>Au:</strong> ${
|
|
315
|
+
<div class="tooltip-header">${n} - ${e.version}</div>
|
|
316
|
+
<div class="tooltip-date"><strong>Du:</strong> ${i}</div>
|
|
317
|
+
<div class="tooltip-date"><strong>Au:</strong> ${s}</div>
|
|
236
318
|
`;
|
|
237
|
-
return
|
|
238
|
-
const p =
|
|
319
|
+
return r.onmouseenter = (d) => this.showTooltip(d, c), r.onmousemove = (d) => this.updateTooltipPos(d), r.onmouseleave = () => this.hideTooltip(), r.onfocus = (d) => {
|
|
320
|
+
const p = r.getBoundingClientRect();
|
|
239
321
|
this.showTooltip({ pageX: p.left + window.scrollX, pageY: p.top + window.scrollY - 40 }, c);
|
|
240
|
-
},
|
|
322
|
+
}, r.onblur = () => this.hideTooltip(), r;
|
|
241
323
|
}
|
|
242
324
|
/**
|
|
243
325
|
* Updates visibility of rows based on filtering and expansion state.
|
|
244
326
|
*/
|
|
245
327
|
updateVisibility() {
|
|
246
|
-
const
|
|
247
|
-
this.rows.forEach((
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
328
|
+
const e = this.rows.filter((t) => t.version.includes(this.filterText));
|
|
329
|
+
this.rows.forEach((t) => {
|
|
330
|
+
const n = t.el.querySelector(".version-label"), o = n.querySelector(".version-link");
|
|
331
|
+
o ? o.innerHTML = this.highlight(t.versionOriginal || t.version) : n.innerHTML = this.highlight(t.versionOriginal || t.version), t.el.classList.remove("row-visible"), t.el.classList.add("row-hidden");
|
|
332
|
+
});
|
|
333
|
+
const i = this.filterText === "" && e.length > this.visibleCount;
|
|
334
|
+
if ((i && !this.isExpanded ? e.slice(0, this.visibleCount) : e).forEach((t) => {
|
|
335
|
+
t.el.classList.remove("row-hidden"), t.el.classList.add("row-visible");
|
|
336
|
+
}), this.showTable && this.updateTableVisibility(), this.toggleContainer.innerHTML = "", i) {
|
|
337
|
+
const t = this.el("button", "timeline-toggle-btn", this.toggleContainer), n = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="${this.isExpanded ? "18 15 12 9 6 15" : "6 9 12 15 18 9"}"></polyline></svg>`;
|
|
338
|
+
t.innerHTML = this.isExpanded ? `${this.t("less")} ${n}` : `${this.t("more", { n: e.length - this.visibleCount })} ${n}`, t.setAttribute("aria-expanded", this.isExpanded), t.onclick = () => {
|
|
252
339
|
this.isExpanded = !this.isExpanded, this.updateVisibility();
|
|
253
340
|
};
|
|
254
341
|
}
|
|
@@ -257,14 +344,14 @@ class v {
|
|
|
257
344
|
* Renders the vertical line indicating current date.
|
|
258
345
|
*/
|
|
259
346
|
renderCurrentDateLine() {
|
|
260
|
-
const
|
|
261
|
-
if (
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
const o = this.el("div", "current-date-badge",
|
|
265
|
-
o.textContent =
|
|
347
|
+
const e = new Date(this.minYear, 0, 1).getTime(), i = new Date(this.maxYear, 11, 31).getTime() - e, s = Date.now() - e;
|
|
348
|
+
if (s < 0 || s > i) return;
|
|
349
|
+
const t = (/* @__PURE__ */ new Date()).toISOString().split("T")[0], n = this.el("div", "current-date-indicator", this.indicators);
|
|
350
|
+
n.style.left = `${s / i * 100}%`, n.setAttribute("role", "separator"), n.setAttribute("aria-label", this.t("today", { date: t }));
|
|
351
|
+
const o = this.el("div", "current-date-badge", n);
|
|
352
|
+
o.textContent = t, o.setAttribute("aria-hidden", "true");
|
|
266
353
|
}
|
|
267
354
|
}
|
|
268
355
|
export {
|
|
269
|
-
|
|
356
|
+
b as default
|
|
270
357
|
};
|
package/dist/timeline.umd.cjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(c,p){typeof exports=="object"&&typeof module<"u"?module.exports=p():typeof define=="function"&&define.amd?define(p):(c=typeof globalThis<"u"?globalThis:c||self,c.Timeline=p())})(this,(function(){"use strict";const c={en:{filter:"Filter versions...",oss:"OSS Support",ent:"Enterprise Support",eol:"Out of Support",less:"Show Less",more:"Show {n} more versions",notes:"View Release Notes for {v}",dark:"Toggle Dark Mode",ossDesc:"Free security updates and bugfixes.",entDesc:"Expert support during OSS plus extended support after EOL.",eolDesc:"End of life. No further updates are provided.",today:"Today's date: {date}",branch:"Branch",initial:"Initial Release",ossEnd:"End OSS",entEnd:"End Enterprise *"},fr:{filter:"Filtrer les versions...",oss:"Support OSS",ent:"Support Entreprise",eol:"Fin de support",less:"Voir moins",more:"Voir {n} versions supplémentaires",notes:"Voir les notes de version pour {v}",dark:"Changer le mode sombre",ossDesc:"Mises à jour de sécurité et corrections de bugs gratuites.",entDesc:"Support pendant la période OSS plus support étendu après.",eolDesc:"Version en fin de vie. Plus de mises à jour.",today:"Date du jour : {date}",branch:"Version",initial:"Sortie initiale",ossEnd:"Fin OSS",entEnd:"Fin Entreprise *"}};class p{constructor(e,i,s={}){this.root=document.getElementById(e),this.root&&(this.options=s,this.visibleCount=s.visibleCount||3,this.showTable=s.showTable!==!1,this.isExpanded=!1,this.isTableExpanded=!1,this.theme="light",this.filterText="",this.translations={...c,...s.i18n||{}},this.locale=s.locale||this.detectLanguage(),this.setupBaseLayout(),this.updateData(i))}detectLanguage(){const e=(navigator.language||"en").split("-")[0];return this.translations[e]?e:"en"}t(e,i={}){let s=(this.translations[this.locale]||this.translations.en)[e]||e;return Object.keys(i).forEach(t=>s=s.replace(`{${t}}`,i[t])),s}setupBaseLayout(){this.root.innerHTML="",this.root.setAttribute("role","application"),this.root.setAttribute("aria-label","Product Lifecycle Timeline"),this.renderToolbar(),this.renderThemeToggle(),this.showTable&&(this.tableContainer=this.el("div","timeline-table-container",this.root)),this.wrapper=this.el("div","timeline-wrapper",this.root),this.wrapper.setAttribute("role","grid"),this.wrapper.setAttribute("aria-readonly","true"),this.axis=this.el("div","timeline-axis",this.wrapper),this.axis.setAttribute("role","row"),this.tracks=this.el("div","timeline-tracks",this.wrapper),this.legendContainer=this.el("div","timeline-legend-container",this.wrapper)}updateData(e){this.data=this.validateData(e||[]),this.calculateTimeRange(),this.render()}validateData(e){return e.filter((i,s)=>{const n=["version","ossStart","ossEnd"].filter(l=>!i[l]);if(n.length>0)return console.warn(`[Timeline] Missing fields for item at index ${s}: ${n.join(", ")}`),!1;const a=["ossStart","ossEnd","enterpriseEnd"].filter(l=>i[l]).filter(l=>isNaN(new Date(i[l]).getTime()));return a.length>0?(console.warn(`[Timeline] Invalid date format for item "${i.version}": ${a.join(", ")}`),!1):!0})}calculateTimeRange(){if(this.currentDate=new Date,!this.data.length){this.minYear=this.currentDate.getFullYear()-1,this.maxYear=this.currentDate.getFullYear()+3;return}const e=this.data.flatMap(i=>[new Date(i.ossStart),new Date(i.enterpriseEnd||i.ossEnd)]);this.minYear=new Date(Math.min(...e)).getFullYear(),this.maxYear=new Date(Math.max(...e)).getFullYear()}renderTable(){const e=this.el("table","timeline-table",this.tableContainer);e.setAttribute("aria-label","Project support");const i=this.el("thead","",e),s=this.el("tr","",i);[this.t("branch"),this.t("initial"),this.t("ossEnd"),this.t("entEnd")].forEach(t=>{this.el("th","",s).textContent=t}),this.tableBody=this.el("tbody","",e),this.tableRows=this.data.map(t=>this.createTableRow(t)),this.tableToggleContainer=this.el("div","timeline-table-toggle",this.tableContainer),this.updateTableVisibility()}createTableRow(e){const i=this.el("tr","",this.tableBody),s=Date.now(),t=new Date(e.ossStart).getTime(),n=new Date(e.ossEnd).getTime(),o=e.enterpriseEnd?new Date(e.enterpriseEnd).getTime():n,a=s>=t&&s<=n?"status-oss":s>n&&s<=o?"status-ent":s>o?"status-expired":"",l=this.el("td","",i);let h=`<span class="table-badge ${a}">${e.versionOriginal||e.version}</span>`;e.releaseNotesUrl?l.innerHTML=`<a href="${e.releaseNotesUrl}" target="_blank" class="table-version-link">${h}</a>`:l.innerHTML=h;const r=this.el("td","",i);r.textContent=e.ossStart,s>t&&(r.className="past-date");const u=this.el("td","",i);u.textContent=e.ossEnd,s>n&&(u.className="past-date");const d=this.el("td","",i);return d.textContent=e.enterpriseEnd||e.ossEnd,s>o&&(d.className="past-date"),{el:i,version:e.version.toLowerCase(),versionOriginal:e.versionOriginal||e.version}}updateTableVisibility(){const e=this.tableRows.filter(t=>t.version.includes(this.filterText));this.tableRows.forEach(t=>{const n=t.el.querySelector(".table-badge");n.innerHTML=this.highlight(t.versionOriginal||t.version),t.el.classList.remove("row-visible"),t.el.classList.add("row-hidden")});const i=this.filterText===""&&e.length>this.visibleCount;if((i&&!this.isTableExpanded?e.slice(0,this.visibleCount):e).forEach(t=>{t.el.classList.remove("row-hidden"),t.el.classList.add("row-visible")}),this.tableToggleContainer.innerHTML="",i){const t=this.el("button","timeline-toggle-btn table-toggle",this.tableToggleContainer),n=`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="${this.isTableExpanded?"18 15 12 9 6 15":"6 9 12 15 18 9"}"></polyline></svg>`;t.innerHTML=this.isTableExpanded?`${this.t("less")} ${n}`:`${this.t("more",{n:e.length-this.visibleCount})} ${n}`,t.onclick=()=>{this.isTableExpanded=!this.isTableExpanded,this.updateTableVisibility()}}}render(){this.showTable&&(this.tableContainer.innerHTML=""),this.axis.innerHTML="",this.tracks.innerHTML="",this.legendContainer.innerHTML="",this.showTable&&this.renderTable(),this.renderAxis(),this.grid=this.el("div","grid-lines-container",this.tracks),this.indicators=this.el("div","indicators-container",this.tracks),this.renderGrid(),this.renderCurrentDateLine(),this.rows=this.data.map(e=>this.createRow(e)),this.toggleContainer=this.el("div","timeline-more-toggle",this.tracks,{paddingTop:"10px",display:"flex",justifyContent:"center",position:"relative",zIndex:"10"}),this.updateVisibility(),this.renderLegend(),this.setupTooltip()}setupTooltip(){this.tooltip&&this.tooltip.remove(),this.tooltip=this.el("div","timeline-tooltip-overlay",document.body),this.tooltip.style.display="none",this.tooltip.setAttribute("role","tooltip")}showTooltip(e,i){this.tooltip.innerHTML=i,this.tooltip.style.display="block",this.updateTooltipPos(e)}updateTooltipPos(e){let s=e.pageX+12,t=e.pageY+12;const n=this.tooltip.offsetWidth,o=this.tooltip.offsetHeight;s+n>window.innerWidth&&(s=e.pageX-n-12),t+o>window.innerHeight&&(t=e.pageY-o-12),this.tooltip.style.left=`${s}px`,this.tooltip.style.top=`${t}px`}hideTooltip(){this.tooltip&&(this.tooltip.style.display="none")}highlight(e){if(!this.filterText)return e;try{const i=new RegExp(`(${this.filterText})`,"gi");return e.replace(i,'<mark class="highlight-match">$1</mark>')}catch{return e}}el(e,i,s,t={}){const n=document.createElement(e);return i&&(n.className=i),Object.assign(n.style,t),s&&s.appendChild(n),n}renderToolbar(){const e=this.el("div","timeline-toolbar",this.root),i=this.el("div","timeline-filter-container",e);i.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>';const s=this.el("input","timeline-filter-input",i);s.placeholder=this.t("filter"),s.value=this.filterText,s.setAttribute("aria-label",this.t("filter")),s.oninput=t=>{this.filterText=t.target.value.toLowerCase().trim(),this.updateVisibility()}}renderThemeToggle(){const e=this.el("button","theme-toggle-btn",this.root);e.title=this.t("dark"),e.setAttribute("aria-label",this.t("dark"));const i={moon:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>',sun:'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>'};e.innerHTML=this.theme==="dark"?i.sun:i.moon,e.onclick=()=>{this.theme=this.theme==="light"?"dark":"light",document.documentElement.setAttribute("data-theme",this.theme),e.innerHTML=this.theme==="dark"?i.sun:i.moon,e.setAttribute("aria-pressed",this.theme==="dark")}}renderLegend(){this.legendContainer.innerHTML=`
|
|
2
2
|
<div class="release-legend" role="complementary" aria-label="Support Legend">
|
|
3
|
-
${["oss","ent","eol"].map(
|
|
4
|
-
<div class="legend-block ${
|
|
3
|
+
${["oss","ent","eol"].map(e=>`
|
|
4
|
+
<div class="legend-block ${e==="ent"?"commercial":e}">
|
|
5
5
|
<div class="legend-icon" aria-hidden="true"></div>
|
|
6
|
-
<div><h3>${this.t(
|
|
6
|
+
<div><h3>${this.t(e)}</h3><p>${this.t(e+"Desc")}</p></div>
|
|
7
7
|
</div>
|
|
8
8
|
`).join("")}
|
|
9
9
|
</div>
|
|
10
|
-
`}renderAxis(){this.el("div","",this.axis,{width:"96px",flexShrink:"0"}).setAttribute("role","presentation");for(let
|
|
11
|
-
<div class="tooltip-header">${n} - ${
|
|
12
|
-
<div class="tooltip-date"><strong>Du:</strong> ${
|
|
13
|
-
<div class="tooltip-date"><strong>Au:</strong> ${
|
|
14
|
-
`;return r.onmouseenter=
|
|
10
|
+
`}renderAxis(){this.el("div","",this.axis,{width:"96px",flexShrink:"0"}).setAttribute("role","presentation");for(let i=this.minYear;i<=this.maxYear;i++){const s=this.el("div","timeline-year",this.axis);s.textContent=i,s.setAttribute("role","columnheader")}}renderGrid(){const e=new Date(this.minYear,0,1).getTime(),i=new Date(this.maxYear,11,31).getTime()-e;for(let s=this.minYear;s<=this.maxYear;s++){const t=this.el("div","year-grid-line",this.grid);t.style.left=`${(new Date(s,0,1).getTime()-e)/i*100}%`,t.setAttribute("role","presentation")}}createRow(e){const i=this.el("div","timeline-row",this.tracks);i.setAttribute("role","row");const s=this.el("div","version-label",i);s.setAttribute("role","rowheader");const t=Date.now(),n=new Date(e.ossStart).getTime(),o=new Date(e.ossEnd).getTime(),a=e.enterpriseEnd?new Date(e.enterpriseEnd).getTime():o,l=t>=n&&t<=o?"status-oss":t>o&&t<=a?"status-ent":t>a?"status-expired":"";if(l&&s.classList.add(l),e.releaseNotesUrl){const r=this.el("a","version-link",s);r.href=e.releaseNotesUrl,r.target="_blank",r.innerHTML=this.highlight(e.version),r.title=this.t("notes",{v:e.version}),r.setAttribute("aria-label",this.t("notes",{v:e.version}))}else s.innerHTML=this.highlight(e.version);const h=this.el("div","track-container",i);return h.setAttribute("role","gridcell"),h.appendChild(this.createBar(e,e.ossStart,e.enterpriseEnd||e.ossEnd,"bar-ent",this.t("ent"))),h.appendChild(this.createBar(e,e.ossStart,e.ossEnd,"bar-oss",this.t("oss"))),{el:i,version:e.version.toLowerCase(),versionOriginal:e.version}}createBar(e,i,s,t,n){const o=new Date(i).getTime(),a=new Date(s).getTime(),l=new Date(this.minYear,0,1).getTime(),h=new Date(this.maxYear,11,31).getTime()-l,r=this.el("div",`bar-segment ${t}`);r.style.left=`${(o-l)/h*100}%`,r.style.width=`${(a-o)/h*100}%`,r.setAttribute("role","img"),r.setAttribute("aria-label",`${e.version} ${n}: ${i} to ${s}`),r.tabIndex=0;const u=`
|
|
11
|
+
<div class="tooltip-header">${n} - ${e.version}</div>
|
|
12
|
+
<div class="tooltip-date"><strong>Du:</strong> ${i}</div>
|
|
13
|
+
<div class="tooltip-date"><strong>Au:</strong> ${s}</div>
|
|
14
|
+
`;return r.onmouseenter=d=>this.showTooltip(d,u),r.onmousemove=d=>this.updateTooltipPos(d),r.onmouseleave=()=>this.hideTooltip(),r.onfocus=d=>{const g=r.getBoundingClientRect();this.showTooltip({pageX:g.left+window.scrollX,pageY:g.top+window.scrollY-40},u)},r.onblur=()=>this.hideTooltip(),r}updateVisibility(){const e=this.rows.filter(t=>t.version.includes(this.filterText));this.rows.forEach(t=>{const n=t.el.querySelector(".version-label"),o=n.querySelector(".version-link");o?o.innerHTML=this.highlight(t.versionOriginal||t.version):n.innerHTML=this.highlight(t.versionOriginal||t.version),t.el.classList.remove("row-visible"),t.el.classList.add("row-hidden")});const i=this.filterText===""&&e.length>this.visibleCount;if((i&&!this.isExpanded?e.slice(0,this.visibleCount):e).forEach(t=>{t.el.classList.remove("row-hidden"),t.el.classList.add("row-visible")}),this.showTable&&this.updateTableVisibility(),this.toggleContainer.innerHTML="",i){const t=this.el("button","timeline-toggle-btn",this.toggleContainer),n=`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="${this.isExpanded?"18 15 12 9 6 15":"6 9 12 15 18 9"}"></polyline></svg>`;t.innerHTML=this.isExpanded?`${this.t("less")} ${n}`:`${this.t("more",{n:e.length-this.visibleCount})} ${n}`,t.setAttribute("aria-expanded",this.isExpanded),t.onclick=()=>{this.isExpanded=!this.isExpanded,this.updateVisibility()}}}renderCurrentDateLine(){const e=new Date(this.minYear,0,1).getTime(),i=new Date(this.maxYear,11,31).getTime()-e,s=Date.now()-e;if(s<0||s>i)return;const t=new Date().toISOString().split("T")[0],n=this.el("div","current-date-indicator",this.indicators);n.style.left=`${s/i*100}%`,n.setAttribute("role","separator"),n.setAttribute("aria-label",this.t("today",{date:t}));const o=this.el("div","current-date-badge",n);o.textContent=t,o.setAttribute("aria-hidden","true")}}return p}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lifecycle-timeline",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A beautiful, premium JS timeline component for visualizing product lifecycles, OSS support, and enterprise support dates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/timeline.umd.cjs",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dev": "vite",
|
|
23
23
|
"build": "vite build",
|
|
24
24
|
"preview": "vite preview",
|
|
25
|
-
"test": "
|
|
25
|
+
"test": "vitest run",
|
|
26
26
|
"prepublishOnly": "npm run build",
|
|
27
27
|
"release": "npm run build && npm publish"
|
|
28
28
|
},
|
|
@@ -34,8 +34,17 @@
|
|
|
34
34
|
"support",
|
|
35
35
|
"oss",
|
|
36
36
|
"enterprise",
|
|
37
|
+
"js",
|
|
38
|
+
"javascript",
|
|
39
|
+
"react",
|
|
37
40
|
"vanilla-js",
|
|
38
|
-
"premium-ui"
|
|
41
|
+
"premium-ui",
|
|
42
|
+
"accessibility",
|
|
43
|
+
"i18n",
|
|
44
|
+
"responsive",
|
|
45
|
+
"product-management",
|
|
46
|
+
"visualizer",
|
|
47
|
+
"support-lifecycle"
|
|
39
48
|
],
|
|
40
49
|
"author": "Eric REBOISSON",
|
|
41
50
|
"license": "MIT",
|
|
@@ -48,6 +57,9 @@
|
|
|
48
57
|
},
|
|
49
58
|
"homepage": "https://github.com/ericreboisson/lifecycle-timeline#readme",
|
|
50
59
|
"devDependencies": {
|
|
51
|
-
"
|
|
60
|
+
"@testing-library/dom": "^10.4.1",
|
|
61
|
+
"jsdom": "^27.4.0",
|
|
62
|
+
"vite": "^7.3.0",
|
|
63
|
+
"vitest": "^4.0.16"
|
|
52
64
|
}
|
|
53
65
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -18,6 +18,17 @@ export interface TimelineOptions {
|
|
|
18
18
|
* Defaults to browser language.
|
|
19
19
|
*/
|
|
20
20
|
locale?: 'en' | 'fr' | string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Custom translations to merge or override.
|
|
24
|
+
*/
|
|
25
|
+
i18n?: Record<string, Record<string, string>>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether to show the data table between filter and timeline.
|
|
29
|
+
* @default true
|
|
30
|
+
*/
|
|
31
|
+
showTable?: boolean;
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
export default class Timeline {
|
|
@@ -30,9 +41,20 @@ export default class Timeline {
|
|
|
30
41
|
constructor(elementId: string, data: TimelineVersion[], options?: TimelineOptions);
|
|
31
42
|
|
|
32
43
|
/**
|
|
33
|
-
*
|
|
44
|
+
* Sets up the initial layout.
|
|
45
|
+
*/
|
|
46
|
+
setupBaseLayout(): void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Updates the timeline data and re-renders.
|
|
50
|
+
* @param newData Array of version data.
|
|
51
|
+
*/
|
|
52
|
+
updateData(newData: TimelineVersion[]): void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Renders the entire timeline.
|
|
34
56
|
*/
|
|
35
|
-
|
|
57
|
+
render(): void;
|
|
36
58
|
|
|
37
59
|
/**
|
|
38
60
|
* Renders the toolbar (search/filter).
|