Slackライクな絵文字入力
Slackライクな絵文字入力
code:script.js
const Asearch = (function() {
var INITPAT, INITSTATE, MAXCHAR;
INITPAT = 0x80000000;
MAXCHAR = 0x100;
Asearch.prototype.isupper = function(c) {
return (c >= 0x41) && (c <= 0x5a);
};
Asearch.prototype.islower = function(c) {
return (c >= 0x61) && (c <= 0x7a);
};
Asearch.prototype.tolower = function(c) {
if (this.isupper(c)) {
return c + 0x20;
} else {
return c;
}
};
Asearch.prototype.toupper = function(c) {
if (this.islower(c)) {
return c - 0x20;
} else {
return c;
}
};
function Asearch(source) {
var c, i, j, len, mask, ref, ref1;
this.source = source;
this.shiftpat = [];
this.epsilon = 0;
this.acceptpat = 0;
mask = INITPAT;
for (c = i = 0, ref = MAXCHAR; 0 <= ref ? i < ref : i > ref; c = 0 <= ref ? ++i : --i) {
}
ref1 = this.unpack(this.source);
for (j = 0, len = ref1.length; j < len; j++) {
if (c === 0x20) {
this.epsilon |= mask;
} else {
mask >>>= 1;
}
}
this.acceptpat = mask;
return this;
}
Asearch.prototype.state = function(state, str) {
var c, i, i0, i1, i2, i3, len, mask, ref;
if (state == null) {
state = INITSTATE;
}
if (str == null) {
str = '';
}
ref = this.unpack(str);
for (i = 0, len = ref.length; i < len; i++) {
i3 = (i3 & this.epsilon) | ((i3 & mask) >>> 1) | (i2 >>> 1) | i2;
i2 = (i2 & this.epsilon) | ((i2 & mask) >>> 1) | (i1 >>> 1) | i1;
i1 = (i1 & this.epsilon) | ((i1 & mask) >>> 1) | (i0 >>> 1) | i0;
i0 = (i0 & this.epsilon) | ((i0 & mask) >>> 1);
i1 |= i0 >>> 1;
i2 |= i1 >>> 1;
i3 |= i2 >>> 1;
}
};
Asearch.prototype.match = function(str, ambig) {
var s;
if (ambig == null) {
ambig = 0;
}
s = this.state(INITSTATE, str);
if (!(ambig < INITSTATE.length)) {
ambig = INITSTATE.length - 1;
}
return (sambig & this.acceptpat) !== 0; };
Asearch.prototype.unpack = function(str) {
var bytes, c, code, i, len, ref;
bytes = [];
ref = str.split('');
for (i = 0, len = ref.length; i < len; i++) {
code = c.charCodeAt(0);
if (code > 0xFF) {
bytes.push((code & 0xFF00) >>> 8);
}
bytes.push(code & 0xFF);
}
return bytes;
};
return Asearch;
})();
const projectName = scrapbox.Project.name;
let emojis = [];
const box = $('<div>').addClass('form-group').css("position", "absolute");
const container = $('<div>').addClass('dropdown');
box.append(container);
let items = $('<ul>').addClass('dropdown-menu');
container.append(items);
$('#editor').append(box);
fetch(/api/pages/${projectName}?limit=10000, { credentials: 'same-origin'})
.then( res => res.text())
.then( text => {
const data = JSON.parse( text );
const pages = data.pages;
pages.filter( page => (page.image !== null && page.title.match(/^\w\s\-\++$/))) .forEach( page => {
emojis.push({
name: page.title,
path: page.title,
icon: /api/pages/${projectName}/${page.title}/icon,
})
})
})
scrapbox.PageMenu.addMenu({
title: 'emoji',
})
scrapbox.PageMenu('emoji').addItem({
title: "load emojis from /emoji",
onClick: () => {
fetch('/api/pages/emoji?limit=10000')
.then( res => res.text())
.then( text => {
const data = JSON.parse( text );
const pages = data.pages;
pages.filter( page => (page.image !== null && page.title.match(/^\w\s\-\++$/))) .forEach( page => {
for( let emoji of emojis ) {
if( emoji.name === page.title )return;
}
emojis.push({
name: page.title,
path: '/emoji/' + page.title,
icon: /api/pages/emoji/${page.title}/icon,
})
})
})
}
})
// TODO: 様々な文字列が来る場合を考慮する
const taberareloo = ( word, list ) => {
const targetWord = word.replace(':', '');
const regStr = targetWord.split('').reduce( (pre, cur) => pre + cur + '.*' ).replace('+', '\\+');
const reg = RegExp(regStr,'i');
return list.filter( item => item.name.match(reg));
}
const asearched = ( word, list ) => {
const targetWord = word.replace(':', '');
const a = new Asearch( targetWord );
const limitCount = Math.floor( targetWord.length/ 4 ) + 1;
let result = [];
for(let i = 0; i <= limitCount; i++){
let matched = list.filter( item => a.match( item.name, i));
let notExisted = matched.filter( item => {
for( let r of result){
if(r.name === item.name){
return false;
}
}
return true;
})
}
return result;
}
const fizzSearch = ( word, list ) => {
const a = asearched( word, list );
const b = taberareloo( word, list );
const c = b.filter( item => {
for( let r of a ){
if( r.name == item.name){
return false;
}
}
return true;
})
}
let stack = "";
const editor = $('#editor');
const open = () => container.addClass("open");
const close = () => {
stack = "";
container.removeClass("open");
}
const replaceText = (text, cursor, emojiPath) => {
cursor.focus();
setTimeout(()=>{
for(let i = 0; i < text.length; i++){
var ke1 = document.createEvent("Events");
ke1.initEvent("keydown", true, true);
ke1.keyCode = ke1.which = 8; // Backspace
cursor.dispatchEvent(ke1);
}
document.execCommand('insertText',null, [${emojiPath}.icon] );
close();
}, 50)
}
editor.keydown( e => {
const key = e.key;
if(key === undefined ) return;
if( stack === "" && key !== ":"){
close();
return
};
if ($('.cursor-line').text().trim() == 'code:'
|| $('.cursor-line .code-block').length == 1) {
close()
return;
}
if( key === ':' && stack.length !== 0){
let name = stack.replace(':', '');
for(let emoji of emojis){
if( emoji.name === name ){
let cursor = $('#text-input')0; replaceText(stack + ":", cursor, emoji.path);
return;
}
}
close()
return;
}
const cursor = $('#text-input')0; stack += e.key;
let focused = $(':focus');
if(focused.is(items.find('li > a'))){
cursor.focus();
}
}
if( stack.length === 2 ){
if( key === " " ){
stack = "";
return;
}
open();
}
switch(key){
case 'Backspace':
stack = stack.slice(0, stack.length - 1);
if(stack.length === 0){
close();
return;
}
break;
case 'ArrowUp':
let focusedUp = $(':focus');
if( focusedUp.is(items.find('li > a').eq(0)) ){
e.stopPropagation();
cursor.focus();
}else if( !focusedUp.is(items.find('li > a')) ){
close();
return;
}
break;
case 'ArrowDown':
let focusedDown = $(':focus');
if( !focusedDown.is(items.find('li > a'))) {
e.stopPropagation();
e.preventDefault();
items.find("li > a").eq(0).focus();
}
break;
case 'Escape':
case 'ArrowLeft':
case 'ArrowRight':
case 'Home':
case 'End':
case 'PageUp':
case 'PageDown':
close();
break;
case 'Enter':
if( stack.length === 1 ){
close();
break;
}
let focused = $(':focus');
if(!focused.is(items.find('li > a'))){
e.stopPropagation();
e.preventDefault();
items.find('li > a').eq(0).click();
}
break;
}
if( stack.length <= 1 || !key.match(/^\w\s\:\-\+$|Backspace/)) return; const matchedEmoji = fizzSearch(stack, emojis)
if( matchedEmoji.length === 0){
close();
return;
}
const newItems = $('<ul>').addClass('dropdown-menu');
matchedEmoji.forEach( ( emoji, index) => {
if( index > 30 ) return;
const li = $('<li>').addClass('dropdown-item');
const a = $('<a>').attr("tabindex", "0");
const img = $('<img>').attr("src", emoji.icon)
.addClass("icon").css({ height: "17px", float: "left"});
const nameTag = $('<div>').text(" :" + emoji.name + ":");
a.append(img);
a.append(nameTag);
li.append(a);
newItems.append(li);
a.on('click', () => {
cursor.focus();
replaceText(stack, cursor, emoji.path);
})
a.on('keypress', ev => {
if(ev.key === "Enter"){
ev.preventDefault();
ev.stopPropagation();
replaceText(stack, cursor, emoji.path);
}
})
})
items.replaceWith(newItems);
items = newItems;
let css = {};
cursor.style.cssText.split(';').filter( text => text !== '' )
.forEach( text => {
const props = text.split(':').map( text => text.replace(' ', '').replace('px', ''));
});
box.css({
top: ${parseInt(css.top) + parseInt(css.height) + 3}px,
left: ${css.left}px,
});
})