generated at
shirai
settingsに設定がある。


script.js
scrapbox.TimeStamp.addFormat('HH:mm ')

script.js
const Asearch = (function() { var INITPAT, INITSTATE, MAXCHAR; INITPAT = 0x80000000; MAXCHAR = 0x100; INITSTATE = [INITPAT, 0, 0, 0]; 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) { this.shiftpat[c] = 0; } ref1 = this.unpack(this.source); for (j = 0, len = ref1.length; j < len; j++) { c = ref1[j]; if (c === 0x20) { this.epsilon |= mask; } else { this.shiftpat[c] |= mask; this.shiftpat[this.toupper(c)] |= mask; this.shiftpat[this.tolower(c)] |= mask; 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 = ''; } i0 = state[0]; i1 = state[1]; i2 = state[2]; i3 = state[3]; ref = this.unpack(str); for (i = 0, len = ref.length; i < len; i++) { c = ref[i]; mask = this.shiftpat[c]; 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; } return [i0, i1, i2, i3]; }; 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 (s[ambig] & 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++) { c = ref[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', image: 'https://gyazo.com/d57fea8a143650375af1c8bba1fc1370/raw' }) 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; }) result = [ ...result, ...notExisted]; } 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; }) return [...a, ...c]; } 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]; if( key.match(/^[\w\s\-\:\+]$/) ){ 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', '')); css[props[0]] = props[1]; }); box.css({ top: `${parseInt(css.top) + parseInt(css.height) + 3}px`, left: `${css.left}px`, }); })