Inline Emojo Picker (#317)

* Inline emoji picker

* Inline text editor
master
Nekobit 2022-07-17 01:02:22 -04:00 committed by GitHub
parent 76cf6fb696
commit 4b47faa1ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 243 additions and 11 deletions

View File

@ -1721,6 +1721,7 @@ button.close {
.modal-dialog-centered.modal-dialog-scrollable {
flex-direction: column;
justify-content: center;
height: 100%;
}
.modal-dialog-centered.modal-dialog-scrollable .modal-content {
max-height: none;
@ -6087,3 +6088,59 @@ g {
margin-top: -5px;
}
}
.ghostdiv
{
display: block;
white-space: pre-wrap;
word-break: break-word;
/* Attempt to copy the textarea/input padding */
padding: 15px;
}
#speed-carot-modal
{
background-color: var(--gray-700);
min-width: 190px;
max-width: 190px;
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
}
#speed-carot-modal .speed-modal-option
{
border-bottom: 1px solid #606060;
padding: 4px;
cursor: pointer;
}
#speed-carot-modal .speed-modal-option:hover,
#speed-carot-modal .speed-modal-option:focus,
#speed-carot-modal .speed-modal-option.selected
{
background-color: rgba(255, 255, 255, 0.2);
}
#speed-carot-modal .speed-modal-image
{
object-fit: contain;
width: 32px;
height: 32px;
}
#speed-carot-modal .speed-modal-option span
{
overflow: hidden;
display: inline-block;
vertical-align: middle;
margin-left: 10px;
width: 125px;
max-width: 125px;
text-overflow: ellipsis;
}

View File

@ -10,7 +10,8 @@ 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
Copyright (C) 2022 Dr Steven Transmisia, anti-evil engineer,
2022 Nekobit, king autist
*/
// Status
@ -372,6 +373,176 @@ function emojiAddToInput(event)
localStorage.setItem("favorite_emojis", JSON.stringify(favorite_emojis));
}
(function() {
const insertAt = (str, sub, pos) => `${str.slice(0, pos)}${sub}${str.slice(pos)}`;
let emoji_typing_state = false;
function update_ghost_div_textarea(text)
{
let ghostdiv = text.parentNode.querySelector(".ghostdiv");
if (!ghostdiv) return;
ghostdiv.innerText = text.value.substring(0, text.selectionStart);
ghostdiv.innerHTML += "<span></span>";
// Now lets get coordinates
ghostdiv.style.display = "initial";
let end = ghostdiv.querySelector("span");
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
// Just leave it global, I don't care
let speed_carot_modal = document.createElement("div");
speed_carot_modal.id = "speed-carot-modal";
speed_carot_modal.style.position = "absolute";
speed_carot_modal.style.left = "0px";
speed_carot_modal.style.top = "0px";
speed_carot_modal.style.display = "none";
document.body.appendChild(speed_carot_modal);
let current_word = "";
let emojo_index = 0;
function curr_word_is_emoji()
{
return current_word && current_word.charAt(0) == ":" &&
current_word.charAt(current_word.length-1) != ":";
}
function populate_speed_emoji_modal(results, textbox)
{
if (!results || results.size === 0)
{
speed_carot_modal.style.display = "none";
return -1;
}
emojo_index = 0;
speed_carot_modal.innerHTML = "";
const MAXXX = 25;
// Not sure why the results is a Set... but oh well
let i = 0;
for (let result of results)
{
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 = `/e/${result}.webp`;
let emoji_option_text = document.createElement("span");
emoji_option_text.title = result;
emoji_option_text.innerText = result;
emoji_option.onclick = (e) => {
speed_carot_modal.style.display = "none";
textbox.value = textbox.value.replace(new RegExp(current_word+"(?=\\s|$)", "g"), `:${result}:`)
};
// 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)
{
if (event.target.tagName.toLowerCase() !== 'textarea') return;
const box_coords = update_ghost_div_textarea(event.target);
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 = /\S+$/.exec(text.slice(0, coords === -1 ? text.length : coords));
if (current_word) current_word = current_word.toString();
/* 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);
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 = modal_pos.x + box_coords.x - 35 + "px";
speed_carot_modal.style.top = modal_pos.y + box_coords.y + 14 + "px";
// Do the search (and do something with it)
populate_speed_emoji_modal(emojisSearchDictionary.searchFor(current_word.substr(1)), event.target);
}
else {
speed_carot_modal.style.display = "none";
}
}
// Update emoji position
document.addEventListener('input', update_speed_emoji_modal, false);
// Update emoji position
document.addEventListener('keydown', (e) => {
let select_items = speed_carot_modal.querySelectorAll(".speed-modal-option");
if (!select_items || !curr_word_is_emoji()) return false;
// Up or down arrow or enter
if (e.keyCode == 38 || e.keyCode == 40 || e.keyCode == 13)
{
if (emojo_index > select_items.length)
emojo_index = select_items;
select_items[emojo_index].classList.remove("selected");
switch (e.keyCode)
{
case 38: // Up arrow
if (emojo_index)
emojo_index--;
break;
case 40: // Down arrow
if (emojo_index < select_items.length-1) emojo_index++;
break;
case 13:
select_items[emojo_index].click();
default:
break;
}
select_items[emojo_index].classList.add("selected");
e.preventDefault();
}
}, false);
})();
function loadEmojis(inputTargetIDName)
{
if(!emojiEngineStarted)
@ -380,6 +551,7 @@ function loadEmojis(inputTargetIDName)
emojiRequest.send();
}
if (inputTargetIDName)
emojiInputTargetDOM = document.getElementById(inputTargetIDName);
}

View File

@ -122,7 +122,7 @@
<span class="font-weight-bold">Comment {{'Replies' if (replies | length)>1 else 'Reply'}}: <a href="{{c.post.permalink}}">{{c.post.realtitle(v) | safe}}</a></span>
{% elif c.post.author_id==v.id and c.level == 1 and is_notification_page%}
<span class="font-weight-bold">Post Reply: <a href="{{c.post.permalink}}">{{c.post.realtitle(v) | safe}}</a></span>
{% elif is_notification_page and c.parent_submission in v.subscribed_idlist %}
{% elif is_notification_page and c.parent_submission in v.subscribed_idlist() %}
<span class="font-weight-bold">Subscribed Thread: <a href="{{c.post.permalink}}">{{c.post.realtitle(v) | safe}}</a></span>
{% elif is_notification_page %}
<span class="font-weight-bold">Username Mention: <a href="{{c.post.permalink}}">{{c.post.realtitle(v) | safe}}</a></span>
@ -450,9 +450,9 @@
<button class="btn caction py-0 nobackground px-1 text-muted" role="button" data-bs-toggle="modal" data-bs-target="#awardModal" data-url="/award/comment/{{c.id}}"><i class="fas fa-gift" aria-hidden="true"></i>Give Award</button>
<button id="unsave-{{c.id}}" class="btn caction py-0 nobackground px-1 {% if c.id in v.saved_comment_idlist %}d-md-inline-block{% endif %} text-muted d-none" role="button" onclick="post_toast(this,'/unsave_comment/{{c.id}}','save-{{c.id}}','unsave-{{c.id}}','d-md-inline-block')"><i class="fas fa-save"></i>Unsave</button>
<button id="unsave-{{c.id}}" class="btn caction py-0 nobackground px-1 {% if c.id in v.saved_comment_idlist() %}d-md-inline-block{% endif %} text-muted d-none" role="button" onclick="post_toast(this,'/unsave_comment/{{c.id}}','save-{{c.id}}','unsave-{{c.id}}','d-md-inline-block')"><i class="fas fa-save"></i>Unsave</button>
<button id="save-{{c.id}}" class="btn caction py-0 nobackground px-1 {% if c.id not in v.saved_comment_idlist %}d-md-inline-block{% endif %} text-muted d-none" role="button" onclick="post_toast(this,'/save_comment/{{c.id}}','save-{{c.id}}','unsave-{{c.id}}','d-md-inline-block')"><i class="fas fa-save"></i>Save</button>
<button id="save-{{c.id}}" class="btn caction py-0 nobackground px-1 {% if c.id not in v.saved_comment_idlist() %}d-md-inline-block{% endif %} text-muted d-none" role="button" onclick="post_toast(this,'/save_comment/{{c.id}}','save-{{c.id}}','unsave-{{c.id}}','d-md-inline-block')"><i class="fas fa-save"></i>Save</button>
{% endif %}
{% if c.parent_submission %}
@ -552,6 +552,7 @@
<input type="hidden" name="parent_fullname" value="{{c.fullname}}">
<input autocomplete="off" id="reply-form-submission-{{c.fullname}}" type="hidden" name="submission" value="{{c.post.id}}">
<textarea required autocomplete="off" {% if v.longpost %}minlength="280"{% else %}minlength="1"{% endif %} maxlength="{% if v.bird %}140{% else %}10000{% endif %}" oninput="markdown('reply-form-body-{{c.fullname}}', 'reply-edit-{{c.id}}');charLimit('reply-form-body-{{c.fullname}}','charcount-{{c.id}}')" id="reply-form-body-{{c.fullname}}" data-fullname="{{c.fullname}}" name="body" form="reply-to-t3_{{c.id}}" class="comment-box form-control rounded" aria-label="With textarea" placeholder="Add your comment..." rows="3"></textarea>
<div class="ghostdiv" style="display:none;"></div>
<div class="text-small font-weight-bold mt-1" id="charcount-{{c.id}}" style="right: 1rem; bottom: 0.5rem; z-index: 3;"></div>
@ -655,9 +656,9 @@
<a class="list-group-item" role="button" data-bs-toggle="modal" data-bs-target="#awardModal" data-url="/award/comment/{{c.id}}"><i class="fas fa-gift mr-2" aria-hidden="true"></i>Give Award</a>
<a id="save2-{{c.id}}" class="list-group-item {% if c.id in v.saved_comment_idlist %}d-none{% endif %}" role="button" data-bs-dismiss="modal" onclick="post_toast(this,'/save_comment/{{c.id}}','save2-{{c.id}}','unsave2-{{c.id}}','d-none')"><i class="fas fa-save mr-2"></i>Save</a>
<a id="save2-{{c.id}}" class="list-group-item {% if c.id in v.saved_comment_idlist() %}d-none{% endif %}" role="button" data-bs-dismiss="modal" onclick="post_toast(this,'/save_comment/{{c.id}}','save2-{{c.id}}','unsave2-{{c.id}}','d-none')"><i class="fas fa-save mr-2"></i>Save</a>
<a id="unsave2-{{c.id}}" class="list-group-item {% if c.id not in v.saved_comment_idlist %}d-none{% endif %}" role="button" onclick="post_toast(this,'/unsave_comment/{{c.id}}','save2-{{c.id}}','unsave2-{{c.id}}','d-none')" data-bs-dismiss="modal"><i class="fas fa-save mr-2"></i>Unsave</a>
<a id="unsave2-{{c.id}}" class="list-group-item {% if c.id not in v.saved_comment_idlist() %}d-none{% endif %}" role="button" onclick="post_toast(this,'/unsave_comment/{{c.id}}','save2-{{c.id}}','unsave2-{{c.id}}','d-none')" data-bs-dismiss="modal"><i class="fas fa-save mr-2"></i>Unsave</a>
{% if c.author_id == v.id %}
<a role="button" data-bs-dismiss="modal" onclick="toggleEdit('{{c.id}}')" class="list-group-item"><i class="fas fa-edit mr-2"></i>Edit</a>

View File

@ -983,6 +983,7 @@
<input type="hidden" name="parent_fullname" value="t2_{{p.id}}">
<input autocomplete="off" id="reply-form-submission-{{p.fullname}}" type="hidden" name="submission" value="{{p.id}}">
<textarea required autocomplete="off" {% if not (p and p.id in ADMIGGERS) %}{% if v.longpost %}minlength="280"{% elif v.bird %}maxlength="140"{% endif %}{% endif %} minlength="1" maxlength="10000" oninput="markdown('reply-form-body-{{p.fullname}}', 'form-preview-{{p.id}}');charLimit('reply-form-body-{{p.fullname}}','charcount-reply')" id="reply-form-body-{{p.fullname}}" data-fullname="{{p.fullname}}" class="comment-box form-control rounded" id="comment-form" name="body" form="reply-to-{{p.fullname}}" aria-label="With textarea" placeholder="Add your comment..." rows="3"></textarea>
<div class="ghostdiv" style="display:none;"></div>
<div class="text-small font-weight-bold mt-1" id="charcount-reply" style="right: 1rem; bottom: 0.5rem; z-index: 3;"></div>

View File

@ -125,6 +125,7 @@
<label for="body" 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 20000 characters."></i></label>
<textarea form="submitform" id="post-text" class="form-control rounded" aria-label="With textarea" placeholder="Optional if you have a link or an image." rows="7" name="body" oninput="markdown('post-text','preview');charLimit('post-text','character-count-submit-text-form');checkForRequired();savetext()" {% if v.longpost %}minlength="280"{% endif %} maxlength="{% if v.bird %}140{% else %}20000{% endif %}" required></textarea>
<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>