<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF xmlns="http://purl.org/rss/1.0/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel rdf:about="https://let.hatelabo.jp/s_hiiragi/rss">
    <link>https://let.hatelabo.jp/s_hiiragi/rss</link>
    <description></description>
    <title>Bookmarklets from s_hiiragi</title>
    <items>
      <rdf:Seq>
        <rdf:li rdf:resource="https://let.hatelabo.jp/s_hiiragi/let/gYC-xtmZ6rn0ZQ"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/s_hiiragi/let/gYC-ybDNnfbaNg"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/s_hiiragi/let/gYC-xI6zppGxKQ"/>
      </rdf:Seq>
    </items>
  </channel>
  <item rdf:about="https://let.hatelabo.jp/s_hiiragi/let/gYC-xtmZ6rn0ZQ">
    <link>https://let.hatelabo.jp/s_hiiragi/let/gYC-xtmZ6rn0ZQ</link>
    <dc:date>2012-11-04T04:11:00Z</dc:date>
    <description>見出しっぽいものをマウスオーバー or ダブルクリックすると目次を表示</description>
    <dc:creator>s_hiiragi</dc:creator>
    <title>[Let] implicit-toc.js</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2Fs_hiiragi%2Flet%2FgYC-xtmZ6rn0ZQ.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;implicit-toc.js&lt;/a&gt;&lt;pre&gt;/**
 * @title        implicit-toc.js
 * @description  見出しっぽいものをマウスオーバー or ダブルクリックすると目次を表示
 * @include      http://*
 * @license      MIT License
 * 
 * @author       s_hiiragi
 * @updated      2012/11/03 14:40:51
 */

/* Implicit Table of Contents
 * 
 * 概要
 * 
 * マウスオーバー or ダブルクリックしたテキストが見出しっぽければ、
 * 同様の見出しを探して目次を表示します。
 */

/* 技術的な説明
 * 
 * 見出しの判定基準
 *   - 要素が見出し要素(header, h{n}, li)だった (liはマウスオーバー時のみ)
 *   - 要素が見出し要素(header, h{n}, li)に含まれている (liはマウスオーバー時のみ)
 * 
 * 同様の見出しの検索法
 *   1. 見出し要素から(#hoge &amp;gt; fuga &amp;gt; piyo &amp;gt; ...)のようなCSSセレクタを生成
 *   2. セレクタにマッチする見出し要素を取得
 */

/* TODO
 * 
 * - 見出し要素にidが設定されている or name付きa要素が含まれている場合、
 *   そのid or nameをCaptionViewのアンカーに設定する (scrollToの代わりに)。
 */
(function() {
	// メインコード
	
	var debug //= true;
	
	var Caption, CaptionListView, CaptionView;
	defineClasses();
	log('Caption', Caption);
	log('CaptionListView', CaptionListView);
	log('CaptionView', CaptionView);
	
	document.addEventListener('dblclick', function(event) {
		log('dblclick', event);
		log('selection', window.getSelection());
		
		var selCaption = getSelectedCaption();
		log('selCaption', selCaption);
		if (!selCaption) return;
		
		showCaptionList(selCaption);
	}, true);
	
	document.addEventListener('mouseover', function(event) {
		log('mouseover', event);
		log(event.target.tagName);
		console.warn('mouseover ', 
			(event.fromElement? event.fromElement.tagName: null) + ' =&amp;gt; ' + event.toElement.tagName);
		
		// [ [from] to ] の場合
		//   ※ DOMツリー上で to 要素に from 要素が含まれていることを表している
		// event.targetが見出し要素だとすると、
		//   見出し要素内の要素から見出し要素への移動は無視する
		if (event.target.contains(event.relatedTarget)) {
			return;
		}
		// 以降は [ from [to] ] の場合
		// [from][to]の場合、[[from] div], [div [to]]のように分かれてイベントが来るので
		// [[from] div]は見出し要素と無関係なので無視されて、
		// 結局、[div [to]] == [from [to]]を処理することとなる。
		
		// TODO リストが二重に表示される問題 - 2012/11/04
		// 
		// 問題のケース
		//   DIV =&amp;gt; H2 ... リスト表示
		//   H2 =&amp;gt; SPAN(祖先要素のH2に置換) ... リスト表示
		// 
		// 同じセレクタのリストは表示しないようにする。
		// globalプロパティで表示中のセレクタを管理し、同じセレクタなら排除
		
		var selElement = event.target;
		var reHeader = /^(?:header|h\d)$/i;
		if (!reHeader.test(selElement.tagName)) {
			// 先祖要素にheader, h{n}がないか調べる
			var header = getAncestorElement(selElement, reHeader);
			if (!header) return;
			
			selElement = header;
			console.warn('replace selElement', header);
		}
		
		var selCaption = new Caption(selElement)
			.selected(true);
		log('selCaption', selCaption);
		if (!selCaption) return;
		
		showCaptionList(selCaption);
	}, true);
	
	return;
	
	
	
	
	// ロジック
	
	function log(/* arg1, arg2, ..., argN */) {
		if (debug) {
			console.debug.apply(console, arguments);
		}
	}
	
	function assert(message, value) {
		if (debug &amp;amp;&amp;amp; !value) {
			console.error(message);
		}
	}
	
	function showCaptionList(selCaption)
	{
		var captionList = getSiblingCaptions(selCaption);
		log('captionList', captionList);
		
		// 見出し数が1(= 選択した項目のみ)の場合はリストを表示しない
		if (captionList.length == 1) return;
		
		console.warn(':::: mouseover :::: ');
		
		var captionListView = new CaptionListView(captionList);
		log('captionListView', captionListView);
		captionListView.show();
		
//		console.warn('captionList.entered ' + !!captionListView.data.entered);
		selCaption.element().style.bordr = '1px solid red';
		
		selCaption.
			onMouseLeave(function() {
//				console.warn('caption.leave');
				setTimeout(function() {
//					console.warn('caption.leave.timeout ' + !captionListView.data.entered);
					if (!captionListView.data.entered) {
						captionListView.dispose();
					}
				}, 200);
			});
		captionListView.
			onMouseEnter(function() {
				this.data.entered = true;
//				console.warn('captionList.enter ');
			}).
			onMouseLeave(function() {
//				console.warn('captionList.leave ');
				if (this.data.entered) {
					this.dispose();
				}
			});
		
		/*
		caption -&amp;gt; list -&amp;gt; 外 : ok
		caption -&amp;gt; 外
		*/
	}
	
	function getSelectedCaption() {
		var selElement = window.getSelection().anchorNode;
		if (selElement.nodeType != Node.ELEMENT_NODE)
			selElement = selElement.parentElement;
		log('selElement', selElement);
		
		if (/^(?:input|textarea)$/i.test(selElement.tagName))
			return null;
		
		// 先祖要素にheader, h{n}, liがないか調べる
		var reHeader = /^(?:header|h\d|li)$/i;
		if (!reHeader.test(selElement.tagName)) {
			var header = getAncestorElement(selElement, reHeader);
			if (header) {
				selElement = header;
				console.warn('replace selElement', header);
			}
		}
		
		return new Caption(selElement)
			.selected(true);
	}
	
	function getSiblingCaptions(targetCaption) {
		var targetElt = targetCaption.element();
		
		var selector = getSiblingsCssSelector(targetElt);
		log('selector', selector);
		console.warn('selector', selector);
		
		var siblings = 
			Array.prototype.slice.call(document.querySelectorAll(selector)).
			map(function(e) {
				if (e == targetElt) {
					return targetCaption;
				}
				return new Caption(e);
			});
		return siblings;
	}
	
	/**
	 * 対象要素と同じ階層の要素を抽出するためのCSSセレクタを取得
	 * 
	 * 対象要素から遡ってタグ名を取得し、'&amp;gt;'で連結する
	 *   途中でidを持った要素が出てきたら終了
	 *     ただし対象要素のidは除く (同じ階層の要素を抽出できなくなるため)
	 * 
	 * @param element
	 *   対象要素
	 * @return string
	 *   CSSセレクタ
	 */
	function getSiblingsCssSelector(element) {
		var selectorParts = [ element.tagName ];
		for (var e = element.parentElement; e != document; e = e.parentElement) {
			if (e.id) {
				selectorParts.unshift('#' + e.id);
				break;
			}
			selectorParts.unshift(e.tagName);
		}
		return selectorParts.join(' &amp;gt; ');
	}
	
	/**
	 * 条件に一致する先祖要素を取得
	 * @param element
	 *   探索を開始する要素
	 * @param ancestorTester String | RegExp | Function
	 *   String: タグ名(大文字/小文字は区別しない)
	 *   RegExp: タグ名とマッチングする正規表現
	 *   Function: テスト関数
	 *     function(element: HTMLElement): Boolean
	 * @return HTMLElement or null
	 *   先祖要素
	 */
	function getAncestorElement(element, ancestorTester) {
		if (typeof ancestorTester == 'string')
		{
			var tagName = ancestorTester.toLowerCase();
			ancestorTester = function (e) {
				return (e.tagName.toLowerCase() == tagName);
			};
		}
		else if (ancestorTester instanceof RegExp) {
			var re = ancestorTester;
			ancestorTester = function (e) {
				return re.test(e.tagName);
			};
		}
		
		var e = element;
		while (e != null &amp;amp;&amp;amp; !ancestorTester(e)) {
			e = e.parentElement;
		}
		return e;
	}
	
	
	
	
	// クラス
	
	/**
	 * 各クラスを定義
	 */
	function defineClasses() {
		/**
		 * 見出しオブジェクト
		 */
		Caption = function(element) {
			this._element = element;
			this._text = element.textContent.
				match(/^.*/)[0].
				replace(/&amp;lt;/g, '&amp;amp;lt;');
			if (this._text.length &amp;gt; 20) {
				this._text = this._text.substring(0, 20) + '…';
			}
			this._selected = false;
			this._listeners = [];
			this._data = {};
		};
		Caption.prototype = {
			element: function() {
				return this._element;
			}, 
			text: function() {
				return this._text;
			}, 
			selected: function(value) {
				if (arguments.length == 0)
					return !!this._selected;
				this._selected = !!value;
				return this;
			}, 
			rect: function() {
				if (this._rect) {
					return this._rect;
				}
				
				var rect = this._element.getBoundingClientRect();
				this._rect = {
					left   : window.scrollX + rect.left, 
					right  : window.scrollX + rect.right, 
					top    : window.scrollY + rect.top, 
					bottom : window.scrollY + rect.bottom
				};
				return this._rect;
			}, 
			dispose: function() {
				var that = this;
				this._listeners.forEach(function(e) {
					that._element.removeEventListener(e.type, e.listener, e.useCapture);
				});
			}, 
			onMouseLeave: function(callback) {
				var e = this._element;
				var that = this;
				e.addEventListener('mouseout', listener, true);
				this._listeners.push({type: 'mouseout', listener: listener, useCapture: true});
				
				function listener(event) {
					if (e.contains(event.relatedTarget))
						return;
					return callback.apply(that);
				}
				return this;
			}
		};
		
		/**
		 * 見出しリストビュー
		 */
		CaptionListView = function(captionList) {
			this._captionList = captionList;
			
			var selCaption = null;
			captionList.
				forEach(function(e) {
					if (e.selected()) {
						selCaption = e;
					}
				});
			this._selectedCaption = selCaption;
			assert('selCaption != null', selCaption != null);
			
			var ul = document.createElement('ul');
			var x = selCaption.rect().left, 
				y = selCaption.rect().bottom// + 5;
			var props = {
				display            : 'none', 
				'list-style-type'  : 'none', 
				position           : 'absolute', 
				left               : x + 'px', 
				top                : y + 'px', 
				margin             : '0px', 
				padding            : '4px', 
				border             : '1px solid #888', 
				'background-color' : '#EEE', 
				
				'max-height': '300px', 
				'overflow-y': 'scroll'
			};
			for (var i in props) {
				ul.style.setProperty(i, props[i]);
			}
			document.body.appendChild(ul);
			this._element = ul;
			
			var that = this;
			captionList.
				forEach(function(caption) {
					var cv = new CaptionView(caption);
					ul.appendChild(cv.element());
					
					cv.element().addEventListener('click', function() {
						that.dispose();
					}, true);
				});
			
			this._listeners = [];
			this.data = {};
		};
		CaptionListView.prototype = {
			element: function() {
				return this._element;
			}, 
			show: function() {
				this._element.style.display = 'block';
				document.addEventListener('click', this, true);
				return this;
			}, 
			hide: function() {
				this._element.style.display = 'none';
				this._removeEventListeners();
				return this;
			}, 
			handleEvent: function(event) {
				switch (event.type) {
				case 'click': // TODO この処理は不要かも
					log(event);
					var e = this._selectedCaption.element();
					if (e.contains(event.target)) return;
					this.dispose();
					break;
				}
			}, 
			dispose: function() {
				console.warn('dispose');
				
				this._element.parentNode.removeChild(this._element);
				this._removeEventListeners();
				this._selectedCaption.dispose();
			}, 
			_removeEventListeners: function() {
				document.removeEventListener('click', this, true);
				var that = this;
				this._listeners.forEach(function(e) {
					that._element.removeEventListener(e.type, e.listener, e.useCapture);
				});
			}, 
			onMouseEnter: function(callback) {
				var e = this._element;
				var that = this;
				e.addEventListener('mouseover', listener, true);
				this._listeners.push({type: 'mouseout', listener: listener, useCapture: true});
				
				function listener(event) {
					if (e.contains(event.relatedTarget))
						return;
					return callback.apply(that);
				}
				return this;
			}, 
			onMouseLeave: function(callback) {
				var e = this._element;
				var that = this;
				e.addEventListener('mouseout', listener, true);
				this._listeners.push({type: 'mouseout', listener: listener, useCapture: true});
				
				function listener(event) {
					if (e.contains(event.relatedTarget))
						return;
					return callback.apply(that);
				}
				return this;
			}
		};
		
		/**
		 * 見出しビュー
		 * 
		 * 見出しリストビューに含まれる
		 */
		CaptionView = function(caption) {
			this._caption = caption;
			
			var li = document.createElement('li');
			
			if (caption.selected()) {
				// 選択中の見出し
				// 強調表示する
				li.style.fontWeight = 'bold';
				li.textContent = caption.text();
			} else {
				// その他の見出し
				// クリックした時、見出しの位置にスクロールする
				var a = document.createElement('a');
				a.style.cursor = 'pointer';
				a.onclick = function() {
					window.scrollTo(caption.rect().left - 10, caption.rect().top - 10);
				};
				a.textContent = caption.text();
				li.appendChild(a);
			}
			
			this._element = li;
		};
		CaptionView.prototype = {
			element: function() {
				return this._element;
			}
		};
	}
})();
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/s_hiiragi/let/gYC-ybDNnfbaNg">
    <link>https://let.hatelabo.jp/s_hiiragi/let/gYC-ybDNnfbaNg</link>
    <dc:date>2012-02-16T06:22:43Z</dc:date>
    <description>IEのブックマークに登録して、起動時に押して下さい</description>
    <dc:creator>s_hiiragi</dc:creator>
    <title>[Let] 前回の起動からの経過時間を表示</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2Fs_hiiragi%2Flet%2FgYC-ybDNnfbaNg.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;前回の起動からの経過時間を表示&lt;/a&gt;&lt;pre&gt;/*
 * @title 前回の起動からの経過時間を表示
 * @description IEのブックマークに登録して、起動時に押して下さい
 * @include http://*
 * @license MIT License
 * @require 
 */

javascript:(function(l,k,t,d,_){(d=t-l[k])&amp;amp;&amp;amp;(d=new Date(d))&amp;amp;&amp;amp;alert('\u524d\u56de\u306e\u8d77\u52d5\u304b\u3089'+(((_=d.getUTCMonth())?_+'\u6708':'')+((_=d.getUTCDate()-1)?_+'\u65e5':'')+((_=d.getUTCHours())?_+'\u6642\u9593':'')+((_=d.getUTCMinutes())?_+'\u5206':'')||d.getUTCSeconds()+'\u79d2')+'\u7d4c\u904e\u3057\u3066\u3044\u307e\u3059\u3002');l[k]=t})(localStorage,'__last_startup',+new Date)
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/s_hiiragi/let/gYC-xI6zppGxKQ">
    <link>https://let.hatelabo.jp/s_hiiragi/let/gYC-xI6zppGxKQ</link>
    <dc:date>2011-06-09T10:42:56Z</dc:date>
    <description>Googleトップページのギターの演奏を記録したり、再生したりします</description>
    <dc:creator>s_hiiragi</dc:creator>
    <title>[Let] Google Guitarlet</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2Fs_hiiragi%2Flet%2FgYC-xI6zppGxKQ.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;Google Guitarlet&lt;/a&gt;&lt;pre&gt;/*
 * @title Google Guitarlet
 * @description Googleトップページのギターの演奏を記録したり、再生したりします
 * @include http://www.google.co.jp
 * @license MIT License
 * @require 
 */

/* 動作環境: 
 *   Firefox 4.0.1で動作確認　
 *   Google Chromeでは動作しない
 * 
 * 操作方法
 *   あらかじめGoogleトップページのギター下に表示されているキーボードボタンを押します
 *   
 *   Rキー  記録開始
 *   Sキー  記録した演奏を再生
 *   
 *   録音中の操作
 *   Eキー  記録終了
 */

javascript:
(function(){
	var state_ = '_';
	window.addEventListener('keydown', function(e){
		var key = e.keyCode;
		
		switch( state_ ){
		case '_':
			if( key === 'R'.charCodeAt(0) ){
				state_ = 'R';
				document.title = '録音中';
				startRecording();
				
				e.stopPropagation();
			}else if( key === 'S'.charCodeAt(0) ){
				state_ = 'P';
				document.title = '再生中';
				perform(function(){
					state_ = '_';
					document.title = '再生終了';
				});
				
				e.stopPropagation();
			}
			break;
		case 'R':
			if( key === 'E'.charCodeAt(0) ){
				state_ = '_';
				stopRecording();
				document.title = '録音終了';
				
				e.stopPropagation();
			}
			break;
		case 'P':
			break;
		}
	}, true);
	
	var r = [];
	function startRecording(){
		r = [];
		document.body.addEventListener('keydown', keyRecorder, true);
	}
	function stopRecording(){
		document.body.removeEventListener('keydown', keyRecorder, true);
	}
	function keyRecorder(e){
		r.push({key: e.keyCode, time: e.timeStamp});
	}
	
	function perform(f){
		var i = 0;
		(function(){
			var e = r[i++];
			sendKey(e.key);
			if( i &amp;lt; r.length )
				setTimeout(arguments.callee, r[i].time - e.time);
			else f();
		})();
	}
	
	function sendKey(k){
		var e = document.createEvent('KeyboardEvent');
		e.initKeyEvent('keydown', !0, !0, window, !1, !1, !1, !1, k, 0);
		document.body.dispatchEvent(e);
	}
})();
&lt;/pre&gt;</content:encoded>
  </item>
</rdf:RDF>
