/*
 * styles.js: 代替スタイルシート切替JavaScript
 *
 * Copyright (c) 2003-2009 confetto. <confetto@s31.xrea.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * The latest version is here: http://confetto.s31.xrea.com/
 * $Id: styles.js,v 2.0 2009/01/25 17:56:20 confetto Exp $
 */

(function() {
/********************************** 初期設定 **********************************/

// フォームを追加する要素のID。そのような要素が無い場合は自動的に、BODY要素の先
// 頭にこのID属性値を持ったDIV要素を追加します。
var FORM_CONTAINER_ID = 'header';

// フォームのラベル。
var FORM_LABEL = '';

// フォームのアクセスキー。""を指定するとアクセスキーを設定しません。
var FORM_ACCESSKEY = 's';

// フォームの種類。
//	0: プルダウンメニュー
//	1: リストとラジオボタン
var FORM_TYPE = 0;

// すべての優先・代替スタイルシートを無効にする選択肢の名前。メニューの最初に追
// 加されます。''を指定すると追加されません。各スタイル名と重複してはいけません。
var BASE_STYLE_NAME = '標準';

// すべてのスタイルシートを無効にする選択肢の名前。メニューの最後に追加されます。
// 各スタイル名と重複してはいけません。
var PLAIN_STYLE_NAME = 'plain';

// クッキーの名前。
var COOKIE_NAME = 'styles';

// クッキーの有効期限。日数で指定。
var COOKIE_EXP_DAYS = 30;

// クッキーのパス設定。例えば、サイトのホームディレクトリのパス("/"や"/~user/")
// を指定すれば、あるページでのスタイル切替をサイト内のすべてのページに反映させ
// られます。
var COOKIE_PATH = '';

// 追加代替スタイルシート。文書に設定する以外に、このスクリプトで代替スタイルシ
// ートを追加できます。外部スタイルシートのURIとスタイル名とを対にして、以下のよ
// うにカンマで区切って並べてください。
var ADDITIONAL_SHEETS = [
//	{ title : 'スタイル1', href : '/styles/style1.css' },
	{ title : 'Mode 1', href : './style/res_nav.css' },
	{ title : 'Mode 2', href : './style/ex_nav.css' }
];

/********************************** ルーチン **********************************/

var SwitchForm = function() {};

SwitchForm.prototype = {
	/** フォームのラベル */
	label : 'Switch',
	
	/** フォームのアクセスキー */
	accesskey : '',
	
	/** フォームの既定値 */
	defaultValue : null,
	
	/**
	 * フォーム切り替えイベント。何もしない。
	 *
	 * @param value 選択された値
	 */
	onswitch : function(value) {},
	
	/**
	 * objectの全プロパティを自身にコピーして自身を拡張する。
	 *
	 * @param {Object} object コピー元のオブジェクト
	 * @return {SwitchForm} 自身のオブジェクト
	 */
	extend : function(object) {
		for (var property in object)
			this[property] = object[property];
		return this;
	},
	
	/**
	 * ノードを生成する。継承先での実装必須。
	 *
	 * @return {Node} 生成されたノード
	 */
	toNode : function() { throw Error() },
	
	/**
	 * フォームに追加されるイベントハンドラ。継承先での実装必須。
	 *
	 * @param {Event} event ディスパッチされたイベント
	 */
	handleEvent : function(event) { throw Error() },
	
	/**
	 * 初期化する。継承先のコンストラクタで呼ばれる必要あり。
	 */
	initialize : function() {
		// handleEvent()を自身に拘束する。
		var self = this;
		var handleEvent = this.handleEvent;
		this.handleEvent = function() { handleEvent.apply(self, arguments) };
	},
	
	/**
	 * フォームの値を反復する。
	 *
	 * @param {Function} iterator フォームの値を引数に取るイテレータ
	 * @param {Object} context iteratorを実行するときのthis
	 */
	eachValue : function(iterator, context) {}
};

/******************************************************************************/

/**
 * プルダウンメニューのフォーム。
 */
var ListBoxForm = function() {
	this.initialize();
};

ListBoxForm.prototype = new SwitchForm().extend({
	toNode : function() {
		var labelElement = document.createElement('label');
		var labelTextNode = document.createTextNode(this.label);
		var selectElement = document.createElement('select');
		
		if (this.accessKey)
			labelElement.setAttribute('accesskey', this.accessKey);
		
		labelElement.appendChild(labelTextNode);
		labelElement.appendChild(selectElement);
		
		this.eachValue(function(value) {
			var optionElement = document.createElement('option');
			var optionTextNode = document.createTextNode(value);
			
			if (value === this.defaultValue)
				optionElement.setAttribute('selected', 'selected');
			
			optionElement.appendChild(optionTextNode);
			selectElement.appendChild(optionElement);
		}, this);
		
		Event.add(selectElement, 'change', this.handleEvent);
		Event.add(selectElement, 'keyup', this.handleEvent);
		
		return labelElement;
	},
	
	handleEvent : function(event) {
		var target = event.currentTarget;
//		if (/^select$/i.test(target.nodeName))
			this.onswitch(target.options[target.selectedIndex].text);
	}
});

/******************************************************************************/

/**
 * リストとラジオボタンのフォーム。
 */
var RadioButtonForm = function() {
	this.initialize();
};

RadioButtonForm.prototype = new SwitchForm().extend({
	toNode : function() {
		var fieldSetElement = document.createElement('fieldset');
		var legendElement = document.createElement('legend');
		var legendTextNode = document.createTextNode(this.label);
		var uListElement = document.createElement('ul');
		
		if (this.accessKey)
			legendElement.setAttribute('accesskey', this.accessKey);
		
		fieldSetElement.appendChild(legendElement);
		fieldSetElement.appendChild(uListElement);
		legendElement.appendChild(legendTextNode);
		
		this.eachValue(function(value) {
			var listElement = document.createElement('li');
			var labelElement = document.createElement('label');
			var inputElement = document.createElement('input');
			var labelTextNode = document.createTextNode(value);
			
			inputElement.setAttribute('type', 'radio');
			inputElement.setAttribute('name', 'style');
			inputElement.setAttribute('value', value);
			
			if (value === this.defaultValue)
				inputElement.setAttribute('checked', 'checked');
			
			if (UserAgent.msie) {
				inputElement = document.createElement(
					'<input type=radio name=style value="' + value +
					(value === this.defaultValue ? '" checked>' : '">'));
			}
			
			uListElement.appendChild(listElement);
			listElement.appendChild(labelElement);
			labelElement.appendChild(inputElement);
			labelElement.appendChild(labelTextNode);
			
			Event.add(inputElement, 'click', this.handleEvent);
			Event.add(inputElement, 'keyup', this.handleEvent);
		}, this);
		return fieldSetElement;
	},
	
	handleEvent : function(event) {
		var target = event.currentTarget;
		if (/*/^input$/i.test(target.nodeName) &&*/ target.checked)
			this.onswitch(target.value);
	}
});

/******************************************************************************/

var StopIteration = {};

/******************************************************************************/

/**
 * StyleSheetListオブジェクトのラッパ。
 */
var StyleSheetList = {
	s : document.styleSheets,

	/**
	 * スタイルシートの名前を反復する。
	 *
	 * @param {Function} iterator スタイルシートの名前を引数に取る反復子
	 * @param {Function} filter スタイルシートを引数に取り真偽値を返すフィルタ
	 * @param {Object} context iteratorを実行するときのthis
	 */
	eachName : function(iterator, filter, context) {
		filter = filter || function() { return true };
		var cache = {};
		try {
			for (var i = 0; i < this.s.length; i++) {
				if (filter(this.s[i]) && !cache[this.s[i].title]) {
					cache[this.s[i].title] = true;
					iterator.call(context, this.s[i].title);
				}
			}
		} catch (e) {
			if (e !== StopIteration)
				throw e;
		}
	},
	
	/**
	 * 指定された名前のスタイルシートが存在するかどうかを検査する。
	 *
	 * @param {String} name スタイルシートの名前
	 * @return {Boolean} 存在するならTRUE、さもなくばFALSE
	 */
	includesName : function(name) {
		for (var i = 0; i < this.s.length; i++)
			if (this.s[i].title === name)
				return true;
		return false;
	},
	
	/**
	 * スタイルシートを切り替える。
	 *
	 * @param {String} name 切り替えるスタイルシートの名前
	 */
	Switch : function(name) {
		for (var i = 0; i < this.s.length; i++) {
			this.s[i].disabled = true;	// Opera9で必要?
			this.s[i].disabled =
				this.s[i].title !== '' && this.s[i].title !== name;
		}
	},

	/**
	 * すべて無効にする。
	 */
	disableAll : function() {
//		for (var i = 0; i < this.s.length; i++)
//			this.s[i].disabled = true;
	},
	
	/**
	 * LINK要素でスタイルシートを追加する。attributesのプロパティをLINK要素の属
	 * 性に設定する。rel属性もしくはtype属性が省略されると、それぞれ 'alternate
	 * stylesheet'、'text/css'を設定する。HEAD要素が存在するときに呼び出されなけ
	 * ればならない。
	 *
	 * @param {Object} attributes LINK要素の属性をプロパティに持ったオブジェクト
	 */
	addSheet : function(attributes) {
		var headElement = document.getElementsByTagName('head')[0];
		var linkElement = document.createElement('link');
		
		attributes.rel = attributes.rel || 'alternate stylesheet';
		attributes.type = attributes.type || 'text/css';
		
		for (var name in attributes)
			if (attributes.hasOwnProperty(name))
				linkElement.setAttribute(name, attributes[name]);
		headElement.appendChild(linkElement);
	},
	
	/**
	 * StyleSheetList未実装ブラウザ向けに、LINK要素の配列で初期化する。
	 */
	initByLinks : function() {
		this.s = [];
		var linkElements = document.getElementsByTagName('link');
		for (var i = 0; i < linkElements.length; i++)
			if (/stylesheet/i.test(linkElements[i].rel))
				this.s.push(linkElements[i]);
	}
};

/******************************************************************************/

/**
 * MSIEのためのEvent。イベント発生時に生成しなければならない。
 */
var EventIE = function() {
	this.target = window.event.srcElement;
	this.currentTarget = this.target;	// 場当たりな代用品
};

/******************************************************************************/

var Event = {
	/**
	 * イベントを追加する。
	 *
	 * @param {EventTarget} target listenerを追加するオブジェクト
	 * @param {String} type 追加するイベントの型
	 * @param {Function} listner targetに追加する関数
	 */
	add : function(target, type, listener) {
		if (target.addEventListener) {
			target.addEventListener(type, listener, false);
		} else if (target.attachEvent) {	// MSIE5+ 代用品
			target.attachEvent('on' + type,
				function() { listener(new EventIE()) });
			target = null;	// メモリリーク防止
//		} else {
//			target['on' + type] = listener;
		}
	},
	
	/**
	 * DOMContentLoadedイベントを追加する。
	 *
	 * @param {Function} listener 追加する関数
	 */
	onDOMReady : function(listener) {
		var dispatched = false;
		var wrapper = function() {
			if (!dispatched) {
				dispatched = true;
				listener.apply(this, arguments);
			}
		};
		this.add(window, 'load', wrapper);
		this.add(window, 'DOMContentLoaded', wrapper);
		
		// MSIEのためのDOMContentLoaded (暫定)
		// 参考: http://dean.edwards.name/weblog/2006/06/again/
		if (UserAgent.msie) {
			var scripts = document.getElementsByTagName('script');
			document.write('<script defer src=//:><\/script>');
			scripts[scripts.length - 1].onreadystatechange = function() {
				if (this.readyState === 'complete')
					wrapper();
			};
		}
	}
};

/******************************************************************************/

/**
 * Cookieを操作する。
 *
 * @param {String} name 名前
 * @param {Number} days 有効期限までの日数 (省略可)
 * @param {String} path 有効になるパス (省略可)
 * @param {String} domain 有効になるドメイン (省略可)
 */
var Cookie = function(name, days, path, domain) {
	this.name = name;
	this.days = days;
	this.path = path;
	this.domain = domain;
	this.value = this.get(name);
};

Cookie.prototype = {
	setValue : function(value) {
		this.value = value;
		this.set(this.name, value, this.days, this.path, this.domain);
	},
	
	get : function(name) {
		name = encodeURIComponent(name);
		name = name.replace(/([$^.*+?=!:|\/()\[\]{}])/g, '\\$1');
		var regexp = new RegExp('(?:^|; ?)' + name + '=([^;]*)');
		var matched = regexp.exec(document.cookie);
		return matched ? decodeURIComponent(matched[1]) : '';
	},
	
	/**
	 * Cookieを発行する。
	 *
	 * @param {String} name 名前
	 * @param {String} value 値
	 * @param {Number} days 有効期限までの日数 (省略可)
	 * @param {String} path 有効になるパス (省略可)
	 * @param {String} domain 有効になるドメイン (省略可)
	 */
	set : function(name, value, days, path, domain) {
		var result = encodeURIComponent(name) + '=' + encodeURIComponent(value);
		if (days)
			result += '; expires=' + this.expires(days);
		if (path)
			result += '; path=' + path;
		if (domain)
			result += '; domain=' + domain;
		document.cookie = result;
	},
	
	expires : function(days) {
		var date = new Date();
		date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
		return date.toUTCString();
	}
};

/******************************************************************************/

/**
 * User-Agentの種類とバージョンを判別する。各ブラウザを表すプロパティにバージョ
 * ンを表す数値が入る。該当しないプロパティは定義しない(undefined)。
 *
 *	gecko	Gecko (Mozilla等)
 *	opera	Opera
 *	webkit	WebKit (Safari等)
 *	msie	MSIE (Trident)
 */
var UserAgent = new function() {
	var ua = navigator.userAgent;
	var matched;
	
	if (matched = /\bGecko\/(\d+)/.exec(ua))
		this.gecko = Number(matched[1]);
	
	// Operaの一部はMSIEを詐称する。
	else if (matched = /\bOpera[\/\s](\d+\.\d+)/.exec(ua))
		this.opera = Number(matched[1]);
	
	else if (matched = /\bAppleWebKit\/(\d+\.\d+)/.exec(ua))
		this.webkit = Number(matched[1]);
	
	else if (matched = /\bMSIE (\d+\.\d+)/.exec(ua))
		if (!/\bMac_PowerPC\b/.test(ua))
			this.msie = Number(matched[1]);
};

/******************************************************************************/

var StyleSwitcher = {
	cookie : new Cookie(COOKIE_NAME, COOKIE_EXP_DAYS, COOKIE_PATH),
	
	/**
	 * Cookieを取得し、スタイルシートを初期化する。
	 */
	initSheets : function() {
		if (!document.styleSheets || UserAgent.webkit)
			StyleSheetList.initByLinks();
		
		if (this.cookie.value)
			this.Switch(this.cookie.value);
	},
	
	/**
	 * フォームを追加する。
	 */
	appendForm : function() {
		var form = new [ListBoxForm, RadioButtonForm][FORM_TYPE]();
		var self = this;
		
		form.extend({
			onswitch : function(name) { self.Switch(name) },
			eachValue : this.eachFormValue,
			defaultValue : this.getFormDefaultValue(),
			label : FORM_LABEL,
			accessKey : FORM_ACCESSKEY
		});
		
		this.getFormContainer().appendChild(form.toNode());
	},
	
	/**
	 * スタイルシートを切り替え、Cookieを発行する。
	 *
	 * @param {String} name スタイルシートの名前
	 */
	Switch : function(name) {
		switch (name) {
			case PLAIN_STYLE_NAME:
				StyleSheetList.disableAll();
				break;
			default:
				StyleSheetList.Switch(name);
		}
		this.cookie.setValue(name);
	},
	
	/**
	 * フォームを追加する要素を取得するか、もしくは生成する。
	 *
	 * @return {Element} フォームを追加する要素
	 */
	getFormContainer : function() {
		var container = document.getElementById(FORM_CONTAINER_ID);
		if (!container) {
			var bodyElement = document.getElementsByTagName('body')[0];
			container  = document.createElement('div');
			container.setAttribute('id', FORM_CONTAINER_ID);
			bodyElement.insertBefore(container, bodyElement.firstChild);
		}
		return container;
	},
	
	/**
	 * フォームに表示する値を反復する。
	 *
	 * @param {Function} iterator 表示する値を引数に取るイテレータ
	 * @param {Object} context iteratorを実行するときのthis
	 */
	eachFormValue : function(iterator, context) {
		if (BASE_STYLE_NAME && StyleSheetList.includesName(''))
			iterator.call(context, BASE_STYLE_NAME);
		
		StyleSheetList.eachName(iterator,
			function(s) { return s.title !== '' }, context);
		
//		iterator.call(context, PLAIN_STYLE_NAME);
	},
	
	/**
	 * フォームの既定値を取得する。
	 *
	 * @return {String} 取得した値
	 */
	getFormDefaultValue : function() {
//		var result = PLAIN_STYLE_NAME;
		var filter = function(s) { return !s.disabled };
		StyleSheetList.eachName(function(name) {
			if (name) {
				result = name;
				throw StopIteration;
			} else {
				// BASE_STYLE_NAME未設定時の動作は未定義
				result = BASE_STYLE_NAME;
			}
		}, filter);
		return result;
	}
};

/******************************************************************************/

/*
 * メインルーチン
 */
if (document.getElementById) {
	for (var i = 0; i < ADDITIONAL_SHEETS.length; i++)
		StyleSheetList.addSheet(ADDITIONAL_SHEETS[i]);
	
	// 古いGeckoはページ読み込み前にスタイルシートを切り替えられない。
	if (!(UserAgent.gecko < 20030624)) // Mozilla1.4+?
		StyleSwitcher.initSheets();
	
	Event.onDOMReady(function() {
		StyleSwitcher.initSheets();
		StyleSwitcher.appendForm();
	});
}

})();
