enabled(){ this.ENABLE_CUSTOM_KEYBOARD = false; this.selectedSkinTone = ""; this.currentKeywords = []; this.skinToneList = [ "", "1F3FB", "1F3FC", "1F3FD", "1F3FE", "1F3FF" ]; this.skinToneNonDefaultList = [ "1F3FB", "1F3FC", "1F3FD", "1F3FE", "1F3FF" ]; this.skinToneData = [ [ "", "#FFDD67" ], [ "1F3FB", "#FFE1BD" ], [ "1F3FC", "#FED0AC" ], [ "1F3FD", "#D6A57C" ], [ "1F3FE", "#B47D56" ], [ "1F3FF", "#8A6859" ], ]; this.emojiData1 = []; // no skin tones, prepended this.emojiData2 = {}; // contains emojis with skin tones this.emojiData3 = []; // no skin tones, appended this.emojiNames = []; var me = this; // styles this.css = window.TDPF_createCustomStyle(this); this.css.insert(".emoji-keyboard { position: absolute; width: 15.35em; background-color: white; border-radius: 1px; font-size: 24px; z-index: 9999 }"); this.css.insert(".emoji-keyboard-list { height: 10.14em; padding: 0.1em; box-sizing: border-box; overflow-y: auto }"); this.css.insert(".emoji-keyboard-list .separator { height: 26px }"); this.css.insert(".emoji-keyboard-list img { padding: 0.1em !important; width: 1em; height: 1em; vertical-align: -0.1em; cursor: pointer }"); this.css.insert(".emoji-keyboard-search { height: auto; padding: 4px 10px 8px; background-color: #292f33; border-radius: 1px 1px 0 0 }"); this.css.insert(".emoji-keyboard-search input { width: 100%; border-radius: 1px; }"); this.css.insert(".emoji-keyboard-skintones { height: 1.3em; text-align: center; background-color: #292f33; border-radius: 0 0 1px 1px }"); this.css.insert(".emoji-keyboard-skintones div { width: 0.8em; height: 0.8em; margin: 0.25em 0.1em; border-radius: 50%; display: inline-block; box-sizing: border-box; cursor: pointer }"); this.css.insert(".emoji-keyboard-skintones .sel { border: 2px solid rgba(0, 0, 0, 0.35); box-shadow: 0 0 2px 0 rgba(255, 255, 255, 0.65), 0 0 1px 0 rgba(255, 255, 255, 0.4) inset }"); this.css.insert(".emoji-keyboard-skintones :hover { border: 2px solid rgba(0, 0, 0, 0.25); box-shadow: 0 0 1px 0 rgba(255, 255, 255, 0.65), 0 0 1px 0 rgba(255, 255, 255, 0.25) inset }"); this.css.insert("#emoji-keyboard-tweet-input { padding: 0 !important; line-height: 18px }"); this.css.insert("#emoji-keyboard-tweet-input img { padding: 0.1em !important; width: 1em; height: 1em; vertical-align: -0.25em }"); this.css.insert("#emoji-keyboard-tweet-input:empty::before { content: \"What's happening?\"; display: inline-block; color: #ced8de }"); this.css.insert(".js-docked-compose .compose-text-container.td-emoji-keyboard-swap .js-compose-text { position: absolute; z-index: -9999; left: 0; opacity: 0 }"); this.css.insert(".compose-text-container:not(.td-emoji-keyboard-swap) #emoji-keyboard-tweet-input { display: none; }"); this.css.insert(".js-compose-text { font-family: \"Twitter Color Emoji\", Helvetica, Arial, Verdana, sans-serif; }"); // layout var buttonHTML = '<button class="needsclick btn btn-on-blue txt-left padding-v--9 emoji-keyboard-popup-btn"><i class="icon icon-heart"></i></button>'; this.prevComposeMustache = TD.mustaches["compose/docked_compose.mustache"]; TD.mustaches["compose/docked_compose.mustache"] = TD.mustaches["compose/docked_compose.mustache"].replace('<div class="cf margin-t--12 margin-b--30">', '<div class="cf margin-t--12 margin-b--30">'+buttonHTML); var maybeDockedComposePanel = $(".js-docked-compose"); if (maybeDockedComposePanel.length){ maybeDockedComposePanel.find(".cf.margin-t--12.margin-b--30").first().append(buttonHTML); } // keyboard generation this.currentKeyboard = null; this.currentSpanner = null; var hideKeyboard = (refocus) => { $(this.currentKeyboard).remove(); this.currentKeyboard = null; $(this.currentSpanner).remove(); this.currentSpanner = null; this.currentKeywords = []; this.composePanelScroller.trigger("scroll"); $(".emoji-keyboard-popup-btn").removeClass("is-selected"); if (refocus){ if ($(".compose-text-container", ".js-docked-compose").hasClass("td-emoji-keyboard-swap")){ $("#emoji-keyboard-tweet-input").focus(); } else{ $(".js-compose-text", ".js-docked-compose").focus(); } } }; var generateEmojiHTML = skinTone => { let index = 0; let html = [ "<p style='font-size:13px;color:#444;margin:4px;text-align:center'>Please, note that most emoji will not show up properly in the text box above, but they will display in the tweet.</p>" ]; for(let array of [ this.emojiData1, this.emojiData2[skinTone], this.emojiData3 ]){ for(let emoji of array){ if (emoji === "___"){ html.push("<div class='separator'></div>"); } else{ html.push(TD.util.cleanWithEmoji(emoji).replace(' class="emoji" draggable="false"', '')); index++; } } } return html.join(""); }; var updateFilters = () => { let keywords = this.currentKeywords; let container = $(this.currentKeyboard.children[1]); let emoji = container.children("img"); let info = container.children("p:first"); let separators = container.children("div"); if (keywords.length === 0){ info.css("display", "block"); separators.css("display", "block"); emoji.css("display", "inline"); } else{ info.css("display", "none"); separators.css("display", "none"); emoji.css("display", "none"); emoji.filter(index => keywords.every(kw => me.emojiNames[index].includes(kw))).css("display", "inline"); } }; var selectSkinTone = skinTone => { let selectedEle = this.currentKeyboard.children[2].querySelector("[data-tone='"+this.selectedSkinTone+"']"); selectedEle && selectedEle.classList.remove("sel"); this.selectedSkinTone = skinTone; this.currentKeyboard.children[1].innerHTML = generateEmojiHTML(skinTone); this.currentKeyboard.children[2].querySelector("[data-tone='"+this.selectedSkinTone+"']").classList.add("sel"); updateFilters(); }; this.generateKeyboard = (left, top) => { var outer = document.createElement("div"); outer.classList.add("emoji-keyboard"); outer.style.left = left+"px"; outer.style.top = top+"px"; var keyboard = document.createElement("div"); keyboard.classList.add("emoji-keyboard-list"); keyboard.addEventListener("click", function(e){ let ele = e.target; if (ele.tagName === "IMG"){ insertEmoji(ele.getAttribute("src"), ele.getAttribute("alt")); } e.stopPropagation(); }); var search = document.createElement("div"); search.innerHTML = "<input type='text' placeholder='Search...'>"; search.classList.add("emoji-keyboard-search"); var skintones = document.createElement("div"); skintones.innerHTML = me.skinToneData.map(entry => "<div data-tone='"+entry[0]+"' style='background-color:"+entry[1]+"'></div>").join(""); skintones.classList.add("emoji-keyboard-skintones"); outer.appendChild(search); outer.appendChild(keyboard); outer.appendChild(skintones); $(".js-app").append(outer); skintones.addEventListener("click", function(e){ if (e.target.hasAttribute("data-tone")){ selectSkinTone(e.target.getAttribute("data-tone") || ""); } e.stopPropagation(); }); search.addEventListener("click", function(e){ e.stopPropagation(); }); var searchInput = search.children[0]; searchInput.focus(); searchInput.addEventListener("input", function(e){ me.currentKeywords = e.target.value.split(" ").filter(kw => kw.length > 0).map(kw => kw.toLowerCase()); updateFilters(); e.stopPropagation(); }); searchInput.addEventListener("focus", function(){ $(this).select(); }); this.currentKeyboard = outer; selectSkinTone(this.selectedSkinTone); this.currentSpanner = document.createElement("div"); this.currentSpanner.style.height = ($(this.currentKeyboard).height()-10)+"px"; $(".emoji-keyboard-popup-btn").parent().after(this.currentSpanner); this.composePanelScroller.trigger("scroll"); }; var getKeyboardTop = () => { let button = $(".emoji-keyboard-popup-btn"); return button.offset().top+button.outerHeight()+me.composePanelScroller.scrollTop()+8; }; var focusWithCaretAtEnd = () => { let range = document.createRange(); range.selectNodeContents(me.composeInputNew[0]); range.collapse(false); let sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); }; var insertEmoji = (src, alt) => { if (this.ENABLE_CUSTOM_KEYBOARD){ replaceEditor(true); if (!hasSelectionInEditor()){ focusWithCaretAtEnd(); } document.execCommand("insertHTML", false, `<img src="${src}" alt="${alt}">`); } else{ let input = $(".js-compose-text", ".js-docked-compose"); let val = input.val(); let posStart = input[0].selectionStart; let posEnd = input[0].selectionEnd; input.val(val.slice(0, posStart)+alt+val.slice(posEnd)); input.trigger("change"); input.focus(); input[0].selectionStart = posStart+alt.length; input[0].selectionEnd = posStart+alt.length; } }; // general event handlers this.emojiKeyboardButtonClickEvent = function(e){ if (me.currentKeyboard){ hideKeyboard(); $(this).blur(); } else{ me.generateKeyboard($(this).offset().left, getKeyboardTop()); $(this).addClass("is-selected"); } e.stopPropagation(); }; this.composerScrollEvent = function(e){ if (me.currentKeyboard){ me.currentKeyboard.style.marginTop = (-$(this).scrollTop())+"px"; } }; this.composerSendingEvent = function(e){ hideKeyboard(); }; this.documentClickEvent = function(e){ if (me.currentKeyboard && $(e.target).closest(".compose-text-container").length === 0){ hideKeyboard(); } }; this.documentKeyEvent = function(e){ if (me.currentKeyboard && e.keyCode === 27){ // escape hideKeyboard(true); e.stopPropagation(); } }; this.uploadFilesEvent = function(e){ if (me.currentKeyboard){ me.currentKeyboard.style.top = getKeyboardTop()+"px"; } }; // new editor event handlers var prevOldInputVal = ""; var isEditorActive = false; var hasSelectionInEditor = function(){ let sel = window.getSelection(); return sel.anchorNode && $(sel.anchorNode).closest("#emoji-keyboard-tweet-input").length; }; var migrateEditorText = function(){ let selStart = me.composeInputOrig[0].selectionStart; let selEnd = me.composeInputOrig[0].selectionEnd; let val = me.composeInputOrig.val(); let splitStart = val.substring(0, selStart).split("\n"); let splitEnd = val.substring(0, selEnd).split("\n"); me.composeInputNew.text(val); me.composeInputNew.html(me.composeInputNew.text().replace(/^(.*?)$/gm, "<div>$1</div>").replace(/<div><\/div>/g, "<div><br></div>")); let sel = window.getSelection(); let range = document.createRange(); if (me.composeInputNew[0].children.length === 0){ focusWithCaretAtEnd(); } else{ me.composeInputNew.focus(); range.setStart(me.composeInputNew[0].children[splitStart.length-1].firstChild, splitStart.length > 1 ? selStart-val.lastIndexOf("\n", selStart)-1 : selStart); range.setEnd(me.composeInputNew[0].children[splitEnd.length-1].firstChild, splitEnd.length > 1 ? selEnd-val.lastIndexOf("\n", selEnd)-1 : selEnd); } sel.removeAllRanges(); sel.addRange(range); }; var replaceEditor = function(useCustom){ if (useCustom && !isEditorActive){ isEditorActive = true; } else if (!useCustom && isEditorActive){ isEditorActive = false; } else return; $(".compose-text-container", ".js-docked-compose").toggleClass("td-emoji-keyboard-swap", isEditorActive); if (isEditorActive){ migrateEditorText(); } else{ me.composeInputOrig.focus(); } }; this.composeOldInputFocusEvent = function(){ return if !isEditorActive; let val = $(this).val(); if (val.length === 0){ replaceEditor(false); } else if (val != prevOldInputVal){ setTimeout(migrateEditorText, 1); } else{ focusWithCaretAtEnd(); } prevOldInputVal = val; }; var allowedShortcuts = [ 65 /* A */, 67 /* C */, 86 /* V */, 89 /* Y */, 90 /* Z */ ]; this.composeInputKeyEvent = function(e){ if (e.type === "keydown" && (e.ctrlKey || e.metaKey)){ if (e.keyCode === 13){ // enter $(".js-send-button", ".js-docked-compose").click(); } else if (e.keyCode >= 48 && !allowedShortcuts.includes(e.keyCode)){ e.preventDefault(); return; } } if (e.keyCode !== 27){ // escape e.stopPropagation(); } }; this.composeInputUpdateEvent = function(){ let clone = $(this).clone(); clone.children("div").each(function(){ let ele = $(this)[0]; ele.outerHTML = "\n"+ele.innerHTML; }); clone.children("img").each(function(){ let ele = $(this)[0]; ele.outerHTML = ele.getAttribute("alt"); }); me.composeInputOrig.val(prevOldInputVal = clone.text()); me.composeInputOrig.trigger("change"); if (prevOldInputVal.length === 0){ replaceEditor(false); } /* TODO if (!emoji.length){ let sel = window.getSelection(); let selStart = -1, selEnd = -1; if ($(sel.anchorNode).closest("#emoji-keyboard-tweet-input").length && sel.rangeCount === 1){ let range = sel.getRangeAt(0); // TODO figure out offset } replaceEditor(false); me.composeInputOrig.focus(); if (selStart !== -1){ me.composeInputOrig[0].selectionStart = selStart; me.composeInputOrig[0].selectionEnd = selEnd; } }*/ }; this.composeInputPasteEvent = function(e){ // contenteditable with <img alt> handles copying just fine e.preventDefault(); let text = e.originalEvent.clipboardData.getData("text/plain"); text && document.execCommand("insertText", false, text); }; // TODO handle @ and hashtags } ready(){ this.composeDrawer = $("[data-drawer='compose']"); this.composePanelScroller = $(".js-compose-scroller", ".js-docked-compose").first().children().first(); this.composePanelScroller.on("scroll", this.composerScrollEvent); $(".emoji-keyboard-popup-btn").on("click", this.emojiKeyboardButtonClickEvent); $(document).on("click", this.documentClickEvent); $(document).on("keydown", this.documentKeyEvent); $(document).on("uiComposeImageAdded", this.uploadFilesEvent); this.composeDrawer.on("uiComposeTweetSending", this.composerSendingEvent); // Editor this.composeInputOrig = $(".js-compose-text", ".js-docked-compose").first(); this.composeInputNew = $('<div id="emoji-keyboard-tweet-input" contenteditable="true" class="compose-text txt-size--14 scroll-v scroll-styled-v scroll-styled-h scroll-alt td-detect-image-paste"></div>').insertAfter(this.composeInputOrig); if (this.ENABLE_CUSTOM_KEYBOARD){ this.composeInputOrig.on("focus", this.composeOldInputFocusEvent); this.composeInputNew.on("keydown keypress keyup", this.composeInputKeyEvent); this.composeInputNew.on("input", this.composeInputUpdateEvent); this.composeInputNew.on("paste", this.composeInputPasteEvent); } // HTML generation var convUnicode = function(codePt){ if (codePt > 0xFFFF){ codePt -= 0x10000; return String.fromCharCode(0xD800+(codePt>>10), 0xDC00+(codePt&0x3FF)); } else{ return String.fromCharCode(codePt); } }; $TDP.readFileRoot(this.$token, "emoji-ordering.txt").then(contents => { for(let skinTone of this.skinToneList){ this.emojiData2[skinTone] = []; } // declaration inserters let mapUnicode = pt => convUnicode(parseInt(pt, 16)); let addDeclaration1 = decl => { this.emojiData1.push(decl.split(" ").map(mapUnicode).join("")); }; let addDeclaration2 = (tone, decl) => { let gen = decl.split(" ").map(mapUnicode).join(""); if (tone === null){ for(let skinTone of this.skinToneList){ this.emojiData2[skinTone].push(gen); } } else{ this.emojiData2[tone].push(gen); } }; let addDeclaration3 = decl => { this.emojiData3.push(decl.split(" ").map(mapUnicode).join("")); }; // line reading let skinToneState = 0; for(let line of contents.split("\n")){ if (line[0] === '@'){ switch(skinToneState){ case 0: this.emojiData1.push("___"); break; case 1: this.skinToneList.forEach(skinTone => this.emojiData2[skinTone].push("___")); break; case 2: this.emojiData3.push("___"); break; } continue; } else if (line[0] === '#'){ if (line[1] === '1'){ skinToneState = 1; } else if (line[1] === '2'){ skinToneState = 2; } continue; } let semicolon = line.indexOf(';'); let decl = line.slice(0, semicolon); let desc = line.slice(semicolon+1).toLowerCase(); if (skinToneState === 1){ let skinIndex = decl.indexOf('$'); if (skinIndex !== -1){ let declPre = decl.slice(0, skinIndex); let declPost = decl.slice(skinIndex+1); for(let skinTone of this.skinToneNonDefaultList){ this.emojiData2[skinTone].pop(); addDeclaration2(skinTone, declPre+skinTone+declPost); } } else{ addDeclaration2(null, decl); this.emojiNames.push(desc); } } else if (skinToneState === 2){ addDeclaration3(decl); this.emojiNames.push(desc); } else if (skinToneState === 0){ addDeclaration1(decl); this.emojiNames.push(desc); } } }).catch(err => { $TD.alert("error", "Problem loading emoji keyboard: "+err.message); }); } disabled(){ this.css.remove(); if (this.currentKeyboard){ $(this.currentKeyboard).remove(); } if (this.currentSpanner){ $(this.currentSpanner).remove(); } this.composeInputNew.remove(); this.composeInputOrig.off("focus", this.composeOldInputFocusEvent); this.composePanelScroller.off("scroll", this.composerScrollEvent); $(".emoji-keyboard-popup-btn").off("click", this.emojiKeyboardButtonClickEvent); $(".emoji-keyboard-popup-btn").remove(); $(document).off("click", this.documentClickEvent); $(document).off("keydown", this.documentKeyEvent); $(document).off("uiComposeImageAdded", this.uploadFilesEvent); this.composeDrawer.off("uiComposeTweetSending", this.composerSendingEvent); TD.mustaches["compose/docked_compose.mustache"] = this.prevComposeMustache; }