Wrote the fucking thing
parent
da63b2841d
commit
a9adaf6d2a
|
@ -6559,8 +6559,11 @@ g {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#speed-carot-modal
|
.quick-emoji-dropdown
|
||||||
{
|
{
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
background-color: var(--gray-700);
|
background-color: var(--gray-700);
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
@ -6569,31 +6572,34 @@ g {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
|
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
z-index: 1000000001;
|
z-index: 1000000001;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
#speed-carot-modal .speed-modal-option
|
.quick-emoji-dropdown .quick-emoji-option
|
||||||
{
|
{
|
||||||
|
text-align: left;
|
||||||
border-bottom: 1px solid #606060;
|
border-bottom: 1px solid #606060;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#speed-carot-modal .speed-modal-option:hover,
|
.quick-emoji-dropdown .quick-emoji-option:hover,
|
||||||
#speed-carot-modal .speed-modal-option:focus,
|
.quick-emoji-dropdown .quick-emoji-option:focus,
|
||||||
#speed-carot-modal .speed-modal-option.selected
|
.quick-emoji-dropdown .quick-emoji-option.selected
|
||||||
{
|
{
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#speed-carot-modal .speed-modal-image
|
.quick-emoji-dropdown .quick-emoji-image
|
||||||
{
|
{
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#speed-carot-modal .speed-modal-option span
|
.quick-emoji-dropdown .quick-emoji-option span
|
||||||
{
|
{
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -6706,7 +6712,7 @@ div.markdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
#speed-carot-modal .speed-modal-image
|
.quick-emoji-dropdown .quick-emoji-image
|
||||||
{
|
{
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
|
|
@ -12,7 +12,7 @@ function getMessageFromJsonData(success, json) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showToast(success, message) {
|
function showToast(success, message) {
|
||||||
const oldToast = bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-' + (success ? 'error': 'success'))); // intentionally reversed here: this is the old toast
|
const oldToast = bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-' + (success ? 'error' : 'success'))); // intentionally reversed here: this is the old toast
|
||||||
oldToast.hide();
|
oldToast.hide();
|
||||||
let element = success ? "toast-post-success" : "toast-post-error";
|
let element = success ? "toast-post-success" : "toast-post-error";
|
||||||
let textElement = element + "-text";
|
let textElement = element + "-text";
|
||||||
|
@ -23,7 +23,7 @@ function showToast(success, message) {
|
||||||
bootstrap.Toast.getOrCreateInstance(document.getElementById(element)).show();
|
bootstrap.Toast.getOrCreateInstance(document.getElementById(element)).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createXhrWithFormKey(url, form=new FormData(), method='POST') {
|
function createXhrWithFormKey(url, form = new FormData(), method = 'POST') {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open(method, url);
|
xhr.open(method, url);
|
||||||
xhr.setRequestHeader('xhr', 'xhr');
|
xhr.setRequestHeader('xhr', 'xhr');
|
||||||
|
@ -38,12 +38,12 @@ function postToast(t, url, data, extraActionsOnSuccess, extraActionsOnFailure) {
|
||||||
|
|
||||||
let form = new FormData();
|
let form = new FormData();
|
||||||
if (typeof data === 'object' && data !== null) {
|
if (typeof data === 'object' && data !== null) {
|
||||||
for(let k of Object.keys(data)) {
|
for (let k of Object.keys(data)) {
|
||||||
form.append(k, data[k]);
|
form.append(k, data[k]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const xhr = createXhrWithFormKey(url, form);
|
const xhr = createXhrWithFormKey(url, form);
|
||||||
xhr[0].onload = function() {
|
xhr[0].onload = function () {
|
||||||
const success = xhr[0].status >= 200 && xhr[0].status < 300;
|
const success = xhr[0].status >= 200 && xhr[0].status < 300;
|
||||||
|
|
||||||
if (!(extraActionsOnSuccess == reload && success)) {
|
if (!(extraActionsOnSuccess == reload && success)) {
|
||||||
|
@ -88,19 +88,18 @@ function postToastSwitch(t, url, button1, button2, cls, extraActionsOnSuccess) {
|
||||||
{
|
{
|
||||||
},
|
},
|
||||||
(xhr) => {
|
(xhr) => {
|
||||||
if (button1)
|
if (button1) {
|
||||||
{
|
|
||||||
if (typeof button1 == 'boolean') {
|
if (typeof button1 == 'boolean') {
|
||||||
location.reload()
|
location.reload()
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
document.getElementById(button1).classList.toggle(cls);
|
document.getElementById(button1).classList.toggle(cls);
|
||||||
}
|
}
|
||||||
catch (e) {}
|
catch (e) { }
|
||||||
try {
|
try {
|
||||||
document.getElementById(button2).classList.toggle(cls);
|
document.getElementById(button2).classList.toggle(cls);
|
||||||
}
|
}
|
||||||
catch (e) {}
|
catch (e) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof extraActionsOnSuccess == 'function')
|
if (typeof extraActionsOnSuccess == 'function')
|
||||||
|
@ -108,8 +107,7 @@ function postToastSwitch(t, url, button1, button2, cls, extraActionsOnSuccess) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!location.pathname.endsWith('/submit') && !location.pathname.endsWith('/chat'))
|
if (!location.pathname.endsWith('/submit') && !location.pathname.endsWith('/chat')) {
|
||||||
{
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (!((e.ctrlKey || e.metaKey) && e.key === "Enter")) return;
|
if (!((e.ctrlKey || e.metaKey) && e.key === "Enter")) return;
|
||||||
|
|
||||||
|
@ -147,18 +145,17 @@ function autoExpand(field) {
|
||||||
let computed = window.getComputedStyle(field);
|
let computed = window.getComputedStyle(field);
|
||||||
|
|
||||||
let height = parseInt(computed.getPropertyValue('border-top-width'), 10)
|
let height = parseInt(computed.getPropertyValue('border-top-width'), 10)
|
||||||
+ parseInt(computed.getPropertyValue('padding-top'), 10)
|
+ parseInt(computed.getPropertyValue('padding-top'), 10)
|
||||||
+ field.scrollHeight
|
+ field.scrollHeight
|
||||||
+ parseInt(computed.getPropertyValue('padding-bottom'), 10)
|
+ parseInt(computed.getPropertyValue('padding-bottom'), 10)
|
||||||
+ parseInt(computed.getPropertyValue('border-bottom-width'), 10);
|
+ parseInt(computed.getPropertyValue('border-bottom-width'), 10);
|
||||||
|
|
||||||
field.style.height = height + 'px';
|
field.style.height = height + 'px';
|
||||||
if (Math.abs(window.scrollX - xpos) < 1 && Math.abs(window.scrollY - ypos) < 1) return;
|
if (Math.abs(window.scrollX - xpos) < 1 && Math.abs(window.scrollY - ypos) < 1) return;
|
||||||
window.scrollTo(xpos,ypos);
|
window.scrollTo(xpos, ypos);
|
||||||
};
|
};
|
||||||
|
|
||||||
function smoothScrollTop()
|
function smoothScrollTop() {
|
||||||
{
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,8 +190,7 @@ function expandImage(url) {
|
||||||
document.getElementById("expanded-image").src = '';
|
document.getElementById("expanded-image").src = '';
|
||||||
document.getElementById("expanded-image-wrap-link").href = '';
|
document.getElementById("expanded-image-wrap-link").href = '';
|
||||||
|
|
||||||
if (!url)
|
if (!url) {
|
||||||
{
|
|
||||||
url = e.target.dataset.src
|
url = e.target.dataset.src
|
||||||
if (!url) url = e.target.src
|
if (!url) url = e.target.src
|
||||||
}
|
}
|
||||||
|
@ -206,7 +202,7 @@ function expandImage(url) {
|
||||||
|
|
||||||
function bs_trigger(e) {
|
function bs_trigger(e) {
|
||||||
let tooltipTriggerList = [].slice.call(e.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
let tooltipTriggerList = [].slice.call(e.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
tooltipTriggerList.map(function(element){
|
tooltipTriggerList.map(function (element) {
|
||||||
return bootstrap.Tooltip.getOrCreateInstance(element);
|
return bootstrap.Tooltip.getOrCreateInstance(element);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -231,18 +227,18 @@ function showmore(t) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(d) {
|
function formatDate(d) {
|
||||||
const options = {year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short'};
|
const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' };
|
||||||
return d.toLocaleTimeString([], options)
|
return d.toLocaleTimeString([], options)
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamps = document.querySelectorAll('[data-time]');
|
const timestamps = document.querySelectorAll('[data-time]');
|
||||||
|
|
||||||
for (const e of timestamps) {
|
for (const e of timestamps) {
|
||||||
e.innerHTML = formatDate(new Date(e.dataset.time*1000));
|
e.innerHTML = formatDate(new Date(e.dataset.time * 1000));
|
||||||
};
|
};
|
||||||
|
|
||||||
function timestamp(t, ti) {
|
function timestamp(t, ti) {
|
||||||
const date = formatDate(new Date(ti*1000));
|
const date = formatDate(new Date(ti * 1000));
|
||||||
t.setAttribute("data-bs-original-title", date);
|
t.setAttribute("data-bs-original-title", date);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -265,8 +261,7 @@ function areyousure(t) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepare_to_pause(audio) {
|
function prepare_to_pause(audio) {
|
||||||
for (const e of document.querySelectorAll('video,audio'))
|
for (const e of document.querySelectorAll('video,audio')) {
|
||||||
{
|
|
||||||
e.addEventListener('play', () => {
|
e.addEventListener('play', () => {
|
||||||
if (!audio.paused) audio.pause();
|
if (!audio.paused) audio.pause();
|
||||||
});
|
});
|
||||||
|
@ -298,7 +293,7 @@ function sendFormXHR(form, extraActionsOnSuccess) {
|
||||||
xhr.open("POST", actionPath);
|
xhr.open("POST", actionPath);
|
||||||
xhr.setRequestHeader('xhr', 'xhr');
|
xhr.setRequestHeader('xhr', 'xhr');
|
||||||
|
|
||||||
xhr.onload = function() {
|
xhr.onload = function () {
|
||||||
const success = xhr.status >= 200 && xhr.status < 300;
|
const success = xhr.status >= 200 && xhr.status < 300;
|
||||||
|
|
||||||
if (!(extraActionsOnSuccess == reload && success)) {
|
if (!(extraActionsOnSuccess == reload && success)) {
|
||||||
|
@ -358,7 +353,7 @@ function sort_table(t) {
|
||||||
attr = parseInt(attr.replace(/,/g, ''))
|
attr = parseInt(attr.replace(/,/g, ''))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items.push({ele, attr});
|
items.push({ ele, attr });
|
||||||
}
|
}
|
||||||
if (sortAscending[n]) {
|
if (sortAscending[n]) {
|
||||||
items.sort((a, b) => a.attr > b.attr ? 1 : -1);
|
items.sort((a, b) => a.attr > b.attr ? 1 : -1);
|
||||||
|
@ -401,8 +396,7 @@ if (location.pathname != '/chat' && (gbrowser == 'iphone' || gbrowser == 'mac'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const screen_width = (innerWidth > 0) ? innerWidth : screen.width;
|
const screen_width = (innerWidth > 0) ? innerWidth : screen.width;
|
||||||
function focusSearchBar(element)
|
function focusSearchBar(element) {
|
||||||
{
|
|
||||||
if (screen_width >= 768) {
|
if (screen_width >= 768) {
|
||||||
element.focus();
|
element.focus();
|
||||||
}
|
}
|
||||||
|
@ -428,13 +422,13 @@ function insertText(input, text) {
|
||||||
input.setRangeText(text);
|
input.setRangeText(text);
|
||||||
|
|
||||||
if (window.chrome !== undefined)
|
if (window.chrome !== undefined)
|
||||||
setTimeout(function(){
|
setTimeout(function () {
|
||||||
input.focus();
|
input.focus();
|
||||||
for(let i = 0; i < 2; i++)
|
for (let i = 0; i < 2; i++)
|
||||||
input.setSelectionRange(newPos, newPos);
|
input.setSelectionRange(newPos, newPos);
|
||||||
|
|
||||||
input.focus();
|
input.focus();
|
||||||
for(let i = 0; i < 2; i++)
|
for (let i = 0; i < 2; i++)
|
||||||
input.setSelectionRange(newPos, newPos);
|
input.setSelectionRange(newPos, newPos);
|
||||||
}, 1);
|
}, 1);
|
||||||
else
|
else
|
||||||
|
@ -445,6 +439,167 @@ function insertText(input, text) {
|
||||||
handle_disabled(input)
|
handle_disabled(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a promise which can be used to await when the event loop is idle. */
|
||||||
|
const idle = () => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
requestIdleCallback(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shamelessly copied from https://github.com/component/textarea-caret-position/blob/master/index.js
|
||||||
|
* This code makes the assumption that the style of the textarea/input won't change.
|
||||||
|
* @returns {{top: number, left: number, height: number, bottom: number, right: number, x: number, y: number }}
|
||||||
|
*/
|
||||||
|
const getCaretPos = (() => {
|
||||||
|
// We'll copy the properties below into the mirror div.
|
||||||
|
// Note that some browsers, such as Firefox, do not concatenate properties
|
||||||
|
// into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
|
||||||
|
// so we have to list every single property explicitly.
|
||||||
|
const properties = [
|
||||||
|
'direction', // RTL support
|
||||||
|
'boxSizing',
|
||||||
|
'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
|
||||||
|
'height',
|
||||||
|
'overflowX',
|
||||||
|
'overflowY', // copy the scrollbar for IE
|
||||||
|
|
||||||
|
'borderTopWidth',
|
||||||
|
'borderRightWidth',
|
||||||
|
'borderBottomWidth',
|
||||||
|
'borderLeftWidth',
|
||||||
|
'borderStyle',
|
||||||
|
|
||||||
|
'paddingTop',
|
||||||
|
'paddingRight',
|
||||||
|
'paddingBottom',
|
||||||
|
'paddingLeft',
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
|
||||||
|
'fontStyle',
|
||||||
|
'fontVariant',
|
||||||
|
'fontWeight',
|
||||||
|
'fontStretch',
|
||||||
|
'fontSize',
|
||||||
|
'fontSizeAdjust',
|
||||||
|
'lineHeight',
|
||||||
|
'fontFamily',
|
||||||
|
|
||||||
|
'textAlign',
|
||||||
|
'textTransform',
|
||||||
|
'textIndent',
|
||||||
|
'textDecoration', // might not make a difference, but better be safe
|
||||||
|
|
||||||
|
'letterSpacing',
|
||||||
|
'wordSpacing',
|
||||||
|
|
||||||
|
'tabSize',
|
||||||
|
'MozTabSize'
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
const cache = new Map();
|
||||||
|
|
||||||
|
const isFirefox = window.mozInnerScreenX != null;
|
||||||
|
|
||||||
|
/** @param {HTMLTextAreaElement} element */
|
||||||
|
return (element) => {
|
||||||
|
const position = element.selectionEnd;
|
||||||
|
const computed = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
|
||||||
|
const isInput = element.nodeName === 'INPUT';
|
||||||
|
|
||||||
|
let div, style;
|
||||||
|
if (cache.has(element)) {
|
||||||
|
div = cache.get(element);
|
||||||
|
style = div.style;
|
||||||
|
} else {
|
||||||
|
// The mirror div will replicate the textarea's style
|
||||||
|
div = document.createElement('div');
|
||||||
|
cache.set(element, div);
|
||||||
|
div.id = 'input-textarea-caret-position-mirror-div';
|
||||||
|
document.body.appendChild(div);
|
||||||
|
|
||||||
|
style = div.style;
|
||||||
|
|
||||||
|
// Default textarea styles
|
||||||
|
style.whiteSpace = 'pre-wrap';
|
||||||
|
if (!isInput) {
|
||||||
|
style.overflowWrap = 'break-word'; // only for textarea-s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position off-screen
|
||||||
|
style.position = 'absolute'; // required to return coordinates properly
|
||||||
|
style.visibility = 'hidden'; // not 'display: none' because we want rendering
|
||||||
|
|
||||||
|
// Transfer the element's properties to the div
|
||||||
|
properties.forEach(function (prop) {
|
||||||
|
if (isInput && prop === 'lineHeight') {
|
||||||
|
// Special case for <input>s because text is rendered centered and line height may be != height
|
||||||
|
if (computed.boxSizing === "border-box") {
|
||||||
|
var height = parseInt(computed.height);
|
||||||
|
var outerHeight =
|
||||||
|
parseInt(computed.paddingTop) +
|
||||||
|
parseInt(computed.paddingBottom) +
|
||||||
|
parseInt(computed.borderTopWidth) +
|
||||||
|
parseInt(computed.borderBottomWidth);
|
||||||
|
var targetHeight = outerHeight + parseInt(computed.lineHeight);
|
||||||
|
if (height > targetHeight) {
|
||||||
|
style.lineHeight = height - outerHeight + "px";
|
||||||
|
} else if (height === targetHeight) {
|
||||||
|
style.lineHeight = computed.lineHeight;
|
||||||
|
} else {
|
||||||
|
style.lineHeight = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
style.lineHeight = computed.height;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
style[prop] = computed[prop];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFirefox) {
|
||||||
|
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
|
||||||
|
if (element.scrollHeight > parseInt(computed.height))
|
||||||
|
style.overflowY = 'scroll';
|
||||||
|
} else {
|
||||||
|
style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
|
||||||
|
}
|
||||||
|
|
||||||
|
div.textContent = element.value.substring(0, position);
|
||||||
|
// The second special handling for input type="text" vs textarea:
|
||||||
|
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
|
||||||
|
if (isInput) {
|
||||||
|
div.textContent = div.textContent.replace(/\s/g, '\u00a0');
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = document.createElement('span');
|
||||||
|
// Wrapping must be replicated *exactly*, including when a long word gets
|
||||||
|
// onto the next line, with whitespace at the end of the line before (#7).
|
||||||
|
// The *only* reliable way to do that is to copy the *entire* rest of the
|
||||||
|
// textarea's content into the <span> created at the caret position.
|
||||||
|
// For inputs, just '.' would be enough, but no need to bother.
|
||||||
|
span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
|
||||||
|
div.appendChild(span);
|
||||||
|
|
||||||
|
const rect = element.getClientRects()[0];
|
||||||
|
const top = rect.top + window.scrollY + span.offsetTop;
|
||||||
|
const left = rect.left + window.scrollX + span.offsetLeft;
|
||||||
|
const height = parseInt(computed['lineHeight']);
|
||||||
|
const coordinates = {
|
||||||
|
x: left,
|
||||||
|
y: top,
|
||||||
|
top,
|
||||||
|
bottom: top + height,
|
||||||
|
left,
|
||||||
|
right: left + 1,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
|
||||||
|
return coordinates;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
//FILE SHIT
|
//FILE SHIT
|
||||||
|
@ -510,8 +665,8 @@ function handle_files(input, newfiles) {
|
||||||
for (let file of newfiles) {
|
for (let file of newfiles) {
|
||||||
if (file.name == 'image.png') {
|
if (file.name == 'image.png') {
|
||||||
const blob = file.slice(0, file.size, 'image/png');
|
const blob = file.slice(0, file.size, 'image/png');
|
||||||
const new_name = Math.random().toString(32).substring(2,10) + '.png'
|
const new_name = Math.random().toString(32).substring(2, 10) + '.png'
|
||||||
file = new File([blob], new_name, {type: 'image/png'});
|
file = new File([blob], new_name, { type: 'image/png' });
|
||||||
}
|
}
|
||||||
oldfiles[ta.id].items.add(file);
|
oldfiles[ta.id].items.add(file);
|
||||||
insertText(ta, `[${file.name}]`);
|
insertText(ta, `[${file.name}]`);
|
||||||
|
@ -519,8 +674,7 @@ function handle_files(input, newfiles) {
|
||||||
|
|
||||||
input.files = oldfiles[ta.id].files;
|
input.files = oldfiles[ta.id].files;
|
||||||
|
|
||||||
if (input.files.length > 20)
|
if (input.files.length > 20) {
|
||||||
{
|
|
||||||
window.alert("You can't upload more than 20 files at one time!")
|
window.alert("You can't upload more than 20 files at one time!")
|
||||||
input.value = null
|
input.value = null
|
||||||
oldfiles[ta.id] = new DataTransfer();
|
oldfiles[ta.id] = new DataTransfer();
|
||||||
|
@ -549,8 +703,7 @@ file_upload = document.getElementById('file-upload');
|
||||||
|
|
||||||
if (file_upload) {
|
if (file_upload) {
|
||||||
function display_url_image() {
|
function display_url_image() {
|
||||||
if (file_upload.files)
|
if (file_upload.files) {
|
||||||
{
|
|
||||||
const file = file_upload.files[0]
|
const file = file_upload.files[0]
|
||||||
const char_limit = screen_width >= 768 ? 50 : 10;
|
const char_limit = screen_width >= 768 ? 50 : 10;
|
||||||
file_upload.previousElementSibling.textContent = file.name.substr(0, char_limit);
|
file_upload.previousElementSibling.textContent = file.name.substr(0, char_limit);
|
||||||
|
@ -591,7 +744,7 @@ if (file_upload) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.onpaste = function(event) {
|
document.onpaste = function (event) {
|
||||||
const files = structuredClone(event.clipboardData.files);
|
const files = structuredClone(event.clipboardData.files);
|
||||||
if (!files.length) return
|
if (!files.length) return
|
||||||
|
|
||||||
|
|
|
@ -1,203 +1,206 @@
|
||||||
const cursormarseyEl = document.getElementById("cursormarsey");
|
// How about we don't dump everything into the global scope?
|
||||||
const heartEl = document.getElementById("cursormarsey-heart");
|
{
|
||||||
|
const cursormarseyEl = document.getElementById("cursormarsey");
|
||||||
|
const heartEl = document.getElementById("cursormarsey-heart");
|
||||||
|
|
||||||
function getInitialPosition(max) {
|
function getInitialPosition(max) {
|
||||||
return Math.max(32, Math.floor(Math.random() * max));
|
return Math.max(32, Math.floor(Math.random() * max));
|
||||||
}
|
|
||||||
let cursormarseyPosX = getInitialPosition(screen.availWidth - 20);
|
|
||||||
let cursormarseyPosY = getInitialPosition(screen.availHeight - 50);
|
|
||||||
cursormarseyEl.style.left = `${cursormarseyPosX}px`;
|
|
||||||
cursormarseyEl.style.top = `${cursormarseyPosY}px`;
|
|
||||||
heartEl.style.left = `${cursormarseyPosX+16}px`;
|
|
||||||
heartEl.style.top = `${cursormarseyPosY-16}px`;
|
|
||||||
|
|
||||||
let mousePosX = cursormarseyPosX;
|
|
||||||
let mousePosY = cursormarseyPosY;
|
|
||||||
|
|
||||||
let frameCount = 0;
|
|
||||||
let idleTime = 0;
|
|
||||||
let idleAnimation = null;
|
|
||||||
let idleAnimationFrame = 0;
|
|
||||||
const cursormarseySpeed = 10;
|
|
||||||
const spriteSets = {
|
|
||||||
idle: [[-3, -3]],
|
|
||||||
alert: [[-7, -3]],
|
|
||||||
scratchSelf: [
|
|
||||||
[-5, 0],
|
|
||||||
[-6, 0],
|
|
||||||
[-7, 0],
|
|
||||||
],
|
|
||||||
scratchWallN: [
|
|
||||||
[0, 0],
|
|
||||||
[0, -1],
|
|
||||||
],
|
|
||||||
scratchWallS: [
|
|
||||||
[-7, -1],
|
|
||||||
[-6, -2],
|
|
||||||
],
|
|
||||||
scratchWallE: [
|
|
||||||
[-2, -2],
|
|
||||||
[-2, -3],
|
|
||||||
],
|
|
||||||
scratchWallW: [
|
|
||||||
[-4, 0],
|
|
||||||
[-4, -1],
|
|
||||||
],
|
|
||||||
tired: [[-3, -2]],
|
|
||||||
sleeping: [
|
|
||||||
[-2, 0],
|
|
||||||
[-2, -1],
|
|
||||||
],
|
|
||||||
N: [
|
|
||||||
[-1, -2],
|
|
||||||
[-1, -3],
|
|
||||||
],
|
|
||||||
NE: [
|
|
||||||
[0, -2],
|
|
||||||
[0, -3],
|
|
||||||
],
|
|
||||||
E: [
|
|
||||||
[-3, 0],
|
|
||||||
[-3, -1],
|
|
||||||
],
|
|
||||||
SE: [
|
|
||||||
[-5, -1],
|
|
||||||
[-5, -2],
|
|
||||||
],
|
|
||||||
S: [
|
|
||||||
[-6, -3],
|
|
||||||
[-7, -2],
|
|
||||||
],
|
|
||||||
SW: [
|
|
||||||
[-5, -3],
|
|
||||||
[-6, -1],
|
|
||||||
],
|
|
||||||
W: [
|
|
||||||
[-4, -2],
|
|
||||||
[-4, -3],
|
|
||||||
],
|
|
||||||
NW: [
|
|
||||||
[-1, 0],
|
|
||||||
[-1, -1],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
function setSprite(name, frame) {
|
|
||||||
const sprite = spriteSets[name][frame % spriteSets[name].length];
|
|
||||||
cursormarseyEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetIdleAnimation() {
|
|
||||||
idleAnimation = null;
|
|
||||||
idleAnimationFrame = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function idle() {
|
|
||||||
idleTime += 1;
|
|
||||||
|
|
||||||
// every ~ 20 seconds
|
|
||||||
if (idleTime > 10 && true && idleAnimation == null) {
|
|
||||||
let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
|
|
||||||
if (cursormarseyPosX < 32) {
|
|
||||||
avalibleIdleAnimations.push("scratchWallW");
|
|
||||||
}
|
}
|
||||||
if (cursormarseyPosY < 32) {
|
let cursormarseyPosX = getInitialPosition(screen.availWidth - 20);
|
||||||
avalibleIdleAnimations.push("scratchWallN");
|
let cursormarseyPosY = getInitialPosition(screen.availHeight - 50);
|
||||||
}
|
|
||||||
if (cursormarseyPosX > innerWidth - 32) {
|
|
||||||
avalibleIdleAnimations.push("scratchWallE");
|
|
||||||
}
|
|
||||||
if (cursormarseyPosY > innerHeight - 32) {
|
|
||||||
avalibleIdleAnimations.push("scratchWallS");
|
|
||||||
}
|
|
||||||
idleAnimation =
|
|
||||||
avalibleIdleAnimations[
|
|
||||||
Math.floor(Math.random() * avalibleIdleAnimations.length)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (idleAnimation) {
|
|
||||||
case "sleeping":
|
|
||||||
if (idleAnimationFrame < 8) {
|
|
||||||
setSprite("tired", 0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
|
|
||||||
if (idleAnimationFrame > 192) {
|
|
||||||
resetIdleAnimation();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "scratchWallN":
|
|
||||||
case "scratchWallS":
|
|
||||||
case "scratchWallE":
|
|
||||||
case "scratchWallW":
|
|
||||||
case "scratchSelf":
|
|
||||||
setSprite(idleAnimation, idleAnimationFrame);
|
|
||||||
if (idleAnimationFrame > 9) {
|
|
||||||
resetIdleAnimation();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setSprite("idle", 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
idleAnimationFrame += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function frame() {
|
|
||||||
frameCount += 1;
|
|
||||||
const diffX = cursormarseyPosX - mousePosX;
|
|
||||||
const diffY = cursormarseyPosY - mousePosY;
|
|
||||||
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
|
|
||||||
|
|
||||||
if (distance < cursormarseySpeed || distance < 100) {
|
|
||||||
idle();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
idleAnimation = null;
|
|
||||||
idleAnimationFrame = 0;
|
|
||||||
|
|
||||||
if (idleTime > 1) {
|
|
||||||
setSprite("alert", 0);
|
|
||||||
// count down after being alerted before moving
|
|
||||||
idleTime = Math.min(idleTime, 7);
|
|
||||||
idleTime -= 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
direction = diffY / distance > 0.5 ? "N" : "";
|
|
||||||
direction += diffY / distance < -0.5 ? "S" : "";
|
|
||||||
direction += diffX / distance > 0.5 ? "W" : "";
|
|
||||||
direction += diffX / distance < -0.5 ? "E" : "";
|
|
||||||
setSprite(direction, frameCount);
|
|
||||||
|
|
||||||
cursormarseyPosX -= (diffX / distance) * cursormarseySpeed;
|
|
||||||
cursormarseyPosY -= (diffY / distance) * cursormarseySpeed;
|
|
||||||
|
|
||||||
cursormarseyPosX = Math.min(Math.max(16, cursormarseyPosX), innerWidth - 16);
|
|
||||||
cursormarseyPosY = Math.min(Math.max(16, cursormarseyPosY), innerHeight - 16);
|
|
||||||
|
|
||||||
cursormarseyEl.style.left = `${cursormarseyPosX}px`;
|
cursormarseyEl.style.left = `${cursormarseyPosX}px`;
|
||||||
cursormarseyEl.style.top = `${cursormarseyPosY}px`;
|
cursormarseyEl.style.top = `${cursormarseyPosY}px`;
|
||||||
heartEl.style.left = `${cursormarseyPosX+16}px`;
|
heartEl.style.left = `${cursormarseyPosX+16}px`;
|
||||||
heartEl.style.top = `${cursormarseyPosY-16}px`;
|
heartEl.style.top = `${cursormarseyPosY-16}px`;
|
||||||
}
|
|
||||||
|
|
||||||
document.onmousemove = (event) => {
|
let mousePosX = cursormarseyPosX;
|
||||||
mousePosX = event.clientX;
|
let mousePosY = cursormarseyPosY;
|
||||||
mousePosY = event.clientY;
|
|
||||||
|
let frameCount = 0;
|
||||||
|
let idleTime = 0;
|
||||||
|
let idleAnimation = null;
|
||||||
|
let idleAnimationFrame = 0;
|
||||||
|
const cursormarseySpeed = 10;
|
||||||
|
const spriteSets = {
|
||||||
|
idle: [[-3, -3]],
|
||||||
|
alert: [[-7, -3]],
|
||||||
|
scratchSelf: [
|
||||||
|
[-5, 0],
|
||||||
|
[-6, 0],
|
||||||
|
[-7, 0],
|
||||||
|
],
|
||||||
|
scratchWallN: [
|
||||||
|
[0, 0],
|
||||||
|
[0, -1],
|
||||||
|
],
|
||||||
|
scratchWallS: [
|
||||||
|
[-7, -1],
|
||||||
|
[-6, -2],
|
||||||
|
],
|
||||||
|
scratchWallE: [
|
||||||
|
[-2, -2],
|
||||||
|
[-2, -3],
|
||||||
|
],
|
||||||
|
scratchWallW: [
|
||||||
|
[-4, 0],
|
||||||
|
[-4, -1],
|
||||||
|
],
|
||||||
|
tired: [[-3, -2]],
|
||||||
|
sleeping: [
|
||||||
|
[-2, 0],
|
||||||
|
[-2, -1],
|
||||||
|
],
|
||||||
|
N: [
|
||||||
|
[-1, -2],
|
||||||
|
[-1, -3],
|
||||||
|
],
|
||||||
|
NE: [
|
||||||
|
[0, -2],
|
||||||
|
[0, -3],
|
||||||
|
],
|
||||||
|
E: [
|
||||||
|
[-3, 0],
|
||||||
|
[-3, -1],
|
||||||
|
],
|
||||||
|
SE: [
|
||||||
|
[-5, -1],
|
||||||
|
[-5, -2],
|
||||||
|
],
|
||||||
|
S: [
|
||||||
|
[-6, -3],
|
||||||
|
[-7, -2],
|
||||||
|
],
|
||||||
|
SW: [
|
||||||
|
[-5, -3],
|
||||||
|
[-6, -1],
|
||||||
|
],
|
||||||
|
W: [
|
||||||
|
[-4, -2],
|
||||||
|
[-4, -3],
|
||||||
|
],
|
||||||
|
NW: [
|
||||||
|
[-1, 0],
|
||||||
|
[-1, -1],
|
||||||
|
],
|
||||||
};
|
};
|
||||||
window.marseykoInterval = setInterval(frame, 100);
|
|
||||||
|
|
||||||
document.addEventListener('click', (event) => {
|
function setSprite(name, frame) {
|
||||||
cursormarseyEl.style.removeProperty("pointer-events");
|
const sprite = spriteSets[name][frame % spriteSets[name].length];
|
||||||
let elementClicked = document.elementFromPoint(event.clientX,event.clientY);
|
cursormarseyEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
|
||||||
if (elementClicked.id === cursormarseyEl.id) {
|
|
||||||
heartEl.classList.remove("d-none");
|
|
||||||
setTimeout(() => {
|
|
||||||
heartEl.classList.add("d-none");
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
cursormarseyEl.style.pointerEvents = "none";
|
|
||||||
});
|
function resetIdleAnimation() {
|
||||||
|
idleAnimation = null;
|
||||||
|
idleAnimationFrame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idle = () => {
|
||||||
|
idleTime += 1;
|
||||||
|
|
||||||
|
// every ~ 20 seconds
|
||||||
|
if (idleTime > 10 && true && idleAnimation == null) {
|
||||||
|
let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
|
||||||
|
if (cursormarseyPosX < 32) {
|
||||||
|
avalibleIdleAnimations.push("scratchWallW");
|
||||||
|
}
|
||||||
|
if (cursormarseyPosY < 32) {
|
||||||
|
avalibleIdleAnimations.push("scratchWallN");
|
||||||
|
}
|
||||||
|
if (cursormarseyPosX > innerWidth - 32) {
|
||||||
|
avalibleIdleAnimations.push("scratchWallE");
|
||||||
|
}
|
||||||
|
if (cursormarseyPosY > innerHeight - 32) {
|
||||||
|
avalibleIdleAnimations.push("scratchWallS");
|
||||||
|
}
|
||||||
|
idleAnimation =
|
||||||
|
avalibleIdleAnimations[
|
||||||
|
Math.floor(Math.random() * avalibleIdleAnimations.length)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (idleAnimation) {
|
||||||
|
case "sleeping":
|
||||||
|
if (idleAnimationFrame < 8) {
|
||||||
|
setSprite("tired", 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
|
||||||
|
if (idleAnimationFrame > 192) {
|
||||||
|
resetIdleAnimation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "scratchWallN":
|
||||||
|
case "scratchWallS":
|
||||||
|
case "scratchWallE":
|
||||||
|
case "scratchWallW":
|
||||||
|
case "scratchSelf":
|
||||||
|
setSprite(idleAnimation, idleAnimationFrame);
|
||||||
|
if (idleAnimationFrame > 9) {
|
||||||
|
resetIdleAnimation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setSprite("idle", 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
idleAnimationFrame += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame = () => {
|
||||||
|
frameCount += 1;
|
||||||
|
const diffX = cursormarseyPosX - mousePosX;
|
||||||
|
const diffY = cursormarseyPosY - mousePosY;
|
||||||
|
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
|
||||||
|
|
||||||
|
if (distance < cursormarseySpeed || distance < 100) {
|
||||||
|
idle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
idleAnimation = null;
|
||||||
|
idleAnimationFrame = 0;
|
||||||
|
|
||||||
|
if (idleTime > 1) {
|
||||||
|
setSprite("alert", 0);
|
||||||
|
// count down after being alerted before moving
|
||||||
|
idleTime = Math.min(idleTime, 7);
|
||||||
|
idleTime -= 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
direction = diffY / distance > 0.5 ? "N" : "";
|
||||||
|
direction += diffY / distance < -0.5 ? "S" : "";
|
||||||
|
direction += diffX / distance > 0.5 ? "W" : "";
|
||||||
|
direction += diffX / distance < -0.5 ? "E" : "";
|
||||||
|
setSprite(direction, frameCount);
|
||||||
|
|
||||||
|
cursormarseyPosX -= (diffX / distance) * cursormarseySpeed;
|
||||||
|
cursormarseyPosY -= (diffY / distance) * cursormarseySpeed;
|
||||||
|
|
||||||
|
cursormarseyPosX = Math.min(Math.max(16, cursormarseyPosX), innerWidth - 16);
|
||||||
|
cursormarseyPosY = Math.min(Math.max(16, cursormarseyPosY), innerHeight - 16);
|
||||||
|
|
||||||
|
cursormarseyEl.style.left = `${cursormarseyPosX}px`;
|
||||||
|
cursormarseyEl.style.top = `${cursormarseyPosY}px`;
|
||||||
|
heartEl.style.left = `${cursormarseyPosX+16}px`;
|
||||||
|
heartEl.style.top = `${cursormarseyPosY-16}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.onmousemove = (event) => {
|
||||||
|
mousePosX = event.clientX;
|
||||||
|
mousePosY = event.clientY;
|
||||||
|
};
|
||||||
|
window.marseykoInterval = setInterval(frame, 100);
|
||||||
|
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
cursormarseyEl.style.removeProperty("pointer-events");
|
||||||
|
let elementClicked = document.elementFromPoint(event.clientX,event.clientY);
|
||||||
|
if (elementClicked.id === cursormarseyEl.id) {
|
||||||
|
heartEl.classList.remove("d-none");
|
||||||
|
setTimeout(() => {
|
||||||
|
heartEl.classList.add("d-none");
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
cursormarseyEl.style.pointerEvents = "none";
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,585 +1,444 @@
|
||||||
/*
|
// This code isn't for feeble minds, you might not understand it, Dr. Transmisia.
|
||||||
This program is free software: you can redistribute it and/or modify
|
// Lappland, you are an absolute idiot and an embarrassment to the Rhodesian people.
|
||||||
it under the terms of the GNU Affero General Public License as
|
// I have done the very thing that you decried impractical.
|
||||||
published by the Free Software Foundation, either version 3 of the
|
// The dainty hands of a trans goddess wrote this code. Watch the way her fingers
|
||||||
License, or (at your option) any later version.
|
// dance across the keyboard and learn.
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Copyright (C) 2022 Dr Steven Transmisia, anti-evil engineer,
|
// MIT License. Written by @transbitch
|
||||||
2022 Nekobit, king autist
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Status
|
|
||||||
/**
|
/**
|
||||||
* inactive - user has not tried using an emoji
|
* currently unused, the type of each emoji that https://rdrama.net/emojis.json returns.
|
||||||
* loading - user has tried to use an emoji, and the engine is initializing itself
|
* @typedef {object} EmojiDef
|
||||||
* ready - engine can handle all emoji usage
|
* @property {number} author_id
|
||||||
* @type {"inactive"|"loading"|"ready"}
|
* @property {string} author_original_username
|
||||||
|
* @property {string} author_username
|
||||||
|
* @property {number} count
|
||||||
|
* @property {number} created_utc
|
||||||
|
* @property {string} kind
|
||||||
|
* @property {string} name
|
||||||
|
* @property {number | null} submitter_id
|
||||||
|
* @property {string[]} tags
|
||||||
*/
|
*/
|
||||||
let emojiEngineState = "inactive";
|
|
||||||
|
|
||||||
// DOM stuff
|
|
||||||
const classesSelectorDOM = document.getElementById("emoji-modal-tabs");
|
|
||||||
const emojiButtonTemplateDOM = document.getElementById("emoji-button-template");
|
|
||||||
const emojiResultsDOM = document.getElementById("tab-content");
|
|
||||||
|
|
||||||
const emojiSelectSuffixDOMs = document.getElementsByClassName("emoji-suffix");
|
|
||||||
const emojiSelectPostfixDOMs= document.getElementsByClassName("emoji-postfix");
|
|
||||||
|
|
||||||
const emojiNotFoundDOM = document.getElementById("no-emojis-found");
|
|
||||||
const emojiWorkingDOM = document.getElementById("emojis-work");
|
|
||||||
|
|
||||||
const emojiSearchBarDOM = document.getElementById('emoji_search');
|
|
||||||
|
|
||||||
let emojiInputTargetDOM = undefined;
|
|
||||||
|
|
||||||
// Emojis usage stats. I don't really like this format but I'll keep it for backward comp.
|
|
||||||
const favorite_emojis = JSON.parse(localStorage.getItem("favorite_emojis")) || {};
|
|
||||||
|
|
||||||
/** Associative array of all the emojis' DOM */
|
|
||||||
let emojiDOMs = {};
|
|
||||||
|
|
||||||
let globalEmojis;
|
|
||||||
|
|
||||||
const EMOIJ_SEARCH_ENGINE_MIN_INTERVAL = 350;
|
|
||||||
let emojiSearcher = {
|
|
||||||
working: false,
|
|
||||||
queries: [],
|
|
||||||
|
|
||||||
addQuery: function(query)
|
|
||||||
{
|
|
||||||
this.queries.push(query);
|
|
||||||
if (!this.working)
|
|
||||||
this.work();
|
|
||||||
},
|
|
||||||
|
|
||||||
work: async function work() {
|
|
||||||
this.working = true;
|
|
||||||
|
|
||||||
while(this.queries.length > 0)
|
|
||||||
{
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
// Get last input
|
|
||||||
const query = this.queries[this.queries.length - 1].toLowerCase();
|
|
||||||
this.queries = [];
|
|
||||||
|
|
||||||
// To improve perf we avoid showing all emojis at the same time.
|
|
||||||
if (query === "")
|
|
||||||
{
|
|
||||||
await classesSelectorDOM.children[0].children[0].click();
|
|
||||||
classesSelectorDOM.children[0].children[0].classList.add("active");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search
|
|
||||||
const resultSet = emojisSearchDictionary.completeSearch(query);
|
|
||||||
|
|
||||||
// update stuff
|
|
||||||
for(const [emojiName, emojiDOM] of Object.entries(emojiDOMs))
|
|
||||||
emojiDOM.hidden = !resultSet.has(emojiName);
|
|
||||||
|
|
||||||
emojiNotFoundDOM.hidden = resultSet.size !== 0;
|
|
||||||
|
|
||||||
let sleepTime = EMOIJ_SEARCH_ENGINE_MIN_INTERVAL - (Date.now() - startTime);
|
|
||||||
if (sleepTime > 0)
|
|
||||||
await new Promise(r => setTimeout(r, sleepTime));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.working = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// tags dictionary. KEEP IT SORT
|
|
||||||
class EmoijsDictNode
|
|
||||||
{
|
|
||||||
constructor(tag, name) {
|
|
||||||
this.tag = tag;
|
|
||||||
this.emojiNames = [name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const emojisSearchDictionary = {
|
|
||||||
dict: [],
|
|
||||||
|
|
||||||
updateTag: function(tag, emojiName) {
|
|
||||||
if (tag === undefined || emojiName === undefined)
|
|
||||||
return;
|
|
||||||
|
|
||||||
let low = 0;
|
|
||||||
let high = this.dict.length;
|
|
||||||
|
|
||||||
while (low < high) {
|
|
||||||
let mid = (low + high) >>> 1;
|
|
||||||
if (this.dict[mid].tag < tag)
|
|
||||||
low = mid + 1;
|
|
||||||
else
|
|
||||||
high = mid;
|
|
||||||
}
|
|
||||||
|
|
||||||
let target = low;
|
|
||||||
if (this.dict[target] !== undefined && this.dict[target].tag === tag)
|
|
||||||
this.dict[target].emojiNames.push(emojiName);
|
|
||||||
else
|
|
||||||
this.dict.splice(target ,0,new EmoijsDictNode(tag, emojiName));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We also check for substrings! (sigh)
|
|
||||||
* @param {String} tag
|
|
||||||
* @returns {Set}
|
|
||||||
*/
|
|
||||||
completeSearch: function(query) {
|
|
||||||
query = query.toLowerCase()
|
|
||||||
const result = new Set();
|
|
||||||
|
|
||||||
for(let i = 0; i < this.dict.length; i++)
|
|
||||||
if (this.dict[i].tag.startsWith('@'))
|
|
||||||
{
|
|
||||||
if (this.dict[i].tag == query)
|
|
||||||
for(let j = 0; j < this.dict[i].emojiNames.length; j++)
|
|
||||||
result.add(this.dict[i].emojiNames[j])
|
|
||||||
}
|
|
||||||
else if(this.dict[i].tag.includes(query))
|
|
||||||
for(let j = 0; j < this.dict[i].emojiNames.length; j++)
|
|
||||||
result.add(this.dict[i].emojiNames[j])
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// get public emojis list
|
|
||||||
function fetchEmojis() {
|
|
||||||
const headers = new Headers({xhr: "xhr"})
|
|
||||||
return fetch("/emojis_json", {
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(emojis => {
|
|
||||||
if (! (emojis instanceof Array ))
|
|
||||||
throw new TypeError("[EMOJI DIALOG] rDrama's server should have sent a JSON-coded Array!");
|
|
||||||
|
|
||||||
globalEmojis = emojis.map(({name, author, count}) => ({name, author, count}));
|
|
||||||
|
|
||||||
let classes = ["Marsey", "Platy", "Wolf", "Donkey Kong", "Tay", "Capy", "Carp", "Marsey Flags", "Marsey Alphabet", "Classic", "Rage", "Wojak", "Misc"]
|
|
||||||
|
|
||||||
const bussyDOM = document.createElement("div");
|
|
||||||
|
|
||||||
for(let i = 0; i < emojis.length; i++)
|
|
||||||
{
|
|
||||||
const emoji = emojis[i];
|
|
||||||
|
|
||||||
emojisSearchDictionary.updateTag(emoji.name, emoji.name);
|
|
||||||
|
|
||||||
if (emoji.author_username !== undefined && emoji.author_username !== null)
|
|
||||||
emojisSearchDictionary.updateTag(`@${emoji.author_username.toLowerCase()}`, emoji.name);
|
|
||||||
|
|
||||||
if (emoji.author_original_username !== undefined && emoji.author_original_username !== null)
|
|
||||||
emojisSearchDictionary.updateTag(`@${emoji.author_original_username.toLowerCase()}`, emoji.name);
|
|
||||||
|
|
||||||
if (emoji.author_prelock_username !== undefined && emoji.author_prelock_username !== null)
|
|
||||||
emojisSearchDictionary.updateTag(`@${emoji.author_prelock_username.toLowerCase()}`, emoji.name);
|
|
||||||
|
|
||||||
if (emoji.tags instanceof Array)
|
|
||||||
for(let i = 0; i < emoji.tags.length; i++)
|
|
||||||
emojisSearchDictionary.updateTag(emoji.tags[i], emoji.name);
|
|
||||||
|
|
||||||
// Create emoji DOM
|
|
||||||
const emojiDOM = document.importNode(emojiButtonTemplateDOM.content, true).children[0];
|
|
||||||
|
|
||||||
emojiDOM.title = emoji.name
|
|
||||||
if (emoji.author_username !== undefined && emoji.author_username !== null)
|
|
||||||
emojiDOM.title += "\nauthor\t" + emoji.author_username
|
|
||||||
if (emoji.count !== undefined)
|
|
||||||
emojiDOM.title += "\nused\t" + emoji.count;
|
|
||||||
emojiDOM.dataset.className = emoji.kind;
|
|
||||||
emojiDOM.dataset.emojiName = emoji.name;
|
|
||||||
emojiDOM.onclick = emojiAddToInput;
|
|
||||||
emojiDOM.hidden = true;
|
|
||||||
|
|
||||||
const emojiIMGDOM = emojiDOM.children[0];
|
|
||||||
emojiIMGDOM.src = `${SITE_FULL_IMAGES}/e/${emoji.name}.webp`
|
|
||||||
emojiIMGDOM.alt = emoji.name;
|
|
||||||
/** Disableing lazy loading seems to reduce cpu usage somehow (?)
|
|
||||||
* idk it is difficult to benchmark */
|
|
||||||
emojiIMGDOM.loading = "lazy";
|
|
||||||
|
|
||||||
// Save reference
|
|
||||||
emojiDOMs[emoji.name] = emojiDOM;
|
|
||||||
|
|
||||||
// Add to the document!
|
|
||||||
bussyDOM.appendChild(emojiDOM);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create header
|
|
||||||
for(let className of classes)
|
|
||||||
{
|
|
||||||
let classSelectorDOM = document.createElement("li");
|
|
||||||
classSelectorDOM.classList.add("nav-item");
|
|
||||||
|
|
||||||
let classSelectorLinkDOM = document.createElement("button");
|
|
||||||
classSelectorLinkDOM.type = "button";
|
|
||||||
classSelectorLinkDOM.classList.add("nav-link", "emojitab");
|
|
||||||
classSelectorLinkDOM.dataset.bsToggle = "tab";
|
|
||||||
classSelectorLinkDOM.dataset.className = className;
|
|
||||||
classSelectorLinkDOM.textContent = className;
|
|
||||||
classSelectorLinkDOM.addEventListener('click', switchEmojiTab);
|
|
||||||
|
|
||||||
classSelectorDOM.appendChild(classSelectorLinkDOM);
|
|
||||||
classesSelectorDOM.appendChild(classSelectorDOM);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show favorite for start.
|
|
||||||
classesSelectorDOM.children[0].children[0].click();
|
|
||||||
|
|
||||||
// Send it to the render machine!
|
|
||||||
emojiResultsDOM.appendChild(bussyDOM);
|
|
||||||
|
|
||||||
emojiResultsDOM.hidden = false;
|
|
||||||
emojiWorkingDOM.hidden = true;
|
|
||||||
emojiSearchBarDOM.disabled = false;
|
|
||||||
|
|
||||||
emojiEngineState = "ready";
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @typedef {{[index: string]: [string, number][]}} EmojiTags
|
||||||
* @param {Event} e
|
* @typedef {{[kind: string]: [string, number][]}} EmojiKinds
|
||||||
*/
|
*/
|
||||||
function switchEmojiTab(e)
|
|
||||||
|
class EmojiEngine {
|
||||||
|
_res;
|
||||||
|
/** @type {Promise<void>} */
|
||||||
|
loaded = new Promise(res => this._res = res);
|
||||||
|
hasLoaded = false;
|
||||||
|
|
||||||
|
/** @type {EmojiTags} */
|
||||||
|
tags = {};
|
||||||
|
|
||||||
|
/** @type {EmojiKinds} */
|
||||||
|
kinds = {};
|
||||||
|
|
||||||
|
// Memoize this value so we don't have to recompute it.
|
||||||
|
_tag_entries;
|
||||||
|
|
||||||
|
/** @type {{[index: string]: HTMLDivElement}} */
|
||||||
|
emojiDom = {};
|
||||||
|
|
||||||
|
/** @type {{[index: string]: number}} */
|
||||||
|
emojiNameCount = {};
|
||||||
|
|
||||||
|
/** @type {(name: string) => void} */
|
||||||
|
onInsert;
|
||||||
|
|
||||||
|
init = async () => {
|
||||||
|
if (this.hasLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.loadTags(),
|
||||||
|
this.loadKinds(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this._tag_entries = Object.entries(this.tags);
|
||||||
|
|
||||||
|
this._res();
|
||||||
|
this.hasLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTags = async () => {
|
||||||
|
this.tags = await (await fetch('/emoji_tags.json')).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadKinds = async () => {
|
||||||
|
this.kinds = await (await fetch('/emoji_kinds.json')).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
search = async (query, maxLength = Infinity) => {
|
||||||
|
await this.loaded;
|
||||||
|
|
||||||
|
const resultsSet = new Set();
|
||||||
|
const results = [];
|
||||||
|
for (const [tag, entries] of this._tag_entries) {
|
||||||
|
if (!tag.includes(query)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, count] of entries) {
|
||||||
|
if (resultsSet.has(name)) {
|
||||||
|
continue;
|
||||||
|
} else if (count < results[maxLength - 1]?.[1]) {
|
||||||
|
// All the other emojis in this tag have less uses. We can stop here.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsSet.add(name);
|
||||||
|
// Insert into the array sorted.
|
||||||
|
let i = results.length;
|
||||||
|
while (i > 0 && count > results[i - 1][1]) {
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
results.splice(i, 0, [name, count]);
|
||||||
|
if (results.length >= maxLength) {
|
||||||
|
const [name] = results.pop();
|
||||||
|
resultsSet.delete(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.map(([name]) => name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a dom element for a list of emojis in quick dropdown.
|
||||||
|
* @param {string[]} emojiNames
|
||||||
|
*/
|
||||||
|
getQuickDoms = (emojiNames) => {
|
||||||
|
return emojiNames.map(this.getQuickDom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} emojiName
|
||||||
|
* @returns DOM element for an emoji quick dropdown.
|
||||||
|
*/
|
||||||
|
getQuickDom = (emojiName) => {
|
||||||
|
if (this.emojiDom[emojiName]) {
|
||||||
|
return this.emojiDom[emojiName];
|
||||||
|
}
|
||||||
|
|
||||||
|
const emojiEl = document.createElement('button');
|
||||||
|
emojiEl.classList.add('quick-emoji-option', 'emoji-option');
|
||||||
|
emojiEl.addEventListener('click', (e) => {
|
||||||
|
this.onInsert(emojiName);
|
||||||
|
});
|
||||||
|
|
||||||
|
const emojiImgEl = document.createElement('img');
|
||||||
|
emojiImgEl.classList.add('quick-emoji-image', 'emoji-option-image');
|
||||||
|
emojiImgEl.src = emojiEngine.src(emojiName);
|
||||||
|
emojiEl.appendChild(emojiImgEl);
|
||||||
|
|
||||||
|
const emojiNameEl = document.createElement('span');
|
||||||
|
emojiNameEl.textContent = emojiName;
|
||||||
|
emojiEl.appendChild(emojiNameEl);
|
||||||
|
|
||||||
|
this.emojiDom[emojiName] = emojiEl;
|
||||||
|
return emojiEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
src = (name) => {
|
||||||
|
return `${SITE_FULL_IMAGES}/e/${name}.webp`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emojiEngine = new EmojiEngine();
|
||||||
|
|
||||||
|
// Quick emoji dropdown & emoji insertion
|
||||||
{
|
{
|
||||||
const className = e.currentTarget.dataset.className;
|
const emojiDropdownEl = document.createElement('div');
|
||||||
|
emojiDropdownEl.classList.add('quick-emoji-dropdown');
|
||||||
|
/** @type {null | HTMLTextAreaElement} */
|
||||||
|
let inputEl = null;
|
||||||
|
let visible = false;
|
||||||
|
let typingEmojiCanceled = false;
|
||||||
|
let firstDomEl = null;
|
||||||
|
let firstEmojiName = null;
|
||||||
|
let caretPos = 0;
|
||||||
|
|
||||||
emojiSearchBarDOM.value = "";
|
// Used by onclick attrib of the smile button
|
||||||
focusSearchBar(emojiSearchBarDOM);
|
window.openEmojiModal = (id) => {
|
||||||
emojiNotFoundDOM.hidden = true;
|
inputEl = document.getElementById(id);
|
||||||
|
initEmojiModal();
|
||||||
|
}
|
||||||
|
|
||||||
// Special case: favorites
|
emojiEngine.onInsert = (name) => {
|
||||||
if (className === "favorite")
|
if (!inputEl) {
|
||||||
{
|
return;
|
||||||
for(const emojiDOM of Object.values(emojiDOMs))
|
}
|
||||||
emojiDOM.hidden = true;
|
const match = matchTypingEmoji();
|
||||||
|
if (match) {
|
||||||
|
// We are inserting an emoji which we are typing.
|
||||||
|
inputEl.value = `${inputEl.value.slice(0, match.index)}:${name}:${inputEl.value.slice(match.index + name.length)} `;
|
||||||
|
// Draw the focus back to this element.
|
||||||
|
inputEl.focus();
|
||||||
|
} else {
|
||||||
|
// We are inserting a new emoji.
|
||||||
|
const start = inputEl.value.slice(0, caretPos);
|
||||||
|
const end = inputEl.value.slice(caretPos);
|
||||||
|
const insert = `:${name}:${end.length === 0 ? ' ' : ''}`;
|
||||||
|
inputEl.value = `${start}${insert}${end}`;
|
||||||
|
caretPos += insert.length;
|
||||||
|
inputEl.setSelectionRange(caretPos, caretPos);
|
||||||
|
}
|
||||||
|
|
||||||
const favs = Object.keys(Object.fromEntries(
|
typingEmojiCanceled = false;
|
||||||
Object.entries(favorite_emojis).sort(([,a],[,b]) => b-a)
|
update();
|
||||||
)).slice(0, 25);
|
|
||||||
|
|
||||||
for (const emoji of favs)
|
// This updates the preview.
|
||||||
if (emojiDOMs[emoji] instanceof HTMLElement)
|
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
emojiDOMs[emoji].hidden = false;
|
|
||||||
|
|
||||||
return;
|
// Update the favorite count.
|
||||||
}
|
if (name in favoriteEmojis) {
|
||||||
|
favoriteEmojis[name]++;
|
||||||
|
} else {
|
||||||
|
favoriteEmojis[name] = 1;
|
||||||
|
}
|
||||||
|
localStorage.setItem("favorite_emojis", JSON.stringify(favoriteEmojis));
|
||||||
|
}
|
||||||
|
|
||||||
for(const emojiDOM of Object.values(emojiDOMs))
|
const inputCanTakeEmojis = (el = inputEl) => {
|
||||||
emojiDOM.hidden = emojiDOM.dataset.className !== className;
|
return el?.dataset && 'emojis' in el.dataset;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('emoji-container').scrollTop = 0;
|
const matchTypingEmoji = () => {
|
||||||
|
return inputEl?.value.substring(0, inputEl.selectionEnd).match(/:([\w!#]+)$/);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypingEmoji = () => {
|
||||||
|
return matchTypingEmoji()?.[1] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTypingEmoji = () => {
|
||||||
|
return inputCanTakeEmojis() && getTypingEmoji();
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTypingEmoji = () => {
|
||||||
|
typingEmojiCanceled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = async () => {
|
||||||
|
const typing = isTypingEmoji();
|
||||||
|
visible = typing && !typingEmojiCanceled;
|
||||||
|
if (!visible) {
|
||||||
|
emojiDropdownEl.parentElement?.removeChild(emojiDropdownEl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const oldFirst = firstDomEl;
|
||||||
|
document.body.appendChild(emojiDropdownEl);
|
||||||
|
const search = await emojiEngine.search(getTypingEmoji(), 15);
|
||||||
|
firstEmojiName = search[0];
|
||||||
|
const domEls = emojiEngine.getQuickDoms(search);
|
||||||
|
firstDomEl = domEls[0];
|
||||||
|
if (oldFirst !== firstDomEl) {
|
||||||
|
oldFirst?.classList.remove('selected');
|
||||||
|
firstDomEl.classList.add('selected');
|
||||||
|
}
|
||||||
|
emojiDropdownEl.replaceChildren(...domEls);
|
||||||
|
const { left, bottom } = getCaretPos(inputEl);
|
||||||
|
// Using transform instead of top/left is faster.
|
||||||
|
emojiDropdownEl.style.transform = `translate(${left}px, ${bottom}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a listener when we start typing.
|
||||||
|
/**
|
||||||
|
* @param {FocusEvent} e
|
||||||
|
*/
|
||||||
|
const onKeyStart = (e) => {
|
||||||
|
if (inputCanTakeEmojis(e.target)) {
|
||||||
|
inputEl = e.target;
|
||||||
|
emojiEngine.init();
|
||||||
|
window.removeEventListener('keydown', onKeyStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', onKeyStart);
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (!visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isFocused = document.activeElement === inputEl
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
typingEmojiCanceled = true;
|
||||||
|
update();
|
||||||
|
} else if (e.key === 'Enter' && isFocused) {
|
||||||
|
emojiEngine.onInsert(firstEmojiName);
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'Tab' && isFocused) {
|
||||||
|
firstDomEl.focus();
|
||||||
|
firstDomEl.classList.remove("selected");
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
['input', 'click', 'focus'].forEach((event) => {
|
||||||
|
window.addEventListener(event, (e) => {
|
||||||
|
if (inputCanTakeEmojis(e.target)) {
|
||||||
|
inputEl = e.target;
|
||||||
|
caretPos = inputEl.selectionEnd;
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
if (!isTypingEmoji()) {
|
||||||
|
endTypingEmoji();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const emojitab of document.getElementsByClassName('emojitab')) {
|
/** @type {{ [name: string]: number }} */
|
||||||
emojitab.addEventListener('click', (e)=>{switchEmojiTab(e)})
|
const favoriteEmojis = JSON.parse(localStorage.getItem("favorite_emojis")) || {};
|
||||||
}
|
|
||||||
|
|
||||||
async function start_search() {
|
const initEmojiModal = (() => {
|
||||||
emojiSearcher.addQuery(emojiSearchBarDOM.value.trim());
|
let hasInit = false;
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
if (hasInit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hasInit = true;
|
||||||
|
|
||||||
// Remove any selected tab, now it is meaningless
|
await emojiEngine.init();
|
||||||
for(let i = 0; i < classesSelectorDOM.children.length; i++)
|
|
||||||
classesSelectorDOM.children[i].children[0].classList.remove("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
document.getElementById('emojis-work').style.display = 'none';
|
||||||
* Add the selected emoji to the targeted text area
|
|
||||||
* @param {Event} event
|
|
||||||
*/
|
|
||||||
function emojiAddToInput(event)
|
|
||||||
{
|
|
||||||
// This should not happen if used properly but whatever
|
|
||||||
if (!(emojiInputTargetDOM instanceof HTMLTextAreaElement) && !(emojiInputTargetDOM instanceof HTMLInputElement))
|
|
||||||
return;
|
|
||||||
|
|
||||||
let strToInsert = event.currentTarget.dataset.emojiName;
|
/** @type {{ [tabName: string]: HTMLDivElement }} */
|
||||||
|
const tabContentEls = {}
|
||||||
|
|
||||||
for(let i = 0; i < emojiSelectPostfixDOMs.length; i++)
|
/** @type {(kind: string, el: HTMLButtonElement) => void} */
|
||||||
if (emojiSelectPostfixDOMs[i].checked)
|
const addTabClickListener = (kind, el) => {
|
||||||
strToInsert = strToInsert + emojiSelectPostfixDOMs[i].value;
|
el.addEventListener('click', (e) => {
|
||||||
|
setTab(kind);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for(let i = 0; i < emojiSelectSuffixDOMs.length; i++)
|
const favorites = Object.entries(favoriteEmojis).sort((a, b) => b[1] - a[1]);
|
||||||
if (emojiSelectSuffixDOMs[i].checked)
|
/** @type {{ [name: string]: HTMLButtonElement }} */
|
||||||
strToInsert = emojiSelectSuffixDOMs[i].value + strToInsert;
|
const favoriteClones = {};
|
||||||
|
|
||||||
strToInsert = ":" + strToInsert + ":"
|
const favoriteContentEl = (() => {
|
||||||
insertText(emojiInputTargetDOM, strToInsert)
|
const content = document.createElement('div');
|
||||||
|
tabContentEls['favorite'] = content;
|
||||||
|
return content;
|
||||||
|
})();
|
||||||
|
|
||||||
// kick-start the preview
|
let currentTab = 'favorite';
|
||||||
emojiInputTargetDOM.dispatchEvent(new Event('input'));
|
const setTab = (kind) => {
|
||||||
|
currentTab = kind;
|
||||||
|
tabContent.replaceChildren(tabContentEls[kind]);
|
||||||
|
}
|
||||||
|
|
||||||
// Update favs. from old code
|
const emojiModal = document.getElementById('emojiModal');
|
||||||
if (favorite_emojis[event.currentTarget.dataset.emojiName])
|
const emojiTabsEl = document.getElementById('emoji-modal-tabs');
|
||||||
favorite_emojis[event.currentTarget.dataset.emojiName] += 1;
|
const tabContent = document.getElementById('emoji-tab-content');
|
||||||
else
|
/** @type {HTMLInputElement} */
|
||||||
favorite_emojis[event.currentTarget.dataset.emojiName] = 1;
|
const searchInputEl = document.getElementById('emoji_search');
|
||||||
localStorage.setItem("favorite_emojis", JSON.stringify(favorite_emojis));
|
searchInputEl.disabled = false;
|
||||||
}
|
const searchResultsContainerEl = document.createElement('div');
|
||||||
|
let isSearching = false;
|
||||||
|
/** @type {{ [index: string ]: HTMLButtonElement }} */
|
||||||
|
const searchResultsEl = {};
|
||||||
|
const favoriteTabEl = document.getElementById('emoji-modal-tabs-favorite');
|
||||||
|
addTabClickListener('favorite', favoriteTabEl);
|
||||||
|
|
||||||
let emoji_typing_state = false;
|
window.emojiSearch = async () => {
|
||||||
|
if (searchInputEl.value.length === 0 && isSearching) {
|
||||||
|
isSearching = false;
|
||||||
|
setTab(currentTab);
|
||||||
|
} else if (searchInputEl.value.length > 0 && !isSearching) {
|
||||||
|
isSearching = true;
|
||||||
|
tabContent.replaceChildren(searchResultsContainerEl);
|
||||||
|
}
|
||||||
|
|
||||||
function update_ghost_div_textarea(text)
|
if (isSearching) {
|
||||||
{
|
const query = searchInputEl.value;
|
||||||
let ghostdiv
|
requestIdleCallback(() => {
|
||||||
|
emojiEngine.search(query).then((results) => {
|
||||||
|
requestIdleCallback(() => {
|
||||||
|
searchResultsContainerEl.replaceChildren(...results.map((name) => searchResultsEl[name]));
|
||||||
|
}, { timeout: 100 });
|
||||||
|
});
|
||||||
|
}, { timeout: 100 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (location.pathname == '/chat')
|
const promises = Object.entries(emojiEngine.kinds).map(([kind, emojis]) => new Promise((res) => {
|
||||||
ghostdiv = document.getElementById("ghostdiv-chat");
|
const tabEl = (() => {
|
||||||
else
|
const tab = document.createElement('li');
|
||||||
ghostdiv = text.parentNode.getElementsByClassName("ghostdiv")[0];
|
const button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.classList.add('nav-link', 'emojitab');
|
||||||
|
button.dataset.bsToggle = 'tab';
|
||||||
|
button.textContent = kind;
|
||||||
|
tab.appendChild(button);
|
||||||
|
emojiTabsEl.appendChild(tab);
|
||||||
|
addTabClickListener(kind, tab);
|
||||||
|
return tab;
|
||||||
|
})();
|
||||||
|
|
||||||
if (!ghostdiv) return;
|
const tabContentEl = (() => {
|
||||||
|
const tabContent = document.createElement('div');
|
||||||
|
return tabContent;
|
||||||
|
})();
|
||||||
|
|
||||||
ghostdiv.textContent = text.value.substring(0, text.selectionStart);
|
tabContentEls[kind] = tabContentEl;
|
||||||
|
|
||||||
ghostdiv.insertAdjacentHTML('beforeend', "<span></span>");
|
const tick = () => {
|
||||||
|
for (const [name, count] of emojis) {
|
||||||
|
const buttonEl = document.createElement('button');
|
||||||
|
buttonEl.type = 'button';
|
||||||
|
buttonEl.classList.add('btn', 'm-1', 'px-0', 'emoji2');
|
||||||
|
buttonEl.title = `${name} (${count})`;
|
||||||
|
|
||||||
// Now lets get coordinates
|
const imgEl = document.createElement('img');
|
||||||
|
imgEl.loading = 'lazy';
|
||||||
|
imgEl.src = emojiEngine.src(name);
|
||||||
|
imgEl.alt = name;
|
||||||
|
buttonEl.appendChild(imgEl);
|
||||||
|
|
||||||
ghostdiv.style.display = "block";
|
const searchClone = buttonEl.cloneNode(true);
|
||||||
let end = ghostdiv.querySelector("span");
|
const els = [buttonEl, searchClone];
|
||||||
const carot_coords = end.getBoundingClientRect();
|
|
||||||
const ghostdiv_coords = ghostdiv.getBoundingClientRect();
|
|
||||||
ghostdiv.style.display = "none";
|
|
||||||
return { pos: text.selectionStart, x: carot_coords.x, y: carot_coords.y - ghostdiv_coords.y };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used for anything where a user is typing, specifically for the emoji modal
|
if (name in favoriteEmojis) {
|
||||||
// Just leave it global, I don't care
|
const favoriteClone = buttonEl.cloneNode(true);
|
||||||
let speed_carot_modal = document.createElement("div");
|
favoriteClone.title = `${name} (${favoriteEmojis[name]})`;
|
||||||
speed_carot_modal.id = "speed-carot-modal";
|
els.push(favoriteClone);
|
||||||
speed_carot_modal.style.position = "absolute";
|
favoriteClones[name] = favoriteClone;
|
||||||
speed_carot_modal.style.left = "0px";
|
}
|
||||||
speed_carot_modal.style.top = "0px";
|
|
||||||
speed_carot_modal.style.display = "none";
|
els.forEach((el) => {
|
||||||
document.body.appendChild(speed_carot_modal);
|
el.addEventListener('click', (e) => {
|
||||||
|
emojiEngine.onInsert(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
let e
|
tabContentEl.appendChild(buttonEl);
|
||||||
|
searchResultsEl[name] = searchClone;
|
||||||
|
}
|
||||||
|
|
||||||
let current_word = "";
|
res();
|
||||||
let selecting;
|
|
||||||
let emoji_index = 0;
|
|
||||||
|
|
||||||
function curr_word_is_emoji()
|
}
|
||||||
{
|
requestIdleCallback(tick, { timeout: 250 });
|
||||||
return current_word && current_word.charAt(0) == ":" &&
|
}));
|
||||||
current_word.charAt(current_word.length-1) != ":";
|
|
||||||
}
|
|
||||||
|
|
||||||
function close_inline_speed_emoji_modal() {
|
Promise.all(promises).then(() => {
|
||||||
selecting = false;
|
for (const [name] of favorites) {
|
||||||
speed_carot_modal.style.display = "none";
|
if (!(name in favoriteClones)) {
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
favoriteContentEl.appendChild(favoriteClones[name]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function populate_speed_emoji_modal(results, textbox)
|
setTab(currentTab);
|
||||||
{
|
}
|
||||||
selecting = true;
|
})();
|
||||||
|
|
||||||
if (!results || results.size === 0)
|
|
||||||
{
|
|
||||||
speed_carot_modal.style.display = "none";
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
emoji_index = 0;
|
|
||||||
speed_carot_modal.scrollTop = 0;
|
|
||||||
speed_carot_modal.innerHTML = "";
|
|
||||||
const MAXXX = 50;
|
|
||||||
// Not sure why the results is a Set... but oh well
|
|
||||||
let i = 0;
|
|
||||||
for (let emoji of results)
|
|
||||||
{
|
|
||||||
let name = emoji.name
|
|
||||||
|
|
||||||
if (i++ > MAXXX) return i;
|
|
||||||
let emoji_option = document.createElement("div");
|
|
||||||
emoji_option.className = "speed-modal-option emoji-option " + (i === 1 ? "selected" : "");
|
|
||||||
emoji_option.tabIndex = 0;
|
|
||||||
let emoji_option_img = document.createElement("img");
|
|
||||||
emoji_option_img.className = "speed-modal-image emoji-option-image";
|
|
||||||
// This is a bit
|
|
||||||
emoji_option_img.src = `${SITE_FULL_IMAGES}/e/${name}.webp`
|
|
||||||
let emoji_option_text = document.createElement("span");
|
|
||||||
|
|
||||||
emoji_option_text.title = name;
|
|
||||||
|
|
||||||
if (emoji.author_username !== undefined && emoji.author_username !== null)
|
|
||||||
emoji_option_text.title += "\nauthor\t" + emoji.author_username
|
|
||||||
|
|
||||||
if (emoji.count !== undefined)
|
|
||||||
emoji_option_text.title += "\nused\t" + emoji.count;
|
|
||||||
|
|
||||||
emoji_option_text.textContent = name;
|
|
||||||
|
|
||||||
if (current_word.includes("#")) name = `#${name}`
|
|
||||||
if (current_word.includes("!")) name = `!${name}`
|
|
||||||
|
|
||||||
emoji_option.addEventListener('click', () => {
|
|
||||||
close_inline_speed_emoji_modal()
|
|
||||||
textbox.value = textbox.value.replace(new RegExp(current_word+"(?=\\s|$)", "gi"), `:${name}: `)
|
|
||||||
textbox.focus()
|
|
||||||
if (typeof markdown === "function" && textbox.dataset.preview) {
|
|
||||||
markdown(textbox)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Pack
|
|
||||||
emoji_option.appendChild(emoji_option_img);
|
|
||||||
emoji_option.appendChild(emoji_option_text);
|
|
||||||
speed_carot_modal.appendChild(emoji_option);
|
|
||||||
}
|
|
||||||
if (i === 0) speed_carot_modal.style.display = "none";
|
|
||||||
else speed_carot_modal.style.display = "initial";
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_speed_emoji_modal(event)
|
|
||||||
{
|
|
||||||
const box_coords = update_ghost_div_textarea(event.target);
|
|
||||||
|
|
||||||
box_coords.x = Math.min(box_coords.x, screen_width - 150)
|
|
||||||
|
|
||||||
let text = event.target.value;
|
|
||||||
|
|
||||||
// Unused, but left incase anyone wants to use this more efficient method for emojos
|
|
||||||
switch (event.data)
|
|
||||||
{
|
|
||||||
case ':':
|
|
||||||
emoji_typing_state = true;
|
|
||||||
break;
|
|
||||||
case ' ':
|
|
||||||
emoji_typing_state = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current word at string, such as ":marse" or "word"
|
|
||||||
let coords = text.indexOf(' ',box_coords.pos);
|
|
||||||
current_word = /:[!#a-zA-Z0-9_]+(?=\n|$)/.exec(text.slice(0, coords === -1 ? text.length : coords));
|
|
||||||
if (current_word) current_word = current_word[0].toLowerCase();
|
|
||||||
|
|
||||||
/* We could also check emoji_typing_state here, which is less accurate but more efficient. I've
|
|
||||||
* kept it unless someone wants to provide an option to toggle it for performance */
|
|
||||||
if (curr_word_is_emoji() && current_word != ":")
|
|
||||||
{
|
|
||||||
loadEmojis(null, null).then( () => {
|
|
||||||
let modal_pos = event.target.getBoundingClientRect();
|
|
||||||
modal_pos.x += window.scrollX;
|
|
||||||
modal_pos.y += window.scrollY;
|
|
||||||
|
|
||||||
speed_carot_modal.style.display = "initial";
|
|
||||||
speed_carot_modal.style.left = box_coords.x - 30 + "px";
|
|
||||||
speed_carot_modal.style.top = modal_pos.y + box_coords.y + 14 + "px";
|
|
||||||
|
|
||||||
// Do the search (and do something with it)
|
|
||||||
const resultSet = emojisSearchDictionary.completeSearch(current_word.substring(1).replace(/[#!]/g, ""));
|
|
||||||
|
|
||||||
const found = globalEmojis.filter(i => resultSet.has(i.name));
|
|
||||||
|
|
||||||
populate_speed_emoji_modal(found, event.target);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
speed_carot_modal.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function speed_carot_navigate(event)
|
|
||||||
{
|
|
||||||
if (!selecting) return;
|
|
||||||
|
|
||||||
let select_items = speed_carot_modal.querySelectorAll(".speed-modal-option");
|
|
||||||
if (!select_items || !curr_word_is_emoji()) return;
|
|
||||||
|
|
||||||
const modal_keybinds = {
|
|
||||||
// go up one, wrapping around to the bottom if pressed at the top
|
|
||||||
ArrowUp: () => emoji_index = ((emoji_index - 1) + select_items.length) % select_items.length,
|
|
||||||
// go down one, wrapping around to the top if pressed at the bottom
|
|
||||||
ArrowDown: () => emoji_index = ((emoji_index + 1) + select_items.length) % select_items.length,
|
|
||||||
// select the emoji
|
|
||||||
Enter: () => select_items[emoji_index].click(),
|
|
||||||
}
|
|
||||||
if (event.key in modal_keybinds)
|
|
||||||
{
|
|
||||||
select_items[emoji_index].classList.remove("selected");
|
|
||||||
modal_keybinds[event.key]();
|
|
||||||
select_items[emoji_index].classList.add("selected");
|
|
||||||
select_items[emoji_index].scrollIntoView({inline: "end", block: "nearest"});
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertGhostDivs(element) {
|
|
||||||
let forms = element.querySelectorAll("textarea, .allow-emojis");
|
|
||||||
forms.forEach(i => {
|
|
||||||
let ghostdiv
|
|
||||||
if (i.id == 'input-text-chat') {
|
|
||||||
ghostdiv = document.getElementsByClassName("ghostdiv")[0];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ghostdiv = document.createElement("div");
|
|
||||||
ghostdiv.className = "ghostdiv";
|
|
||||||
ghostdiv.style.display = "none";
|
|
||||||
i.after(ghostdiv);
|
|
||||||
}
|
|
||||||
i.addEventListener('input', update_speed_emoji_modal, false);
|
|
||||||
i.addEventListener('keydown', speed_carot_navigate, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojiModal = document.getElementById('emojiModal')
|
|
||||||
|
|
||||||
function loadEmojis(t, inputTargetIDName)
|
|
||||||
{
|
|
||||||
selecting = false;
|
|
||||||
speed_carot_modal.style.display = "none";
|
|
||||||
|
|
||||||
if (inputTargetIDName) {
|
|
||||||
emojiInputTargetDOM = document.getElementById(inputTargetIDName);
|
|
||||||
emojiModal.addEventListener('hide.bs.modal', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
emojiInputTargetDOM.focus();
|
|
||||||
}, 200);
|
|
||||||
}, {once : true});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t && t.dataset.previousModal) {
|
|
||||||
emojiModal.addEventListener('hide.bs.modal', () => {
|
|
||||||
bootstrap.Modal.getOrCreateInstance(document.getElementById(t.dataset.previousModal)).show()
|
|
||||||
}, {once : true});
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (emojiEngineState) {
|
|
||||||
case "inactive":
|
|
||||||
emojiEngineState = "loading"
|
|
||||||
return fetchEmojis();
|
|
||||||
case "loading":
|
|
||||||
// this works because once the fetch completes, the first keystroke callback will fire and use the current value
|
|
||||||
return Promise.reject();
|
|
||||||
case "ready":
|
|
||||||
return Promise.resolve();
|
|
||||||
default:
|
|
||||||
throw Error("Unknown emoji engine state");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('emojiModal').addEventListener('shown.bs.modal', function () {
|
|
||||||
focusSearchBar(emojiSearchBarDOM);
|
|
||||||
setTimeout(() => {
|
|
||||||
focusSearchBar(emojiSearchBarDOM);
|
|
||||||
}, 200);
|
|
||||||
setTimeout(() => {
|
|
||||||
focusSearchBar(emojiSearchBarDOM);
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
|
@ -1,116 +0,0 @@
|
||||||
// This code isn't for feeble minds, you might not understand it Dr. Transmisia.
|
|
||||||
// The dainty hands of a trans goddess wrote this code. Watch the way her fingers
|
|
||||||
// dance across the keyboard and learn.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {object} EmojiDef
|
|
||||||
* @property {number} author_id
|
|
||||||
* @property {string} author_original_username
|
|
||||||
* @property {string} author_username
|
|
||||||
* @property {number} count
|
|
||||||
* @property {number} created_utc
|
|
||||||
* @property {string} kind
|
|
||||||
* @property {string} name
|
|
||||||
* @property {number | null} submitter_id
|
|
||||||
* @property {string[]} tags
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Returns a promise which can be used to await when the event loop is idle. */
|
|
||||||
const idle = () => {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
requestIdleCallback(resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class TagDict {
|
|
||||||
/**
|
|
||||||
* @type {Map<string,Set<string>>}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_map = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} tag
|
|
||||||
*/
|
|
||||||
get = (tag) => {
|
|
||||||
const set = this._map.get(tag);
|
|
||||||
|
|
||||||
if (!set) {
|
|
||||||
const newSet = new Set();
|
|
||||||
this._map.set(tag, newSet);
|
|
||||||
return newSet;
|
|
||||||
} else {
|
|
||||||
return set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} tag
|
|
||||||
* @param {string} emojiName
|
|
||||||
*/
|
|
||||||
add = (tag, emojiName) => {
|
|
||||||
this.get(tag).add(emojiName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} tag
|
|
||||||
* @param {string} emojiName
|
|
||||||
*/
|
|
||||||
delete = (tag, emojiName) => {
|
|
||||||
this.get(tag).delete(emojiName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If emojiName is not provided, returns whether the tag exists.
|
|
||||||
* Otherwise, returns whether the tag contains the emoji.
|
|
||||||
* @param {string} tag
|
|
||||||
* @param {string} [emojiName]
|
|
||||||
*/
|
|
||||||
has = (tag, emojiName) => {
|
|
||||||
if (emojiName) {
|
|
||||||
return this.get(tag).has(emojiName);
|
|
||||||
} else {
|
|
||||||
return this._map.has(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Symbol.iterator] = () => this._map[Symbol.iterator]();
|
|
||||||
}
|
|
||||||
|
|
||||||
class EmojiEngine {
|
|
||||||
tags = new TagDict();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {EmojiDef[]} emojis
|
|
||||||
*/
|
|
||||||
init = async (emojis) => {
|
|
||||||
for (const emoji of emojis) {
|
|
||||||
this.tags.add(emoji.name, emoji.name);
|
|
||||||
|
|
||||||
for (const tag of emoji.tags) {
|
|
||||||
this.tags.add(tag, emoji.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// I left out tagging by author... lets see if anyone complains...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
search = async(query) => {
|
|
||||||
if (query?.length < 2) {
|
|
||||||
return new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = new Set();
|
|
||||||
for (const [tag, names] of this.tags) {
|
|
||||||
if (tag.includes(query) || query.includes(tag)) {
|
|
||||||
for (const name of names) {
|
|
||||||
results.add(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojiEngine = new EmojiEngine();
|
|
||||||
emojiEngine.init();
|
|
|
@ -87,7 +87,10 @@ def emoji_list(v, kind):
|
||||||
|
|
||||||
|
|
||||||
@cache.cached(make_cache_key=lambda nsfw:f"emojis_{nsfw}")
|
@cache.cached(make_cache_key=lambda nsfw:f"emojis_{nsfw}")
|
||||||
def get_emojis(nsfw):
|
def get_emojis(nsfw = None):
|
||||||
|
if nsfw is None:
|
||||||
|
nsfw = g.show_nsfw
|
||||||
|
|
||||||
emojis = g.db.query(Emoji, User).join(User, Emoji.author_id == User.id).filter(Emoji.submitter_id == None)
|
emojis = g.db.query(Emoji, User).join(User, Emoji.author_id == User.id).filter(Emoji.submitter_id == None)
|
||||||
|
|
||||||
if not nsfw:
|
if not nsfw:
|
||||||
|
@ -106,14 +109,79 @@ def get_emojis(nsfw):
|
||||||
collected.append(emoji.json())
|
collected.append(emoji.json())
|
||||||
return collected
|
return collected
|
||||||
|
|
||||||
@app.get("/emojis_json")
|
@app.get("/emojis.json")
|
||||||
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
||||||
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
||||||
@auth_required
|
@auth_required
|
||||||
def emojis(v):
|
def emojis(v):
|
||||||
return get_emojis(g.show_nsfw)
|
return get_emojis()
|
||||||
|
|
||||||
|
@cache.cached(make_cache_key=lambda nsfw:f"emoji_tags_{nsfw}")
|
||||||
|
@app.get("/emoji_tags.json")
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
||||||
|
@auth_required
|
||||||
|
def emoji_tags(v):
|
||||||
|
emojis = get_emojis()
|
||||||
|
|
||||||
|
tags = {}
|
||||||
|
|
||||||
|
def add_to_tag(tag: str, emoji: Emoji):
|
||||||
|
#Do not add empty tags.
|
||||||
|
if not tag:
|
||||||
|
return
|
||||||
|
|
||||||
|
if tag not in tags:
|
||||||
|
tags[tag] = []
|
||||||
|
|
||||||
|
tags[tag].append([emoji['name'], emoji['count']])
|
||||||
|
|
||||||
|
for emoji in emojis:
|
||||||
|
add_to_tag(emoji['name'], emoji)
|
||||||
|
add_to_tag(emoji['name'][len(emoji['kind'].replace(' ', '')):], emoji)
|
||||||
|
|
||||||
|
for tag in emoji['tags']:
|
||||||
|
add_to_tag(tag, emoji)
|
||||||
|
|
||||||
|
return tags
|
||||||
|
|
||||||
|
@cache.cached(make_cache_key=lambda nsfw:f"emoji_tags_{nsfw}")
|
||||||
|
@app.get("/emoji_names_count.json")
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
||||||
|
@auth_required
|
||||||
|
def emoji_names_count(v):
|
||||||
|
emojis = get_emojis()
|
||||||
|
|
||||||
|
names = {}
|
||||||
|
|
||||||
|
for emoji in emojis:
|
||||||
|
names[emoji['name']] = emoji['count']
|
||||||
|
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
@cache.cached(make_cache_key=lambda nsfw:f"emoji_tags_{nsfw}")
|
||||||
|
@app.get("/emoji_kinds.json")
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
||||||
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID)
|
||||||
|
@auth_required
|
||||||
|
def emoji_kinds(v):
|
||||||
|
order = ["Marsey", "Platy", "Wolf", "Donkey Kong", "Tay", "Capy", "Carp", "Marsey Flags", "Marsey Alphabet", "Classic", "Rage", "Wojak", "Misc"]
|
||||||
|
emoji_kinds = {}
|
||||||
|
|
||||||
|
for kind in order:
|
||||||
|
emoji_kinds[kind] = []
|
||||||
|
|
||||||
|
for emoji in get_emojis():
|
||||||
|
kind = emoji['kind']
|
||||||
|
if kind not in emoji_kinds:
|
||||||
|
emoji_kinds[kind] = []
|
||||||
|
|
||||||
|
emoji_kinds[kind].append([emoji['name'], emoji['count']])
|
||||||
|
|
||||||
|
# Flask will sort the keys alphabetically, so we need to jsonify this manually.
|
||||||
|
return json.dumps(emoji_kinds)
|
||||||
|
|
||||||
@app.get('/sidebar')
|
@app.get('/sidebar')
|
||||||
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400)
|
||||||
|
|
|
@ -255,7 +255,7 @@
|
||||||
{% if v and (v.id == c.author_id or v.admin_level >= PERMS['POST_COMMENT_EDITING']) %}
|
{% if v and (v.id == c.author_id or v.admin_level >= PERMS['POST_COMMENT_EDITING']) %}
|
||||||
<div id="comment-edit-{{c.id}}" class="d-none comment-write collapsed child">
|
<div id="comment-edit-{{c.id}}" class="d-none comment-write collapsed child">
|
||||||
<input hidden name="formkey" value="{{v|formkey}}">
|
<input hidden name="formkey" value="{{v|formkey}}">
|
||||||
<textarea autocomplete="off" {% if v.longpost %}minlength="280"{% endif %} maxlength="{% if v.bird %}140{% else %}10000{% endif %}" data-preview="preview-edit-{{c.id}}" data-nonce="{{g.nonce}}" data-oninput="markdown(this);charLimit('comment-edit-body-{{c.id}}','charcount-edit-{{c.id}}');handle_disabled(this)" id="comment-edit-body-{{c.id}}" data-id="{{c.id}}" name="body" form="comment-edit-form-{{c.id}}" class="file-ta comment-box form-control rounded" placeholder="Add your comment..." rows="3">{{c.body}}</textarea>
|
<textarea data-emojis autocomplete="off" {% if v.longpost %}minlength="280"{% endif %} maxlength="{% if v.bird %}140{% else %}10000{% endif %}" data-preview="preview-edit-{{c.id}}" data-nonce="{{g.nonce}}" data-oninput="markdown(this);charLimit('comment-edit-body-{{c.id}}','charcount-edit-{{c.id}}');handle_disabled(this)" id="comment-edit-body-{{c.id}}" data-id="{{c.id}}" name="body" form="comment-edit-form-{{c.id}}" class="file-ta comment-box form-control rounded" placeholder="Add your comment..." rows="3">{{c.body}}</textarea>
|
||||||
|
|
||||||
<div class="text-small font-weight-bold mt-1" id="charcount-edit-{{c.id}}" style="right: 1rem; bottom: 0.5rem; z-index: 3"></div>
|
<div class="text-small font-weight-bold mt-1" id="charcount-edit-{{c.id}}" style="right: 1rem; bottom: 0.5rem; z-index: 3"></div>
|
||||||
|
|
||||||
|
@ -554,7 +554,7 @@
|
||||||
<div id="comment-form-space-{{c.id}}" class="comment-write collapsed child">
|
<div id="comment-form-space-{{c.id}}" class="comment-write collapsed child">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input hidden name="formkey" value="{{v|formkey}}">
|
<input hidden name="formkey" value="{{v|formkey}}">
|
||||||
<textarea data-fullname="{{c.fullname}}" required autocomplete="off" minlength="1" maxlength="10000" name="body" form="reply-to-c_{{c.id}}" data-id="{{c.id}}" class="file-ta comment-box form-control rounded" id="reply-form-body-{{c.id}}" rows="3" data-preview="message-reply-{{c.id}}" data-nonce="{{g.nonce}}" data-oninput="markdown(this);handle_disabled(this)"></textarea>
|
<textarea data-emojis data-fullname="{{c.fullname}}" required autocomplete="off" minlength="1" maxlength="10000" name="body" form="reply-to-c_{{c.id}}" data-id="{{c.id}}" class="file-ta comment-box form-control rounded" id="reply-form-body-{{c.id}}" rows="3" data-preview="message-reply-{{c.id}}" data-nonce="{{g.nonce}}" data-oninput="markdown(this);handle_disabled(this)"></textarea>
|
||||||
|
|
||||||
<div class="format-btns">
|
<div class="format-btns">
|
||||||
{{macros.emoji_btn('reply-form-body-' ~ c.id)}}
|
{{macros.emoji_btn('reply-form-body-' ~ c.id)}}
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
<div class="w-lg-100">
|
<div class="w-lg-100">
|
||||||
<form id="sidebar" action="/h/{{hole}}/sidebar" method="post" data-nonce="{{g.nonce}}" data-onsubmit="sendFormXHR(this)">
|
<form id="sidebar" action="/h/{{hole}}/sidebar" method="post" data-nonce="{{g.nonce}}" data-onsubmit="sendFormXHR(this)">
|
||||||
<input hidden name="formkey" value="{{v|formkey}}">
|
<input hidden name="formkey" value="{{v|formkey}}">
|
||||||
<textarea autocomplete="off" maxlength="10000" class="form-control rounded" id="bio-text" placeholder="Enter sidebar here..." rows="10" name="sidebar" form="sidebar">{% if hole.sidebar %}{{hole.sidebar}}{% endif %}</textarea>
|
<textarea data-emojis autocomplete="off" maxlength="10000" class="form-control rounded" id="bio-text" placeholder="Enter sidebar here..." rows="10" name="sidebar" form="sidebar">{% if hole.sidebar %}{{hole.sidebar}}{% endif %}</textarea>
|
||||||
<div class="d-flex mt-2">
|
<div class="d-flex mt-2">
|
||||||
<input autocomplete="off" class="btn btn-primary ml-auto" type="submit" value="Save">
|
<input autocomplete="off" class="btn btn-primary ml-auto" type="submit" value="Save">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
|
|
||||||
<div id="note_section">
|
<div id="note_section">
|
||||||
<label id="notelabel" for="note" class="pt-4">Note (optional):</label>
|
<label id="notelabel" for="note" class="pt-4">Note (optional):</label>
|
||||||
<textarea autocomplete="off" id="note" maxlength="200" class="form-control" placeholder="Note to include in award notification..."></textarea>
|
<textarea data-emojis autocomplete="off" id="note" maxlength="200" class="form-control" placeholder="Note to include in award notification..."></textarea>
|
||||||
{{macros.emoji_btn('note', 'awardModal')}}
|
{{macros.emoji_btn('note', 'awardModal')}}
|
||||||
{{macros.gif_btn('note', 'awardModal')}}
|
{{macros.gif_btn('note', 'awardModal')}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div id="emoji-modal-tabs-container">
|
<div id="emoji-modal-tabs-container">
|
||||||
<ul class="nav nav-pills py-2" id="emoji-modal-tabs">
|
<ul class="nav nav-pills py-2" id="emoji-modal-tabs">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<button type="button" class="nav-link active emojitab" data-class-name="favorite" data-bs-toggle="tab">⭐ Favorite ⭐</button>
|
<button type="button" id="emoji-modal-tabs-favorite" class="nav-link active emojitab" data-class-name="favorite" data-bs-toggle="tab">⭐ Favorite ⭐</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-3">
|
<div class="px-3">
|
||||||
<input disabled autocomplete="off" class="form-control px-2" type="text" id="emoji_search" placeholder="Search.." data-nonce="{{g.nonce}}" data-onchange="start_search()" {% if not (v and v.poor) %}data-oninput="start_search()"{% endif %}>
|
<input disabled autocomplete="off" class="form-control px-2" type="text" id="emoji_search" placeholder="Search.." data-nonce="{{g.nonce}}" data-onchange="emojiSearch()" {% if not (v and v.poor) %}data-oninput="emojiSearch()"{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-3 d-flex flex-row">
|
<div class="px-3 d-flex flex-row">
|
||||||
<fieldset class="pt-2 pr-2 pl-1">
|
<fieldset class="pt-2 pr-2 pl-1">
|
||||||
|
@ -57,13 +57,7 @@
|
||||||
<div id="emojis-work" class="tab-content py-3 pl-4">
|
<div id="emojis-work" class="tab-content py-3 pl-4">
|
||||||
I am working as hard as I can, sweaty... 🚴
|
I am working as hard as I can, sweaty... 🚴
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-content" class="tab-content d-flex flex-wrap" hidden style="text-align:center">
|
<div id="emoji-tab-content" class="tab-content d-flex flex-wrap" hidden style="text-align:center"></div>
|
||||||
<template id="emoji-button-template">
|
|
||||||
<button type="button" class="btn m-1 px-0 emoji2" data-bs-toggle="tooltip">
|
|
||||||
<img loading="lazy">
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -188,8 +188,8 @@
|
||||||
<form id="post-edit-form-{{p.id}}" action="/edit_post/{{p.id}}" method="post" enctype="multipart/form-data" data-nonce="{{g.nonce}}" data-onsubmit="sendFormXHRReload(this)">
|
<form id="post-edit-form-{{p.id}}" action="/edit_post/{{p.id}}" method="post" enctype="multipart/form-data" data-nonce="{{g.nonce}}" data-onsubmit="sendFormXHRReload(this)">
|
||||||
<input hidden name="formkey" value="{{v|formkey}}">
|
<input hidden name="formkey" value="{{v|formkey}}">
|
||||||
<input hidden name="current_page" value="{{request.path}}">
|
<input hidden name="current_page" value="{{request.path}}">
|
||||||
<textarea id="post-edit-title" autocomplete="off" max-length="500" name="title" class="comment-box form-control rounded" required placeholder="title">{{p.title}}</textarea>
|
<textarea data-emojis id="post-edit-title" autocomplete="off" max-length="500" name="title" class="comment-box form-control rounded" required placeholder="title">{{p.title}}</textarea>
|
||||||
<textarea autocomplete="off" name="body" {% if v.longpost %}minlength="280"{% endif %} maxlength="{% if v.bird %}140{% else %}{{POST_BODY_LENGTH_LIMIT(v)}}{% endif %}" data-preview="post-edit-{{p.id}}" data-nonce="{{g.nonce}}" data-oninput="markdown(this);charLimit('post-edit-box-{{p.id}}','charcount-post-edit')" id="post-edit-box-{{p.id}}" form="post-edit-form-{{p.id}}" class="file-ta comment-box form-control rounded" placeholder="Add text to your post..." rows="10" data-id="{{p.id}}">{{p.body}}</textarea>
|
<textarea data-emojis autocomplete="off" name="body" {% if v.longpost %}minlength="280"{% endif %} maxlength="{% if v.bird %}140{% else %}{{POST_BODY_LENGTH_LIMIT(v)}}{% endif %}" data-preview="post-edit-{{p.id}}" data-nonce="{{g.nonce}}" data-oninput="markdown(this);charLimit('post-edit-box-{{p.id}}','charcount-post-edit')" id="post-edit-box-{{p.id}}" form="post-edit-form-{{p.id}}" class="file-ta comment-box form-control rounded" placeholder="Add text to your post..." rows="10" data-id="{{p.id}}">{{p.body}}</textarea>
|
||||||
|
|
||||||
<div class="text-small font-weight-bold mt-1" id="charcount-post-edit" style="right: 1rem; bottom: 0.5rem; z-index: 3"></div>
|
<div class="text-small font-weight-bold mt-1" id="charcount-post-edit" style="right: 1rem; bottom: 0.5rem; z-index: 3"></div>
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
<label class='mt-4' for="title">Post Title</label>
|
<label class='mt-4' for="title">Post Title</label>
|
||||||
<textarea autocomplete="off" class="form-control" id="post-title" type="text" name="title" placeholder="Required" value="{{title}}" minlength="1" maxlength="500" required data-nonce="{{g.nonce}}" data-oninput="checkForRequired();savetext()"></textarea>
|
<textarea data-emojis autocomplete="off" class="form-control" id="post-title" type="text" name="title" placeholder="Required" value="{{title}}" minlength="1" maxlength="500" required data-nonce="{{g.nonce}}" data-oninput="checkForRequired();savetext()"></textarea>
|
||||||
|
|
||||||
{{macros.emoji_btn('post-title')}}
|
{{macros.emoji_btn('post-title')}}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="mt-3">Text<i class="fas fa-info-circle text-gray-400 ml-1" data-bs-toggle="tooltip" data-bs-placement="top" title="Uses markdown. Limited to {{POST_BODY_LENGTH_LIMIT(v)}} characters."></i></label>
|
<label class="mt-3">Text<i class="fas fa-info-circle text-gray-400 ml-1" data-bs-toggle="tooltip" data-bs-placement="top" title="Uses markdown. Limited to {{POST_BODY_LENGTH_LIMIT(v)}} characters."></i></label>
|
||||||
<textarea form="submitform" id="post-text" class="file-ta form-control rounded" placeholder="Optional if you have a link or an image." rows="7" name="body" data-preview="preview" data-nonce="{{g.nonce}}" data-oninput="markdown(this);charLimit('post-text','character-count-submit-text-form');checkForRequired();savetext()" {% if v.longpost %}minlength="280"{% endif %} maxlength="{% if v.bird %}140{% else %}{{POST_BODY_LENGTH_LIMIT(v)}}{% endif %}" required></textarea>
|
<textarea data-emojis form="submitform" id="post-text" class="file-ta form-control rounded" placeholder="Optional if you have a link or an image." rows="7" name="body" data-preview="preview" data-nonce="{{g.nonce}}" data-oninput="markdown(this);charLimit('post-text','character-count-submit-text-form');checkForRequired();savetext()" {% if v.longpost %}minlength="280"{% endif %} maxlength="{% if v.bird %}140{% else %}{{POST_BODY_LENGTH_LIMIT(v)}}{% endif %}" required></textarea>
|
||||||
<div class="ghostdiv" style="display:none"></div>
|
<div class="ghostdiv" style="display:none"></div>
|
||||||
<div class="text-small font-weight-bold mt-1" id="character-count-submit-text-form" style="right: 1rem; bottom: 0.5rem; z-index: 3"></div>
|
<div class="text-small font-weight-bold mt-1" id="character-count-submit-text-form" style="right: 1rem; bottom: 0.5rem; z-index: 3"></div>
|
||||||
|
|
||||||
|
|
|
@ -191,7 +191,7 @@
|
||||||
</div>
|
</div>
|
||||||
<form class="d-none toggleable" id="message" action="/@{{u.username}}/message" method="post" data-nonce="{{g.nonce}}" data-onsubmit="sendMessage(this)">
|
<form class="d-none toggleable" id="message" action="/@{{u.username}}/message" method="post" data-nonce="{{g.nonce}}" data-onsubmit="sendMessage(this)">
|
||||||
<input hidden name="formkey" value="{{v|formkey}}">
|
<input hidden name="formkey" value="{{v|formkey}}">
|
||||||
<textarea autocomplete="off" id="input-message" form="message" name="message" rows="3" minlength="1" maxlength="10000" class="file-ta form-control b2 mt-1" data-preview="message-preview" data-nonce="{{g.nonce}}" data-oninput="markdown(this);handle_disabled(this)"></textarea>
|
<textarea data-emojis autocomplete="off" id="input-message" form="message" name="message" rows="3" minlength="1" maxlength="10000" class="file-ta form-control b2 mt-1" data-preview="message-preview" data-nonce="{{g.nonce}}" data-oninput="markdown(this);handle_disabled(this)"></textarea>
|
||||||
|
|
||||||
<div class="format-btns">
|
<div class="format-btns">
|
||||||
{{macros.emoji_btn('input-message')}}
|
{{macros.emoji_btn('input-message')}}
|
||||||
|
@ -208,7 +208,7 @@
|
||||||
|
|
||||||
<div class="d-none mt-3 toggleable" id="coin-transfer">
|
<div class="d-none mt-3 toggleable" id="coin-transfer">
|
||||||
<input autocomplete="off" id="coin-transfer-amount" class="form-control" name="amount" type="number" data-nonce="{{g.nonce}}" data-oninput="updateTax()">
|
<input autocomplete="off" id="coin-transfer-amount" class="form-control" name="amount" type="number" data-nonce="{{g.nonce}}" data-oninput="updateTax()">
|
||||||
<textarea autocomplete="off" id="coin-transfer-reason" maxlength=200 type="text" class="form-control" name="reason" placeholder="Gift message! (optional)"></textarea>
|
<textarea data-emojis autocomplete="off" id="coin-transfer-reason" maxlength=200 type="text" class="form-control" name="reason" placeholder="Gift message! (optional)"></textarea>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
{{macros.emoji_btn('coin-transfer-reason')}}
|
{{macros.emoji_btn('coin-transfer-reason')}}
|
||||||
{{macros.gif_btn('coin-transfer-reason')}}
|
{{macros.gif_btn('coin-transfer-reason')}}
|
||||||
|
@ -221,7 +221,7 @@
|
||||||
|
|
||||||
<div class="d-none mt-3 toggleable" id="bux-transfer">
|
<div class="d-none mt-3 toggleable" id="bux-transfer">
|
||||||
<input autocomplete="off" id="bux-transfer-amount" class="form-control" name="amount" type="number" data-nonce="{{g.nonce}}" data-oninput="updateBux()">
|
<input autocomplete="off" id="bux-transfer-amount" class="form-control" name="amount" type="number" data-nonce="{{g.nonce}}" data-oninput="updateBux()">
|
||||||
<textarea autocomplete="off" id="bux-transfer-reason" type="text" class="form-control" name="reason" placeholder="Gift message! (optional)"></textarea>
|
<textarea data-emojis autocomplete="off" id="bux-transfer-reason" type="text" class="form-control" name="reason" placeholder="Gift message! (optional)"></textarea>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
{{macros.emoji_btn('bux-transfer-reason')}}
|
{{macros.emoji_btn('bux-transfer-reason')}}
|
||||||
{{macros.gif_btn('bux-transfer-reason')}}
|
{{macros.gif_btn('bux-transfer-reason')}}
|
||||||
|
@ -508,7 +508,7 @@
|
||||||
{% if v and v.id != u.id %}
|
{% if v and v.id != u.id %}
|
||||||
<form class="d-none toggleable text-left" id='message-mobile' action="/@{{u.username}}/message" method="post" data-nonce="{{g.nonce}}" data-onsubmit="sendMessage(this)">
|
<form class="d-none toggleable text-left" id='message-mobile' action="/@{{u.username}}/message" method="post" data-nonce="{{g.nonce}}" data-onsubmit="sendMessage(this)">
|
||||||
<input class="mt-1" hidden name="formkey" value="{{v|formkey}}">
|
<input class="mt-1" hidden name="formkey" value="{{v|formkey}}">
|
||||||
<textarea autocomplete="off" id="input-message-mobile" form="message-mobile" name="message" rows="3" minlength="1" maxlength="10000" class="file-ta form-control" data-preview="message-preview-mobile" data-nonce="{{g.nonce}}" data-oninput="markdown(this);handle_disabled(this)" required></textarea>
|
<textarea data-emojis autocomplete="off" id="input-message-mobile" form="message-mobile" name="message" rows="3" minlength="1" maxlength="10000" class="file-ta form-control" data-preview="message-preview-mobile" data-nonce="{{g.nonce}}" data-oninput="markdown(this);handle_disabled(this)" required></textarea>
|
||||||
|
|
||||||
<div class="format-btns">
|
<div class="format-btns">
|
||||||
{{macros.emoji_btn('input-message-mobile')}}
|
{{macros.emoji_btn('input-message-mobile')}}
|
||||||
|
@ -525,7 +525,7 @@
|
||||||
|
|
||||||
<div class="d-none mt-3 toggleable" id="coin-transfer-mobile">
|
<div class="d-none mt-3 toggleable" id="coin-transfer-mobile">
|
||||||
<input autocomplete="off" id="coin-transfer-amount-mobile" class="form-control" name="amount" type="number" data-nonce="{{g.nonce}}" data-oninput="updateTax(true)">
|
<input autocomplete="off" id="coin-transfer-amount-mobile" class="form-control" name="amount" type="number" data-nonce="{{g.nonce}}" data-oninput="updateTax(true)">
|
||||||
<textarea autocomplete="off" id="coin-transfer-reason-mobile" maxlength=200 type="text" class="form-control" name="reason" placeholder="Gift message! (optional)"></textarea>
|
<textarea data-emojis autocomplete="off" id="coin-transfer-reason-mobile" maxlength=200 type="text" class="form-control" name="reason" placeholder="Gift message! (optional)"></textarea>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
{{macros.emoji_btn('coin-transfer-reason-mobile')}}
|
{{macros.emoji_btn('coin-transfer-reason-mobile')}}
|
||||||
{{macros.gif_btn('coin-transfer-reason-mobile')}}
|
{{macros.gif_btn('coin-transfer-reason-mobile')}}
|
||||||
|
@ -538,7 +538,7 @@
|
||||||
|
|
||||||
<div class="d-none mt-3 toggleable" id="bux-transfer-mobile">
|
<div class="d-none mt-3 toggleable" id="bux-transfer-mobile">
|
||||||
<input autocomplete="off" id="bux-transfer-amount-mobile" class="form-control" name="amount" type="number" data-nonce="{{g.nonce}}" data-oninput="updateBux(true)">
|
<input autocomplete="off" id="bux-transfer-amount-mobile" class="form-control" name="amount" type="number" data-nonce="{{g.nonce}}" data-oninput="updateBux(true)">
|
||||||
<textarea autocomplete="off" id="bux-transfer-reason-mobile" type="text" class="form-control" name="reason" placeholder="Gift message! (optional)"></textarea>
|
<textarea data-emojis autocomplete="off" id="bux-transfer-reason-mobile" type="text" class="form-control" name="reason" placeholder="Gift message! (optional)"></textarea>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
{{macros.emoji_btn('bux-transfer-reason-mobile')}}
|
{{macros.emoji_btn('bux-transfer-reason-mobile')}}
|
||||||
{{macros.gif_btn('bux-transfer-reason-mobile')}}
|
{{macros.gif_btn('bux-transfer-reason-mobile')}}
|
||||||
|
|
|
@ -112,7 +112,7 @@
|
||||||
|
|
||||||
|
|
||||||
{% macro emoji_btn(textarea_id, previous_modal) %}
|
{% macro emoji_btn(textarea_id, previous_modal) %}
|
||||||
<button type="button" class="btn btn-secondary format m-0 mr-1" data-nonce="{{g.nonce}}" data-onclick="loadEmojis(this, '{{textarea_id}}')" data-bs-toggle="modal" data-bs-target="#emojiModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add Emoji" {% if previous_modal %}data-previous-modal="{{previous_modal}}"{% endif %}>
|
<button type="button" class="btn btn-secondary format m-0 mr-1" data-nonce="{{g.nonce}}" data-onclick="openEmojiModal('{{textarea_id}}')" data-bs-toggle="modal" data-bs-target="#emojiModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add Emoji" {% if previous_modal %}data-previous-modal="{{previous_modal}}"{% endif %}>
|
||||||
<i class="fas fa-smile-beam"></i>
|
<i class="fas fa-smile-beam"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@ -134,7 +134,7 @@
|
||||||
<div id="comment-form-space-{{target_fullname}}" class="comment-write {{subwrapper_css_classes}}">
|
<div id="comment-form-space-{{target_fullname}}" class="comment-write {{subwrapper_css_classes}}">
|
||||||
<input hidden name="formkey" value="{{v|formkey}}">
|
<input hidden name="formkey" value="{{v|formkey}}">
|
||||||
<input hidden name="parent_fullname" value="{target_fullname}}">
|
<input hidden name="parent_fullname" value="{target_fullname}}">
|
||||||
<textarea required autocomplete="off" {% if not (p and p.id in ADMIGGER_THREADS) %}{% if v.longpost %}minlength="280"{% elif v.bird %}maxlength="140"{% endif %}{% endif %} minlength="1" maxlength="10000" data-preview="form-preview-{{target_fullname}}" data-nonce="{{g.nonce}}" data-oninput="markdown(this);charLimit('reply-form-body-{{target_fullname}}','charcount-{{target_fullname}}');handle_disabled(this)" id="reply-form-body-{{target_fullname}}" data-fullname="{{target_fullname}}" class="file-ta comment-box form-control rounded" name="body" form="reply-to-{{target_fullname}}" placeholder="Add your comment..." rows="3"></textarea>
|
<textarea data-emojis required autocomplete="off" {% if not (p and p.id in ADMIGGER_THREADS) %}{% if v.longpost %}minlength="280"{% elif v.bird %}maxlength="140"{% endif %}{% endif %} minlength="1" maxlength="10000" data-preview="form-preview-{{target_fullname}}" data-nonce="{{g.nonce}}" data-oninput="markdown(this);charLimit('reply-form-body-{{target_fullname}}','charcount-{{target_fullname}}');handle_disabled(this)" id="reply-form-body-{{target_fullname}}" data-fullname="{{target_fullname}}" class="file-ta comment-box form-control rounded" name="body" form="reply-to-{{target_fullname}}" placeholder="Add your comment..." rows="3"></textarea>
|
||||||
|
|
||||||
<div class="text-small font-weight-bold mt-1" id="charcount-{{target_fullname}}" style="right: 1rem; bottom: 0.5rem; z-index: 3"></div>
|
<div class="text-small font-weight-bold mt-1" id="charcount-{{target_fullname}}" style="right: 1rem; bottom: 0.5rem; z-index: 3"></div>
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="comment-write mt-4 mb-3 mx-3">
|
<div class="comment-write mt-4 mb-3 mx-3">
|
||||||
<textarea autocomplete="off" maxlength="10000" class="comment-box form-control rounded" name="body" placeholder="Add your comment..." rows="3" data-href="/login?redirect={{request.full_path | urlencode}}"></textarea>
|
<textarea data-emojis autocomplete="off" maxlength="10000" class="comment-box form-control rounded" name="body" placeholder="Add your comment..." rows="3" data-href="/login?redirect={{request.full_path | urlencode}}"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card border-0 mt-4">
|
<div class="card border-0 mt-4">
|
||||||
|
@ -346,7 +346,7 @@
|
||||||
{{gif_btn('input-text-chat')}}
|
{{gif_btn('input-text-chat')}}
|
||||||
{{file_btn('file', False, True)}}
|
{{file_btn('file', False, True)}}
|
||||||
|
|
||||||
<textarea id="input-text-chat" minlength="1" maxlength="{% if SITE == 'rdrama.net' %}200{% else %}1000{% endif %}" {% if g.browser in ("iphone","mac") %}style="font-size:16px!important"{% endif %} class="file-ta form-control ml-2" placeholder="Message" autocomplete="off" autofocus rows="1"></textarea>
|
<textarea data-emojis id="input-text-chat" minlength="1" maxlength="{% if SITE == 'rdrama.net' %}200{% else %}1000{% endif %}" {% if g.browser in ("iphone","mac") %}style="font-size:16px!important"{% endif %} class="file-ta form-control ml-2" placeholder="Message" autocomplete="off" autofocus rows="1"></textarea>
|
||||||
|
|
||||||
<i id="chatsend" data-nonce="{{g.nonce}}" data-onclick="send()" class="btn btn-secondary fas fa-reply ml-1 my-auto" style="transform:rotateY(180deg)"></i>
|
<i id="chatsend" data-nonce="{{g.nonce}}" data-onclick="send()" class="btn btn-secondary fas fa-reply ml-1 my-auto" style="transform:rotateY(180deg)"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue