diff --git a/.gitignore b/.gitignore index 1c6c3a3..ddcf26e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ dist/ -ts/**/*.js \ No newline at end of file +ts/**/*.js +.vscode/ \ No newline at end of file diff --git a/scss/main.scss b/scss/main.scss index 0110dbd..7a32749 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -77,6 +77,12 @@ body { color: $red; } + .copy { + color: $dimmed_foreground; + cursor: pointer; + margin-left: 5px; + } + .description { margin-top: 20px; text-align: left; diff --git a/ts/CancelledEvent.ts b/ts/CancelledEvent.ts index 81fa536..e284cec 100644 --- a/ts/CancelledEvent.ts +++ b/ts/CancelledEvent.ts @@ -4,6 +4,7 @@ import * as moment from 'moment'; import ComponentsArray from './ComponentsArray'; import Countdown from './Countdown'; import UiComponent from './UiComponent'; +import Timestamp from './Timestamp'; export default class CancelledEvent implements UiComponent { constructor(private _event: dto.Event) { @@ -12,13 +13,7 @@ export default class CancelledEvent implements UiComponent { appendTo(entry: HTMLElement | null): void { new html.Div( new ComponentsArray([ - new html.Div( - new html.Href( - `#_${this._event.datetime.utc().unix()}`, - new html.Text(`${this._event.datetime.utc().unix()}`) - ), - {"class": "timestamp"} - ), + new Timestamp(this._event.timestamp()), new html.H1( new html.Href( `${this._event.url}`, diff --git a/ts/CurrentEvent.ts b/ts/CurrentEvent.ts index 4bab432..5d714ba 100644 --- a/ts/CurrentEvent.ts +++ b/ts/CurrentEvent.ts @@ -5,6 +5,7 @@ import ComponentsArray from './ComponentsArray'; import Countdown from './Countdown'; import UiComponent from './UiComponent'; import TwitchPlayer from './TwitchPlayer'; +import Timestamp from './Timestamp' export default class CurrentEvent implements UiComponent { constructor(private _event: dto.Event) { @@ -13,13 +14,7 @@ export default class CurrentEvent implements UiComponent { appendTo(entry: HTMLElement | null): void { new html.Div( new ComponentsArray([ - new html.Div( - new html.Href( - `#_${this._event.datetime.utc().unix()}`, - new html.Text(`${this._event.datetime.utc().unix()}`) - ), - {"class": "timestamp"} - ), + new Timestamp(this._event.timestamp()), new html.Div( new html.Href( this._event.channel ? this._event.channel : "https://twitch.tv/tsoding", diff --git a/ts/Event.ts b/ts/Event.ts index 4195fe3..3550b3b 100644 --- a/ts/Event.ts +++ b/ts/Event.ts @@ -13,7 +13,7 @@ export default class Event implements UiComponent { } appendTo(entry: HTMLElement | null): void { - let secondsDiff = moment().diff(this._event.datetime, 'seconds'); + const secondsDiff = moment().diff(this._event.datetime, 'seconds'); if (this.isCancelled()) { new CancelledEvent(this._event).appendTo(entry); @@ -25,7 +25,8 @@ export default class Event implements UiComponent { new FutureEvent(this._event).appendTo(entry); } - const hashId = "#_" + this._event.datetime.utc().unix(); + const hashId = "#_" + this._event.timestamp(); + if (window.location.hash == hashId) { window.location.hash = ""; setTimeout(() => { window.location.hash = hashId; }, 0); diff --git a/ts/EventsForDay.ts b/ts/EventsForDay.ts index d537091..9e8645a 100644 --- a/ts/EventsForDay.ts +++ b/ts/EventsForDay.ts @@ -67,8 +67,8 @@ export default class EventsForDay implements UiComponent { .map( (e) => new Event( this._state.eventPatches - ? new dto.PatchedEvent(e, this._state.eventPatches[e.datetime.utc().unix()]) - : e, + ? new dto.PatchedEvent(e as dto.Event, this._state.eventPatches[e.datetime.utc().unix()]) + : e as dto.Event, this._state.cancelledEvents ) ) diff --git a/ts/FutureEvent.ts b/ts/FutureEvent.ts index e72cb20..b467c80 100644 --- a/ts/FutureEvent.ts +++ b/ts/FutureEvent.ts @@ -4,6 +4,7 @@ import * as moment from 'moment'; import ComponentsArray from './ComponentsArray'; import Countdown from './Countdown'; import UiComponent from './UiComponent'; +import Timestamp from './Timestamp'; export default class FutureEvent implements UiComponent { constructor(private _event: dto.Event) { @@ -12,13 +13,7 @@ export default class FutureEvent implements UiComponent { appendTo(entry: HTMLElement | null): void { new html.Div( new ComponentsArray([ - new html.Div( - new html.Href( - `#_${this._event.datetime.utc().unix()}`, - new html.Text(`${this._event.datetime.utc().unix()}`) - ), - {"class": "timestamp"} - ), + new Timestamp(this._event.timestamp()), new html.H1( new html.Href( `${this._event.url}`, diff --git a/ts/PastEvent.ts b/ts/PastEvent.ts index 9eb2195..bc917a6 100644 --- a/ts/PastEvent.ts +++ b/ts/PastEvent.ts @@ -4,6 +4,7 @@ import * as moment from 'moment'; import ComponentsArray from './ComponentsArray'; import Countdown from './Countdown'; import UiComponent from './UiComponent'; +import Timestamp from './Timestamp'; export default class PastEvent implements UiComponent { constructor(private _event: dto.Event) { @@ -12,13 +13,7 @@ export default class PastEvent implements UiComponent { appendTo(entry: HTMLElement | null): void { new html.Div( new ComponentsArray([ - new html.Div( - new html.Href( - `#_${this._event.datetime.utc().unix()}`, - new html.Text(`${this._event.datetime.utc().unix()}`) - ), - {"class": "timestamp"} - ), + new Timestamp(this._event.timestamp()), new html.H1( new html.Href( `${this._event.url}`, diff --git a/ts/Timestamp.ts b/ts/Timestamp.ts new file mode 100644 index 0000000..e14351c --- /dev/null +++ b/ts/Timestamp.ts @@ -0,0 +1,35 @@ +import * as dto from './dto'; +import * as html from './html'; +import * as moment from 'moment'; +import ComponentsArray from './ComponentsArray'; +import Countdown from './Countdown'; +import UiComponent from './UiComponent'; +import TwitchPlayer from './TwitchPlayer'; +import copyToClipboard from './util/copyToClipboard'; + +export default class Timestamp implements UiComponent { + constructor(private _timestamp: string) { + } + + onCopyClick = () => { + copyToClipboard(this._timestamp) + } + + appendTo(entry: HTMLElement | null): void { + new html.Div( + new ComponentsArray([ + new html.Href( + `#_${this._timestamp}`, + new html.Text(`${this._timestamp}`) + ), + new html.Tag( + "i", + new html.Empty(), + {"class": "copy fas fa-copy fa-lg"}, + {"click": this.onCopyClick} + ) + ]), + {"class": "timestamp"} + ).appendTo(entry); + } +} diff --git a/ts/dto/Event.ts b/ts/dto/Event.ts index aa7a161..2ed9482 100644 --- a/ts/dto/Event.ts +++ b/ts/dto/Event.ts @@ -1,9 +1,14 @@ import * as moment from 'moment'; -export default interface Event { - datetime: moment.Moment, - title: string, - description: string, - url: string, - channel: string, +export default class Event { + constructor(public datetime: moment.Moment, + public title: string, + public description: string, + public url: string, + public channel: string) { + } + + timestamp() { + return this.datetime.utc().unix().toString(); + } } diff --git a/ts/dto/PatchedEvent.ts b/ts/dto/PatchedEvent.ts index e4fa0cb..a26233e 100644 --- a/ts/dto/PatchedEvent.ts +++ b/ts/dto/PatchedEvent.ts @@ -2,19 +2,9 @@ import * as moment from 'moment'; import Event from './Event'; import EventPatch from './EventPatch'; -export default class PatchedEvent implements Event { - datetime: moment.Moment; - title: string; - description: string; - url: string; - channel: string; - +export default class PatchedEvent extends Event { constructor(event: Event, eventPatch: EventPatch | undefined) { - this.datetime = event.datetime; - this.title = event.title; - this.description = event.description; - this.url = event.url; - this.channel = event.channel; + super(event.datetime, event.title, event.description, event.url, event.channel); if (eventPatch) { if (eventPatch.title) { diff --git a/ts/html/Tag.ts b/ts/html/Tag.ts index 4176f98..fd7a8fc 100644 --- a/ts/html/Tag.ts +++ b/ts/html/Tag.ts @@ -3,7 +3,8 @@ import UiComponent from '../UiComponent'; export default class Tag implements UiComponent { constructor(private _name: string, private _body?: UiComponent, - private _attrs?: {[key: string]: any}) { + private _attrs?: {[key: string]: any}, + private _events?: {[key: string]: EventListenerOrEventListenerObject}) { } appendTo(entry: HTMLElement | null): void { @@ -21,6 +22,12 @@ export default class Tag implements UiComponent { } } + if (this._events) { + for (let eventKey in this._events) { + element.addEventListener(eventKey, this._events[eventKey]) + } + } + if (this._body) { this._body.appendTo(element); } diff --git a/ts/util/copyToClipboard.ts b/ts/util/copyToClipboard.ts new file mode 100644 index 0000000..78ef2ae --- /dev/null +++ b/ts/util/copyToClipboard.ts @@ -0,0 +1,49 @@ +// Adapted from https://github.com/feross/clipboard-copy + +export default function copyToClipboard(text: string) { + // Use the Async Clipboard API when available. Requires a secure browing context (i.e. HTTPS) + if ((navigator as any).clipboard) { + return (navigator as any).clipboard.writeText(text) + } + + // ...Otherwise, use document.execCommand() fallback + + // Put the text to copy into a + const span = document.createElement('span') + span.textContent = text + + // Preserve consecutive spaces and newlines + span.style.whiteSpace = 'pre' + + // Add the to the page + document.body.appendChild(span) + + // Make a selection object representing the range of text selected by the user + const selection = window.getSelection() + const range = window.document.createRange() + + if (!selection) { + return Promise.reject() + } + + selection.removeAllRanges() + range.selectNode(span) + selection.addRange(range) + + // Copy text to the clipboard + let success = false + try { + success = window.document.execCommand('copy') + } catch (err) { + console.log('Failed to copy text. Error: ', err) + } + + // Cleanup + selection.removeAllRanges() + window.document.body.removeChild(span) + + // The Async Clipboard API returns a promise that may reject with `undefined` so we match that here for consistency. + return success + ? Promise.resolve() + : Promise.reject() +} \ No newline at end of file