diff --git a/Makefile b/Makefile index f9d39c3..92a6910 100644 --- a/Makefile +++ b/Makefile @@ -16,22 +16,24 @@ MAKEPAGES := $(TMPDIR)/make-pages YAMLS != find -L $(DATADIR) -name $(INFONAME) -SCRIPTS != find script -name '*.js' +TSCRIPTS != find script -name '*.ts' STYLES != find style -name '*.css' STYLESVGS != find style -name '*.svg' STYLEPNGS != find style -name '*.png' + FONTS != find fonts \ -iname '*.eot' -or -iname '*.svg' -or \ -iname '*.ttf' -or -iname '*.woff' -or \ -iname '*.woff2' -or -iname '*.css' -STATIC = $(SCRIPTS) $(STYLES) $(STYLEPNGS) $(STYLESVGS) $(FONTS) -BSTATIC = $(patsubst %,$(BUILDDIR)/%,$(STATIC)) +STATIC = $(STYLES) $(STYLEPNGS) $(STYLESVGS) $(FONTS) +BSTATIC = $(patsubst %,$(BUILDDIR)/%,$(STATIC)) +BSCRIPTS = $(patsubst %.ts,$(BUILDDIR)/%.js,$(TSCRIPTS)) .PHONY: all build all: build -build: $(BUILDDIR)/index.html $(BSTATIC) +build: $(BUILDDIR)/index.html $(BSTATIC) $(BSCRIPTS) $(BUILDDIR)/index.html: $(DATADIR)/index.yaml $(MAKEPAGES) echo "[index] "$@ @@ -46,6 +48,12 @@ $(BUILDDIR)/%: $(TMPDIR)/% $(call copy,--link) +$(TMPDIR)/%.js: %.ts + echo "[tsc] "$@ + tsc --strict --noEmitOnError \ + --lib dom,es2021 --target es2015 \ + --outDir $(dir $@) $^ + $(TMPDIR)/%_small.png: $(DATADIR)/%.png $(call resize,$(SMALL),$(SMALL),^,-gravity center -crop 1:1+0) diff --git a/make-pages/GalleryPage.hs b/make-pages/GalleryPage.hs index 686dda9..5331e21 100644 --- a/make-pages/GalleryPage.hs +++ b/make-pages/GalleryPage.hs @@ -42,7 +42,7 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0 - + $0.nsfwScript $title diff --git a/make-pages/NsfwWarning.hs b/make-pages/NsfwWarning.hs index 5e820ab..1f4066b 100644 --- a/make-pages/NsfwWarning.hs +++ b/make-pages/NsfwWarning.hs @@ -12,7 +12,7 @@ instance CanBuild What where script :: Maybe What -> Builder script Nothing = "" -script (Just _) = [b||] +script (Just _) = [b||] dialog :: Maybe What -> Builder dialog Nothing = "" diff --git a/make-pages/SinglePage.hs b/make-pages/SinglePage.hs index 02fd3e5..b249fa5 100644 --- a/make-pages/SinglePage.hs +++ b/make-pages/SinglePage.hs @@ -141,7 +141,7 @@ make' root siteName prefix nsfw _dataDir dir - + $nsfwScript $bgStyle diff --git a/script/gallery.js b/script/gallery.js deleted file mode 100644 index 10502c3..0000000 --- a/script/gallery.js +++ /dev/null @@ -1,204 +0,0 @@ -(function() { -'use strict'; - -let reqBoxes; -let excBoxes; -let allBoxes; -let tags; -let itemsByYear; - -let showSingles = false; - - -function fillSets() { - let checkedValues = boxes => - new Set(boxes.filter(b => b.checked).map(b => b.value)); - - return [checkedValues(reqBoxes), checkedValues(excBoxes)]; -} - -function updateItems() { - let [reqTags, excTags] = fillSets(); - let anyReq = reqTags.size > 0; - - for (let [year, items] of itemsByYear) { - let hide = true; - - for (let item of items) { - let req = tags.get(item).some(x => reqTags.has(x)); - let exc = tags.get(item).some(x => excTags.has(x)); - let hidden = exc || (anyReq && !req); - - item.hidden = hidden; - hide &&= hidden; - } - - document.getElementById(`marker-${year}`).hidden = hide; - } - - function disp(pfx, tags) { - return Array(...tags).map(x => pfx + x).join('\u2003'); // em space - } - let plus = disp('+\u2009', reqTags); // thin space - let minus = disp('-\u2009', excTags); - document.getElementById('filters-details').dataset.filters = - `${plus}\u2003${minus}`.trim(); -} - -function update() { - updateItems(); - history.pushState(null, "", makeFragment()); -} - -function converseId(id) { - if (id.match(/^require/)) { - return id.replace('require', 'exclude'); - } else { - return id.replace('exclude', 'require'); - } -} - -function toggle(checkbox) { - if (checkbox.checked) - document.getElementById(converseId(checkbox.id)).checked = false; - - update(); -} - - -function clearForm() { - allBoxes.forEach(b => b.checked = b.defaultChecked); -} - -function clear(e) { - clearForm(); - update(); - if (e) e.preventDefault(); -} - -function toggleSingles(e) { - showSingles = !showSingles; - - for (let li of document.querySelectorAll('.filterlist li')) { - let count = +li.querySelector('label').dataset.count; - if (count <= 1) { - li.hidden = !showSingles; - } - } - - if (e) e.preventDefault(); -} - - -function makeFragment() { - let ids = allBoxes.filter(b => b.checked).map(b => b.id); - if (ids.length == 0) { - return '#all'; - } else if (allBoxes.every(b => b.checked == b.defaultChecked)) { - return '#'; - } else { - return '#' + ids.join(';'); - } -} - -function useFragment() { - let frag = decodeURIComponent(location.hash).replace(/^#/, ''); - let details = document.getElementById('filters-details'); - - if (!frag) { - clearForm(); - } else if (frag == 'all') { - allBoxes.forEach(b => b.checked = false); - details.open = false; - } else { - let set = new Set(frag.split(';')); - let re = /^(require|exclude)_|hide_filters/; - if (Array.from(set).every(x => re.test(x))) { - allBoxes.forEach(b => b.checked = set.has(b.id)); - details.open = !frag.match(/hide_filters|example\b/); - } - } - - updateItems(); -} - - -function sortFilters(cmp) { - function sort1(id) { - let elt = document.getElementById(id); - let children = [...elt.childNodes]; - children.sort(cmp); - for (let c of children) { - elt.removeChild(c); - elt.appendChild(c); - } - } - - sort1('require'); - sort1('exclude'); -} - -function sortFiltersAlpha(e) { - function getName(x) { - if (x.nodeType == Node.ELEMENT_NODE) { - return x.getElementsByTagName('input')[0].value; - } else { - return ''; - } - } - sortFilters((a, b) => getName(a).localeCompare(getName(b))); - e.preventDefault(); -} - -function sortFiltersUses(e) { - function getUses(x) { - if (x.nodeType == Node.ELEMENT_NODE) { - return parseInt(x.getElementsByTagName('label')[0].dataset.count); - } else { - return 0; - } - } - sortFilters((a, b) => getUses(b) - getUses(a)); - e.preventDefault(); -} - - -function setup() { - function inputs(id) { - let iter = document.getElementById(id).getElementsByTagName('input'); - return Array.from(iter); - } - - let items = Array.from(document.getElementsByClassName('post')); - - itemsByYear = new Map; - for (let item of items) { - let year = item.dataset.year; - if (!itemsByYear.has(year)) itemsByYear.set(year, new Set); - itemsByYear.get(year).add(item); - } - - reqBoxes = inputs('require'); - excBoxes = inputs('exclude'); - allBoxes = [...reqBoxes, ...excBoxes]; - - tags = new Map(items.map(item => [item, item.dataset.tags.split(';')])); - - - allBoxes.forEach(b => b.addEventListener('change', () => toggle(b))); - - function addClick(id, f) { - document.getElementById(id).addEventListener('click', f); - } - addClick('clear', clear); - addClick('sortalpha', sortFiltersAlpha); - addClick('sortuses', sortFiltersUses); - addClick('singles', toggleSingles); - - window.addEventListener('popstate', useFragment); - - useFragment(); -} - -window.addEventListener('DOMContentLoaded', setup); -})(); diff --git a/script/gallery.ts b/script/gallery.ts new file mode 100644 index 0000000..1d0cd39 --- /dev/null +++ b/script/gallery.ts @@ -0,0 +1,220 @@ +type Boxes = Set; + +let reqBoxes: Boxes; +let excBoxes: Boxes; +let allBoxes: Boxes; +let tags: Map; +let itemsByYear: Map>; + +let showSingles = false; + + +function fillSets() { + function checkedValues(boxes: Boxes) { + return new Set(Array.from(boxes).filter(b => b.checked).map(b => b.value)); + } + + return [checkedValues(reqBoxes), checkedValues(excBoxes)]; +} + +function updateItems() { + let [reqTags, excTags] = fillSets(); + let anyReq = reqTags.size > 0; + + for (let [year, items] of itemsByYear) { + let hide = true; + + for (let item of items) { + let req = tags.get(item)?.some(x => reqTags.has(x)) ?? false; + let exc = tags.get(item)?.some(x => excTags.has(x)) ?? false; + let hidden = exc || (anyReq && !req); + + item.hidden = hidden; + hide &&= hidden; + } + + let marker = document.getElementById(`marker-${year}`); + if (marker !== null) marker.hidden = hide; + } + + function disp(pfx: string, tags: string[]) { + return Array(...tags).map(x => pfx + x).join('\u2003'); // em space + } + let plus = disp('+\u2009', Array.from(reqTags)); // thin space + let minus = disp('-\u2009', Array.from(excTags)); + document.getElementById('filters-details')!.dataset.filters = + `${plus}\u2003${minus}`.trim(); +} + +function update() { + updateItems(); + history.pushState(null, "", makeFragment()); +} + +function converseId(id: string) { + if (id.match(/^require/)) { + return id.replace('require', 'exclude'); + } else { + return id.replace('exclude', 'require'); + } +} + +function toggle(checkbox: HTMLInputElement) { + if (checkbox.checked) { + let converse = document.getElementById(converseId(checkbox.id)) as HTMLInputElement; + converse.checked = false; + } + update(); +} + + +function clearForm() { + allBoxes.forEach(b => b.checked = b.defaultChecked); +} + +function clear(e: Event) { + clearForm(); + update(); + e.preventDefault(); +} + +function toggleSingles(e: Event) { + showSingles = !showSingles; + + let elems = Array.from(document.querySelectorAll('.filterlist li')); + for (let li of elems) { + let countStr = li.querySelector('label')?.dataset.count; + let count = countStr ? +countStr : 0; + if (count <= 1 && li instanceof HTMLElement) { + li.hidden = !showSingles; + } + } + + if (e) e.preventDefault(); +} + + +function makeFragment() { + let allBoxesArr = Array.from(allBoxes); + let ids = allBoxesArr.filter(b => b.checked).map(b => b.id); + if (ids.length == 0) { + return '#all'; + } else if (allBoxesArr.every(b => b.checked == b.defaultChecked)) { + return '#'; + } else { + return '#' + ids.join(';'); + } +} + +function useFragment() { + let frag = decodeURIComponent(location.hash).replace(/^#/, ''); + let details = document.getElementById('filters-details') as HTMLDetailsElement; + + if (!frag) { + clearForm(); + } else if (frag == 'all') { + allBoxes.forEach(b => b.checked = false); + details.open = false; + } else { + let set = new Set(frag.split(';')); + let re = /^(require|exclude)_|hide_filters/; + if (Array.from(set).every(x => re.test(x))) { + allBoxes.forEach(b => b.checked = set.has(b.id)); + details.open = !frag.match(/hide_filters|example\b/); + } + } + + updateItems(); +} + + +function sortFilters(cmp: (a: Node, b: Node) => number) { + function sort1(id: string) { + let elt = document.getElementById(id); + if (elt === null) return; + + let children = Array.from(elt.childNodes); + children.sort(cmp); + for (let c of children) { + elt.removeChild(c); + elt.appendChild(c); + } + } + + sort1('require'); + sort1('exclude'); +} + +function sortFiltersAlpha(e: Event) { + function getName(node: Node) { + if (node.nodeType == Node.ELEMENT_NODE) { + let elt = node as Element; + return elt.getElementsByTagName('input')[0].value; + } else { + return ''; + } + } + sortFilters((a, b) => getName(a).localeCompare(getName(b))); + e.preventDefault(); +} + +function sortFiltersUses(e: Event) { + function getUses(node: Node) { + if (node.nodeType == Node.ELEMENT_NODE) { + let elt = node as Element; + let countStr = elt.getElementsByTagName('label')[0].dataset.count; + return countStr ? +countStr : 0; + } else { + return 0; + } + } + sortFilters((a, b) => getUses(b) - getUses(a)); + e.preventDefault(); +} + + +function setup() { + function inputs(id: string) { + let iter = document.getElementById(id)!.getElementsByTagName('input'); + return new Set(Array.from(iter)); + } + + let items = Array.from(document.getElementsByClassName('post')) as HTMLElement[]; + + itemsByYear = new Map; + for (let item of items) { + let year = item.dataset.year; + if (year !== undefined) { + if (!itemsByYear.has(year)) { + itemsByYear.set(year, new Set([item])); + } else { + itemsByYear.get(year)?.add(item); + } + } + } + + reqBoxes = inputs('require'); + excBoxes = inputs('exclude'); + allBoxes = new Set([...reqBoxes, ...excBoxes]); + + tags = new Map(items.map(item => [item, item.dataset.tags?.split(';') ?? []])); + + + allBoxes.forEach(b => b.addEventListener('change', () => toggle(b))); + + function addClick(id: string, f: (e: Event) => void) { + document.getElementById(id)!.addEventListener('click', f); + } + addClick('clear', clear); + addClick('sortalpha', sortFiltersAlpha); + addClick('sortuses', sortFiltersUses); + addClick('singles', toggleSingles); + + window.addEventListener('popstate', useFragment); + + useFragment(); +} + +window.addEventListener('DOMContentLoaded', setup); + +export {}; diff --git a/script/nsfw-warning.js b/script/nsfw-warning.js deleted file mode 100644 index 493f36b..0000000 --- a/script/nsfw-warning.js +++ /dev/null @@ -1,38 +0,0 @@ -(function () { -'use strict'; - -let nsfwOk = 'nsfw-ok'; - -function alreadyYes() { - return localStorage.getItem(nsfwOk); -} - -function dismiss() { - let dialog = document.getElementById('nsfw-dialog'); - dialog.parentElement.removeChild(dialog); -} - -function yes() { - localStorage.setItem(nsfwOk, 1); - dismiss(); -} - -// now just a normal link -/* -function no() { - document.location = '//crouton.net'; -} -*/ - -function setup() { - if (alreadyYes()) { - dismiss(); - } else { - document.getElementById('nsfw-yes').onclick = yes; - // document.getElementById('nsfw-no').onclick = no; - } -} - -document.addEventListener('DOMContentLoaded', setup); - -})(); diff --git a/script/nsfw-warning.ts b/script/nsfw-warning.ts new file mode 100644 index 0000000..ac901c5 --- /dev/null +++ b/script/nsfw-warning.ts @@ -0,0 +1,28 @@ +const nsfwOk = 'nsfw-ok'; + +function alreadyYes() { + return localStorage.getItem(nsfwOk) !== undefined; +} + +function dismiss() { + let dialog = document.getElementById('nsfw-dialog')!; + dialog.parentElement?.removeChild(dialog); +} + +function yes() { + localStorage.setItem(nsfwOk, '1'); + dismiss(); +} + +function setup() { + if (alreadyYes()) { + dismiss(); + } else { + document.getElementById('nsfw-yes')!.onclick = yes; + // nsfw-no is a normal link + } +} + +document.addEventListener('DOMContentLoaded', setup); + +export {}; diff --git a/script/single.js b/script/single.js deleted file mode 100644 index 84c6309..0000000 --- a/script/single.js +++ /dev/null @@ -1,110 +0,0 @@ -(function() { -'use strict'; - -let mainfig; -let mainimg; -let mainlink; -let altButtons; -let skipAll; - -let opened = new Set; - -function openCW(id, caption, focusLink = false) { - if (id) opened.add(id); - mainfig.removeChild(caption); - mainlink.tabIndex = 0; - if (focusLink) mainlink.focus(); -} - -function addCWListeners(id, caption) { - if (caption) { - caption.addEventListener('click', e => openCW(id, caption)); - caption.addEventListener('keyup', - e => { if (e.key == 'Enter') openCW(id, caption, true) }); - } -} - -function setImage(id, src, href, cw) { - let caption = document.getElementById('cw'); - let newCaption; - - let checked = skipAll ? skipAll.checked : false; - - if (!checked && !opened.has(id) && cw) { - newCaption = document.getElementById('cw-template') - .content.firstElementChild.cloneNode(true); - newCaption.querySelector('#cw-text').innerHTML = cw; - addCWListeners(id, newCaption); - } - - if (caption) { - openCW(null, caption); - } - if (newCaption) { - mainfig.insertBefore(newCaption, mainlink); - mainlink.tabIndex = -1; - } - - mainimg.src = src; - mainlink.href = href; -} - -function activateButton(button, doPush = true) { - setImage(button.id, button.value, - button.dataset.link, button.dataset.warning); - - if (doPush) history.pushState(null, '', '#' + button.id); -} - -function useFragment(firstLoad = false) { - let button = altButtons[0]; - - let frag = decodeURIComponent(location.hash).replace(/^#/, ''); - if (frag) { - let selected = document.getElementById(frag); - if (selected) button = selected; - } - - let id; - - if (button) { - id = button.id; - button.checked = true; - activateButton(button, false); - } - - if (firstLoad) addCWListeners(id, document.getElementById('cw')); -} - -function setup() { - mainfig = document.getElementById('mainfig'); - mainimg = document.getElementById('mainimg'); - mainlink = document.getElementById('mainlink'); - skipAll = document.getElementById('skipAll'); - - let alts = document.getElementById('alts'); - if (alts) { - let inputs = Array.from(alts.getElementsByTagName('input')); - altButtons = inputs.filter(e => e.name == 'variant'); - } else { - altButtons = []; - } - - for (let button of altButtons) { - button.onchange = e => { if (button.checked) activateButton(button); }; - } - - if (skipAll) { - skipAll.onchange = e => { if (skipAll.checked) { - let caption = document.getElementById('cw'); - if (caption) { openCW(null, caption, false); } - } }; - } - - window.addEventListener('popstate', e => useFragment()); - useFragment(true); -} - -window.addEventListener('DOMContentLoaded', setup); - -})(); diff --git a/script/single.ts b/script/single.ts new file mode 100644 index 0000000..0623093 --- /dev/null +++ b/script/single.ts @@ -0,0 +1,114 @@ +let mainfig: HTMLElement; +let mainimg: HTMLImageElement; +let mainlink: HTMLAnchorElement; +let altButtons: HTMLInputElement[]; +let skipAll: HTMLInputElement; + +let opened = new Set; + +function openCW(id: string | null, caption: HTMLElement | null, + focusLink = false) { + if (id !== null) opened.add(id); + if (caption !== null) mainfig.removeChild(caption); + mainlink.tabIndex = 0; + if (focusLink) mainlink.focus(); +} + +function addCWListeners(id: string | null, + caption: HTMLElement | null) { + if (caption) { + caption.addEventListener('click', _e => openCW(id, caption)); + caption.addEventListener('keyup', + e => { if (e.key == 'Enter') openCW(id, caption, true) }); + } +} + +function setImage(id: string | null, + src: string, + href: string, + cw: string) { + let caption = document.getElementById('cw'); + let newCaption: HTMLElement | null = null; + + let checked = skipAll ? skipAll.checked : false; + + if (!checked && !opened.has(id) && cw) { + let template = document.getElementById('cw-template') as HTMLTemplateElement; + newCaption = template.content.firstElementChild!.cloneNode(true) as HTMLElement; + newCaption.querySelector('#cw-text')!.innerHTML = cw; + addCWListeners(id, newCaption); + } + + if (caption) { + openCW(null, caption); + } + if (newCaption !== null) { + mainfig.insertBefore(newCaption, mainlink); + mainlink.tabIndex = -1; + } + + mainimg.src = src; + mainlink.href = href; +} + +function activateButton(button: HTMLInputElement, + doPush = true) { + setImage(button.id, button.value, + button.dataset.link!, + button.dataset.warning!); + + if (doPush) history.pushState(null, '', '#' + button.id); +} + +function useFragment(firstLoad = false) { + let button = altButtons[0]; + + let frag = decodeURIComponent(location.hash).replace(/^#/, ''); + if (frag) { + let selected = document.getElementById(frag) as HTMLInputElement; + if (selected) button = selected; + } + + let id: string | null = null; + + if (button) { + id = button.id; + button.checked = true; + activateButton(button, false); + } + + if (firstLoad) addCWListeners(id, document.getElementById('cw')); +} + +function setup() { + mainfig = document.getElementById('mainfig')!; + mainimg = document.getElementById('mainimg') as HTMLImageElement; + mainlink = document.getElementById('mainlink') as HTMLAnchorElement; + skipAll = document.getElementById('skipAll') as HTMLInputElement; + + let alts = document.getElementById('alts'); + if (alts) { + let inputs = Array.from(alts.getElementsByTagName('input')); + altButtons = inputs.filter(e => e.name == 'variant'); + } else { + altButtons = []; + } + + for (let button of altButtons) { + button.onchange = _e => { if (button.checked) activateButton(button); }; + } + + if (skipAll) { + skipAll.onchange = _e => { if (skipAll.checked) { + let caption = document.getElementById('cw'); + if (caption) { openCW(null, caption, false); } + } }; + } + + window.addEventListener('popstate', _e => useFragment()); + useFragment(true); +} + +window.addEventListener('DOMContentLoaded', setup); + +export {}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4c1a853 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "strict": true, + "noEmitOnError": true, + "lib": ["ES2021", "dom"], + "target": "ES2015" + } +}