diff --git a/assets/misuzu.js/embed.js b/assets/misuzu.js/embed.js deleted file mode 100644 index 39bfcaf..0000000 --- a/assets/misuzu.js/embed.js +++ /dev/null @@ -1,128 +0,0 @@ -#include utils.js -#include uiharu.js -#include aembed.js -#include iembed.js -#include vembed.js - -var MszEmbed = (function() { - let uiharu = undefined; - - return { - init: function(endPoint) { - uiharu = new Uiharu(endPoint); - }, - handle: function(targets) { - if(!Array.isArray(targets)) - targets = Array.from(targets); - - const filtered = new Map; - for(const target of targets) { - if(!(target instanceof HTMLElement) - || !('dataset' in target) - || !('mszEmbedUrl' in target.dataset)) - continue; - - const cleanUrl = target.dataset.mszEmbedUrl.replace(/ /, '%20'); - if(cleanUrl.indexOf('https://') !== 0 - && cleanUrl.indexOf('http://') !== 0 - && cleanUrl.indexOf('//') !== 0) { - target.textContent = target.dataset.mszEmbedUrl; - continue; - } - - $rc(target); - target.appendChild($e({ - tag: 'i', - attrs: { - className: 'fas fa-2x fa-spinner fa-pulse', - style: { - width: '32px', - height: '32px', - lineHeight: '32px', - textAlign: 'center', - }, - }, - })); - - if(filtered.has(cleanUrl)) - filtered.get(cleanUrl).push(target); - else - filtered.set(cleanUrl, [target]); - } - - const replaceWithUrl = function(targets, url) { - for(const target of targets) { - let body = $e({ - tag: 'a', - attrs: { - className: 'link', - href: url, - target: '_blank', - rel: 'noopener noreferrer', - }, - child: url - }); - $ib(target, body); - $r(target); - } - }; - - filtered.forEach(function(targets, url) { - uiharu.lookupOne(url, function(metadata) { - if(metadata.error) { - replaceWithUrl(targets, url); - console.error(metadata.error); - return; - } - - if(metadata.title === undefined) { - replaceWithUrl(targets, url); - console.warn('Media is no longer available.'); - return; - } - - let phc = undefined, - options = { - onembed: console.log, - }; - - if(metadata.type === 'youtube:video') { - phc = MszVideoEmbedPlaceholder; - options.type = 'youtube'; - options.player = MszVideoEmbedYouTube; - } else if(metadata.type === 'niconico:video') { - phc = MszVideoEmbedPlaceholder; - options.type = 'nicovideo'; - options.player = MszVideoEmbedNicoNico; - } else if(metadata.is_video) { - phc = MszVideoEmbedPlaceholder; - options.type = 'external'; - options.player = MszVideoEmbedPlayer; - //options.frame = MszVideoEmbedFrame; - options.nativeControls = true; - options.autosize = false; - options.maxWidth = 640; - options.maxHeight = 360; - } else if(metadata.is_audio) { - phc = MszAudioEmbedPlaceholder; - options.type = 'external'; - options.player = MszAudioEmbedPlayer; - options.nativeControls = true; - } else if(metadata.is_image) { - phc = MszImageEmbed; - options.type = 'external'; - } - - if(phc === undefined) - return; - - for(const target of targets) { - const placeholder = new phc(metadata, options, target); - if(placeholder !== undefined) - placeholder.replaceElement(target); - } - }); - }); - }, - }; -})(); diff --git a/assets/misuzu.js/aembed.js b/assets/misuzu.js/embed/audio.js similarity index 98% rename from assets/misuzu.js/aembed.js rename to assets/misuzu.js/embed/audio.js index 53e41bd..aaee05a 100644 --- a/assets/misuzu.js/aembed.js +++ b/assets/misuzu.js/embed/audio.js @@ -1,4 +1,4 @@ -#include utils.js +#include utility.js #include watcher.js const MszAudioEmbedPlayerEvents = function() { @@ -84,7 +84,8 @@ const MszAudioEmbedPlayer = function(metadata, options) { getType: function() { return 'external'; }, }; - watchers.proxy(pub); + pub.watch = (name, handler) => watchers.watch(name, handler); + pub.unwatch = (name, handler) => watchers.unwatch(name, handler); player.addEventListener('play', function() { watchers.call('play', pub); }); diff --git a/assets/misuzu.js/embed/embed.js b/assets/misuzu.js/embed/embed.js new file mode 100644 index 0000000..66e1859 --- /dev/null +++ b/assets/misuzu.js/embed/embed.js @@ -0,0 +1,135 @@ +#include utility.js +#include embed/audio.js +#include embed/image.js +#include embed/video.js +#include ext/uiharu.js + +const MszEmbed = (function() { + let uiharu = undefined; + + return { + init: function(endPoint) { + uiharu = new MszUiharu(endPoint); + }, + handle: function(targets) { + if(!Array.isArray(targets)) + targets = Array.from(targets); + + const filtered = new Map; + for(const target of targets) { + if(!(target instanceof HTMLElement) + || !('dataset' in target) + || !('mszEmbedUrl' in target.dataset)) + continue; + + const cleanUrl = target.dataset.mszEmbedUrl.replace(/ /, '%20'); + if(cleanUrl.indexOf('https://') !== 0 + && cleanUrl.indexOf('http://') !== 0 + && cleanUrl.indexOf('//') !== 0) { + target.textContent = target.dataset.mszEmbedUrl; + continue; + } + + $rc(target); + target.appendChild($e({ + tag: 'i', + attrs: { + className: 'fas fa-2x fa-spinner fa-pulse', + style: { + width: '32px', + height: '32px', + lineHeight: '32px', + textAlign: 'center', + }, + }, + })); + + if(filtered.has(cleanUrl)) + filtered.get(cleanUrl).push(target); + else + filtered.set(cleanUrl, [target]); + } + + const replaceWithUrl = function(targets, url) { + for(const target of targets) { + let body = $e({ + tag: 'a', + attrs: { + className: 'link', + href: url, + target: '_blank', + rel: 'noopener noreferrer', + }, + child: url + }); + $ib(target, body); + $r(target); + } + }; + + filtered.forEach(function(targets, url) { + uiharu.lookupOne(url) + .catch(ex => { + replaceWithUrl(targets, url); + console.error(ex); + }) + .then(result => { + const metadata = result.body(); + + if(metadata.error) { + replaceWithUrl(targets, url); + console.error(metadata.error); + return; + } + + if(metadata.title === undefined) { + replaceWithUrl(targets, url); + console.warn('Media is no longer available.'); + return; + } + + let phc = undefined, + options = { + onembed: console.log, + }; + + if(metadata.type === 'youtube:video') { + phc = MszVideoEmbedPlaceholder; + options.type = 'youtube'; + options.player = MszVideoEmbedYouTube; + } else if(metadata.type === 'niconico:video') { + phc = MszVideoEmbedPlaceholder; + options.type = 'nicovideo'; + options.player = MszVideoEmbedNicoNico; + } else if(metadata.is_video) { + phc = MszVideoEmbedPlaceholder; + options.type = 'external'; + options.player = MszVideoEmbedPlayer; + //options.frame = MszVideoEmbedFrame; + options.nativeControls = true; + options.autosize = false; + options.maxWidth = 640; + options.maxHeight = 360; + } else if(metadata.is_audio) { + phc = MszAudioEmbedPlaceholder; + options.type = 'external'; + options.player = MszAudioEmbedPlayer; + options.nativeControls = true; + } else if(metadata.is_image) { + phc = MszImageEmbed; + options.type = 'external'; + } + + if(phc === undefined) + return; + + for(const target of targets) { + const placeholder = new phc(metadata, options, target); + if(placeholder !== undefined) + placeholder.replaceElement(target); + } + }); + }); + }, + }; +})(); diff --git a/assets/misuzu.js/iembed.js b/assets/misuzu.js/embed/image.js similarity index 97% rename from assets/misuzu.js/iembed.js rename to assets/misuzu.js/embed/image.js index 847258e..9cd387f 100644 --- a/assets/misuzu.js/iembed.js +++ b/assets/misuzu.js/embed/image.js @@ -1,4 +1,4 @@ -#include utils.js +#include utility.js const MszImageEmbed = function(metadata, options, target) { options = options || {}; diff --git a/assets/misuzu.js/vembed.js b/assets/misuzu.js/embed/video.js similarity index 95% rename from assets/misuzu.js/vembed.js rename to assets/misuzu.js/embed/video.js index f3a33a5..122f08c 100644 --- a/assets/misuzu.js/vembed.js +++ b/assets/misuzu.js/embed/video.js @@ -1,5 +1,5 @@ -#include utils.js -#include rng.js +#include utility.js +#include uniqstr.js #include watcher.js const MszVideoEmbedPlayerEvents = function() { @@ -265,12 +265,13 @@ const MszVideoEmbedPlayer = function(metadata, options) { getHeight: function() { return height; }, }; - watchers.proxy(pub); + pub.watch = (name, handler) => watchers.watch(name, handler); + pub.unwatch = (name, handler) => watchers.unwatch(name, handler); if(shouldObserveResize) player.addEventListener('resize', function() { setSize(player.videoWidth, player.videoHeight); }); - player.addEventListener('play', function() { watchers.call('play', pub); }); + player.addEventListener('play', function() { watchers.call('play'); }); const pPlay = function() { player.play(); }; pub.play = pPlay; @@ -280,7 +281,7 @@ const MszVideoEmbedPlayer = function(metadata, options) { let stopCalled = false; player.addEventListener('pause', function() { - watchers.call(stopCalled ? 'stop' : 'pause', pub); + watchers.call(stopCalled ? 'stop' : 'pause'); stopCalled = false; }); @@ -301,9 +302,9 @@ const MszVideoEmbedPlayer = function(metadata, options) { player.addEventListener('volumechange', function() { if(lastMuteState !== player.muted) { lastMuteState = player.muted; - watchers.call('mute', pub, [lastMuteState]); + watchers.call('mute', lastMuteState); } else - watchers.call('volume', pub, [player.volume]); + watchers.call('volume', player.volume); }); const pSetMuted = function(state) { player.muted = state; }; @@ -319,21 +320,21 @@ const MszVideoEmbedPlayer = function(metadata, options) { pub.getPlaybackRate = pGetPlaybackRate; player.addEventListener('ratechange', function() { - watchers.call('rate', pub, [player.playbackRate]); + watchers.call('rate', player.playbackRate); }); const pSetPlaybackRate = function(rate) { player.playbackRate = rate; }; pub.setPlaybackRate = pSetPlaybackRate; window.addEventListener('durationchange', function() { - watchers.call('duration', pub, [player.duration]); + watchers.call('duration', player.duration); }); const pGetDuration = function() { return player.duration; }; pub.getDuration = pGetDuration; window.addEventListener('timeupdate', function() { - watchers.call('time', pub, [player.currentTime]); + watchers.call('time', player.currentTime); }); const pGetTime = function() { return player.currentTime; }; @@ -410,7 +411,8 @@ const MszVideoEmbedYouTube = function(metadata, options) { getPlayerId: function() { return playerId; }, }; - watchers.proxy(pub); + pub.watch = (name, handler) => watchers.watch(name, handler); + pub.unwatch = (name, handler) => watchers.unwatch(name, handler); const postMessage = function(data) { player.contentWindow.postMessage(JSON.stringify(data), ytOrigin); @@ -463,33 +465,33 @@ const MszVideoEmbedYouTube = function(metadata, options) { lastPlayerState = state; if(eventName !== undefined && eventName !== lastPlayerStateEvent) { lastPlayerStateEvent = eventName; - watchers.call(eventName, pub); + watchers.call(eventName); } }; const handleMuted = function(muted) { isMuted = muted; - watchers.call('mute', pub, [isMuted]); + watchers.call('mute', isMuted); }; const handleVolume = function(value) { volume = value / 100; - watchers.call('volume', pub, [volume]); + watchers.call('volume', volume); }; const handleRate = function(rate) { playbackRate = rate; - watchers.call('rate', pub, [playbackRate]); + watchers.call('rate', playbackRate); }; const handleDuration = function(time) { duration = time; - watchers.call('duration', pub, [duration]); + watchers.call('duration', duration); }; const handleTime = function(time) { currentTime = time; - watchers.call('time', pub, [currentTime]); + watchers.call('time', currentTime); }; const handlePresetRates = function(rates) { @@ -610,7 +612,8 @@ const MszVideoEmbedNicoNico = function(metadata, options) { getPlayerId: function() { return playerId; }, }; - watchers.proxy(pub); + pub.watch = (name, handler) => watchers.watch(name, handler); + pub.unwatch = (name, handler) => watchers.unwatch(name, handler); const postMessage = function(name, data) { if(name === undefined) @@ -660,28 +663,28 @@ const MszVideoEmbedNicoNico = function(metadata, options) { if(eventName !== undefined && eventName !== lastPlayerStateEvent) { lastPlayerStateEvent = eventName; - watchers.call(eventName, pub); + watchers.call(eventName); } }; const handleMuted = function(muted) { isMuted = muted; - watchers.call('mute', pub, [isMuted]); + watchers.call('mute', isMuted); }; const handleVolume = function(value) { volume = value; - watchers.call('volume', pub, [volume]); + watchers.call('volume', volume); }; const handleDuration = function(time) { duration = time / 1000; - watchers.call('duration', pub, [duration]); + watchers.call('duration', duration); }; const handleTime = function(time) { currentTime = time / 1000; - watchers.call('time', pub, [currentTime]); + watchers.call('time', currentTime); }; const metadataHanders = { diff --git a/assets/misuzu.js/events/christmas2019.js b/assets/misuzu.js/events/christmas2019.js index 364f327..bebdc27 100644 --- a/assets/misuzu.js/events/christmas2019.js +++ b/assets/misuzu.js/events/christmas2019.js @@ -1,35 +1,49 @@ -#include utils.js +#include utility.js -Misuzu.Events.Christmas2019 = function() { - this.propName = propName = 'msz-christmas-' + (new Date).getFullYear().toString(); +const MszChristmas2019EventInfo = function() { + return { + isActive: () => { + const d = new Date; + return d.getMonth() === 11 && d.getDate() > 5 && d.getDate() < 27; + }, + dispatch: () => { + const impl = new MszChristmas2019Event; + impl.dispatch(); + return impl; + }, + }; }; -Misuzu.Events.Christmas2019.prototype.changeColour = function() { - var count = parseInt(localStorage.getItem(this.propName)); - document.body.style.setProperty('--header-accent-colour', (count++ % 2) ? 'green' : 'red'); - localStorage.setItem(this.propName, count.toString()); -}; -Misuzu.Events.Christmas2019.prototype.isActive = function() { - var d = new Date; - return d.getMonth() === 11 && d.getDate() > 5 && d.getDate() < 27; -}; -Misuzu.Events.Christmas2019.prototype.dispatch = function() { - var headerBg = $q('.header__background'), - menuBgs = $qa('.header__desktop__submenu__background'); - - if(!localStorage.getItem(this.propName)) - localStorage.setItem(this.propName, '0'); - - if(headerBg) - headerBg.style.transition = 'background-color .4s'; - - setTimeout(function() { - if(headerBg) - headerBg.style.transition = 'background-color 1s'; - - for(var i = 0; i < menuBgs.length; i++) - menuBgs[i].style.transition = 'background-color 1s'; - }, 1000); - - this.changeColour(); - setInterval(this.changeColour, 10000); + +const MszChristmas2019Event = function() { + const propName = 'msz-christmas-' + (new Date).getFullYear().toString(); + const headerBg = $q('.header__background'); + const menuBgs = Array.from($qa('.header__desktop__submenu__background')); + + if(!localStorage.getItem(propName)) + localStorage.setItem(propName, '0'); + + const changeColour = () => { + let count = parseInt(localStorage.getItem(propName)); + document.body.style.setProperty('--header-accent-colour', (count++ % 2) ? 'green' : 'red'); + localStorage.setItem(propName, count.toString()); + }; + + return { + changeColour: changeColour, + dispatch: () => { + if(headerBg) + headerBg.style.transition = 'background-color .4s'; + + setTimeout(() => { + if(headerBg) + headerBg.style.transition = 'background-color 1s'; + + for(const menuBg of menuBgs) + menuBg.style.transition = 'background-color 1s'; + }, 1000); + + changeColour(); + setInterval(changeColour, 10000); + }, + }; }; diff --git a/assets/misuzu.js/events/events.js b/assets/misuzu.js/events/events.js index 85b5998..960a050 100644 --- a/assets/misuzu.js/events/events.js +++ b/assets/misuzu.js/events/events.js @@ -1,15 +1,15 @@ -Misuzu.Events = {}; +const MszSeasonalEvents = function() { + const events = []; -#include events/christmas2019.js - -Misuzu.Events.getList = function() { - return [ - new Misuzu.Events.Christmas2019, - ]; -}; -Misuzu.Events.dispatch = function() { - var list = Misuzu.Events.getList(); - for(var i = 0; i < list.length; ++i) - if(list[i].isActive()) - list[i].dispatch(); + return { + add: eventInfo => { + if(!events.includes(eventInfo)) + events.push(eventInfo); + }, + dispatch: () => { + for(const info of events) + if(info.isActive()) + info.dispatch(); + }, + }; }; diff --git a/assets/misuzu.js/ext/eeprom.js b/assets/misuzu.js/ext/eeprom.js new file mode 100644 index 0000000..5eda181 --- /dev/null +++ b/assets/misuzu.js/ext/eeprom.js @@ -0,0 +1,37 @@ +#include utility.js + +const MszEEPROM = (() => { + let eepromScript; + + return { + init: () => { + return new Promise((resolve, reject) => { + if(eepromScript !== undefined) { + resolve(false); + return; + } + + if(typeof peepPath !== 'string') { + reject(); + return; + } + + const scriptElem = $e({ + tag: 'script', + attrs: { + src: `${peepPath}/eeprom.js`, + charset: 'utf-8', + type: 'text/javascript', + onerror: () => reject(), + onload: () => { + eepromScript = scriptElem; + resolve(true); + }, + }, + }); + + document.body.appendChild(scriptElem); + }); + }, + }; +})(); diff --git a/assets/misuzu.js/sakuya.js b/assets/misuzu.js/ext/sakuya.js similarity index 97% rename from assets/misuzu.js/sakuya.js rename to assets/misuzu.js/ext/sakuya.js index cb3b3c3..af67921 100644 --- a/assets/misuzu.js/sakuya.js +++ b/assets/misuzu.js/ext/sakuya.js @@ -1,4 +1,4 @@ -const Sakuya = (function() { +const MszSakuya = (function() { const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }); const divisions = [ { amount: 60, name: 'seconds' }, diff --git a/assets/misuzu.js/ext/uiharu.js b/assets/misuzu.js/ext/uiharu.js new file mode 100644 index 0000000..6c0299d --- /dev/null +++ b/assets/misuzu.js/ext/uiharu.js @@ -0,0 +1,15 @@ +#include utility.js + +const MszUiharu = function(apiUrl) { + const maxBatchSize = 4; + const lookupOneUrl = apiUrl + '/metadata'; + + return { + lookupOne: async targetUrl => { + if(typeof targetUrl !== 'string') + throw 'targetUrl must be a string'; + + return $x.post(lookupOneUrl, { type: 'json' }, targetUrl); + }, + }; +}; diff --git a/assets/misuzu.js/forum/editor.js b/assets/misuzu.js/forum/editor.js deleted file mode 100644 index f3c27c6..0000000 --- a/assets/misuzu.js/forum/editor.js +++ /dev/null @@ -1,437 +0,0 @@ -#include forum/forum.js - -Misuzu.Forum.Editor = {}; -Misuzu.Forum.Editor.allowWindowClose = false; -Misuzu.Forum.Editor.init = function() { - const postingForm = $q('.js-forum-posting'); - if(!postingForm) - return; - - const postingButtons = postingForm.querySelector('.js-forum-posting-buttons'), - postingText = postingForm.querySelector('.js-forum-posting-text'), - postingParser = postingForm.querySelector('.js-forum-posting-parser'), - postingPreview = postingForm.querySelector('.js-forum-posting-preview'), - postingMode = postingForm.querySelector('.js-forum-posting-mode'), - previewButton = document.createElement('button'), - bbcodeButtons = $q('.forum__post__actions--bbcode'), - markdownButtons = $q('.forum__post__actions--markdown'), - markupButtons = $qa('.forum__post__action--tag'); - - // Initialise EEPROM, code sucks ass but it's getting nuked soon again anyway - if(typeof peepPath === 'string') - document.body.appendChild($e({ - tag: 'script', - attrs: { - src: peepPath + '/eeprom.js', - charset: 'utf-8', - type: 'text/javascript', - onload: function() { - const eepromClient = new EEPROM(peepApp, peepPath + '/uploads', ''); - - const eepromHistory = $e({ - attrs: { - className: 'eeprom-widget-history-items', - }, - }); - - const eepromHandleFileUpload = function(file) { - const uploadElemNameValue = $e({ - attrs: { - className: 'eeprom-widget-file-name-value', - title: file.name, - }, - child: file.name, - }); - - const uploadElemName = $e({ - tag: 'a', - attrs: { - className: 'eeprom-widget-file-name', - target: '_blank', - }, - child: uploadElemNameValue, - }); - - const uploadElemProgressText = $e({ - attrs: { - className: 'eeprom-widget-file-progress', - }, - child: 'Please wait...', - }); - - const uploadElemProgressBarValue = $e({ - attrs: { - className: 'eeprom-widget-file-bar-fill', - style: { - width: '0%', - }, - }, - }); - - const uploadElem = $e({ - attrs: { - className: 'eeprom-widget-file', - }, - child: [ - { - attrs: { - className: 'eeprom-widget-file-info', - }, - child: [ - uploadElemName, - uploadElemProgressText, - ], - }, - { - attrs: { - className: 'eeprom-widget-file-bar', - }, - child: uploadElemProgressBarValue, - }, - ], - }); - - if(eepromHistory.children.length > 0) - $ib(eepromHistory.firstChild, uploadElem); - else - eepromHistory.appendChild(uploadElem); - - const explodeUploadElem = function() { - $r(uploadElem); - }; - - const uploadTask = eepromClient.createUpload(file); - - uploadTask.onProgress = function(progressInfo) { - const progressValue = progressInfo.progress.toString() + '%'; - uploadElemProgressBarValue.style.width = progressValue; - uploadElemProgressText.textContent = progressValue + ' (' + (progressInfo.total - progressInfo.loaded).toString() + ' bytes remaining)'; - }; - - uploadTask.onFailure = function(errorInfo) { - if(!errorInfo.userAborted) { - let errorText = 'Was unable to upload file.'; - - switch(errorInfo.error) { - case EEPROM.ERR_INVALID: - errorText = 'Upload request was invalid.'; - break; - case EEPROM.ERR_AUTH: - errorText = 'Upload authentication failed, refresh and try again.'; - break; - case EEPROM.ERR_ACCESS: - errorText = 'You\'re not allowed to upload files.'; - break; - case EEPROM.ERR_GONE: - errorText = 'Upload client has a configuration error or the server is gone.'; - break; - case EEPROM.ERR_DMCA: - errorText = 'This file has been uploaded before and was removed for copyright reasons, you cannot upload this file.'; - break; - case EEPROM.ERR_SERVER: - errorText = 'Upload server returned a critical error, try again later.'; - break; - case EEPROM.ERR_SIZE: - if(errorInfo.maxSize < 1) - errorText = 'Selected file is too large.'; - else { - const _t = ['bytes', 'KB', 'MB', 'GB', 'TB'], - _i = parseInt(Math.floor(Math.log(errorInfo.maxSize) / Math.log(1024))), - _s = Math.round(errorInfo.maxSize / Math.pow(1024, _i), 2); - - errorText = 'Upload may not be larger than %1 %2.'.replace('%1', _s).replace('%2', _t[_i]); - } - break; - } - - uploadElem.classList.add('eeprom-widget-file-fail'); - uploadElemProgressText.textContent = errorText; - Misuzu.showMessageBox(errorText, 'Upload Error'); - } - }; - - uploadTask.onComplete = function(fileInfo) { - uploadElem.classList.add('eeprom-widget-file-done'); - uploadElemName.href = fileInfo.url; - uploadElemProgressText.textContent = ''; - - const insertTheLinkIntoTheBoxEx2 = function() { - const parserMode = parseInt(postingParser.value); - let insertText = location.protocol + fileInfo.url; - - if(parserMode == 1) { // bbcode - if(fileInfo.isImage()) - insertText = '[img]' + fileInfo.url + '[/img]'; - else if(fileInfo.isAudio()) - insertText = '[audio]' + fileInfo.url + '[/audio]'; - else if(fileInfo.isVideo()) - insertText = '[video]' + fileInfo.url + '[/video]'; - } else if(parserMode == 2) { // markdown - if(fileInfo.isMedia()) - insertText = '![](' + fileInfo.url + ')'; - } - - $insertTags(postingText, insertText, ''); - postingText.value = postingText.value.trim(); - }; - - uploadElemProgressText.appendChild($e({ - tag: 'a', - attrs: { - href: 'javascript:void(0);', - onclick: function() { insertTheLinkIntoTheBoxEx2(); }, - }, - child: 'Insert', - })); - uploadElemProgressText.appendChild($t(' ')); - uploadElemProgressText.appendChild($e({ - tag: 'a', - attrs: { - href: 'javascript:void(0);', - onclick: function() { - eepromClient.deleteUpload(fileInfo).start(); - explodeUploadElem(); - }, - }, - child: 'Delete', - })); - - insertTheLinkIntoTheBoxEx2(); - }; - - uploadTask.start(); - }; - - const eepromFormInput = $e({ - tag: 'input', - attrs: { - type: 'file', - multiple: 'multiple', - className: 'eeprom-widget-form-input', - onchange: function(ev) { - const files = this.files; - for(const file of files) - eepromHandleFileUpload(file); - this.value = ''; - }, - }, - }); - - const eepromForm = $e({ - tag: 'label', - attrs: { - className: 'eeprom-widget-form', - }, - child: [ - eepromFormInput, - { - attrs: { - className: 'eeprom-widget-form-text', - }, - child: 'Select Files...', - } - ], - }); - - const eepromWidget = $e({ - attrs: { - className: 'eeprom-widget', - }, - child: [ - eepromForm, - { - attrs: { - className: 'eeprom-widget-history', - }, - child: eepromHistory, - }, - ], - }); - postingForm.appendChild(eepromWidget); - - postingText.addEventListener('paste', function(ev) { - if(ev.clipboardData && ev.clipboardData.files.length > 0) { - ev.preventDefault(); - - const files = ev.clipboardData.files; - for(const file of files) - eepromHandleFileUpload(file); - } - }); - - document.body.addEventListener('dragenter', function(ev) { - ev.preventDefault(); - ev.stopPropagation(); - }); - document.body.addEventListener('dragover', function(ev) { - ev.preventDefault(); - ev.stopPropagation(); - }); - document.body.addEventListener('dragleave', function(ev) { - ev.preventDefault(); - ev.stopPropagation(); - }); - document.body.addEventListener('drop', function(ev) { - ev.preventDefault(); - ev.stopPropagation(); - if(ev.dataTransfer && ev.dataTransfer.files.length > 0) { - const files = ev.dataTransfer.files; - for(const file of files) - eepromHandleFileUpload(file); - } - }); - }, - onerror: function(ev) { - console.error('Failed to initialise EEPROM: ', ev); - }, - }, - })); - - // hack: don't prompt user when hitting submit, really need to make this not stupid. - postingButtons.firstElementChild.addEventListener('click', function() { - Misuzu.Forum.Editor.allowWindowClose = true; - }); - - window.addEventListener('beforeunload', function(ev) { - if(!Misuzu.Forum.Editor.allowWindowClose && postingText.value.length > 0) { - ev.preventDefault(); - ev.returnValue = ''; - } - }); - - for(var i = 0; i < markupButtons.length; ++i) - (function(currentBtn) { - currentBtn.addEventListener('click', function(ev) { - $insertTags(postingText, currentBtn.dataset.tagOpen, currentBtn.dataset.tagClose); - }); - })(markupButtons[i]); - - Misuzu.Forum.Editor.switchButtons(parseInt(postingParser.value)); - - var lastPostText = '', - lastPostParser = null; - - postingParser.addEventListener('change', function() { - var postParser = parseInt(postingParser.value); - Misuzu.Forum.Editor.switchButtons(postParser); - - if(postingPreview.hasAttribute('hidden')) - return; - - // dunno if this would even be possible, but ech - if(postParser === lastPostParser) - return; - - postingParser.setAttribute('disabled', 'disabled'); - previewButton.setAttribute('disabled', 'disabled'); - previewButton.classList.add('input__button--busy'); - - Misuzu.Forum.Editor.renderPreview(postParser, lastPostText, function(success, text) { - if(!success) { - Misuzu.showMessageBox(text); - return; - } - - postingPreview.classList[postParser == 2 ? 'add' : 'remove']('markdown'); - - lastPostParser = postParser; - postingPreview.innerHTML = text; - MszEmbed.handle($qa('.js-msz-embed-media')); - - previewButton.removeAttribute('disabled'); - postingParser.removeAttribute('disabled'); - previewButton.classList.remove('input__button--busy'); - }); - }); - - previewButton.className = 'input__button'; - previewButton.textContent = 'Preview'; - previewButton.type = 'button'; - previewButton.value = 'preview'; - previewButton.addEventListener('click', function() { - if(previewButton.value === 'back') { - postingPreview.setAttribute('hidden', 'hidden'); - postingText.removeAttribute('hidden'); - previewButton.value = 'preview'; - previewButton.textContent = 'Preview'; - postingMode.textContent = postingMode.dataset.original; - postingMode.dataset.original = null; - } else { - var postText = postingText.value, - postParser = parseInt(postingParser.value); - - if(lastPostText === postText && lastPostParser === postParser) { - postingPreview.removeAttribute('hidden'); - postingText.setAttribute('hidden', 'hidden'); - previewButton.value = 'back'; - previewButton.textContent = 'Edit'; - postingMode.dataset.original = postingMode.textContent; - postingMode.textContent = 'Previewing'; - return; - } - - postingParser.setAttribute('disabled', 'disabled'); - previewButton.setAttribute('disabled', 'disabled'); - previewButton.classList.add('input__button--busy'); - - Misuzu.Forum.Editor.renderPreview(postParser, postText, function(success, text) { - if(!success) { - Misuzu.showMessageBox(text); - return; - } - - postingPreview.classList[postParser == 2 ? 'add' : 'remove']('markdown'); - - lastPostText = postText; - lastPostParser = postParser; - postingPreview.innerHTML = text; - MszEmbed.handle($qa('.js-msz-embed-media')); - - postingPreview.removeAttribute('hidden'); - postingText.setAttribute('hidden', 'hidden'); - previewButton.value = 'back'; - previewButton.textContent = 'Back'; - previewButton.removeAttribute('disabled'); - postingParser.removeAttribute('disabled'); - previewButton.classList.remove('input__button--busy'); - postingMode.dataset.original = postingMode.textContent; - postingMode.textContent = 'Previewing'; - }); - } - }); - - postingButtons.insertBefore(previewButton, postingButtons.firstChild); -}; -Misuzu.Forum.Editor.switchButtons = function(parser) { - var bbcodeButtons = $q('.forum__post__actions--bbcode'), - markdownButtons = $q('.forum__post__actions--markdown'); - - bbcodeButtons.hidden = parser != 1; - markdownButtons.hidden = parser != 2; -}; -Misuzu.Forum.Editor.renderPreview = function(parser, text, callback) { - if(!callback) - return; - parser = parseInt(parser); - text = text || ''; - - var xhr = new XMLHttpRequest, - formData = new FormData; - - formData.append('post[mode]', 'preview'); - formData.append('post[text]', text); - formData.append('post[parser]', parser.toString()); - - xhr.addEventListener('readystatechange', function() { - if(xhr.readyState !== XMLHttpRequest.DONE) - return; - if(xhr.status === 200) - callback(true, xhr.response); - else - callback(false, 'Failed to render preview.'); - }); - // need to figure out a url registry system again, current one is too much overhead so lets just do this for now - xhr.open('POST', '/forum/posting.php'); - xhr.withCredentials = true; - xhr.send(formData); -}; diff --git a/assets/misuzu.js/forum/editor.jsx b/assets/misuzu.js/forum/editor.jsx new file mode 100644 index 0000000..aebd89b --- /dev/null +++ b/assets/misuzu.js/forum/editor.jsx @@ -0,0 +1,319 @@ +#include msgbox.jsx +#include utility.js +#include ext/eeprom.js + +let MszForumEditorAllowClose = false; + +const MszForumEditor = function(form) { + if(!(form instanceof Element)) + throw 'form must be an instance of element'; + + const buttonsElem = form.querySelector('.js-forum-posting-buttons'), + textElem = form.querySelector('.js-forum-posting-text'), + parserElem = form.querySelector('.js-forum-posting-parser'), + previewElem = form.querySelector('.js-forum-posting-preview'), + modeElem = form.querySelector('.js-forum-posting-mode'), + markupBtns = form.querySelectorAll('.js-forum-posting-markup'); + + const bbBtns = $q('.forum__post__actions--bbcode'), + mdBtns = $q('.forum__post__actions--markdown'); + + let lastPostText = '', + lastPostParser; + + MszEEPROM.init() + .catch(() => console.error('Failed to initialise EEPROM')) + .then(() => { + const eepromClient = new EEPROM(peepApp, `${peepPath}/uploads`, ''); + const eepromHistory =
; + + const eepromHandleFileUpload = file => { + const uploadElemNameValue =
{file.name}
; + const uploadElemName = {uploadElemNameValue}; + const uploadElemProgressText =
Please wait...
; + const uploadElemProgressBarValue =
; + const uploadElem =
+
+ {uploadElemName} + {uploadElemProgressText} +
+
+ {uploadElemProgressBarValue} +
+
; + + if(eepromHistory.children.length > 0) + $ib(eepromHistory.firstChild, uploadElem); + else + eepromHistory.appendChild(uploadElem); + + const explodeUploadElem = () => $r(uploadElem); + const uploadTask = eepromClient.createUpload(file); + + uploadTask.onProgress = function(progressInfo) { + const progressValue = `${progressInfo.progress}%`; + uploadElemProgressBarValue.style.width = progressValue; + uploadElemProgressText.textContent = `${progressValue} (${progressInfo.total - progressInfo.loaded} bytes remaining)`; + }; + + uploadTask.onFailure = function(errorInfo) { + if(!errorInfo.userAborted) { + let errorText = 'Was unable to upload file.'; + + switch(errorInfo.error) { + case EEPROM.ERR_INVALID: + errorText = 'Upload request was invalid.'; + break; + case EEPROM.ERR_AUTH: + errorText = 'Upload authentication failed, refresh and try again.'; + break; + case EEPROM.ERR_ACCESS: + errorText = "You're not allowed to upload files."; + break; + case EEPROM.ERR_GONE: + errorText = 'Upload client has a configuration error or the server is gone.'; + break; + case EEPROM.ERR_DMCA: + errorText = 'This file has been uploaded before and was removed for copyright reasons, you cannot upload this file.'; + break; + case EEPROM.ERR_SERVER: + errorText = 'Upload server returned a critical error, try again later.'; + break; + case EEPROM.ERR_SIZE: + if(errorInfo.maxSize < 1) + errorText = 'Selected file is too large.'; + else { + const types = ['bytes', 'KB', 'MB', 'GB', 'TB'], + typeIndex = parseInt(Math.floor(Math.log(errorInfo.maxSize) / Math.log(1024))), + number = Math.round(errorInfo.maxSize / Math.pow(1024, _i), 2); + + errorText = `Upload may not be larger than ${number} ${types[typeIndex]}.`; + } + break; + } + + uploadElem.classList.add('eeprom-widget-file-fail'); + uploadElemProgressText.textContent = errorText; + MszShowMessageBox(errorText, 'Upload Error'); + } + }; + + uploadTask.onComplete = function(fileInfo) { + uploadElem.classList.add('eeprom-widget-file-done'); + uploadElemName.href = fileInfo.url; + uploadElemProgressText.textContent = ''; + + const insertTheLinkIntoTheBoxEx2 = function() { + const parserMode = parseInt(parserElem.value); + let insertText = location.protocol + fileInfo.url; + + if(parserMode == 1) { // bbcode + if(fileInfo.isImage()) + insertText = `[img]${fileInfo.url}[/img]`; + else if(fileInfo.isAudio()) + insertText = `[audio]${fileInfo.url}[/audio]`; + else if(fileInfo.isVideo()) + insertText = `[video]${fileInfo.url}[/video]`; + } else if(parserMode == 2) { // markdown + if(fileInfo.isMedia()) + insertText = `![](${fileInfo.url})`; + } + + $insertTags(textElem, insertText, ''); + textElem.value = textElem.value.trim(); + }; + + uploadElemProgressText.appendChild( insertTheLinkIntoTheBoxEx2()}>Insert); + uploadElemProgressText.appendChild($t(' ')); + uploadElemProgressText.appendChild( { + eepromClient.deleteUpload(fileInfo).start(); + explodeUploadElem(); + }}>Delete); + + insertTheLinkIntoTheBoxEx2(); + }; + + uploadTask.start(); + }; + + const eepromFormInput = { + const files = eepromFormInput.files; + for(const file of files) + eepromHandleFileUpload(file); + eepromFormInput.value = ''; + }}/>; + + const eepromForm = ; + + const eepromWidget =
+ {eepromForm} +
+ {eepromHistory} +
+
; + + form.appendChild(eepromWidget); + + textElem.addEventListener('paste', ev => { + if(ev.clipboardData && ev.clipboardData.files.length > 0) { + ev.preventDefault(); + + const files = ev.clipboardData.files; + for(const file of files) + eepromHandleFileUpload(file); + } + }); + + document.body.addEventListener('dragenter', ev => { + ev.preventDefault(); + ev.stopPropagation(); + }); + document.body.addEventListener('dragover', ev => { + ev.preventDefault(); + ev.stopPropagation(); + }); + document.body.addEventListener('dragleave', ev => { + ev.preventDefault(); + ev.stopPropagation(); + }); + document.body.addEventListener('drop', ev => { + ev.preventDefault(); + ev.stopPropagation(); + + if(ev.dataTransfer && ev.dataTransfer.files.length > 0) { + const files = ev.dataTransfer.files; + for(const file of files) + eepromHandleFileUpload(file); + } + }); + }); + + // hack: don't prompt user when hitting submit, really need to make this not stupid. + buttonsElem.firstChild.addEventListener('click', () => MszForumEditorAllowClose = true); + + window.addEventListener('beforeunload', function(ev) { + if(!MszForumEditorAllowClose && textElem.value.length > 0) { + ev.preventDefault(); + ev.returnValue = ''; + } + }); + + for(const button of markupBtns) + button.addEventListener('click', () => $insertTags(textElem, button.dataset.tagOpen, button.dataset.tagClose)); + + const switchButtons = parser => { + parser = parseInt(parser); + bbBtns.hidden = parser !== 1; + mdBtns.hidden = parser !== 2; + }; + + const renderPreview = async (parser, text) => { + if(typeof text !== 'string') + return ''; + + const formData = new FormData; + formData.append('post[mode]', 'preview'); + formData.append('post[text]', text); + formData.append('post[parser]', parseInt(parser)); + + const result = await $x.post('/forum/posting.php', { authed: true }, formData); + + return result.body(); + }; + + const previewBtn = ; + previewBtn.addEventListener('click', function() { + if(previewBtn.value === 'back') { + previewElem.setAttribute('hidden', 'hidden'); + textElem.removeAttribute('hidden'); + previewBtn.value = 'preview'; + previewBtn.textContent = 'Preview'; + modeElem.textContent = modeElem.dataset.original; + modeElem.dataset.original = null; + } else { + const postText = textElem.value, + postParser = parseInt(parserElem.value); + + if(lastPostText === postText && lastPostParser === postParser) { + previewElem.removeAttribute('hidden'); + textElem.setAttribute('hidden', 'hidden'); + previewBtn.value = 'back'; + previewBtn.textContent = 'Edit'; + modeElem.dataset.original = modeElem.textContent; + modeElem.textContent = 'Previewing'; + return; + } + + parserElem.setAttribute('disabled', 'disabled'); + previewBtn.setAttribute('disabled', 'disabled'); + previewBtn.classList.add('input__button--busy'); + + renderPreview(postParser, postText) + .catch(() => { + previewElem.innerHTML = ''; + MszShowMessageBox('Failed to render preview.'); + }) + .then(body => { + previewElem.classList.toggle('markdown', postParser === 2); + + lastPostText = postText; + lastPostParser = postParser; + previewElem.innerHTML = body; + + MszEmbed.handle($qa('.js-msz-embed-media')); + + previewElem.removeAttribute('hidden'); + textElem.setAttribute('hidden', 'hidden'); + previewBtn.value = 'back'; + previewBtn.textContent = 'Back'; + previewBtn.removeAttribute('disabled'); + parserElem.removeAttribute('disabled'); + previewBtn.classList.remove('input__button--busy'); + modeElem.dataset.original = modeElem.textContent; + modeElem.textContent = 'Previewing'; + }); + } + }); + buttonsElem.insertBefore(previewBtn, buttonsElem.firstChild); + + switchButtons(parserElem.value); + + parserElem.addEventListener('change', () => { + const postParser = parseInt(parserElem.value); + switchButtons(postParser); + + if(previewElem.hasAttribute('hidden')) + return; + + // dunno if this would even be possible, but ech + if(postParser === lastPostParser) + return; + + parserElem.setAttribute('disabled', 'disabled'); + previewBtn.setAttribute('disabled', 'disabled'); + previewBtn.classList.add('input__button--busy'); + + renderPreview(postParser, lastPostText) + .catch(() => { + previewElem.innerHTML = ''; + MszShowMessageBox('Failed to render preview.'); + }) + .then(body => { + previewElem.classList.add('markdown', postParser === 2); + lastPostParser = postParser; + previewElem.innerHTML = body; + + MszEmbed.handle($qa('.js-msz-embed-media')); + + previewBtn.removeAttribute('disabled'); + parserElem.removeAttribute('disabled'); + previewBtn.classList.remove('input__button--busy'); + }); + }); +}; diff --git a/assets/misuzu.js/forum/forum.js b/assets/misuzu.js/forum/forum.js deleted file mode 100644 index 52d6f67..0000000 --- a/assets/misuzu.js/forum/forum.js +++ /dev/null @@ -1 +0,0 @@ -Misuzu.Forum = {}; diff --git a/assets/misuzu.js/main.js b/assets/misuzu.js/main.js index d3888eb..8c31453 100644 --- a/assets/misuzu.js/main.js +++ b/assets/misuzu.js/main.js @@ -1,135 +1,81 @@ -#include sakuya.js - -var Misuzu = function() { - Sakuya.trackElements($qa('time')); - hljs.initHighlighting(); - - MszEmbed.init(location.protocol + '//uiharu.' + location.host); - - Misuzu.initQuickSubmit(); // only used by the forum posting form - Misuzu.Forum.Editor.init(); - Misuzu.Events.dispatch(); - Misuzu.initLoginPage(); - - MszEmbed.handle($qa('.js-msz-embed-media')); -}; - -#include utils.js -#include embed.js -#include forum/editor.js +#include utility.js +#include embed/embed.js +#include events/christmas2019.js #include events/events.js +#include ext/sakuya.js +#include forum/editor.jsx -Misuzu.showMessageBox = function(text, title, buttons) { - if($q('.messagebox')) - return false; +(async () => { + const initLoginPage = async () => { + const forms = Array.from($qa('.js-login-form')); + if(forms.length < 1) + return; - text = text || ''; - title = title || ''; - buttons = buttons || []; - - var element = document.createElement('div'); - element.className = 'messagebox'; - - var container = element.appendChild(document.createElement('div')); - container.className = 'container messagebox__container'; - - var titleElement = container.appendChild(document.createElement('div')), - titleBackground = titleElement.appendChild(document.createElement('div')), - titleText = titleElement.appendChild(document.createElement('div')); - - titleElement.className = 'container__title'; - titleBackground.className = 'container__title__background'; - titleText.className = 'container__title__text'; - titleText.textContent = title || 'Information'; - - var textElement = container.appendChild(document.createElement('div')); - textElement.className = 'container__content'; - textElement.textContent = text; - - var buttonsContainer = container.appendChild(document.createElement('div')); - buttonsContainer.className = 'messagebox__buttons'; - - var firstButton = null; - - if(buttons.length < 1) { - firstButton = buttonsContainer.appendChild(document.createElement('button')); - firstButton.className = 'input__button'; - firstButton.textContent = 'OK'; - firstButton.addEventListener('click', function() { element.remove(); }); - } else { - for(var i = 0; i < buttons.length; i++) { - var button = buttonsContainer.appendChild(document.createElement('button')); - button.className = 'input__button'; - button.textContent = buttons[i].text; - button.addEventListener('click', function() { - element.remove(); - buttons[i].callback(); - }); - - if(firstButton === null) - firstButton = button; - } - } - - document.body.appendChild(element); - firstButton.focus(); - return true; -}; -Misuzu.initLoginPage = function() { - var updateForm = function(avatarElem, usernameElem) { - var xhr = new XMLHttpRequest; - xhr.addEventListener('readystatechange', function() { - if(xhr.readyState !== 4) + const updateForm = async (avatar, userName) => { + if(!(avatar instanceof Element) || !(userName instanceof Element)) return; - var json = JSON.parse(xhr.responseText); - if(!json) - return; + const result = (await $x.get(`/auth/login.php?resolve=1&name=${encodeURIComponent(userName.value)}`, { type: 'json' })).body(); - if(json.name) - usernameElem.value = json.name; - avatarElem.src = json.avatar; - }); - // need to figure out a url registry system again, current one is too much overhead so lets just do this for now - xhr.open('GET', '/auth/login.php?resolve=1&name=' + encodeURIComponent(usernameElem.value)); - xhr.send(); - }; + avatar.src = result.avatar; + if(result.name.length > 0) + userName.value = result.name; + }; - var loginForms = $c('js-login-form'); + for(const form of forms) { + const avatar = form.querySelector('.js-login-avatar'); + const userName = form.querySelector('.js-login-username'); + let timeOut; - for(var i = 0; i < loginForms.length; ++i) - (function(form) { - var loginTimeOut = 0, - loginAvatar = form.querySelector('.js-login-avatar'), - loginUsername = form.querySelector('.js-login-username'); + await updateForm(avatar, userName); - updateForm(loginAvatar, loginUsername); - loginUsername.addEventListener('keyup', function() { - if(loginTimeOut) + userName.addEventListener('input', function() { + if(timeOut !== undefined) return; - loginTimeOut = setTimeout(function() { - updateForm(loginAvatar, loginUsername); - clearTimeout(loginTimeOut); - loginTimeOut = 0; + + timeOut = setTimeout(() => { + updateForm(avatar, userName) + .finally(() => { + clearTimeout(timeOut); + timeOut = undefined; + }); }, 750); }); - })(loginForms[i]); -}; -Misuzu.initQuickSubmit = function() { - var ctrlSubmit = Array.from($qa('.js-quick-submit, .js-ctrl-enter-submit')); - if(!ctrlSubmit) - return; + } + }; - for(var i = 0; i < ctrlSubmit.length; ++i) - ctrlSubmit[i].addEventListener('keydown', function(ev) { - if((ev.code === 'Enter' || ev.code === 'NumpadEnter') // i hate this fucking language so much - && ev.ctrlKey && !ev.altKey && !ev.shiftKey && !ev.metaKey) { - // hack: prevent forum editor from screaming when using this keycombo - // can probably be done in a less stupid manner - Misuzu.Forum.Editor.allowWindowClose = true; + const initQuickSubmit = () => { + const elems = Array.from($qa('.js-quick-submit, .js-ctrl-enter-submit')); + if(elems.length < 1) + return; - this.form.submit(); - ev.preventDefault(); - } - }); -}; + for(const elem of elems) + elem.addEventListener('keydown', ev => { + if((ev.code === 'Enter' || ev.code === 'NumpadEnter') && ev.ctrlKey && !ev.altKey && !ev.shiftKey && !ev.metaKey) { + // hack: prevent forum editor from screaming when using this keycombo + // can probably be done in a less stupid manner + MszForumEditorAllowClose = true; + + elem.submit(); + ev.preventDefault(); + } + }); + }; + + MszSakuya.trackElements($qa('time')); + hljs.highlightAll(); + + MszEmbed.init(`${location.protocol}//uiharu.${location.host}`); + + // only used by the forum posting form + initQuickSubmit(); + MszForumEditor($q('.js-forum-posting')); + + const events = new MszSeasonalEvents; + events.add(new MszChristmas2019EventInfo); + events.dispatch(); + + await initLoginPage(); + + MszEmbed.handle($qa('.js-msz-embed-media')); +})(); diff --git a/assets/misuzu.js/msgbox.jsx b/assets/misuzu.js/msgbox.jsx new file mode 100644 index 0000000..ba7e9e9 --- /dev/null +++ b/assets/misuzu.js/msgbox.jsx @@ -0,0 +1,49 @@ +#include utility.js + +const MszShowMessageBox = async (text, title, buttons, target) => { + if(typeof text !== 'string') + throw 'text must be a string'; + if(!(target instanceof Element)) + target = document.body; + + if(target.querySelector('.messagebox')) + return false; + + if(typeof title !== 'string') + title = 'Information'; + if(!Array.isArray(buttons)) + buttons = []; + + let buttonsElem; + const html =
+
+
+
+
{title}
+
+
{text}
+ {buttonsElem =
} +
+
; + + let firstButton; + if(buttons.length < 1) { + firstButton = ; + buttonsElem.appendChild(firstButton); + } else { + for(const button of buttons) { + const buttonElem = ; + buttonsElem.appendChild(buttonElem); + + if(firstButton === undefined) + firstButton = buttonElem; + } + } + + target.appendChild(html); + firstButton.focus(); + + return true; +}; diff --git a/assets/misuzu.js/uiharu.js b/assets/misuzu.js/uiharu.js deleted file mode 100644 index 6ff501d..0000000 --- a/assets/misuzu.js/uiharu.js +++ /dev/null @@ -1,58 +0,0 @@ -const Uiharu = function(apiUrl) { - const maxBatchSize = 4; - const lookupOneUrl = apiUrl + '/metadata', - lookupManyUrl = apiUrl + '/metadata/batch'; - - const lookupManyInternal = function(targetUrls, callback) { - const formData = new FormData; - - for(const url of targetUrls) - formData.append('url[]', url); - - const xhr = new XMLHttpRequest; - xhr.addEventListener('load', function() { - callback(JSON.parse(xhr.responseText)); - }); - xhr.addEventListener('error', function(ev) { - callback({ status: xhr.status, error: 'xhr', details: ev }); - }); - xhr.open('POST', lookupManyUrl); - xhr.send(formData); - }; - - return { - lookupOne: function(targetUrl, callback) { - if(typeof callback !== 'function') - throw 'callback is missing'; - targetUrl = (targetUrl || '').toString(); - if(targetUrl.length < 1) - return; - - const xhr = new XMLHttpRequest; - xhr.addEventListener('load', function() { - callback(JSON.parse(xhr.responseText)); - }); - xhr.addEventListener('error', function() { - callback({ status: xhr.status, error: 'xhr', details: ex }); - }); - xhr.open('POST', lookupOneUrl); - xhr.send(targetUrl); - }, - lookupMany: function(targetUrls, callback) { - if(!Array.isArray(targetUrls)) - throw 'targetUrls must be an array of urls'; - if(typeof callback !== 'function') - throw 'callback is missing'; - if(targetUrls < 1) - return; - - if(targetUrls.length <= maxBatchSize) { - lookupManyInternal(targetUrls, callback); - return; - } - - for(let i = 0; i < targetUrls.length; i += maxBatchSize) - lookupManyInternal(targetUrls.slice(i, i + maxBatchSize), callback); - }, - }; -}; diff --git a/assets/misuzu.js/rng.js b/assets/misuzu.js/uniqstr.js similarity index 100% rename from assets/misuzu.js/rng.js rename to assets/misuzu.js/uniqstr.js diff --git a/assets/misuzu.js/utils.js b/assets/misuzu.js/utility.js similarity index 55% rename from assets/misuzu.js/utils.js rename to assets/misuzu.js/utility.js index 80ab28b..71c0037 100644 --- a/assets/misuzu.js/utils.js +++ b/assets/misuzu.js/utility.js @@ -13,6 +13,10 @@ const $ri = function(name) { $r($i(name)); }; +const $rq = function(query) { + $r($q(query)); +}; + const $ib = function(ref, elem) { ref.parentNode.insertBefore(elem, ref); }; @@ -158,17 +162,125 @@ const $as = function(array) { } }; -var $insertTags = function(target, tagOpen, tagClose) { +const $x = (function() { + const send = function(method, url, options, body) { + if(options === undefined) + options = {}; + else if(typeof options !== 'object') + throw 'options must be undefined or an object'; + + const xhr = new XMLHttpRequest; + const requestHeaders = new Map; + + if('headers' in options && typeof options.headers === 'object') + for(const name in options.headers) + if(options.headers.hasOwnProperty(name)) + requestHeaders.set(name.toLowerCase(), options.headers[name]); + + if(typeof options.download === 'function') { + xhr.onloadstart = ev => options.download(ev); + xhr.onprogress = ev => options.download(ev); + xhr.onloadend = ev => options.download(ev); + } + + if(typeof options.upload === 'function') { + xhr.upload.onloadstart = ev => options.upload(ev); + xhr.upload.onprogress = ev => options.upload(ev); + xhr.upload.onloadend = ev => options.upload(ev); + } + + if(options.authed) + xhr.withCredentials = true; + + if(typeof options.timeout === 'number') + xhr.timeout = options.timeout; + + if(typeof options.type === 'string') + xhr.responseType = options.type; + + if(typeof options.abort === 'function') + options.abort(() => xhr.abort()); + + if(typeof options.xhr === 'function') + options.xhr(() => xhr); + + if(typeof body === 'object') { + if(body instanceof URLSearchParams) { + requestHeaders.set('content-type', 'application/x-www-form-urlencoded'); + } else if(body instanceof FormData) { + // content-type is implicitly set + } else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) { + if(!requestHeaders.has('content-type')) + requestHeaders.set('content-type', 'application/octet-stream'); + } else if(!requestHeaders.has('content-type')) { + const bodyParts = []; + for(const name in body) + if(body.hasOwnProperty(name)) + bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name])); + body = bodyParts.join('&'); + requestHeaders.set('content-type', 'application/x-www-form-urlencoded'); + } + } + + return new Promise((resolve, reject) => { + let responseHeaders = undefined; + + xhr.onload = ev => resolve({ + status: xhr.status, + body: () => xhr.response, + text: () => xhr.responseText, + headers: () => { + if(responseHeaders !== undefined) + return responseHeaders; + + responseHeaders = new Map; + + const raw = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/); + for(const name in raw) + if(raw.hasOwnProperty(name)) { + const parts = raw[name].split(': '); + responseHeaders.set(parts.shift(), parts.join(': ')); + } + + return responseHeaders; + }, + xhr: xhr, + ev: ev, + }); + + xhr.onerror = ev => reject({ + xhr: xhr, + ev: ev, + }); + + xhr.open(method, url); + for(const [name, value] of requestHeaders) + xhr.setRequestHeader(name, value); + xhr.send(body); + }); + }; + + return { + send: send, + get: (url, options, body) => send('GET', url, options, body), + post: (url, options, body) => send('POST', url, options, body), + delete: (url, options, body) => send('DELETE', url, options, body), + patch: (url, options, body) => send('PATCH', url, options, body), + put: (url, options, body) => send('PUT', url, options, body), + }; +})(); + +const $insertTags = function(target, tagOpen, tagClose) { tagOpen = tagOpen || ''; tagClose = tagClose || ''; if(document.selection) { target.focus(); - var selected = document.selection.createRange(); + const selected = document.selection.createRange(); selected.text = tagOpen + selected.text + tagClose; target.focus(); } else if(target.selectionStart || target.selectionStart === 0) { - var startPos = target.selectionStart, + const startPos = target.selectionStart, endPos = target.selectionEnd, scrollTop = target.scrollTop; @@ -181,7 +293,7 @@ var $insertTags = function(target, tagOpen, tagClose) { target.focus(); target.selectionStart = startPos + tagOpen.length; target.selectionEnd = endPos + tagOpen.length; - target.scrollTop + scrollTop; + target.scrollTop = scrollTop; } else { target.value += tagOpen + tagClose; target.focus(); diff --git a/assets/misuzu.js/watcher.js b/assets/misuzu.js/watcher.js index be747e4..f541210 100644 --- a/assets/misuzu.js/watcher.js +++ b/assets/misuzu.js/watcher.js @@ -1,83 +1,65 @@ const MszWatcher = function() { - let watchers = []; + const handlers = []; + + const watch = (handler, ...args) => { + if(typeof handler !== 'function') + throw 'handler must be a function'; + if(handlers.includes(handler)) + throw 'handler already registered'; + + handlers.push(handler); + args.push(true); + handler(...args); + }; + + const unwatch = handler => { + $ari(handlers, handler); + }; return { - watch: function(watcher, thisArg, args) { - if(typeof watcher !== 'function') - throw 'watcher must be a function'; - if(watchers.indexOf(watcher) >= 0) - return; - - watchers.push(watcher); - - if(thisArg !== undefined) { - if(!Array.isArray(args)) { - if(args !== undefined) - args = [args]; - else args = []; - } - - // initial call - args.push(true); - - watcher.apply(thisArg, args); - } - }, - unwatch: function(watcher) { - $ari(watchers, watcher); - }, - call: function(thisArg, args) { - if(!Array.isArray(args)) { - if(args !== undefined) - args = [args]; - else args = []; - } - + watch: watch, + unwatch: unwatch, + call: (...args) => { args.push(false); - for(const watcher of watchers) - watcher.apply(thisArg, args); + for(const handler of handlers) + handler(...args); }, }; }; const MszWatcherCollection = function() { - const collection = new Map; + const watchers = new Map; - const watch = function(name, watcher, thisArg, args) { - const watchers = collection.get(name); - if(watchers === undefined) + const getWatcher = name => { + const watcher = watchers.get(name); + if(watcher === undefined) throw 'undefined watcher name'; - watchers.watch(watcher, thisArg, args); + return watcher; }; - const unwatch = function(name, watcher) { - const watchers = collection.get(name); - if(watchers === undefined) - throw 'undefined watcher name'; - watchers.unwatch(watcher); + const watch = (name, handler, ...args) => { + getWatcher(name).watch(handler, ...args); + }; + + const unwatch = (name, handler) => { + getWatcher(name).unwatch(handler); }; return { - define: function(names) { - if(!Array.isArray(names)) - names = [names]; - for(const name of names) - collection.set(name, new MszWatcher); - }, - call: function(name, thisArg, args) { - const watchers = collection.get(name); - if(watchers === undefined) - throw 'undefined watcher name'; - watchers.call(thisArg, args); - }, watch: watch, unwatch: unwatch, - proxy: function(obj) { - obj.watch = function(name, watcher) { - watch(name, watcher); - }; - obj.unwatch = unwatch; + define: names => { + if(typeof names === 'string') + watchers.set(names, new MszWatcher); + else if(Array.isArray(names)) + for(const name of names) + watchers.set(name, new MszWatcher); + else + throw 'names must be an array of names or a single name'; + }, + call: (name, ...args) => { + getWatcher(name).call(...args); }, }; }; diff --git a/templates/forum/posting.twig b/templates/forum/posting.twig index a7c3c0d..de5d3a4 100644 --- a/templates/forum/posting.twig +++ b/templates/forum/posting.twig @@ -79,31 +79,31 @@