コードブロックの頭とコードブロック中は動作しないようにupdate
さんが作ったものを反映させていただいた :aaaa:
を打つと絵文字(scrapbox上のアイコン)が入力できるようになるUserScript 入力した文字に曖昧マッチングした候補が下に出てくる 上下キーを押して選択しEnterを押すか、クリックすると絵文字の文字列 [something.icon]
に置換できる :yutaro: => 
:yuta + Enter => 
入力途中でEnterを押すと一番上の候補が入力される 右の
ボタンから load emojis from /emoji をクリックすると/emoji/にあるアイコンも使えるようになる (デフォルトではロードしたプロジェクト内の半角英数字, _ , 空白文字, "+-"で構成されているアイコンのみがページがデータセットとして読み込まれる) :+1: => 
:parrot: =>
同期的なコミュニケーションのキラーコンテンツは絵文字とリアクションだと思う jQueryあんまり書いたことないからいい感じにかけてるかどうか全くわからない... 全体的にかなり無理やり実装しているので、色々バグがあるかも... 検索にはここに書いてあるやつを雑に実装して使ってみた ドロップダウンメニューは画面上部の検索窓とほぼ同じもの サンプルと解説記事がいっぱいあると組み合わせるだけで色々作れて楽しい!! const Asearch = (function() {
var INITPAT, INITSTATE, MAXCHAR;
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) {
Asearch.prototype.toupper = function(c) {
function Asearch(source) {
var c, i, j, len, mask, ref, ref1;
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++) {
this.shiftpat[c] |= mask;
this.shiftpat[this.toupper(c)] |= mask;
this.shiftpat[this.tolower(c)] |= mask;
Asearch.prototype.state = function(state, str) {
var c, i, i0, i1, i2, i3, len, mask, ref;
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);
Asearch.prototype.match = function(str, ambig) {
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;
for (i = 0, len = ref.length; i < len; i++) {
bytes.push((code & 0xFF00) >>> 8);
const projectName = scrapbox.Project.name;
const box = $('<div>').addClass('form-group').css("position", "absolute");
const container = $('<div>').addClass('dropdown');
let items = $('<ul>').addClass('dropdown-menu');
$('#editor').append(box);
fetch(`/api/pages/${projectName}?limit=10000`, { credentials: 'same-origin'})
.then( res => res.text())
const data = JSON.parse( text );
const pages = data.pages;
pages.filter( page => (page.image !== null && page.title.match(/^[\w\s\-\+]+$/)))
icon: `/api/pages/${projectName}/${page.title}/icon`,
scrapbox.PageMenu.addMenu({
image: 'https://gyazo.com/d57fea8a143650375af1c8bba1fc1370/raw'
scrapbox.PageMenu('emoji').addItem({
title: "load emojis from /emoji",
fetch('/api/pages/emoji?limit=10000')
.then( res => res.text())
const data = JSON.parse( text );
const pages = data.pages;
pages.filter( page => (page.image !== null && page.title.match(/^[\w\s\-\+]+$/)))
for( let emoji of emojis ) {
if( emoji.name === page.title )return;
path: '/emoji/' + page.title,
icon: `/api/pages/emoji/${page.title}/icon`,
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;
for(let i = 0; i <= limitCount; i++){
let matched = list.filter( item => a.match( item.name, i));
let notExisted = matched.filter( item => {
if(r.name === item.name){
result = [ ...result, ...notExisted];
const fizzSearch = ( word, list ) => {
const a = asearched( word, list );
const b = taberareloo( word, list );
const c = b.filter( item => {
if( r.name == item.name){
const editor = $('#editor');
const open = () => container.addClass("open");
container.removeClass("open");
const replaceText = (text, cursor, emojiPath) => {
for(let i = 0; i < text.length; i++){
var ke1 = document.createEvent("Events");
ke1.initEvent("keydown", true, true);
ke1.keyCode = ke1.which = 8;
cursor.dispatchEvent(ke1);
document.execCommand('insertText',null, `[${emojiPath}.icon]` );
if(key === undefined ) return;
if( stack === "" && key !== ":"){
if ($('.cursor-line').text().trim() == 'code:'
|| $('.cursor-line .code-block').length == 1) {
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);
const cursor = $('#text-input')[0];
if( key.match(/^[\w\s\-\:\+]$/) ){
let focused = $(':focus');
if(focused.is(items.find('li > a'))){
if( stack.length === 2 ){
stack = stack.slice(0, stack.length - 1);
let focusedUp = $(':focus');
if( focusedUp.is(items.find('li > a').eq(0)) ){
}else if( !focusedUp.is(items.find('li > a')) ){
let focusedDown = $(':focus');
if( !focusedDown.is(items.find('li > a'))) {
items.find("li > a").eq(0).focus();
if( stack.length === 1 ){
let focused = $(':focus');
if(!focused.is(items.find('li > a'))){
items.find('li > a').eq(0).click();
if( stack.length <= 1 || !key.match(/^[\w\s\:\-\+]$|Backspace/)) return;
const matchedEmoji = fizzSearch(stack, emojis)
if( matchedEmoji.length === 0){
const newItems = $('<ul>').addClass('dropdown-menu');
matchedEmoji.forEach( ( emoji, index) => {
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 + ":");
replaceText(stack, cursor, emoji.path);
replaceText(stack, cursor, emoji.path);
items.replaceWith(newItems);
cursor.style.cssText.split(';').filter( text => text !== '' )
const props = text.split(':').map( text => text.replace(' ', '').replace('px', ''));
css[props[0]] = props[1];
top: `${parseInt(css.top) + parseInt(css.height) + 3}px`,