Feat: refactor FixItDecryptor and add confirm btn

Resolved #437
This commit is contained in:
Cell
2024-04-24 17:39:17 +08:00
parent 20433ed50d
commit 3b89a4eaab
19 changed files with 197 additions and 114 deletions
@@ -3,12 +3,8 @@
text-align: center;
margin-top: 3rem;
.fixit-encryptor-shortcode & {
margin-top: 1rem;
}
#fixit-decryptor-input,
.fixit-decryptor-input,
.fixit-decryptor-btn,
.fixit-encryptor-btn {
display: inline-block;
box-sizing: border-box;
@@ -40,9 +36,8 @@
}
}
#fixit-decryptor-input,
.fixit-decryptor-input {
width: clamp(50%, 400px, 100%);
width: calc(clamp(50%, 450px, 100%) - 100px);
height: 3rem;
padding-left: 1rem;
padding-right: 1rem;
@@ -53,10 +48,11 @@
}
}
.fixit-decryptor-btn,
.fixit-encryptor-btn {
cursor: pointer;
@include transition(all 0.1s ease-out);
padding: 0.6rem 1rem;
padding: 0.8rem 1rem;
background-color: $header-background-color;
@@ -66,6 +62,20 @@
}
}
// fixit-encryptor shortcodes
fixit-encryptor {
.fixit-decryptor-container {
margin-top: 1rem;
}
&.decrypted > .fixit-decryptor-container {
.fixit-decryptor-loading,
.fixit-decryptor-input,
.fixit-decryptor-btn {
display: none;
}
}
}
.encrypted-hidden {
display: none !important;
}
+110 -83
View File
@@ -12,86 +12,125 @@ FixItDecryptor = function (options = {}) {
this.options = options || {};
this.options.duration = this.options.duration || 24 * 60 * 60; // default cache one day
this.decryptedEventSet = new Set();
this.partialDecryptedEventSet = new Set();
this.resetEventSet = new Set();
this.$el = document.querySelector('.fixit-decryptor-container');
/**
* decrypt content
* @param {String} base64EncodeContent encrypted content
* @param {Element} $content content element
* @param {String} salt salt string
* @param {Boolean} [isAll=true] whether to decrypt all content
*/
var _decryptContent = (base64EncodeContent) => {
var _decryptContent = ($content, salt, isAll=true) => {
try {
this.$el.querySelector('.fixit-decryptor-loading').classList.add('d-none');
this.$el.querySelector('#fixit-decryptor-input').classList.add('d-none');
this.$el.querySelector('.fixit-encryptor-btn').classList.remove('d-none');
document.querySelector('#content').insertAdjacentHTML(
if (isAll) {
// decrypt all content
this.$el.querySelector('.fixit-decryptor-loading').classList.add('d-none');
this.$el.querySelector('.fixit-decryptor-input').classList.add('d-none');
this.$el.querySelector('.fixit-decryptor-btn').classList.add('d-none');
this.$el.querySelector('.fixit-encryptor-btn').classList.remove('d-none');
} else {
// decrypt shortcode content
$content.parentElement.classList.add('decrypted');
}
$content.insertAdjacentHTML(
'afterbegin',
CryptoJS.enc.Base64.parse(base64EncodeContent).toString(CryptoJS.enc.Utf8)
CryptoJS.enc.Base64
.parse($content.getAttribute('data-content').replace(salt, ''))
.toString(CryptoJS.enc.Utf8)
);
} catch (err) {
return console.error(err);
}
// decrypted hook
console.log(this.decryptedEventSet)
for (const event of this.decryptedEventSet) {
event();
}
const eventSet = isAll ? this.decryptedEventSet : this.partialDecryptedEventSet;
for (const event of eventSet) {
event($content);
}
};
/**
* validate password
* @param {Element} $decryptor decryptor element
* @param {Element} $content content element
* @param {Function} callback callback function after password validation
* @returns
*/
var _validatePassword = ($decryptor, $content, callback) => {
const password = $content.getAttribute('data-password');
const inputEl = $decryptor.querySelector('.fixit-decryptor-input');
const input = inputEl.value.trim();
const inputMd5 = CryptoJS.MD5(input).toString();
const inputSha256 = CryptoJS.SHA256(input).toString();
const saltLen = input.length % 2 ? input.length : input.length + 1;
inputEl.value = '';
inputEl.blur();
if (!input) {
alert('Please enter the correct password!');
return console.warn('Please enter the correct password!');
}
if (inputMd5 !== password) {
alert(`Password error: ${input} not the correct password!`);
return console.warn(`Password error: ${input} not the correct password!`);
}
callback(inputMd5, inputSha256.slice(saltLen));
}
/**
* initialize FixIt decryptor
*/
_proto.init = () => {
this.addEventListener('decrypted', this.options?.decrypted);
this.addEventListener('partial-decrypted', this.options?.partialDecrypted);
this.addEventListener('reset', this.options?.reset);
this.validateCache();
const _decryptor = this;
this.$el.querySelector('#fixit-decryptor-input')?.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
const $content = document.querySelector('#content');
const password = $content.getAttribute('data-password');
const input = this.value.trim();
const saltLen = input.length % 2 ? input.length : input.length + 1;
const inputMd5 = CryptoJS.MD5(input).toString();
const inputSha256 = CryptoJS.SHA256(input).toString();
this.value = '';
this.blur();
if (!input) {
alert('Please enter the correct password!');
return console.warn('Please enter the correct password!');
}
if (inputMd5 !== password) {
alert(`Password error: ${input} not the correct password!`);
return console.warn(`Password error: ${input} not the correct password!`);
}
const decryptorHandler = () => {
const $content = document.querySelector('#content');
_validatePassword(this.$el, $content, (password, salt) => {
// cache decryption statistics
window.localStorage?.setItem(
`fixit-decryptor/#${location.pathname}`,
JSON.stringify({
expiration: Math.ceil(Date.now() / 1000) + _decryptor.options.duration,
md5: inputMd5,
sha256: inputSha256.slice(saltLen)
expiration: Math.ceil(Date.now() / 1000) + this.options.duration,
password,
salt,
})
);
_decryptContent($content.getAttribute('data-content').replace(inputSha256.slice(saltLen), ''));
_decryptContent($content, salt);
});
};
// bind decryptor input enter keydown event
this.$el.querySelector('#fixit-decryptor-input')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
decryptorHandler();
}
});
this.$el.querySelector('.fixit-encryptor-btn')?.addEventListener('click', function (e) {
// bind decryptor button click event
this.$el.querySelector('.fixit-decryptor-btn')?.addEventListener('click', (e) => {
e.preventDefault();
this.classList.add('d-none')
_decryptor.$el.querySelector('#fixit-decryptor-input').classList.remove('d-none');
decryptorHandler();
});
// bind encryptor button click event
this.$el.querySelector('.fixit-encryptor-btn')?.addEventListener('click', (e) => {
e.preventDefault();
e.target.classList.add('d-none')
this.$el.querySelector('.fixit-decryptor-input').classList.remove('d-none');
this.$el.querySelector('.fixit-decryptor-btn').classList.remove('d-none');
document.querySelector('#content').innerHTML = '';
document.querySelector('#content').insertAdjacentElement(
'afterbegin',
_decryptor.$el
this.$el
);
window.localStorage?.removeItem(`fixit-decryptor/#${location.pathname}`);
// reset hook
for (const event of _decryptor.resetEventSet) {
for (const event of this.resetEventSet) {
event();
}
});
@@ -101,50 +140,31 @@ FixItDecryptor = function (options = {}) {
* initialize fixit-encryptor shortcodes
*/
_proto.initShortcodes = () => {
// TODO TODO shortcode decrypted event
// this.addEventListener('decrypted', this.options?.decrypted);
const _decryptor = this;
const $shortcodes = document.querySelectorAll('fixit-encryptor:not(.decrypted)');
customElements.get('fixit-encryptor') || customElements.define('fixit-encryptor', class extends HTMLElement {});
const $shortcodes = document.querySelectorAll('fixit-encryptor:not(:has(.decrypted))');
$shortcodes.forEach($shortcode => {
const decryptorHandler = () => {
const $decryptor = $shortcode.querySelector('.fixit-decryptor-container');
const $content = $shortcode.querySelector('[data-password][data-content]');
_validatePassword($decryptor, $content, (password, salt) => {
_decryptContent($content, salt, false);
});
};
// bind decryptor input enter keydown event
$shortcode.querySelector('.fixit-decryptor-input')?.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
const $decryptor = this.parentElement.parentElement;
const $content = $decryptor.nextElementSibling;
const password = $content.getAttribute('data-password');
const input = this.value.trim();
const saltLen = input.length % 2 ? input.length : input.length + 1;
const inputMd5 = CryptoJS.MD5(input).toString();
const inputSha256 = CryptoJS.SHA256(input).toString();
this.value = '';
this.blur();
if (!input) {
alert('Please enter the correct password!');
return console.warn('Please enter the correct password!');
}
if (inputMd5 !== password) {
alert(`Password error: ${input} not the correct password!`);
return console.warn(`Password error: ${input} not the correct password!`);
}
try {
const base64EncodeContent = $content.getAttribute('data-content').replace(inputSha256.slice(saltLen), '');
$decryptor.querySelector('.fixit-decryptor-input').classList.add('d-none');
$content.insertAdjacentHTML(
'afterbegin',
CryptoJS.enc.Base64.parse(base64EncodeContent).toString(CryptoJS.enc.Utf8)
);
$decryptor.parentElement.classList.add('decrypted');
} catch (err) {
return console.error(err);
}
// TODO shortcode decrypted hook
// for (const event of _decryptor.decryptedEventSet) {
// event();
// }
decryptorHandler();
}
});
// bind decryptor button click event
$shortcode.querySelector('.fixit-decryptor-btn')?.addEventListener('click', function (e) {
e.preventDefault();
decryptorHandler();
});
});
};
@@ -159,17 +179,18 @@ FixItDecryptor = function (options = {}) {
if (!cachedStat) {
this.$el.querySelector('.fixit-decryptor-loading').classList.add('d-none');
this.$el.querySelector('#fixit-decryptor-input').classList.remove('d-none');
this.$el.querySelector('.fixit-decryptor-input').classList.remove('d-none');
this.$el.querySelector('.fixit-decryptor-btn').classList.remove('d-none');
return this;
}
if (cachedStat?.md5 !== password || Number(cachedStat?.expiration) < Math.ceil(Date.now() / 1000)) {
if (cachedStat?.password !== password || Number(cachedStat?.expiration) < Math.ceil(Date.now() / 1000)) {
this.$el.querySelector('.fixit-decryptor-loading').classList.add('d-none');
this.$el.querySelector('#fixit-decryptor-input').classList.remove('d-none');
this.$el.querySelector('.fixit-decryptor-input').classList.remove('d-none');
window.localStorage?.removeItem(`fixit-decryptor/#${location.pathname}`);
console.warn('The password has expired, please re-enter!');
return this;
}
_decryptContent($content.getAttribute('data-content').replace(cachedStat.sha256, ''));
_decryptContent($content, cachedStat.salt);
return this;
};
@@ -187,6 +208,9 @@ FixItDecryptor = function (options = {}) {
case 'decrypted':
this.decryptedEventSet.add(listener);
break;
case 'partial-decrypted':
this.partialDecryptedEventSet.add(listener);
break;
case 'reset':
this.resetEventSet.add(listener);
break;
@@ -211,6 +235,9 @@ FixItDecryptor = function (options = {}) {
case 'decrypted':
this.decryptedEventSet.delete(listener);
break;
case 'partial-decrypted':
this.partialDecryptedEventSet.delete(listener);
break;
case 'reset':
this.resetEventSet.delete(listener);
break;
+48 -22
View File
@@ -48,8 +48,8 @@ class FixIt {
});
}
initTwemoji() {
this.config.twemoji && twemoji.parse(document.body);
initTwemoji(target = document.body) {
this.config.twemoji && twemoji.parse(target);
}
initMenu() {
@@ -93,7 +93,7 @@ class FixIt {
this.isDark = !this.isDark;
window.localStorage?.setItem('theme', this.isDark ? 'dark' : 'light');
for (let event of this.switchThemeEventSet) {
event();
event(this.isDark);
}
}, false);
});
@@ -333,8 +333,8 @@ class FixIt {
}
}
initDetails() {
this.util.forEach(document.getElementsByClassName('details'), ($details) => {
initDetails(target = document) {
this.util.forEach(target.getElementsByClassName('details'), ($details) => {
const $summary = $details.querySelector('.details-summary');
$summary.addEventListener('click', () => {
$details.classList.toggle('open');
@@ -344,7 +344,8 @@ class FixIt {
initLightGallery() {
if (this.config.lightgallery) {
lightGallery(document.getElementById('content'), {
this.lg && this.lg.destroy(true);
this.lg = lightGallery(document.getElementById('content'), {
plugins: [lgThumbnail, lgZoom],
selector: '.lightgallery',
speed: 400,
@@ -376,7 +377,7 @@ class FixIt {
$preChroma.parentElement.replaceChild($chroma, $preChroma);
$td.appendChild($preChroma);
});
this.util.forEach(document.querySelectorAll('.highlight > .chroma'), ($chroma) => {
this.util.forEach(document.querySelectorAll('.highlight > .chroma:not(:has(.code-header))'), ($chroma) => {
const $codeElements = $chroma.querySelectorAll('pre.chroma > code');
if ($codeElements.length) {
const $code = $codeElements[$codeElements.length - 1];
@@ -455,8 +456,8 @@ class FixIt {
});
}
initTable() {
this.util.forEach(document.querySelectorAll('.content table'), ($table) => {
initTable(target = document) {
this.util.forEach(target.querySelectorAll('.content table'), ($table) => {
const $wrapper = document.createElement('div');
$wrapper.className = 'table-wrapper';
$table.parentElement.replaceChild($wrapper, $table);
@@ -548,9 +549,9 @@ class FixIt {
}, false);
}
initMath() {
initMath(target = document.body) {
if (this.config.math) {
renderMathInElement(document.body, this.config.math);
renderMathInElement(target, this.config.math);
}
}
@@ -561,11 +562,13 @@ class FixIt {
const themes = window.mermaid.themes ?? ['default', 'dark'];
window.mermaid.initialize({
securityLevel: 'loose',
startOnLoad: true,
startOnLoad: false,
theme: this.isDark ? themes[1] : themes[0],
});
this.switchThemeEventSet.add(() => {
window.mermaid.run()
this.switchThemeEventSet.add((isDark) => {
// Mermaid does not provide a method for switching yet. When switching themes, refresh the page.
// TODO save the original mermaid pre and reload the mermaid render
window.location.reload();
});
this.beforeprintEventSet.add(() => {
@@ -579,6 +582,7 @@ class FixIt {
}
echarts.registerTheme('light', this.config.echarts.lightTheme);
echarts.registerTheme('dark', this.config.echarts.darkTheme);
// BUG 主题切换的时候有问题
this._echartsOnSwitchTheme = this._echartsOnSwitchTheme || (() => {
this._echartsArr = this._echartsArr || [];
for (let i = 0; i < this._echartsArr.length; i++) {
@@ -606,10 +610,13 @@ class FixIt {
initMapbox() {
if (this.config.mapbox) {
mapboxgl.accessToken = this.config.mapbox.accessToken;
mapboxgl.setRTLTextPlugin(this.config.mapbox.RTLTextPlugin);
this._mapboxArr = this._mapboxArr || [];
this.util.forEach(document.getElementsByClassName('mapbox'), ($mapbox) => {
if (!mapboxgl.accessToken) {
mapboxgl.accessToken = this.config.mapbox.accessToken;
mapboxgl.setRTLTextPlugin(this.config.mapbox.RTLTextPlugin);
this._mapboxArr = this._mapboxArr || [];
}
// BUG 切换主题时有问题
this.util.forEach(document.querySelectorAll('.mapbox:empty'), ($mapbox) => {
const { lng, lat, zoom, lightStyle, darkStyle, marked, navigation, geolocate, scale, fullscreen } = JSON.parse($mapbox.dataset.options);
const mapbox = new mapboxgl.Map({
container: $mapbox,
@@ -658,7 +665,7 @@ class FixIt {
}
}
initTypeit() {
initTypeit(target = document) {
if (this.config.typeit) {
const typeitConfig = this.config.typeit;
const speed = typeitConfig.speed || 100;
@@ -667,7 +674,7 @@ class FixIt {
const loop = typeitConfig.loop ?? false;
// divide them into different groups according to the data-group attribute value of the element
// results in an object like {group1: [ele1, ele2], group2: [ele3, ele4]}
const typeitElements = document.querySelectorAll('.typeit')
const typeitElements = target.querySelectorAll('.typeit')
const groupMap = Array.from(typeitElements).reduce((acc, ele) => {
const group = ele.dataset.group || ele.id || Math.random().toString(36).substring(2);
acc[group] = acc[group] || [];
@@ -953,12 +960,28 @@ class FixIt {
this.initEcharts();
this.initTypeit();
this.initMapbox();
this.util.forEach(document.querySelectorAll('.encrypted-hidden'), ($element) => {
$element.classList.replace('encrypted-hidden', 'decrypted-shown');
});
this.initToc();
this.initTocListener();
this.initPangu();
this.util.forEach(document.querySelectorAll('.encrypted-hidden'), ($element) => {
$element.classList.replace('encrypted-hidden', 'decrypted-shown');
});
},
partialDecrypted: ($content) => {
this.initTwemoji($content);
this.initDetails($content);
this.initLightGallery();
this.initHighlight();
this.initTable($content);
this.initMath($content);
// TODO this.initEcharts();
this.initTypeit($content);
this.initMapbox();
this.initPangu();
window.mermaid.run && window.mermaid.run();
this.util.forEach($content.querySelectorAll('.encrypted-hidden'), ($element) => {
$element.classList.replace('encrypted-hidden', 'decrypted-shown');
});
},
reset: () => {
this.util.forEach(document.querySelectorAll('.decrypted-shown'), ($element) => {
@@ -970,6 +993,9 @@ class FixIt {
this.decryptor.addEventListener('decrypted', () => {
this.decryptor.initShortcodes();
})
this.decryptor.addEventListener('partial-decrypted', () => {
this.decryptor.initShortcodes();
})
this.decryptor.initShortcodes();
}
this.config.encryption?.all && this.decryptor.init();
+1
View File
@@ -129,6 +129,7 @@ expirationReminder = "Dieser Artikel wurde zuletzt auf {{ .Date }} aktualisiert,
encryptedAbstract = ""
encryptedMessage = ""
password = "Passwort"
enterBtn = ""
encryptyAgain = ""
relatedContent = "Ähnliche Inhalte"
+1
View File
@@ -128,6 +128,7 @@ expirationReminder = "This article was last updated on {{ .Date }}, the content
encryptedAbstract = "This article has been encrypted, so its raw content is invisible!"
encryptedMessage = "Please enter the password"
password = "Password"
enterBtn = "Enter"
encryptyAgain = "Encrypt again"
relatedContent = "Related Content"
+1
View File
@@ -129,6 +129,7 @@ expirationReminder = "Este artículo se actualizó por última vez el {{ .Date }
encryptedAbstract = ""
encryptedMessage = ""
password = "Contraseña"
enterBtn = ""
encryptyAgain = ""
relatedContent = "Contenido relacionado"
+1
View File
@@ -128,6 +128,7 @@ expirationReminder = "Cet article a été mis à jour pour la dernière fois le
encryptedAbstract = "Cet article a été chiffré, son contenu n'est donc pas lisible!"
encryptedMessage = "Entrer le mot de passe"
password = "Mot de passe"
enterBtn = "Entrer"
encryptyAgain = "Chiffrer à nouveau"
relatedContent = "Contenu lié"
+1
View File
@@ -129,6 +129,7 @@ expirationReminder = "यह लेख अंतिम बार {{ .Date }} प
encryptedAbstract = "यह लेख एन्क्रिप्ट किया गया है, इसलिए इसकी मूल सामग्री अदृश्य है!"
encryptedMessage = "कृपया पासवर्ड दर्ज करें"
password = "पासवर्ड"
enterBtn = "दर्ज करें"
encryptyAgain = "फिर से एन्क्रिप्ट करें"
relatedContent = "संबंधित सामग्री"
+1
View File
@@ -129,6 +129,7 @@ expirationReminder = "Questo articolo è stato aggiornato l'ultima volta il {{ .
encryptedAbstract = ""
encryptedMessage = ""
password = "Password"
enterBtn = ""
encryptyAgain = ""
relatedContent = "Contenuti correlati"
+1
View File
@@ -129,6 +129,7 @@ expirationReminder = "Ten artykuł był ostatnio aktualizowany {{ .Date }}, jego
encryptedAbstract = ""
encryptedMessage = ""
password = "Hasło"
enterBtn = ""
encryptyAgain = ""
relatedContent = "Powiązane treści"
+1
View File
@@ -129,6 +129,7 @@ expirationReminder = "Este artigo foi atualizado pela última vez em {{ .Date }}
encryptedAbstract = ""
encryptedMessage = ""
password = "Contrasinha"
enterBtn = ""
encryptyAgain = ""
relatedContent = "Conteúdo relacionado"
+1
View File
@@ -129,6 +129,7 @@ expirationReminder = "Acest articol a fost actualizat ultima dată pe {{ .Date }
encryptedAbstract = ""
encryptedMessage = ""
password = "Parolă"
enterBtn = ""
encryptyAgain = ""
relatedContent = "Conținut înrudit"
+1
View File
@@ -129,6 +129,7 @@ expirationReminder = "Эта статья последний раз обновл
encryptedAbstract = ""
encryptedMessage = ""
password = "арготизм"
enterBtn = ""
encryptyAgain = ""
relatedContent = "Связанный контент"
+1
View File
@@ -129,6 +129,7 @@ expirationReminder = "Овај чланак је последњи пут ажу
encryptedAbstract = ""
encryptedMessage = ""
password = "Паролица"
enterBtn = ""
encryptyAgain = ""
relatedContent = "Повезани садржај"
+1
View File
@@ -128,6 +128,7 @@ expirationReminder = "Bài viết này được cập nhật lần cuối vào {
encryptedAbstract = ""
encryptedMessage = ""
password = "Mật khẩu"
enterBtn = ""
encryptyAgain = ""
relatedContent = "Nội dung liên quan"
+1
View File
@@ -126,6 +126,7 @@ expirationReminder = "本文最后更新于 {{ .Date }},文中内容可能已
encryptedAbstract = "本文已加密,因此其原始内容不可见!"
encryptedMessage = "请输入密码"
password = "密码"
enterBtn = "进入"
encryptyAgain = "重新加密"
relatedContent = "相关内容"
+1
View File
@@ -126,6 +126,7 @@ expirationReminder = "本文最後更新於 {{ .Date }},文中內容可能已
encryptedAbstract = "本文已加密,囙此其原始內容不可見!"
encryptedMessage = "請輸入密碼"
password = "密碼"
enterBtn = "進入"
encryptyAgain = "重新加密"
relatedContent = "相關內容"
+4 -1
View File
@@ -6,8 +6,11 @@
<div class="fixit-decryptor-container">
<img class="fixit-decryptor-loading" src="{{ $loading.RelPermalink }}" alt="decryptor loading" />
<label for="fixit-decryptor-input" title='{{ T "single.password" }}'>
<input type="password" id="fixit-decryptor-input" class="d-none" placeholder="🔑 {{ $msg }}" />
<input type="password" id="fixit-decryptor-input" class="fixit-decryptor-input d-none" placeholder="🔑 {{ $msg }}" />
</label>
<button class="fixit-decryptor-btn d-none">
{{- dict "Class" "fa-solid fa-unlock" | partial "plugin/icon.html" }} {{ T "single.enterBtn" -}}
</button>
<button class="fixit-encryptor-btn d-none">
{{- dict "Class" "fa-solid fa-lock" | partial "plugin/icon.html" }} {{ T "single.encryptyAgain" -}}
</button>
+3
View File
@@ -25,6 +25,9 @@
<label title='{{ T "single.password" }}'>
<input type="password" class="fixit-decryptor-input" placeholder="🔑 {{ $message }}" />
</label>
<button class="fixit-decryptor-btn">
{{- dict "Class" "fa-solid fa-unlock" | partial "plugin/icon.html" }} {{ T "single.enterBtn" -}}
</button>
</div>
<div data-password="{{ md5 $password }}" data-content="{{ $content }}"></div>
</fixit-encryptor>