Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCristhofer Travieso <cristhofert97@gmail.com>2013-11-02 17:15:49 (GMT)
committer Cristhofer Travieso <cristhofert97@gmail.com>2013-11-02 17:15:49 (GMT)
commit90a4b10d3d3bb7f0576bd19c675b7b83aa11958b (patch)
treefb077ddb68a9330436da0be4f214e74083a2e9d5
parent99e261d5c2f483842f76d479bd668c6f133ab777 (diff)
Add css directory ,lib directory and index2.html file. Create toolbar and stop button
Signed-off-by: Cristhofer Travieso <cristhofert97@gmail.com>
-rw-r--r--activity/activity-install.svg63
-rw-r--r--css/activity.css47
-rw-r--r--index2.html27
-rw-r--r--lib/domReady.js129
-rw-r--r--lib/moment.js1662
-rw-r--r--lib/mustache.js610
-rw-r--r--lib/require.js2000
-rw-r--r--lib/sugar-web/LICENSE202
-rw-r--r--lib/sugar-web/README.md7
-rw-r--r--lib/sugar-web/activity/activity.js114
-rw-r--r--lib/sugar-web/activity/shortcut.js57
-rw-r--r--lib/sugar-web/bus.js219
-rw-r--r--lib/sugar-web/datastore.js221
-rw-r--r--lib/sugar-web/env.js27
-rw-r--r--lib/sugar-web/graphics/README.md39
-rw-r--r--lib/sugar-web/graphics/activitypalette.js70
-rw-r--r--lib/sugar-web/graphics/css/sugar.css456
-rw-r--r--lib/sugar-web/graphics/css/sugar.less589
-rw-r--r--lib/sugar-web/graphics/grid.js54
-rw-r--r--lib/sugar-web/graphics/icon.js81
-rw-r--r--lib/sugar-web/graphics/icons/actions/activity-stop.svg6
-rw-r--r--lib/sugar-web/graphics/icons/actions/checkbox-checked-selected.svg27
-rw-r--r--lib/sugar-web/graphics/icons/actions/checkbox-checked.svg27
-rw-r--r--lib/sugar-web/graphics/icons/actions/checkbox-unchecked-selected.svg22
-rw-r--r--lib/sugar-web/graphics/icons/actions/checkbox-unchecked.svg22
-rw-r--r--lib/sugar-web/graphics/icons/actions/dialog-cancel-active.svg6
-rw-r--r--lib/sugar-web/graphics/icons/actions/dialog-cancel.svg6
-rw-r--r--lib/sugar-web/graphics/icons/actions/dialog-ok-active.svg6
-rw-r--r--lib/sugar-web/graphics/icons/actions/dialog-ok.svg6
-rw-r--r--lib/sugar-web/graphics/icons/actions/entry-cancel-active.svg23
-rw-r--r--lib/sugar-web/graphics/icons/actions/entry-cancel-disabled.svg21
-rw-r--r--lib/sugar-web/graphics/icons/actions/entry-cancel.svg21
-rw-r--r--lib/sugar-web/graphics/icons/actions/radio-active-selected.svg31
-rw-r--r--lib/sugar-web/graphics/icons/actions/radio-active.svg31
-rw-r--r--lib/sugar-web/graphics/icons/actions/radio-selected.svg26
-rw-r--r--lib/sugar-web/graphics/icons/actions/radio.svg26
-rw-r--r--lib/sugar-web/graphics/icons/actions/zoom-groups.svg6
-rw-r--r--lib/sugar-web/graphics/icons/actions/zoom-home.svg6
-rw-r--r--lib/sugar-web/graphics/icons/actions/zoom-neighborhood.svg6
-rw-r--r--lib/sugar-web/graphics/icons/emblems/arrow-down.svg20
-rw-r--r--lib/sugar-web/graphics/icons/emblems/arrow-up.svg20
-rw-r--r--lib/sugar-web/graphics/menupalette.js64
-rw-r--r--lib/sugar-web/graphics/palette.js179
-rw-r--r--lib/sugar-web/graphics/radiobuttonsgroup.js59
-rw-r--r--lib/sugar-web/graphics/xocolor.js729
-rw-r--r--lib/sugar-web/package.json9
-rw-r--r--lib/webL10n.js1029
47 files changed, 9095 insertions, 13 deletions
diff --git a/activity/activity-install.svg b/activity/activity-install.svg
index 6ec19c6..ace9976 100644
--- a/activity/activity-install.svg
+++ b/activity/activity-install.svg
@@ -7,10 +7,34 @@
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="55"
height="55"
- id="svg2">
+ id="svg2"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="activity-install.svg">
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1301"
+ inkscape:window-height="744"
+ id="namedview9"
+ showgrid="false"
+ inkscape:zoom="11.34914"
+ inkscape:cx="28.746029"
+ inkscape:cy="9.1360114"
+ inkscape:window-x="65"
+ inkscape:window-y="24"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2" />
<defs
id="defs4" />
<metadata
@@ -21,23 +45,36 @@
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
+ <dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
- transform="translate(0,-997.36218)"
- id="layer1">
+ id="g2988"
+ transform="matrix(1,0,0,1.1310955,-0.23305084,-2.4539613)">
<g
- id="g3030">
- <path
- d="m 19.904201,1003.2636 0,25.5188 -10.3461383,-2.0948 18.0076243,17.573 16.36146,-15.9666 -9.637329,0.9 0.125119,-25.8083 z"
- id="path3004"
- style="fill:fill_color;stroke:stroke_color;stroke-width:1.13395655px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
- <path
- d="m 6.4617358,1044.7775 42.7751092,0"
- id="path3010"
- style="fill:none;stroke:stroke_color;stroke-width:1.20252109px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ style="fill:#000000;stroke:#ffffff"
+ id="layer1"
+ transform="translate(-0.23305083,-996.42998)">
+ <g
+ style="fill:#000000;stroke:#ffffff"
+ id="g3030">
+ <path
+ inkscape:connector-curvature="0"
+ style="fill:#000000;stroke:#ffffff;stroke-width:1.13395655px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path3004"
+ d="m 19.904201,1003.2636 0,25.5188 -10.3461383,-2.0948 18.0076243,17.573 16.36146,-15.9666 -9.637329,0.9 0.125119,-25.8083 z" />
+ <path
+ inkscape:connector-curvature="0"
+ style="fill:#000000;stroke:#ffffff;stroke-width:1.20252109px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path3010"
+ d="m 6.4617358,1044.7775 42.7751092,0" />
+ </g>
</g>
</g>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.49429932px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 7.3104978,52.300673 40.2430052,0"
+ id="path2998"
+ inkscape:connector-curvature="0" />
</svg>
diff --git a/css/activity.css b/css/activity.css
new file mode 100644
index 0000000..8a93851
--- /dev/null
+++ b/css/activity.css
@@ -0,0 +1,47 @@
+#canvas {
+ margin: 0;
+ text-align: center;
+}
+
+#clock-container {
+ position: relative;
+ margin: 0 auto;
+}
+
+canvas {
+ position: absolute;
+ left: 0;
+ top: 0;
+}
+
+#text-time {
+ font-family: monospace;
+ font-size: 35px;
+ font-weight: bold;
+ margin: 0;
+}
+
+#text-date {
+ font-size: 25px;
+ margin: 0;
+}
+
+#main-toolbar #activity-button {
+ background-image: url(../activity/activity-clock.svg);
+}
+
+#main-toolbar #simple-clock-button {
+ background-image: url(../icons/simple-clock.svg);
+}
+
+#main-toolbar #nice-clock-button {
+ background-image: url(../icons/nice-clock.svg);
+}
+
+#main-toolbar #write-time-button {
+ background-image: url(../icons/write-time.svg);
+}
+
+#main-toolbar #write-date-button {
+ background-image: url(../icons/write-date.svg);
+}
diff --git a/index2.html b/index2.html
new file mode 100644
index 0000000..5774204
--- /dev/null
+++ b/index2.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>Clock Activity</title>
+<link rel="stylesheet" href="lib/sugar-web/graphics/css/sugar.css">
+<link rel="stylesheet" href="css/activity.css">
+<script data-main="js/loader" src="lib/require.js"></script>
+</head>
+
+<body>
+ <div id="main-toolbar" class="toolbar">
+ <button class="toolbutton" id="activity-button" title="My Activity"></button>
+ <button class="toolbutton pull-right" id="stop-button" title="Stop"></button>
+ <hr/>
+ </div>
+ <div id="canvas">
+ <IFRAME
+ src="http://activities.sugarlabs.org"
+ marginWidth=0 marginHeight=0 frameBorder=0
+ id=IframeASLO
+ style="width:100%; height:100%;" frameSpacing=0>
+ </IFRAME>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/lib/domReady.js b/lib/domReady.js
new file mode 100644
index 0000000..2b54122
--- /dev/null
+++ b/lib/domReady.js
@@ -0,0 +1,129 @@
+/**
+ * @license RequireJS domReady 2.0.1 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
+ * Available via the MIT or new BSD license.
+ * see: http://github.com/requirejs/domReady for details
+ */
+/*jslint */
+/*global require: false, define: false, requirejs: false,
+ window: false, clearInterval: false, document: false,
+ self: false, setInterval: false */
+
+
+define(function () {
+ 'use strict';
+
+ var isTop, testDiv, scrollIntervalId,
+ isBrowser = typeof window !== "undefined" && window.document,
+ isPageLoaded = !isBrowser,
+ doc = isBrowser ? document : null,
+ readyCalls = [];
+
+ function runCallbacks(callbacks) {
+ var i;
+ for (i = 0; i < callbacks.length; i += 1) {
+ callbacks[i](doc);
+ }
+ }
+
+ function callReady() {
+ var callbacks = readyCalls;
+
+ if (isPageLoaded) {
+ //Call the DOM ready callbacks
+ if (callbacks.length) {
+ readyCalls = [];
+ runCallbacks(callbacks);
+ }
+ }
+ }
+
+ /**
+ * Sets the page as loaded.
+ */
+ function pageLoaded() {
+ if (!isPageLoaded) {
+ isPageLoaded = true;
+ if (scrollIntervalId) {
+ clearInterval(scrollIntervalId);
+ }
+
+ callReady();
+ }
+ }
+
+ if (isBrowser) {
+ if (document.addEventListener) {
+ //Standards. Hooray! Assumption here that if standards based,
+ //it knows about DOMContentLoaded.
+ document.addEventListener("DOMContentLoaded", pageLoaded, false);
+ window.addEventListener("load", pageLoaded, false);
+ } else if (window.attachEvent) {
+ window.attachEvent("onload", pageLoaded);
+
+ testDiv = document.createElement('div');
+ try {
+ isTop = window.frameElement === null;
+ } catch (e) {}
+
+ //DOMContentLoaded approximation that uses a doScroll, as found by
+ //Diego Perini: http://javascript.nwbox.com/IEContentLoaded/,
+ //but modified by other contributors, including jdalton
+ if (testDiv.doScroll && isTop && window.external) {
+ scrollIntervalId = setInterval(function () {
+ try {
+ testDiv.doScroll();
+ pageLoaded();
+ } catch (e) {}
+ }, 30);
+ }
+ }
+
+ //Check if document already complete, and if so, just trigger page load
+ //listeners. Latest webkit browsers also use "interactive", and
+ //will fire the onDOMContentLoaded before "interactive" but not after
+ //entering "interactive" or "complete". More details:
+ //http://dev.w3.org/html5/spec/the-end.html#the-end
+ //http://stackoverflow.com/questions/3665561/document-readystate-of-interactive-vs-ondomcontentloaded
+ //Hmm, this is more complicated on further use, see "firing too early"
+ //bug: https://github.com/requirejs/domReady/issues/1
+ //so removing the || document.readyState === "interactive" test.
+ //There is still a window.onload binding that should get fired if
+ //DOMContentLoaded is missed.
+ if (document.readyState === "complete") {
+ pageLoaded();
+ }
+ }
+
+ /** START OF PUBLIC API **/
+
+ /**
+ * Registers a callback for DOM ready. If DOM is already ready, the
+ * callback is called immediately.
+ * @param {Function} callback
+ */
+ function domReady(callback) {
+ if (isPageLoaded) {
+ callback(doc);
+ } else {
+ readyCalls.push(callback);
+ }
+ return domReady;
+ }
+
+ domReady.version = '2.0.1';
+
+ /**
+ * Loader Plugin API method
+ */
+ domReady.load = function (name, req, onLoad, config) {
+ if (config.isBuild) {
+ onLoad(null);
+ } else {
+ domReady(onLoad);
+ }
+ };
+
+ /** END OF PUBLIC API **/
+
+ return domReady;
+});
diff --git a/lib/moment.js b/lib/moment.js
new file mode 100644
index 0000000..c8a870e
--- /dev/null
+++ b/lib/moment.js
@@ -0,0 +1,1662 @@
+// moment.js
+// version : 2.1.0
+// author : Tim Wood
+// license : MIT
+// momentjs.com
+
+(function (undefined) {
+
+ /************************************
+ Constants
+ ************************************/
+
+ var moment,
+ VERSION = "2.1.0",
+ round = Math.round, i,
+ // internal storage for language config files
+ languages = {},
+
+ // check for nodeJS
+ hasModule = (typeof module !== 'undefined' && module.exports),
+
+ // ASP.NET json date format regex
+ aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
+ aspNetTimeSpanJsonRegex = /(\-)?(\d*)?\.?(\d+)\:(\d+)\:(\d+)\.?(\d{3})?/,
+
+ // format tokens
+ formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,
+ localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
+
+ // parsing token regexes
+ parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
+ parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
+ parseTokenThreeDigits = /\d{3}/, // 000 - 999
+ parseTokenFourDigits = /\d{1,4}/, // 0 - 9999
+ parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
+ parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic.
+ parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
+ parseTokenT = /T/i, // T (ISO seperator)
+ parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
+
+ // preliminary iso regex
+ // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000
+ isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,
+ isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
+
+ // iso time formats and regexes
+ isoTimes = [
+ ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
+ ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
+ ['HH:mm', /(T| )\d\d:\d\d/],
+ ['HH', /(T| )\d\d/]
+ ],
+
+ // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
+ parseTimezoneChunker = /([\+\-]|\d\d)/gi,
+
+ // getter and setter names
+ proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
+ unitMillisecondFactors = {
+ 'Milliseconds' : 1,
+ 'Seconds' : 1e3,
+ 'Minutes' : 6e4,
+ 'Hours' : 36e5,
+ 'Days' : 864e5,
+ 'Months' : 2592e6,
+ 'Years' : 31536e6
+ },
+
+ unitAliases = {
+ ms : 'millisecond',
+ s : 'second',
+ m : 'minute',
+ h : 'hour',
+ d : 'day',
+ w : 'week',
+ M : 'month',
+ y : 'year'
+ },
+
+ // format function strings
+ formatFunctions = {},
+
+ // tokens to ordinalize and pad
+ ordinalizeTokens = 'DDD w W M D d'.split(' '),
+ paddedTokens = 'M D H h m s w W'.split(' '),
+
+ formatTokenFunctions = {
+ M : function () {
+ return this.month() + 1;
+ },
+ MMM : function (format) {
+ return this.lang().monthsShort(this, format);
+ },
+ MMMM : function (format) {
+ return this.lang().months(this, format);
+ },
+ D : function () {
+ return this.date();
+ },
+ DDD : function () {
+ return this.dayOfYear();
+ },
+ d : function () {
+ return this.day();
+ },
+ dd : function (format) {
+ return this.lang().weekdaysMin(this, format);
+ },
+ ddd : function (format) {
+ return this.lang().weekdaysShort(this, format);
+ },
+ dddd : function (format) {
+ return this.lang().weekdays(this, format);
+ },
+ w : function () {
+ return this.week();
+ },
+ W : function () {
+ return this.isoWeek();
+ },
+ YY : function () {
+ return leftZeroFill(this.year() % 100, 2);
+ },
+ YYYY : function () {
+ return leftZeroFill(this.year(), 4);
+ },
+ YYYYY : function () {
+ return leftZeroFill(this.year(), 5);
+ },
+ gg : function () {
+ return leftZeroFill(this.weekYear() % 100, 2);
+ },
+ gggg : function () {
+ return this.weekYear();
+ },
+ ggggg : function () {
+ return leftZeroFill(this.weekYear(), 5);
+ },
+ GG : function () {
+ return leftZeroFill(this.isoWeekYear() % 100, 2);
+ },
+ GGGG : function () {
+ return this.isoWeekYear();
+ },
+ GGGGG : function () {
+ return leftZeroFill(this.isoWeekYear(), 5);
+ },
+ e : function () {
+ return this.weekday();
+ },
+ E : function () {
+ return this.isoWeekday();
+ },
+ a : function () {
+ return this.lang().meridiem(this.hours(), this.minutes(), true);
+ },
+ A : function () {
+ return this.lang().meridiem(this.hours(), this.minutes(), false);
+ },
+ H : function () {
+ return this.hours();
+ },
+ h : function () {
+ return this.hours() % 12 || 12;
+ },
+ m : function () {
+ return this.minutes();
+ },
+ s : function () {
+ return this.seconds();
+ },
+ S : function () {
+ return ~~(this.milliseconds() / 100);
+ },
+ SS : function () {
+ return leftZeroFill(~~(this.milliseconds() / 10), 2);
+ },
+ SSS : function () {
+ return leftZeroFill(this.milliseconds(), 3);
+ },
+ Z : function () {
+ var a = -this.zone(),
+ b = "+";
+ if (a < 0) {
+ a = -a;
+ b = "-";
+ }
+ return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2);
+ },
+ ZZ : function () {
+ var a = -this.zone(),
+ b = "+";
+ if (a < 0) {
+ a = -a;
+ b = "-";
+ }
+ return b + leftZeroFill(~~(10 * a / 6), 4);
+ },
+ z : function () {
+ return this.zoneAbbr();
+ },
+ zz : function () {
+ return this.zoneName();
+ },
+ X : function () {
+ return this.unix();
+ }
+ };
+
+ function padToken(func, count) {
+ return function (a) {
+ return leftZeroFill(func.call(this, a), count);
+ };
+ }
+ function ordinalizeToken(func, period) {
+ return function (a) {
+ return this.lang().ordinal(func.call(this, a), period);
+ };
+ }
+
+ while (ordinalizeTokens.length) {
+ i = ordinalizeTokens.pop();
+ formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
+ }
+ while (paddedTokens.length) {
+ i = paddedTokens.pop();
+ formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
+ }
+ formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
+
+
+ /************************************
+ Constructors
+ ************************************/
+
+ function Language() {
+
+ }
+
+ // Moment prototype object
+ function Moment(config) {
+ extend(this, config);
+ }
+
+ // Duration Constructor
+ function Duration(duration) {
+ var years = duration.years || duration.year || duration.y || 0,
+ months = duration.months || duration.month || duration.M || 0,
+ weeks = duration.weeks || duration.week || duration.w || 0,
+ days = duration.days || duration.day || duration.d || 0,
+ hours = duration.hours || duration.hour || duration.h || 0,
+ minutes = duration.minutes || duration.minute || duration.m || 0,
+ seconds = duration.seconds || duration.second || duration.s || 0,
+ milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0;
+
+ // store reference to input for deterministic cloning
+ this._input = duration;
+
+ // representation for dateAddRemove
+ this._milliseconds = milliseconds +
+ seconds * 1e3 + // 1000
+ minutes * 6e4 + // 1000 * 60
+ hours * 36e5; // 1000 * 60 * 60
+ // Because of dateAddRemove treats 24 hours as different from a
+ // day when working around DST, we need to store them separately
+ this._days = days +
+ weeks * 7;
+ // It is impossible translate months into days without knowing
+ // which months you are are talking about, so we have to store
+ // it separately.
+ this._months = months +
+ years * 12;
+
+ this._data = {};
+
+ this._bubble();
+ }
+
+
+ /************************************
+ Helpers
+ ************************************/
+
+
+ function extend(a, b) {
+ for (var i in b) {
+ if (b.hasOwnProperty(i)) {
+ a[i] = b[i];
+ }
+ }
+ return a;
+ }
+
+ function absRound(number) {
+ if (number < 0) {
+ return Math.ceil(number);
+ } else {
+ return Math.floor(number);
+ }
+ }
+
+ // left zero fill a number
+ // see http://jsperf.com/left-zero-filling for performance comparison
+ function leftZeroFill(number, targetLength) {
+ var output = number + '';
+ while (output.length < targetLength) {
+ output = '0' + output;
+ }
+ return output;
+ }
+
+ // helper function for _.addTime and _.subtractTime
+ function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) {
+ var milliseconds = duration._milliseconds,
+ days = duration._days,
+ months = duration._months,
+ minutes,
+ hours,
+ currentDate;
+
+ if (milliseconds) {
+ mom._d.setTime(+mom._d + milliseconds * isAdding);
+ }
+ // store the minutes and hours so we can restore them
+ if (days || months) {
+ minutes = mom.minute();
+ hours = mom.hour();
+ }
+ if (days) {
+ mom.date(mom.date() + days * isAdding);
+ }
+ if (months) {
+ mom.month(mom.month() + months * isAdding);
+ }
+ if (milliseconds && !ignoreUpdateOffset) {
+ moment.updateOffset(mom);
+ }
+ // restore the minutes and hours after possibly changing dst
+ if (days || months) {
+ mom.minute(minutes);
+ mom.hour(hours);
+ }
+ }
+
+ // check if is an array
+ function isArray(input) {
+ return Object.prototype.toString.call(input) === '[object Array]';
+ }
+
+ // compare two arrays, return the number of differences
+ function compareArrays(array1, array2) {
+ var len = Math.min(array1.length, array2.length),
+ lengthDiff = Math.abs(array1.length - array2.length),
+ diffs = 0,
+ i;
+ for (i = 0; i < len; i++) {
+ if (~~array1[i] !== ~~array2[i]) {
+ diffs++;
+ }
+ }
+ return diffs + lengthDiff;
+ }
+
+ function normalizeUnits(units) {
+ return units ? unitAliases[units] || units.toLowerCase().replace(/(.)s$/, '$1') : units;
+ }
+
+
+ /************************************
+ Languages
+ ************************************/
+
+
+ Language.prototype = {
+ set : function (config) {
+ var prop, i;
+ for (i in config) {
+ prop = config[i];
+ if (typeof prop === 'function') {
+ this[i] = prop;
+ } else {
+ this['_' + i] = prop;
+ }
+ }
+ },
+
+ _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
+ months : function (m) {
+ return this._months[m.month()];
+ },
+
+ _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
+ monthsShort : function (m) {
+ return this._monthsShort[m.month()];
+ },
+
+ monthsParse : function (monthName) {
+ var i, mom, regex;
+
+ if (!this._monthsParse) {
+ this._monthsParse = [];
+ }
+
+ for (i = 0; i < 12; i++) {
+ // make the regex if we don't have it already
+ if (!this._monthsParse[i]) {
+ mom = moment([2000, i]);
+ regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
+ this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (this._monthsParse[i].test(monthName)) {
+ return i;
+ }
+ }
+ },
+
+ _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
+ weekdays : function (m) {
+ return this._weekdays[m.day()];
+ },
+
+ _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
+ weekdaysShort : function (m) {
+ return this._weekdaysShort[m.day()];
+ },
+
+ _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
+ weekdaysMin : function (m) {
+ return this._weekdaysMin[m.day()];
+ },
+
+ weekdaysParse : function (weekdayName) {
+ var i, mom, regex;
+
+ if (!this._weekdaysParse) {
+ this._weekdaysParse = [];
+ }
+
+ for (i = 0; i < 7; i++) {
+ // make the regex if we don't have it already
+ if (!this._weekdaysParse[i]) {
+ mom = moment([2000, 1]).day(i);
+ regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
+ this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (this._weekdaysParse[i].test(weekdayName)) {
+ return i;
+ }
+ }
+ },
+
+ _longDateFormat : {
+ LT : "h:mm A",
+ L : "MM/DD/YYYY",
+ LL : "MMMM D YYYY",
+ LLL : "MMMM D YYYY LT",
+ LLLL : "dddd, MMMM D YYYY LT"
+ },
+ longDateFormat : function (key) {
+ var output = this._longDateFormat[key];
+ if (!output && this._longDateFormat[key.toUpperCase()]) {
+ output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
+ return val.slice(1);
+ });
+ this._longDateFormat[key] = output;
+ }
+ return output;
+ },
+
+ isPM : function (input) {
+ return ((input + '').toLowerCase()[0] === 'p');
+ },
+
+ _meridiemParse : /[ap]\.?m?\.?/i,
+ meridiem : function (hours, minutes, isLower) {
+ if (hours > 11) {
+ return isLower ? 'pm' : 'PM';
+ } else {
+ return isLower ? 'am' : 'AM';
+ }
+ },
+
+ _calendar : {
+ sameDay : '[Today at] LT',
+ nextDay : '[Tomorrow at] LT',
+ nextWeek : 'dddd [at] LT',
+ lastDay : '[Yesterday at] LT',
+ lastWeek : '[Last] dddd [at] LT',
+ sameElse : 'L'
+ },
+ calendar : function (key, mom) {
+ var output = this._calendar[key];
+ return typeof output === 'function' ? output.apply(mom) : output;
+ },
+
+ _relativeTime : {
+ future : "in %s",
+ past : "%s ago",
+ s : "a few seconds",
+ m : "a minute",
+ mm : "%d minutes",
+ h : "an hour",
+ hh : "%d hours",
+ d : "a day",
+ dd : "%d days",
+ M : "a month",
+ MM : "%d months",
+ y : "a year",
+ yy : "%d years"
+ },
+ relativeTime : function (number, withoutSuffix, string, isFuture) {
+ var output = this._relativeTime[string];
+ return (typeof output === 'function') ?
+ output(number, withoutSuffix, string, isFuture) :
+ output.replace(/%d/i, number);
+ },
+ pastFuture : function (diff, output) {
+ var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
+ return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
+ },
+
+ ordinal : function (number) {
+ return this._ordinal.replace("%d", number);
+ },
+ _ordinal : "%d",
+
+ preparse : function (string) {
+ return string;
+ },
+
+ postformat : function (string) {
+ return string;
+ },
+
+ week : function (mom) {
+ return weekOfYear(mom, this._week.dow, this._week.doy).week;
+ },
+ _week : {
+ dow : 0, // Sunday is the first day of the week.
+ doy : 6 // The week that contains Jan 1st is the first week of the year.
+ }
+ };
+
+ // Loads a language definition into the `languages` cache. The function
+ // takes a key and optionally values. If not in the browser and no values
+ // are provided, it will load the language file module. As a convenience,
+ // this function also returns the language values.
+ function loadLang(key, values) {
+ values.abbr = key;
+ if (!languages[key]) {
+ languages[key] = new Language();
+ }
+ languages[key].set(values);
+ return languages[key];
+ }
+
+ // Determines which language definition to use and returns it.
+ //
+ // With no parameters, it will return the global language. If you
+ // pass in a language key, such as 'en', it will return the
+ // definition for 'en', so long as 'en' has already been loaded using
+ // moment.lang.
+ function getLangDefinition(key) {
+ if (!key) {
+ return moment.fn._lang;
+ }
+ if (!languages[key] && hasModule) {
+ try {
+ require('./lang/' + key);
+ } catch (e) {
+ // call with no params to set to default
+ return moment.fn._lang;
+ }
+ }
+ return languages[key];
+ }
+
+
+ /************************************
+ Formatting
+ ************************************/
+
+
+ function removeFormattingTokens(input) {
+ if (input.match(/\[.*\]/)) {
+ return input.replace(/^\[|\]$/g, "");
+ }
+ return input.replace(/\\/g, "");
+ }
+
+ function makeFormatFunction(format) {
+ var array = format.match(formattingTokens), i, length;
+
+ for (i = 0, length = array.length; i < length; i++) {
+ if (formatTokenFunctions[array[i]]) {
+ array[i] = formatTokenFunctions[array[i]];
+ } else {
+ array[i] = removeFormattingTokens(array[i]);
+ }
+ }
+
+ return function (mom) {
+ var output = "";
+ for (i = 0; i < length; i++) {
+ output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
+ }
+ return output;
+ };
+ }
+
+ // format date using native date object
+ function formatMoment(m, format) {
+ var i = 5;
+
+ function replaceLongDateFormatTokens(input) {
+ return m.lang().longDateFormat(input) || input;
+ }
+
+ while (i-- && localFormattingTokens.test(format)) {
+ format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+ }
+
+ if (!formatFunctions[format]) {
+ formatFunctions[format] = makeFormatFunction(format);
+ }
+
+ return formatFunctions[format](m);
+ }
+
+
+ /************************************
+ Parsing
+ ************************************/
+
+
+ // get the regex to find the next token
+ function getParseRegexForToken(token, config) {
+ switch (token) {
+ case 'DDDD':
+ return parseTokenThreeDigits;
+ case 'YYYY':
+ return parseTokenFourDigits;
+ case 'YYYYY':
+ return parseTokenSixDigits;
+ case 'S':
+ case 'SS':
+ case 'SSS':
+ case 'DDD':
+ return parseTokenOneToThreeDigits;
+ case 'MMM':
+ case 'MMMM':
+ case 'dd':
+ case 'ddd':
+ case 'dddd':
+ return parseTokenWord;
+ case 'a':
+ case 'A':
+ return getLangDefinition(config._l)._meridiemParse;
+ case 'X':
+ return parseTokenTimestampMs;
+ case 'Z':
+ case 'ZZ':
+ return parseTokenTimezone;
+ case 'T':
+ return parseTokenT;
+ case 'MM':
+ case 'DD':
+ case 'YY':
+ case 'HH':
+ case 'hh':
+ case 'mm':
+ case 'ss':
+ case 'M':
+ case 'D':
+ case 'd':
+ case 'H':
+ case 'h':
+ case 'm':
+ case 's':
+ return parseTokenOneOrTwoDigits;
+ default :
+ return new RegExp(token.replace('\\', ''));
+ }
+ }
+
+ function timezoneMinutesFromString(string) {
+ var tzchunk = (parseTokenTimezone.exec(string) || [])[0],
+ parts = (tzchunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
+ minutes = +(parts[1] * 60) + ~~parts[2];
+
+ return parts[0] === '+' ? -minutes : minutes;
+ }
+
+ // function to convert string input to date
+ function addTimeToArrayFromToken(token, input, config) {
+ var a, datePartArray = config._a;
+
+ switch (token) {
+ // MONTH
+ case 'M' : // fall through to MM
+ case 'MM' :
+ datePartArray[1] = (input == null) ? 0 : ~~input - 1;
+ break;
+ case 'MMM' : // fall through to MMMM
+ case 'MMMM' :
+ a = getLangDefinition(config._l).monthsParse(input);
+ // if we didn't find a month name, mark the date as invalid.
+ if (a != null) {
+ datePartArray[1] = a;
+ } else {
+ config._isValid = false;
+ }
+ break;
+ // DAY OF MONTH
+ case 'D' : // fall through to DDDD
+ case 'DD' : // fall through to DDDD
+ case 'DDD' : // fall through to DDDD
+ case 'DDDD' :
+ if (input != null) {
+ datePartArray[2] = ~~input;
+ }
+ break;
+ // YEAR
+ case 'YY' :
+ datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000);
+ break;
+ case 'YYYY' :
+ case 'YYYYY' :
+ datePartArray[0] = ~~input;
+ break;
+ // AM / PM
+ case 'a' : // fall through to A
+ case 'A' :
+ config._isPm = getLangDefinition(config._l).isPM(input);
+ break;
+ // 24 HOUR
+ case 'H' : // fall through to hh
+ case 'HH' : // fall through to hh
+ case 'h' : // fall through to hh
+ case 'hh' :
+ datePartArray[3] = ~~input;
+ break;
+ // MINUTE
+ case 'm' : // fall through to mm
+ case 'mm' :
+ datePartArray[4] = ~~input;
+ break;
+ // SECOND
+ case 's' : // fall through to ss
+ case 'ss' :
+ datePartArray[5] = ~~input;
+ break;
+ // MILLISECOND
+ case 'S' :
+ case 'SS' :
+ case 'SSS' :
+ datePartArray[6] = ~~ (('0.' + input) * 1000);
+ break;
+ // UNIX TIMESTAMP WITH MS
+ case 'X':
+ config._d = new Date(parseFloat(input) * 1000);
+ break;
+ // TIMEZONE
+ case 'Z' : // fall through to ZZ
+ case 'ZZ' :
+ config._useUTC = true;
+ config._tzm = timezoneMinutesFromString(input);
+ break;
+ }
+
+ // if the input is null, the date is not valid
+ if (input == null) {
+ config._isValid = false;
+ }
+ }
+
+ // convert an array to a date.
+ // the array should mirror the parameters below
+ // note: all values past the year are optional and will default to the lowest possible value.
+ // [year, month, day , hour, minute, second, millisecond]
+ function dateFromArray(config) {
+ var i, date, input = [];
+
+ if (config._d) {
+ return;
+ }
+
+ for (i = 0; i < 7; i++) {
+ config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
+ }
+
+ // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
+ input[3] += ~~((config._tzm || 0) / 60);
+ input[4] += ~~((config._tzm || 0) % 60);
+
+ date = new Date(0);
+
+ if (config._useUTC) {
+ date.setUTCFullYear(input[0], input[1], input[2]);
+ date.setUTCHours(input[3], input[4], input[5], input[6]);
+ } else {
+ date.setFullYear(input[0], input[1], input[2]);
+ date.setHours(input[3], input[4], input[5], input[6]);
+ }
+
+ config._d = date;
+ }
+
+ // date from string and format string
+ function makeDateFromStringAndFormat(config) {
+ // This array is used to make a Date, either with `new Date` or `Date.UTC`
+ var tokens = config._f.match(formattingTokens),
+ string = config._i,
+ i, parsedInput;
+
+ config._a = [];
+
+ for (i = 0; i < tokens.length; i++) {
+ parsedInput = (getParseRegexForToken(tokens[i], config).exec(string) || [])[0];
+ if (parsedInput) {
+ string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
+ }
+ // don't parse if its not a known token
+ if (formatTokenFunctions[tokens[i]]) {
+ addTimeToArrayFromToken(tokens[i], parsedInput, config);
+ }
+ }
+
+ // add remaining unparsed input to the string
+ if (string) {
+ config._il = string;
+ }
+
+ // handle am pm
+ if (config._isPm && config._a[3] < 12) {
+ config._a[3] += 12;
+ }
+ // if is 12 am, change hours to 0
+ if (config._isPm === false && config._a[3] === 12) {
+ config._a[3] = 0;
+ }
+ // return
+ dateFromArray(config);
+ }
+
+ // date from string and array of format strings
+ function makeDateFromStringAndArray(config) {
+ var tempConfig,
+ tempMoment,
+ bestMoment,
+
+ scoreToBeat = 99,
+ i,
+ currentScore;
+
+ for (i = 0; i < config._f.length; i++) {
+ tempConfig = extend({}, config);
+ tempConfig._f = config._f[i];
+ makeDateFromStringAndFormat(tempConfig);
+ tempMoment = new Moment(tempConfig);
+
+ currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
+
+ // if there is any input that was not parsed
+ // add a penalty for that format
+ if (tempMoment._il) {
+ currentScore += tempMoment._il.length;
+ }
+
+ if (currentScore < scoreToBeat) {
+ scoreToBeat = currentScore;
+ bestMoment = tempMoment;
+ }
+ }
+
+ extend(config, bestMoment);
+ }
+
+ // date from iso format
+ function makeDateFromString(config) {
+ var i,
+ string = config._i,
+ match = isoRegex.exec(string);
+
+ if (match) {
+ // match[2] should be "T" or undefined
+ config._f = 'YYYY-MM-DD' + (match[2] || " ");
+ for (i = 0; i < 4; i++) {
+ if (isoTimes[i][1].exec(string)) {
+ config._f += isoTimes[i][0];
+ break;
+ }
+ }
+ if (parseTokenTimezone.exec(string)) {
+ config._f += " Z";
+ }
+ makeDateFromStringAndFormat(config);
+ } else {
+ config._d = new Date(string);
+ }
+ }
+
+ function makeDateFromInput(config) {
+ var input = config._i,
+ matched = aspNetJsonRegex.exec(input);
+
+ if (input === undefined) {
+ config._d = new Date();
+ } else if (matched) {
+ config._d = new Date(+matched[1]);
+ } else if (typeof input === 'string') {
+ makeDateFromString(config);
+ } else if (isArray(input)) {
+ config._a = input.slice(0);
+ dateFromArray(config);
+ } else {
+ config._d = input instanceof Date ? new Date(+input) : new Date(input);
+ }
+ }
+
+
+ /************************************
+ Relative Time
+ ************************************/
+
+
+ // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
+ function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
+ return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
+ }
+
+ function relativeTime(milliseconds, withoutSuffix, lang) {
+ var seconds = round(Math.abs(milliseconds) / 1000),
+ minutes = round(seconds / 60),
+ hours = round(minutes / 60),
+ days = round(hours / 24),
+ years = round(days / 365),
+ args = seconds < 45 && ['s', seconds] ||
+ minutes === 1 && ['m'] ||
+ minutes < 45 && ['mm', minutes] ||
+ hours === 1 && ['h'] ||
+ hours < 22 && ['hh', hours] ||
+ days === 1 && ['d'] ||
+ days <= 25 && ['dd', days] ||
+ days <= 45 && ['M'] ||
+ days < 345 && ['MM', round(days / 30)] ||
+ years === 1 && ['y'] || ['yy', years];
+ args[2] = withoutSuffix;
+ args[3] = milliseconds > 0;
+ args[4] = lang;
+ return substituteTimeAgo.apply({}, args);
+ }
+
+
+ /************************************
+ Week of Year
+ ************************************/
+
+
+ // firstDayOfWeek 0 = sun, 6 = sat
+ // the day of the week that starts the week
+ // (usually sunday or monday)
+ // firstDayOfWeekOfYear 0 = sun, 6 = sat
+ // the first week is the week that contains the first
+ // of this day of the week
+ // (eg. ISO weeks use thursday (4))
+ function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
+ var end = firstDayOfWeekOfYear - firstDayOfWeek,
+ daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
+ adjustedMoment;
+
+
+ if (daysToDayOfWeek > end) {
+ daysToDayOfWeek -= 7;
+ }
+
+ if (daysToDayOfWeek < end - 7) {
+ daysToDayOfWeek += 7;
+ }
+
+ adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
+ return {
+ week: Math.ceil(adjustedMoment.dayOfYear() / 7),
+ year: adjustedMoment.year()
+ };
+ }
+
+
+ /************************************
+ Top Level Functions
+ ************************************/
+
+ function makeMoment(config) {
+ var input = config._i,
+ format = config._f;
+
+ if (input === null || input === '') {
+ return null;
+ }
+
+ if (typeof input === 'string') {
+ config._i = input = getLangDefinition().preparse(input);
+ }
+
+ if (moment.isMoment(input)) {
+ config = extend({}, input);
+ config._d = new Date(+input._d);
+ } else if (format) {
+ if (isArray(format)) {
+ makeDateFromStringAndArray(config);
+ } else {
+ makeDateFromStringAndFormat(config);
+ }
+ } else {
+ makeDateFromInput(config);
+ }
+
+ return new Moment(config);
+ }
+
+ moment = function (input, format, lang) {
+ return makeMoment({
+ _i : input,
+ _f : format,
+ _l : lang,
+ _isUTC : false
+ });
+ };
+
+ // creating with utc
+ moment.utc = function (input, format, lang) {
+ return makeMoment({
+ _useUTC : true,
+ _isUTC : true,
+ _l : lang,
+ _i : input,
+ _f : format
+ });
+ };
+
+ // creating with unix timestamp (in seconds)
+ moment.unix = function (input) {
+ return moment(input * 1000);
+ };
+
+ // duration
+ moment.duration = function (input, key) {
+ var isDuration = moment.isDuration(input),
+ isNumber = (typeof input === 'number'),
+ duration = (isDuration ? input._input : (isNumber ? {} : input)),
+ matched = aspNetTimeSpanJsonRegex.exec(input),
+ sign,
+ ret;
+
+ if (isNumber) {
+ if (key) {
+ duration[key] = input;
+ } else {
+ duration.milliseconds = input;
+ }
+ } else if (matched) {
+ sign = (matched[1] === "-") ? -1 : 1;
+ duration = {
+ y: 0,
+ d: ~~matched[2] * sign,
+ h: ~~matched[3] * sign,
+ m: ~~matched[4] * sign,
+ s: ~~matched[5] * sign,
+ ms: ~~matched[6] * sign
+ };
+ }
+
+ ret = new Duration(duration);
+
+ if (isDuration && input.hasOwnProperty('_lang')) {
+ ret._lang = input._lang;
+ }
+
+ return ret;
+ };
+
+ // version number
+ moment.version = VERSION;
+
+ // default format
+ moment.defaultFormat = isoFormat;
+
+ // This function will be called whenever a moment is mutated.
+ // It is intended to keep the offset in sync with the timezone.
+ moment.updateOffset = function () {};
+
+ // This function will load languages and then set the global language. If
+ // no arguments are passed in, it will simply return the current global
+ // language key.
+ moment.lang = function (key, values) {
+ if (!key) {
+ return moment.fn._lang._abbr;
+ }
+ if (values) {
+ loadLang(key, values);
+ } else if (!languages[key]) {
+ getLangDefinition(key);
+ }
+ moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
+ };
+
+ // returns language data
+ moment.langData = function (key) {
+ if (key && key._lang && key._lang._abbr) {
+ key = key._lang._abbr;
+ }
+ return getLangDefinition(key);
+ };
+
+ // compare moment object
+ moment.isMoment = function (obj) {
+ return obj instanceof Moment;
+ };
+
+ // for typechecking Duration objects
+ moment.isDuration = function (obj) {
+ return obj instanceof Duration;
+ };
+
+
+ /************************************
+ Moment Prototype
+ ************************************/
+
+
+ moment.fn = Moment.prototype = {
+
+ clone : function () {
+ return moment(this);
+ },
+
+ valueOf : function () {
+ return +this._d + ((this._offset || 0) * 60000);
+ },
+
+ unix : function () {
+ return Math.floor(+this / 1000);
+ },
+
+ toString : function () {
+ return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
+ },
+
+ toDate : function () {
+ return this._offset ? new Date(+this) : this._d;
+ },
+
+ toISOString : function () {
+ return formatMoment(moment(this).utc(), 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+ },
+
+ toArray : function () {
+ var m = this;
+ return [
+ m.year(),
+ m.month(),
+ m.date(),
+ m.hours(),
+ m.minutes(),
+ m.seconds(),
+ m.milliseconds()
+ ];
+ },
+
+ isValid : function () {
+ if (this._isValid == null) {
+ if (this._a) {
+ this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray());
+ } else {
+ this._isValid = !isNaN(this._d.getTime());
+ }
+ }
+ return !!this._isValid;
+ },
+
+ utc : function () {
+ return this.zone(0);
+ },
+
+ local : function () {
+ this.zone(0);
+ this._isUTC = false;
+ return this;
+ },
+
+ format : function (inputString) {
+ var output = formatMoment(this, inputString || moment.defaultFormat);
+ return this.lang().postformat(output);
+ },
+
+ add : function (input, val) {
+ var dur;
+ // switch args to support add('s', 1) and add(1, 's')
+ if (typeof input === 'string') {
+ dur = moment.duration(+val, input);
+ } else {
+ dur = moment.duration(input, val);
+ }
+ addOrSubtractDurationFromMoment(this, dur, 1);
+ return this;
+ },
+
+ subtract : function (input, val) {
+ var dur;
+ // switch args to support subtract('s', 1) and subtract(1, 's')
+ if (typeof input === 'string') {
+ dur = moment.duration(+val, input);
+ } else {
+ dur = moment.duration(input, val);
+ }
+ addOrSubtractDurationFromMoment(this, dur, -1);
+ return this;
+ },
+
+ diff : function (input, units, asFloat) {
+ var that = this._isUTC ? moment(input).zone(this._offset || 0) : moment(input).local(),
+ zoneDiff = (this.zone() - that.zone()) * 6e4,
+ diff, output;
+
+ units = normalizeUnits(units);
+
+ if (units === 'year' || units === 'month') {
+ // average number of days in the months in the given dates
+ diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
+ // difference in months
+ output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
+ // adjust by taking difference in days, average number of days
+ // and dst in the given months.
+ output += ((this - moment(this).startOf('month')) -
+ (that - moment(that).startOf('month'))) / diff;
+ // same as above but with zones, to negate all dst
+ output -= ((this.zone() - moment(this).startOf('month').zone()) -
+ (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
+ if (units === 'year') {
+ output = output / 12;
+ }
+ } else {
+ diff = (this - that);
+ output = units === 'second' ? diff / 1e3 : // 1000
+ units === 'minute' ? diff / 6e4 : // 1000 * 60
+ units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
+ units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
+ units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
+ diff;
+ }
+ return asFloat ? output : absRound(output);
+ },
+
+ from : function (time, withoutSuffix) {
+ return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
+ },
+
+ fromNow : function (withoutSuffix) {
+ return this.from(moment(), withoutSuffix);
+ },
+
+ calendar : function () {
+ var diff = this.diff(moment().startOf('day'), 'days', true),
+ format = diff < -6 ? 'sameElse' :
+ diff < -1 ? 'lastWeek' :
+ diff < 0 ? 'lastDay' :
+ diff < 1 ? 'sameDay' :
+ diff < 2 ? 'nextDay' :
+ diff < 7 ? 'nextWeek' : 'sameElse';
+ return this.format(this.lang().calendar(format, this));
+ },
+
+ isLeapYear : function () {
+ var year = this.year();
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+ },
+
+ isDST : function () {
+ return (this.zone() < this.clone().month(0).zone() ||
+ this.zone() < this.clone().month(5).zone());
+ },
+
+ day : function (input) {
+ var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
+ if (input != null) {
+ if (typeof input === 'string') {
+ input = this.lang().weekdaysParse(input);
+ if (typeof input !== 'number') {
+ return this;
+ }
+ }
+ return this.add({ d : input - day });
+ } else {
+ return day;
+ }
+ },
+
+ month : function (input) {
+ var utc = this._isUTC ? 'UTC' : '',
+ dayOfMonth,
+ daysInMonth;
+
+ if (input != null) {
+ if (typeof input === 'string') {
+ input = this.lang().monthsParse(input);
+ if (typeof input !== 'number') {
+ return this;
+ }
+ }
+
+ dayOfMonth = this.date();
+ this.date(1);
+ this._d['set' + utc + 'Month'](input);
+ this.date(Math.min(dayOfMonth, this.daysInMonth()));
+
+ moment.updateOffset(this);
+ return this;
+ } else {
+ return this._d['get' + utc + 'Month']();
+ }
+ },
+
+ startOf: function (units) {
+ units = normalizeUnits(units);
+ // the following switch intentionally omits break keywords
+ // to utilize falling through the cases.
+ switch (units) {
+ case 'year':
+ this.month(0);
+ /* falls through */
+ case 'month':
+ this.date(1);
+ /* falls through */
+ case 'week':
+ case 'day':
+ this.hours(0);
+ /* falls through */
+ case 'hour':
+ this.minutes(0);
+ /* falls through */
+ case 'minute':
+ this.seconds(0);
+ /* falls through */
+ case 'second':
+ this.milliseconds(0);
+ /* falls through */
+ }
+
+ // weeks are a special case
+ if (units === 'week') {
+ this.weekday(0);
+ }
+
+ return this;
+ },
+
+ endOf: function (units) {
+ return this.startOf(units).add(units, 1).subtract('ms', 1);
+ },
+
+ isAfter: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) > +moment(input).startOf(units);
+ },
+
+ isBefore: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) < +moment(input).startOf(units);
+ },
+
+ isSame: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) === +moment(input).startOf(units);
+ },
+
+ min: function (other) {
+ other = moment.apply(null, arguments);
+ return other < this ? this : other;
+ },
+
+ max: function (other) {
+ other = moment.apply(null, arguments);
+ return other > this ? this : other;
+ },
+
+ zone : function (input) {
+ var offset = this._offset || 0;
+ if (input != null) {
+ if (typeof input === "string") {
+ input = timezoneMinutesFromString(input);
+ }
+ if (Math.abs(input) < 16) {
+ input = input * 60;
+ }
+ this._offset = input;
+ this._isUTC = true;
+ if (offset !== input) {
+ addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
+ }
+ } else {
+ return this._isUTC ? offset : this._d.getTimezoneOffset();
+ }
+ return this;
+ },
+
+ zoneAbbr : function () {
+ return this._isUTC ? "UTC" : "";
+ },
+
+ zoneName : function () {
+ return this._isUTC ? "Coordinated Universal Time" : "";
+ },
+
+ daysInMonth : function () {
+ return moment.utc([this.year(), this.month() + 1, 0]).date();
+ },
+
+ dayOfYear : function (input) {
+ var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
+ return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
+ },
+
+ weekYear : function (input) {
+ var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
+ return input == null ? year : this.add("y", (input - year));
+ },
+
+ isoWeekYear : function (input) {
+ var year = weekOfYear(this, 1, 4).year;
+ return input == null ? year : this.add("y", (input - year));
+ },
+
+ week : function (input) {
+ var week = this.lang().week(this);
+ return input == null ? week : this.add("d", (input - week) * 7);
+ },
+
+ isoWeek : function (input) {
+ var week = weekOfYear(this, 1, 4).week;
+ return input == null ? week : this.add("d", (input - week) * 7);
+ },
+
+ weekday : function (input) {
+ var weekday = (this._d.getDay() + 7 - this.lang()._week.dow) % 7;
+ return input == null ? weekday : this.add("d", input - weekday);
+ },
+
+ isoWeekday : function (input) {
+ // behaves the same as moment#day except
+ // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
+ // as a setter, sunday should belong to the previous week.
+ return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
+ },
+
+ // If passed a language key, it will set the language for this
+ // instance. Otherwise, it will return the language configuration
+ // variables for this instance.
+ lang : function (key) {
+ if (key === undefined) {
+ return this._lang;
+ } else {
+ this._lang = getLangDefinition(key);
+ return this;
+ }
+ }
+ };
+
+ // helper for adding shortcuts
+ function makeGetterAndSetter(name, key) {
+ moment.fn[name] = moment.fn[name + 's'] = function (input) {
+ var utc = this._isUTC ? 'UTC' : '';
+ if (input != null) {
+ this._d['set' + utc + key](input);
+ moment.updateOffset(this);
+ return this;
+ } else {
+ return this._d['get' + utc + key]();
+ }
+ };
+ }
+
+ // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
+ for (i = 0; i < proxyGettersAndSetters.length; i ++) {
+ makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
+ }
+
+ // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
+ makeGetterAndSetter('year', 'FullYear');
+
+ // add plural methods
+ moment.fn.days = moment.fn.day;
+ moment.fn.months = moment.fn.month;
+ moment.fn.weeks = moment.fn.week;
+ moment.fn.isoWeeks = moment.fn.isoWeek;
+
+ // add aliased format methods
+ moment.fn.toJSON = moment.fn.toISOString;
+
+ /************************************
+ Duration Prototype
+ ************************************/
+
+
+ moment.duration.fn = Duration.prototype = {
+ _bubble : function () {
+ var milliseconds = this._milliseconds,
+ days = this._days,
+ months = this._months,
+ data = this._data,
+ seconds, minutes, hours, years;
+
+ // The following code bubbles up values, see the tests for
+ // examples of what that means.
+ data.milliseconds = milliseconds % 1000;
+
+ seconds = absRound(milliseconds / 1000);
+ data.seconds = seconds % 60;
+
+ minutes = absRound(seconds / 60);
+ data.minutes = minutes % 60;
+
+ hours = absRound(minutes / 60);
+ data.hours = hours % 24;
+
+ days += absRound(hours / 24);
+ data.days = days % 30;
+
+ months += absRound(days / 30);
+ data.months = months % 12;
+
+ years = absRound(months / 12);
+ data.years = years;
+ },
+
+ weeks : function () {
+ return absRound(this.days() / 7);
+ },
+
+ valueOf : function () {
+ return this._milliseconds +
+ this._days * 864e5 +
+ (this._months % 12) * 2592e6 +
+ ~~(this._months / 12) * 31536e6;
+ },
+
+ humanize : function (withSuffix) {
+ var difference = +this,
+ output = relativeTime(difference, !withSuffix, this.lang());
+
+ if (withSuffix) {
+ output = this.lang().pastFuture(difference, output);
+ }
+
+ return this.lang().postformat(output);
+ },
+
+ add : function (input, val) {
+ // supports only 2.0-style add(1, 's') or add(moment)
+ var dur = moment.duration(input, val);
+
+ this._milliseconds += dur._milliseconds;
+ this._days += dur._days;
+ this._months += dur._months;
+
+ this._bubble();
+
+ return this;
+ },
+
+ subtract : function (input, val) {
+ var dur = moment.duration(input, val);
+
+ this._milliseconds -= dur._milliseconds;
+ this._days -= dur._days;
+ this._months -= dur._months;
+
+ this._bubble();
+
+ return this;
+ },
+
+ get : function (units) {
+ units = normalizeUnits(units);
+ return this[units.toLowerCase() + 's']();
+ },
+
+ as : function (units) {
+ units = normalizeUnits(units);
+ return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
+ },
+
+ lang : moment.fn.lang
+ };
+
+ function makeDurationGetter(name) {
+ moment.duration.fn[name] = function () {
+ return this._data[name];
+ };
+ }
+
+ function makeDurationAsGetter(name, factor) {
+ moment.duration.fn['as' + name] = function () {
+ return +this / factor;
+ };
+ }
+
+ for (i in unitMillisecondFactors) {
+ if (unitMillisecondFactors.hasOwnProperty(i)) {
+ makeDurationAsGetter(i, unitMillisecondFactors[i]);
+ makeDurationGetter(i.toLowerCase());
+ }
+ }
+
+ makeDurationAsGetter('Weeks', 6048e5);
+ moment.duration.fn.asMonths = function () {
+ return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
+ };
+
+
+ /************************************
+ Default Lang
+ ************************************/
+
+
+ // Set default language, other languages will inherit from English.
+ moment.lang('en', {
+ ordinal : function (number) {
+ var b = number % 10,
+ output = (~~ (number % 100 / 10) === 1) ? 'th' :
+ (b === 1) ? 'st' :
+ (b === 2) ? 'nd' :
+ (b === 3) ? 'rd' : 'th';
+ return number + output;
+ }
+ });
+
+
+ /************************************
+ Exposing Moment
+ ************************************/
+
+
+ // CommonJS module is defined
+ if (hasModule) {
+ module.exports = moment;
+ }
+ /*global ender:false */
+ if (typeof ender === 'undefined') {
+ // here, `this` means `window` in the browser, or `global` on the server
+ // add `moment` as a global object via a string identifier,
+ // for Closure Compiler "advanced" mode
+ this['moment'] = moment;
+ }
+ /*global define:false */
+ if (typeof define === "function" && define.amd) {
+ define("moment", [], function () {
+ return moment;
+ });
+ }
+}).call(this);
diff --git a/lib/mustache.js b/lib/mustache.js
new file mode 100644
index 0000000..932052b
--- /dev/null
+++ b/lib/mustache.js
@@ -0,0 +1,610 @@
+/*!
+ * mustache.js - Logic-less {{mustache}} templates with JavaScript
+ * http://github.com/janl/mustache.js
+ */
+
+/*global define: false*/
+
+(function (root, factory) {
+ if (typeof exports === "object" && exports) {
+ module.exports = factory; // CommonJS
+ } else if (typeof define === "function" && define.amd) {
+ define(factory); // AMD
+ } else {
+ root.Mustache = factory; // <script>
+ }
+}(this, (function () {
+
+ var exports = {};
+
+ exports.name = "mustache.js";
+ exports.version = "0.7.2";
+ exports.tags = ["{{", "}}"];
+
+ exports.Scanner = Scanner;
+ exports.Context = Context;
+ exports.Writer = Writer;
+
+ var whiteRe = /\s*/;
+ var spaceRe = /\s+/;
+ var nonSpaceRe = /\S/;
+ var eqRe = /\s*=/;
+ var curlyRe = /\s*\}/;
+ var tagRe = /#|\^|\/|>|\{|&|=|!/;
+
+ // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
+ // See https://github.com/janl/mustache.js/issues/189
+ function testRe(re, string) {
+ return RegExp.prototype.test.call(re, string);
+ }
+
+ function isWhitespace(string) {
+ return !testRe(nonSpaceRe, string);
+ }
+
+ var isArray = Array.isArray || function (obj) {
+ return Object.prototype.toString.call(obj) === "[object Array]";
+ };
+
+ function escapeRe(string) {
+ return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
+ }
+
+ var entityMap = {
+ "&": "&amp;",
+ "<": "&lt;",
+ ">": "&gt;",
+ '"': '&quot;',
+ "'": '&#39;',
+ "/": '&#x2F;'
+ };
+
+ function escapeHtml(string) {
+ return String(string).replace(/[&<>"'\/]/g, function (s) {
+ return entityMap[s];
+ });
+ }
+
+ // Export the escaping function so that the user may override it.
+ // See https://github.com/janl/mustache.js/issues/244
+ exports.escape = escapeHtml;
+
+ function Scanner(string) {
+ this.string = string;
+ this.tail = string;
+ this.pos = 0;
+ }
+
+ /**
+ * Returns `true` if the tail is empty (end of string).
+ */
+ Scanner.prototype.eos = function () {
+ return this.tail === "";
+ };
+
+ /**
+ * Tries to match the given regular expression at the current position.
+ * Returns the matched text if it can match, the empty string otherwise.
+ */
+ Scanner.prototype.scan = function (re) {
+ var match = this.tail.match(re);
+
+ if (match && match.index === 0) {
+ this.tail = this.tail.substring(match[0].length);
+ this.pos += match[0].length;
+ return match[0];
+ }
+
+ return "";
+ };
+
+ /**
+ * Skips all text until the given regular expression can be matched. Returns
+ * the skipped string, which is the entire tail if no match can be made.
+ */
+ Scanner.prototype.scanUntil = function (re) {
+ var match, pos = this.tail.search(re);
+
+ switch (pos) {
+ case -1:
+ match = this.tail;
+ this.pos += this.tail.length;
+ this.tail = "";
+ break;
+ case 0:
+ match = "";
+ break;
+ default:
+ match = this.tail.substring(0, pos);
+ this.tail = this.tail.substring(pos);
+ this.pos += pos;
+ }
+
+ return match;
+ };
+
+ function Context(view, parent) {
+ this.view = view;
+ this.parent = parent;
+ this.clearCache();
+ }
+
+ Context.make = function (view) {
+ return (view instanceof Context) ? view : new Context(view);
+ };
+
+ Context.prototype.clearCache = function () {
+ this._cache = {};
+ };
+
+ Context.prototype.push = function (view) {
+ return new Context(view, this);
+ };
+
+ Context.prototype.lookup = function (name) {
+ var value = this._cache[name];
+
+ if (!value) {
+ if (name === ".") {
+ value = this.view;
+ } else {
+ var context = this;
+
+ while (context) {
+ if (name.indexOf(".") > 0) {
+ var names = name.split("."), i = 0;
+
+ value = context.view;
+
+ while (value && i < names.length) {
+ value = value[names[i++]];
+ }
+ } else {
+ value = context.view[name];
+ }
+
+ if (value != null) {
+ break;
+ }
+
+ context = context.parent;
+ }
+ }
+
+ this._cache[name] = value;
+ }
+
+ if (typeof value === "function") {
+ value = value.call(this.view);
+ }
+
+ return value;
+ };
+
+ function Writer() {
+ this.clearCache();
+ }
+
+ Writer.prototype.clearCache = function () {
+ this._cache = {};
+ this._partialCache = {};
+ };
+
+ Writer.prototype.compile = function (template, tags) {
+ var fn = this._cache[template];
+
+ if (!fn) {
+ var tokens = exports.parse(template, tags);
+ fn = this._cache[template] = this.compileTokens(tokens, template);
+ }
+
+ return fn;
+ };
+
+ Writer.prototype.compilePartial = function (name, template, tags) {
+ var fn = this.compile(template, tags);
+ this._partialCache[name] = fn;
+ return fn;
+ };
+
+ Writer.prototype.compileTokens = function (tokens, template) {
+ var fn = compileTokens(tokens);
+ var self = this;
+
+ return function (view, partials) {
+ if (partials) {
+ if (typeof partials === "function") {
+ self._loadPartial = partials;
+ } else {
+ for (var name in partials) {
+ self.compilePartial(name, partials[name]);
+ }
+ }
+ }
+
+ return fn(self, Context.make(view), template);
+ };
+ };
+
+ Writer.prototype.render = function (template, view, partials) {
+ return this.compile(template)(view, partials);
+ };
+
+ Writer.prototype._section = function (name, context, text, callback) {
+ var value = context.lookup(name);
+
+ switch (typeof value) {
+ case "object":
+ if (isArray(value)) {
+ var buffer = "";
+
+ for (var i = 0, len = value.length; i < len; ++i) {
+ buffer += callback(this, context.push(value[i]));
+ }
+
+ return buffer;
+ }
+
+ return value ? callback(this, context.push(value)) : "";
+ case "function":
+ var self = this;
+ var scopedRender = function (template) {
+ return self.render(template, context);
+ };
+
+ var result = value.call(context.view, text, scopedRender);
+ return result != null ? result : "";
+ default:
+ if (value) {
+ return callback(this, context);
+ }
+ }
+
+ return "";
+ };
+
+ Writer.prototype._inverted = function (name, context, callback) {
+ var value = context.lookup(name);
+
+ // Use JavaScript's definition of falsy. Include empty arrays.
+ // See https://github.com/janl/mustache.js/issues/186
+ if (!value || (isArray(value) && value.length === 0)) {
+ return callback(this, context);
+ }
+
+ return "";
+ };
+
+ Writer.prototype._partial = function (name, context) {
+ if (!(name in this._partialCache) && this._loadPartial) {
+ this.compilePartial(name, this._loadPartial(name));
+ }
+
+ var fn = this._partialCache[name];
+
+ return fn ? fn(context) : "";
+ };
+
+ Writer.prototype._name = function (name, context) {
+ var value = context.lookup(name);
+
+ if (typeof value === "function") {
+ value = value.call(context.view);
+ }
+
+ return (value == null) ? "" : String(value);
+ };
+
+ Writer.prototype._escaped = function (name, context) {
+ return exports.escape(this._name(name, context));
+ };
+
+ /**
+ * Low-level function that compiles the given `tokens` into a function
+ * that accepts three arguments: a Writer, a Context, and the template.
+ */
+ function compileTokens(tokens) {
+ var subRenders = {};
+
+ function subRender(i, tokens, template) {
+ if (!subRenders[i]) {
+ var fn = compileTokens(tokens);
+ subRenders[i] = function (writer, context) {
+ return fn(writer, context, template);
+ };
+ }
+
+ return subRenders[i];
+ }
+
+ return function (writer, context, template) {
+ var buffer = "";
+ var token, sectionText;
+
+ for (var i = 0, len = tokens.length; i < len; ++i) {
+ token = tokens[i];
+
+ switch (token[0]) {
+ case "#":
+ sectionText = template.slice(token[3], token[5]);
+ buffer += writer._section(token[1], context, sectionText, subRender(i, token[4], template));
+ break;
+ case "^":
+ buffer += writer._inverted(token[1], context, subRender(i, token[4], template));
+ break;
+ case ">":
+ buffer += writer._partial(token[1], context);
+ break;
+ case "&":
+ buffer += writer._name(token[1], context);
+ break;
+ case "name":
+ buffer += writer._escaped(token[1], context);
+ break;
+ case "text":
+ buffer += token[1];
+ break;
+ }
+ }
+
+ return buffer;
+ };
+ }
+
+ /**
+ * Forms the given array of `tokens` into a nested tree structure where
+ * tokens that represent a section have two additional items: 1) an array of
+ * all tokens that appear in that section and 2) the index in the original
+ * template that represents the end of that section.
+ */
+ function nestTokens(tokens) {
+ var tree = [];
+ var collector = tree;
+ var sections = [];
+
+ var token;
+ for (var i = 0, len = tokens.length; i < len; ++i) {
+ token = tokens[i];
+ switch (token[0]) {
+ case '#':
+ case '^':
+ sections.push(token);
+ collector.push(token);
+ collector = token[4] = [];
+ break;
+ case '/':
+ var section = sections.pop();
+ section[5] = token[2];
+ collector = sections.length > 0 ? sections[sections.length - 1][4] : tree;
+ break;
+ default:
+ collector.push(token);
+ }
+ }
+
+ return tree;
+ }
+
+ /**
+ * Combines the values of consecutive text tokens in the given `tokens` array
+ * to a single token.
+ */
+ function squashTokens(tokens) {
+ var squashedTokens = [];
+
+ var token, lastToken;
+ for (var i = 0, len = tokens.length; i < len; ++i) {
+ token = tokens[i];
+ if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
+ lastToken[1] += token[1];
+ lastToken[3] = token[3];
+ } else {
+ lastToken = token;
+ squashedTokens.push(token);
+ }
+ }
+
+ return squashedTokens;
+ }
+
+ function escapeTags(tags) {
+ return [
+ new RegExp(escapeRe(tags[0]) + "\\s*"),
+ new RegExp("\\s*" + escapeRe(tags[1]))
+ ];
+ }
+
+ /**
+ * Breaks up the given `template` string into a tree of token objects. If
+ * `tags` is given here it must be an array with two string values: the
+ * opening and closing tags used in the template (e.g. ["<%", "%>"]). Of
+ * course, the default is to use mustaches (i.e. Mustache.tags).
+ */
+ exports.parse = function (template, tags) {
+ template = template || '';
+ tags = tags || exports.tags;
+
+ if (typeof tags === 'string') tags = tags.split(spaceRe);
+ if (tags.length !== 2) {
+ throw new Error('Invalid tags: ' + tags.join(', '));
+ }
+
+ var tagRes = escapeTags(tags);
+ var scanner = new Scanner(template);
+
+ var sections = []; // Stack to hold section tokens
+ var tokens = []; // Buffer to hold the tokens
+ var spaces = []; // Indices of whitespace tokens on the current line
+ var hasTag = false; // Is there a {{tag}} on the current line?
+ var nonSpace = false; // Is there a non-space char on the current line?
+
+ // Strips all whitespace tokens array for the current line
+ // if there was a {{#tag}} on it and otherwise only space.
+ function stripSpace() {
+ if (hasTag && !nonSpace) {
+ while (spaces.length) {
+ tokens.splice(spaces.pop(), 1);
+ }
+ } else {
+ spaces = [];
+ }
+
+ hasTag = false;
+ nonSpace = false;
+ }
+
+ var start, type, value, chr;
+ while (!scanner.eos()) {
+ start = scanner.pos;
+ value = scanner.scanUntil(tagRes[0]);
+
+ if (value) {
+ for (var i = 0, len = value.length; i < len; ++i) {
+ chr = value.charAt(i);
+
+ if (isWhitespace(chr)) {
+ spaces.push(tokens.length);
+ } else {
+ nonSpace = true;
+ }
+
+ tokens.push(["text", chr, start, start + 1]);
+ start += 1;
+
+ if (chr === "\n") {
+ stripSpace(); // Check for whitespace on the current line.
+ }
+ }
+ }
+
+ start = scanner.pos;
+
+ // Match the opening tag.
+ if (!scanner.scan(tagRes[0])) {
+ break;
+ }
+
+ hasTag = true;
+ type = scanner.scan(tagRe) || "name";
+
+ // Skip any whitespace between tag and value.
+ scanner.scan(whiteRe);
+
+ // Extract the tag value.
+ if (type === "=") {
+ value = scanner.scanUntil(eqRe);
+ scanner.scan(eqRe);
+ scanner.scanUntil(tagRes[1]);
+ } else if (type === "{") {
+ var closeRe = new RegExp("\\s*" + escapeRe("}" + tags[1]));
+ value = scanner.scanUntil(closeRe);
+ scanner.scan(curlyRe);
+ scanner.scanUntil(tagRes[1]);
+ type = "&";
+ } else {
+ value = scanner.scanUntil(tagRes[1]);
+ }
+
+ // Match the closing tag.
+ if (!scanner.scan(tagRes[1])) {
+ throw new Error('Unclosed tag at ' + scanner.pos);
+ }
+
+ // Check section nesting.
+ if (type === '/') {
+ if (sections.length === 0) {
+ throw new Error('Unopened section "' + value + '" at ' + start);
+ }
+
+ var section = sections.pop();
+
+ if (section[1] !== value) {
+ throw new Error('Unclosed section "' + section[1] + '" at ' + start);
+ }
+ }
+
+ var token = [type, value, start, scanner.pos];
+ tokens.push(token);
+
+ if (type === '#' || type === '^') {
+ sections.push(token);
+ } else if (type === "name" || type === "{" || type === "&") {
+ nonSpace = true;
+ } else if (type === "=") {
+ // Set the tags for the next time around.
+ tags = value.split(spaceRe);
+
+ if (tags.length !== 2) {
+ throw new Error('Invalid tags at ' + start + ': ' + tags.join(', '));
+ }
+
+ tagRes = escapeTags(tags);
+ }
+ }
+
+ // Make sure there are no open sections when we're done.
+ var section = sections.pop();
+ if (section) {
+ throw new Error('Unclosed section "' + section[1] + '" at ' + scanner.pos);
+ }
+
+ return nestTokens(squashTokens(tokens));
+ };
+
+ // The high-level clearCache, compile, compilePartial, and render functions
+ // use this default writer.
+ var _writer = new Writer();
+
+ /**
+ * Clears all cached templates and partials in the default writer.
+ */
+ exports.clearCache = function () {
+ return _writer.clearCache();
+ };
+
+ /**
+ * Compiles the given `template` to a reusable function using the default
+ * writer.
+ */
+ exports.compile = function (template, tags) {
+ return _writer.compile(template, tags);
+ };
+
+ /**
+ * Compiles the partial with the given `name` and `template` to a reusable
+ * function using the default writer.
+ */
+ exports.compilePartial = function (name, template, tags) {
+ return _writer.compilePartial(name, template, tags);
+ };
+
+ /**
+ * Compiles the given array of tokens (the output of a parse) to a reusable
+ * function using the default writer.
+ */
+ exports.compileTokens = function (tokens, template) {
+ return _writer.compileTokens(tokens, template);
+ };
+
+ /**
+ * Renders the `template` with the given `view` and `partials` using the
+ * default writer.
+ */
+ exports.render = function (template, view, partials) {
+ return _writer.render(template, view, partials);
+ };
+
+ // This is here for backwards compatibility with 0.4.x.
+ exports.to_html = function (template, view, partials, send) {
+ var result = exports.render(template, view, partials);
+
+ if (typeof send === "function") {
+ send(result);
+ } else {
+ return result;
+ }
+ };
+
+ return exports;
+
+}())));
diff --git a/lib/require.js b/lib/require.js
new file mode 100644
index 0000000..5b26875
--- /dev/null
+++ b/lib/require.js
@@ -0,0 +1,2000 @@
+/** vim: et:ts=4:sw=4:sts=4
+ * @license RequireJS 2.1.4 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
+ * Available via the MIT or new BSD license.
+ * see: http://github.com/jrburke/requirejs for details
+ */
+//Not using strict: uneven strict support in browsers, #392, and causes
+//problems with requirejs.exec()/transpiler plugins that may not be strict.
+/*jslint regexp: true, nomen: true, sloppy: true */
+/*global window, navigator, document, importScripts, setTimeout, opera */
+
+var requirejs, require, define;
+(function (global) {
+ var req, s, head, baseElement, dataMain, src,
+ interactiveScript, currentlyAddingScript, mainScript, subPath,
+ version = '2.1.4',
+ commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
+ cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
+ jsSuffixRegExp = /\.js$/,
+ currDirRegExp = /^\.\//,
+ op = Object.prototype,
+ ostring = op.toString,
+ hasOwn = op.hasOwnProperty,
+ ap = Array.prototype,
+ apsp = ap.splice,
+ isBrowser = !!(typeof window !== 'undefined' && navigator && document),
+ isWebWorker = !isBrowser && typeof importScripts !== 'undefined',
+ //PS3 indicates loaded and complete, but need to wait for complete
+ //specifically. Sequence is 'loading', 'loaded', execution,
+ // then 'complete'. The UA check is unfortunate, but not sure how
+ //to feature test w/o causing perf issues.
+ readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ?
+ /^complete$/ : /^(complete|loaded)$/,
+ defContextName = '_',
+ //Oh the tragedy, detecting opera. See the usage of isOpera for reason.
+ isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]',
+ contexts = {},
+ cfg = {},
+ globalDefQueue = [],
+ useInteractive = false;
+
+ function isFunction(it) {
+ return ostring.call(it) === '[object Function]';
+ }
+
+ function isArray(it) {
+ return ostring.call(it) === '[object Array]';
+ }
+
+ /**
+ * Helper function for iterating over an array. If the func returns
+ * a true value, it will break out of the loop.
+ */
+ function each(ary, func) {
+ if (ary) {
+ var i;
+ for (i = 0; i < ary.length; i += 1) {
+ if (ary[i] && func(ary[i], i, ary)) {
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper function for iterating over an array backwards. If the func
+ * returns a true value, it will break out of the loop.
+ */
+ function eachReverse(ary, func) {
+ if (ary) {
+ var i;
+ for (i = ary.length - 1; i > -1; i -= 1) {
+ if (ary[i] && func(ary[i], i, ary)) {
+ break;
+ }
+ }
+ }
+ }
+
+ function hasProp(obj, prop) {
+ return hasOwn.call(obj, prop);
+ }
+
+ function getOwn(obj, prop) {
+ return hasProp(obj, prop) && obj[prop];
+ }
+
+ /**
+ * Cycles over properties in an object and calls a function for each
+ * property value. If the function returns a truthy value, then the
+ * iteration is stopped.
+ */
+ function eachProp(obj, func) {
+ var prop;
+ for (prop in obj) {
+ if (hasProp(obj, prop)) {
+ if (func(obj[prop], prop)) {
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Simple function to mix in properties from source into target,
+ * but only if target does not already have a property of the same name.
+ */
+ function mixin(target, source, force, deepStringMixin) {
+ if (source) {
+ eachProp(source, function (value, prop) {
+ if (force || !hasProp(target, prop)) {
+ if (deepStringMixin && typeof value !== 'string') {
+ if (!target[prop]) {
+ target[prop] = {};
+ }
+ mixin(target[prop], value, force, deepStringMixin);
+ } else {
+ target[prop] = value;
+ }
+ }
+ });
+ }
+ return target;
+ }
+
+ //Similar to Function.prototype.bind, but the 'this' object is specified
+ //first, since it is easier to read/figure out what 'this' will be.
+ function bind(obj, fn) {
+ return function () {
+ return fn.apply(obj, arguments);
+ };
+ }
+
+ function scripts() {
+ return document.getElementsByTagName('script');
+ }
+
+ //Allow getting a global that expressed in
+ //dot notation, like 'a.b.c'.
+ function getGlobal(value) {
+ if (!value) {
+ return value;
+ }
+ var g = global;
+ each(value.split('.'), function (part) {
+ g = g[part];
+ });
+ return g;
+ }
+
+ /**
+ * Constructs an error with a pointer to an URL with more information.
+ * @param {String} id the error ID that maps to an ID on a web page.
+ * @param {String} message human readable error.
+ * @param {Error} [err] the original error, if there is one.
+ *
+ * @returns {Error}
+ */
+ function makeError(id, msg, err, requireModules) {
+ var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id);
+ e.requireType = id;
+ e.requireModules = requireModules;
+ if (err) {
+ e.originalError = err;
+ }
+ return e;
+ }
+
+ if (typeof define !== 'undefined') {
+ //If a define is already in play via another AMD loader,
+ //do not overwrite.
+ return;
+ }
+
+ if (typeof requirejs !== 'undefined') {
+ if (isFunction(requirejs)) {
+ //Do not overwrite and existing requirejs instance.
+ return;
+ }
+ cfg = requirejs;
+ requirejs = undefined;
+ }
+
+ //Allow for a require config object
+ if (typeof require !== 'undefined' && !isFunction(require)) {
+ //assume it is a config object.
+ cfg = require;
+ require = undefined;
+ }
+
+ function newContext(contextName) {
+ var inCheckLoaded, Module, context, handlers,
+ checkLoadedTimeoutId,
+ config = {
+ waitSeconds: 7,
+ baseUrl: './',
+ paths: {},
+ pkgs: {},
+ shim: {},
+ map: {},
+ config: {}
+ },
+ registry = {},
+ undefEvents = {},
+ defQueue = [],
+ defined = {},
+ urlFetched = {},
+ requireCounter = 1,
+ unnormalizedCounter = 1;
+
+ /**
+ * Trims the . and .. from an array of path segments.
+ * It will keep a leading path segment if a .. will become
+ * the first path segment, to help with module name lookups,
+ * which act like paths, but can be remapped. But the end result,
+ * all paths that use this function should look normalized.
+ * NOTE: this method MODIFIES the input array.
+ * @param {Array} ary the array of path segments.
+ */
+ function trimDots(ary) {
+ var i, part;
+ for (i = 0; ary[i]; i += 1) {
+ part = ary[i];
+ if (part === '.') {
+ ary.splice(i, 1);
+ i -= 1;
+ } else if (part === '..') {
+ if (i === 1 && (ary[2] === '..' || ary[0] === '..')) {
+ //End of the line. Keep at least one non-dot
+ //path segment at the front so it can be mapped
+ //correctly to disk. Otherwise, there is likely
+ //no path mapping for a path starting with '..'.
+ //This can still fail, but catches the most reasonable
+ //uses of ..
+ break;
+ } else if (i > 0) {
+ ary.splice(i - 1, 2);
+ i -= 2;
+ }
+ }
+ }
+ }
+
+ /**
+ * Given a relative module name, like ./something, normalize it to
+ * a real name that can be mapped to a path.
+ * @param {String} name the relative name
+ * @param {String} baseName a real name that the name arg is relative
+ * to.
+ * @param {Boolean} applyMap apply the map config to the value. Should
+ * only be done if this normalization is for a dependency ID.
+ * @returns {String} normalized name
+ */
+ function normalize(name, baseName, applyMap) {
+ var pkgName, pkgConfig, mapValue, nameParts, i, j, nameSegment,
+ foundMap, foundI, foundStarMap, starI,
+ baseParts = baseName && baseName.split('/'),
+ normalizedBaseParts = baseParts,
+ map = config.map,
+ starMap = map && map['*'];
+
+ //Adjust any relative paths.
+ if (name && name.charAt(0) === '.') {
+ //If have a base name, try to normalize against it,
+ //otherwise, assume it is a top-level require that will
+ //be relative to baseUrl in the end.
+ if (baseName) {
+ if (getOwn(config.pkgs, baseName)) {
+ //If the baseName is a package name, then just treat it as one
+ //name to concat the name with.
+ normalizedBaseParts = baseParts = [baseName];
+ } else {
+ //Convert baseName to array, and lop off the last part,
+ //so that . matches that 'directory' and not name of the baseName's
+ //module. For instance, baseName of 'one/two/three', maps to
+ //'one/two/three.js', but we want the directory, 'one/two' for
+ //this normalization.
+ normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
+ }
+
+ name = normalizedBaseParts.concat(name.split('/'));
+ trimDots(name);
+
+ //Some use of packages may use a . path to reference the
+ //'main' module name, so normalize for that.
+ pkgConfig = getOwn(config.pkgs, (pkgName = name[0]));
+ name = name.join('/');
+ if (pkgConfig && name === pkgName + '/' + pkgConfig.main) {
+ name = pkgName;
+ }
+ } else if (name.indexOf('./') === 0) {
+ // No baseName, so this is ID is resolved relative
+ // to baseUrl, pull off the leading dot.
+ name = name.substring(2);
+ }
+ }
+
+ //Apply map config if available.
+ if (applyMap && (baseParts || starMap) && map) {
+ nameParts = name.split('/');
+
+ for (i = nameParts.length; i > 0; i -= 1) {
+ nameSegment = nameParts.slice(0, i).join('/');
+
+ if (baseParts) {
+ //Find the longest baseName segment match in the config.
+ //So, do joins on the biggest to smallest lengths of baseParts.
+ for (j = baseParts.length; j > 0; j -= 1) {
+ mapValue = getOwn(map, baseParts.slice(0, j).join('/'));
+
+ //baseName segment has config, find if it has one for
+ //this name.
+ if (mapValue) {
+ mapValue = getOwn(mapValue, nameSegment);
+ if (mapValue) {
+ //Match, update name to the new value.
+ foundMap = mapValue;
+ foundI = i;
+ break;
+ }
+ }
+ }
+ }
+
+ if (foundMap) {
+ break;
+ }
+
+ //Check for a star map match, but just hold on to it,
+ //if there is a shorter segment match later in a matching
+ //config, then favor over this star map.
+ if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) {
+ foundStarMap = getOwn(starMap, nameSegment);
+ starI = i;
+ }
+ }
+
+ if (!foundMap && foundStarMap) {
+ foundMap = foundStarMap;
+ foundI = starI;
+ }
+
+ if (foundMap) {
+ nameParts.splice(0, foundI, foundMap);
+ name = nameParts.join('/');
+ }
+ }
+
+ return name;
+ }
+
+ function removeScript(name) {
+ if (isBrowser) {
+ each(scripts(), function (scriptNode) {
+ if (scriptNode.getAttribute('data-requiremodule') === name &&
+ scriptNode.getAttribute('data-requirecontext') === context.contextName) {
+ scriptNode.parentNode.removeChild(scriptNode);
+ return true;
+ }
+ });
+ }
+ }
+
+ function hasPathFallback(id) {
+ var pathConfig = getOwn(config.paths, id);
+ if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) {
+ removeScript(id);
+ //Pop off the first array value, since it failed, and
+ //retry
+ pathConfig.shift();
+ context.require.undef(id);
+ context.require([id]);
+ return true;
+ }
+ }
+
+ //Turns a plugin!resource to [plugin, resource]
+ //with the plugin being undefined if the name
+ //did not have a plugin prefix.
+ function splitPrefix(name) {
+ var prefix,
+ index = name ? name.indexOf('!') : -1;
+ if (index > -1) {
+ prefix = name.substring(0, index);
+ name = name.substring(index + 1, name.length);
+ }
+ return [prefix, name];
+ }
+
+ /**
+ * Creates a module mapping that includes plugin prefix, module
+ * name, and path. If parentModuleMap is provided it will
+ * also normalize the name via require.normalize()
+ *
+ * @param {String} name the module name
+ * @param {String} [parentModuleMap] parent module map
+ * for the module name, used to resolve relative names.
+ * @param {Boolean} isNormalized: is the ID already normalized.
+ * This is true if this call is done for a define() module ID.
+ * @param {Boolean} applyMap: apply the map config to the ID.
+ * Should only be true if this map is for a dependency.
+ *
+ * @returns {Object}
+ */
+ function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {
+ var url, pluginModule, suffix, nameParts,
+ prefix = null,
+ parentName = parentModuleMap ? parentModuleMap.name : null,
+ originalName = name,
+ isDefine = true,
+ normalizedName = '';
+
+ //If no name, then it means it is a require call, generate an
+ //internal name.
+ if (!name) {
+ isDefine = false;
+ name = '_@r' + (requireCounter += 1);
+ }
+
+ nameParts = splitPrefix(name);
+ prefix = nameParts[0];
+ name = nameParts[1];
+
+ if (prefix) {
+ prefix = normalize(prefix, parentName, applyMap);
+ pluginModule = getOwn(defined, prefix);
+ }
+
+ //Account for relative paths if there is a base name.
+ if (name) {
+ if (prefix) {
+ if (pluginModule && pluginModule.normalize) {
+ //Plugin is loaded, use its normalize method.
+ normalizedName = pluginModule.normalize(name, function (name) {
+ return normalize(name, parentName, applyMap);
+ });
+ } else {
+ normalizedName = normalize(name, parentName, applyMap);
+ }
+ } else {
+ //A regular module.
+ normalizedName = normalize(name, parentName, applyMap);
+
+ //Normalized name may be a plugin ID due to map config
+ //application in normalize. The map config values must
+ //already be normalized, so do not need to redo that part.
+ nameParts = splitPrefix(normalizedName);
+ prefix = nameParts[0];
+ normalizedName = nameParts[1];
+ isNormalized = true;
+
+ url = context.nameToUrl(normalizedName);
+ }
+ }
+
+ //If the id is a plugin id that cannot be determined if it needs
+ //normalization, stamp it with a unique ID so two matching relative
+ //ids that may conflict can be separate.
+ suffix = prefix && !pluginModule && !isNormalized ?
+ '_unnormalized' + (unnormalizedCounter += 1) :
+ '';
+
+ return {
+ prefix: prefix,
+ name: normalizedName,
+ parentMap: parentModuleMap,
+ unnormalized: !!suffix,
+ url: url,
+ originalName: originalName,
+ isDefine: isDefine,
+ id: (prefix ?
+ prefix + '!' + normalizedName :
+ normalizedName) + suffix
+ };
+ }
+
+ function getModule(depMap) {
+ var id = depMap.id,
+ mod = getOwn(registry, id);
+
+ if (!mod) {
+ mod = registry[id] = new context.Module(depMap);
+ }
+
+ return mod;
+ }
+
+ function on(depMap, name, fn) {
+ var id = depMap.id,
+ mod = getOwn(registry, id);
+
+ if (hasProp(defined, id) &&
+ (!mod || mod.defineEmitComplete)) {
+ if (name === 'defined') {
+ fn(defined[id]);
+ }
+ } else {
+ getModule(depMap).on(name, fn);
+ }
+ }
+
+ function onError(err, errback) {
+ var ids = err.requireModules,
+ notified = false;
+
+ if (errback) {
+ errback(err);
+ } else {
+ each(ids, function (id) {
+ var mod = getOwn(registry, id);
+ if (mod) {
+ //Set error on module, so it skips timeout checks.
+ mod.error = err;
+ if (mod.events.error) {
+ notified = true;
+ mod.emit('error', err);
+ }
+ }
+ });
+
+ if (!notified) {
+ req.onError(err);
+ }
+ }
+ }
+
+ /**
+ * Internal method to transfer globalQueue items to this context's
+ * defQueue.
+ */
+ function takeGlobalQueue() {
+ //Push all the globalDefQueue items into the context's defQueue
+ if (globalDefQueue.length) {
+ //Array splice in the values since the context code has a
+ //local var ref to defQueue, so cannot just reassign the one
+ //on context.
+ apsp.apply(defQueue,
+ [defQueue.length - 1, 0].concat(globalDefQueue));
+ globalDefQueue = [];
+ }
+ }
+
+ handlers = {
+ 'require': function (mod) {
+ if (mod.require) {
+ return mod.require;
+ } else {
+ return (mod.require = context.makeRequire(mod.map));
+ }
+ },
+ 'exports': function (mod) {
+ mod.usingExports = true;
+ if (mod.map.isDefine) {
+ if (mod.exports) {
+ return mod.exports;
+ } else {
+ return (mod.exports = defined[mod.map.id] = {});
+ }
+ }
+ },
+ 'module': function (mod) {
+ if (mod.module) {
+ return mod.module;
+ } else {
+ return (mod.module = {
+ id: mod.map.id,
+ uri: mod.map.url,
+ config: function () {
+ return (config.config && getOwn(config.config, mod.map.id)) || {};
+ },
+ exports: defined[mod.map.id]
+ });
+ }
+ }
+ };
+
+ function cleanRegistry(id) {
+ //Clean up machinery used for waiting modules.
+ delete registry[id];
+ }
+
+ function breakCycle(mod, traced, processed) {
+ var id = mod.map.id;
+
+ if (mod.error) {
+ mod.emit('error', mod.error);
+ } else {
+ traced[id] = true;
+ each(mod.depMaps, function (depMap, i) {
+ var depId = depMap.id,
+ dep = getOwn(registry, depId);
+
+ //Only force things that have not completed
+ //being defined, so still in the registry,
+ //and only if it has not been matched up
+ //in the module already.
+ if (dep && !mod.depMatched[i] && !processed[depId]) {
+ if (getOwn(traced, depId)) {
+ mod.defineDep(i, defined[depId]);
+ mod.check(); //pass false?
+ } else {
+ breakCycle(dep, traced, processed);
+ }
+ }
+ });
+ processed[id] = true;
+ }
+ }
+
+ function checkLoaded() {
+ var map, modId, err, usingPathFallback,
+ waitInterval = config.waitSeconds * 1000,
+ //It is possible to disable the wait interval by using waitSeconds of 0.
+ expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
+ noLoads = [],
+ reqCalls = [],
+ stillLoading = false,
+ needCycleCheck = true;
+
+ //Do not bother if this call was a result of a cycle break.
+ if (inCheckLoaded) {
+ return;
+ }
+
+ inCheckLoaded = true;
+
+ //Figure out the state of all the modules.
+ eachProp(registry, function (mod) {
+ map = mod.map;
+ modId = map.id;
+
+ //Skip things that are not enabled or in error state.
+ if (!mod.enabled) {
+ return;
+ }
+
+ if (!map.isDefine) {
+ reqCalls.push(mod);
+ }
+
+ if (!mod.error) {
+ //If the module should be executed, and it has not
+ //been inited and time is up, remember it.
+ if (!mod.inited && expired) {
+ if (hasPathFallback(modId)) {
+ usingPathFallback = true;
+ stillLoading = true;
+ } else {
+ noLoads.push(modId);
+ removeScript(modId);
+ }
+ } else if (!mod.inited && mod.fetched && map.isDefine) {
+ stillLoading = true;
+ if (!map.prefix) {
+ //No reason to keep looking for unfinished
+ //loading. If the only stillLoading is a
+ //plugin resource though, keep going,
+ //because it may be that a plugin resource
+ //is waiting on a non-plugin cycle.
+ return (needCycleCheck = false);
+ }
+ }
+ }
+ });
+
+ if (expired && noLoads.length) {
+ //If wait time expired, throw error of unloaded modules.
+ err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
+ err.contextName = context.contextName;
+ return onError(err);
+ }
+
+ //Not expired, check for a cycle.
+ if (needCycleCheck) {
+ each(reqCalls, function (mod) {
+ breakCycle(mod, {}, {});
+ });
+ }
+
+ //If still waiting on loads, and the waiting load is something
+ //other than a plugin resource, or there are still outstanding
+ //scripts, then just try back later.
+ if ((!expired || usingPathFallback) && stillLoading) {
+ //Something is still waiting to load. Wait for it, but only
+ //if a timeout is not already in effect.
+ if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
+ checkLoadedTimeoutId = setTimeout(function () {
+ checkLoadedTimeoutId = 0;
+ checkLoaded();
+ }, 50);
+ }
+ }
+
+ inCheckLoaded = false;
+ }
+
+ Module = function (map) {
+ this.events = getOwn(undefEvents, map.id) || {};
+ this.map = map;
+ this.shim = getOwn(config.shim, map.id);
+ this.depExports = [];
+ this.depMaps = [];
+ this.depMatched = [];
+ this.pluginMaps = {};
+ this.depCount = 0;
+
+ /* this.exports this.factory
+ this.depMaps = [],
+ this.enabled, this.fetched
+ */
+ };
+
+ Module.prototype = {
+ init: function (depMaps, factory, errback, options) {
+ options = options || {};
+
+ //Do not do more inits if already done. Can happen if there
+ //are multiple define calls for the same module. That is not
+ //a normal, common case, but it is also not unexpected.
+ if (this.inited) {
+ return;
+ }
+
+ this.factory = factory;
+
+ if (errback) {
+ //Register for errors on this module.
+ this.on('error', errback);
+ } else if (this.events.error) {
+ //If no errback already, but there are error listeners
+ //on this module, set up an errback to pass to the deps.
+ errback = bind(this, function (err) {
+ this.emit('error', err);
+ });
+ }
+
+ //Do a copy of the dependency array, so that
+ //source inputs are not modified. For example
+ //"shim" deps are passed in here directly, and
+ //doing a direct modification of the depMaps array
+ //would affect that config.
+ this.depMaps = depMaps && depMaps.slice(0);
+
+ this.errback = errback;
+
+ //Indicate this module has be initialized
+ this.inited = true;
+
+ this.ignore = options.ignore;
+
+ //Could have option to init this module in enabled mode,
+ //or could have been previously marked as enabled. However,
+ //the dependencies are not known until init is called. So
+ //if enabled previously, now trigger dependencies as enabled.
+ if (options.enabled || this.enabled) {
+ //Enable this module and dependencies.
+ //Will call this.check()
+ this.enable();
+ } else {
+ this.check();
+ }
+ },
+
+ defineDep: function (i, depExports) {
+ //Because of cycles, defined callback for a given
+ //export can be called more than once.
+ if (!this.depMatched[i]) {
+ this.depMatched[i] = true;
+ this.depCount -= 1;
+ this.depExports[i] = depExports;
+ }
+ },
+
+ fetch: function () {
+ if (this.fetched) {
+ return;
+ }
+ this.fetched = true;
+
+ context.startTime = (new Date()).getTime();
+
+ var map = this.map;
+
+ //If the manager is for a plugin managed resource,
+ //ask the plugin to load it now.
+ if (this.shim) {
+ context.makeRequire(this.map, {
+ enableBuildCallback: true
+ })(this.shim.deps || [], bind(this, function () {
+ return map.prefix ? this.callPlugin() : this.load();
+ }));
+ } else {
+ //Regular dependency.
+ return map.prefix ? this.callPlugin() : this.load();
+ }
+ },
+
+ load: function () {
+ var url = this.map.url;
+
+ //Regular dependency.
+ if (!urlFetched[url]) {
+ urlFetched[url] = true;
+ context.load(this.map.id, url);
+ }
+ },
+
+ /**
+ * Checks is the module is ready to define itself, and if so,
+ * define it.
+ */
+ check: function () {
+ if (!this.enabled || this.enabling) {
+ return;
+ }
+
+ var err, cjsModule,
+ id = this.map.id,
+ depExports = this.depExports,
+ exports = this.exports,
+ factory = this.factory;
+
+ if (!this.inited) {
+ this.fetch();
+ } else if (this.error) {
+ this.emit('error', this.error);
+ } else if (!this.defining) {
+ //The factory could trigger another require call
+ //that would result in checking this module to
+ //define itself again. If already in the process
+ //of doing that, skip this work.
+ this.defining = true;
+
+ if (this.depCount < 1 && !this.defined) {
+ if (isFunction(factory)) {
+ //If there is an error listener, favor passing
+ //to that instead of throwing an error.
+ if (this.events.error) {
+ try {
+ exports = context.execCb(id, factory, depExports, exports);
+ } catch (e) {
+ err = e;
+ }
+ } else {
+ exports = context.execCb(id, factory, depExports, exports);
+ }
+
+ if (this.map.isDefine) {
+ //If setting exports via 'module' is in play,
+ //favor that over return value and exports. After that,
+ //favor a non-undefined return value over exports use.
+ cjsModule = this.module;
+ if (cjsModule &&
+ cjsModule.exports !== undefined &&
+ //Make sure it is not already the exports value
+ cjsModule.exports !== this.exports) {
+ exports = cjsModule.exports;
+ } else if (exports === undefined && this.usingExports) {
+ //exports already set the defined value.
+ exports = this.exports;
+ }
+ }
+
+ if (err) {
+ err.requireMap = this.map;
+ err.requireModules = [this.map.id];
+ err.requireType = 'define';
+ return onError((this.error = err));
+ }
+
+ } else {
+ //Just a literal value
+ exports = factory;
+ }
+
+ this.exports = exports;
+
+ if (this.map.isDefine && !this.ignore) {
+ defined[id] = exports;
+
+ if (req.onResourceLoad) {
+ req.onResourceLoad(context, this.map, this.depMaps);
+ }
+ }
+
+ //Clean up
+ delete registry[id];
+
+ this.defined = true;
+ }
+
+ //Finished the define stage. Allow calling check again
+ //to allow define notifications below in the case of a
+ //cycle.
+ this.defining = false;
+
+ if (this.defined && !this.defineEmitted) {
+ this.defineEmitted = true;
+ this.emit('defined', this.exports);
+ this.defineEmitComplete = true;
+ }
+
+ }
+ },
+
+ callPlugin: function () {
+ var map = this.map,
+ id = map.id,
+ //Map already normalized the prefix.
+ pluginMap = makeModuleMap(map.prefix);
+
+ //Mark this as a dependency for this plugin, so it
+ //can be traced for cycles.
+ this.depMaps.push(pluginMap);
+
+ on(pluginMap, 'defined', bind(this, function (plugin) {
+ var load, normalizedMap, normalizedMod,
+ name = this.map.name,
+ parentName = this.map.parentMap ? this.map.parentMap.name : null,
+ localRequire = context.makeRequire(map.parentMap, {
+ enableBuildCallback: true
+ });
+
+ //If current map is not normalized, wait for that
+ //normalized name to load instead of continuing.
+ if (this.map.unnormalized) {
+ //Normalize the ID if the plugin allows it.
+ if (plugin.normalize) {
+ name = plugin.normalize(name, function (name) {
+ return normalize(name, parentName, true);
+ }) || '';
+ }
+
+ //prefix and name should already be normalized, no need
+ //for applying map config again either.
+ normalizedMap = makeModuleMap(map.prefix + '!' + name,
+ this.map.parentMap);
+ on(normalizedMap,
+ 'defined', bind(this, function (value) {
+ this.init([], function () { return value; }, null, {
+ enabled: true,
+ ignore: true
+ });
+ }));
+
+ normalizedMod = getOwn(registry, normalizedMap.id);
+ if (normalizedMod) {
+ //Mark this as a dependency for this plugin, so it
+ //can be traced for cycles.
+ this.depMaps.push(normalizedMap);
+
+ if (this.events.error) {
+ normalizedMod.on('error', bind(this, function (err) {
+ this.emit('error', err);
+ }));
+ }
+ normalizedMod.enable();
+ }
+
+ return;
+ }
+
+ load = bind(this, function (value) {
+ this.init([], function () { return value; }, null, {
+ enabled: true
+ });
+ });
+
+ load.error = bind(this, function (err) {
+ this.inited = true;
+ this.error = err;
+ err.requireModules = [id];
+
+ //Remove temp unnormalized modules for this module,
+ //since they will never be resolved otherwise now.
+ eachProp(registry, function (mod) {
+ if (mod.map.id.indexOf(id + '_unnormalized') === 0) {
+ cleanRegistry(mod.map.id);
+ }
+ });
+
+ onError(err);
+ });
+
+ //Allow plugins to load other code without having to know the
+ //context or how to 'complete' the load.
+ load.fromText = bind(this, function (text, textAlt) {
+ /*jslint evil: true */
+ var moduleName = map.name,
+ moduleMap = makeModuleMap(moduleName),
+ hasInteractive = useInteractive;
+
+ //As of 2.1.0, support just passing the text, to reinforce
+ //fromText only being called once per resource. Still
+ //support old style of passing moduleName but discard
+ //that moduleName in favor of the internal ref.
+ if (textAlt) {
+ text = textAlt;
+ }
+
+ //Turn off interactive script matching for IE for any define
+ //calls in the text, then turn it back on at the end.
+ if (hasInteractive) {
+ useInteractive = false;
+ }
+
+ //Prime the system by creating a module instance for
+ //it.
+ getModule(moduleMap);
+
+ //Transfer any config to this other module.
+ if (hasProp(config.config, id)) {
+ config.config[moduleName] = config.config[id];
+ }
+
+ try {
+ req.exec(text);
+ } catch (e) {
+ return onError(makeError('fromtexteval',
+ 'fromText eval for ' + id +
+ ' failed: ' + e,
+ e,
+ [id]));
+ }
+
+ if (hasInteractive) {
+ useInteractive = true;
+ }
+
+ //Mark this as a dependency for the plugin
+ //resource
+ this.depMaps.push(moduleMap);
+
+ //Support anonymous modules.
+ context.completeLoad(moduleName);
+
+ //Bind the value of that module to the value for this
+ //resource ID.
+ localRequire([moduleName], load);
+ });
+
+ //Use parentName here since the plugin's name is not reliable,
+ //could be some weird string with no path that actually wants to
+ //reference the parentName's path.
+ plugin.load(map.name, localRequire, load, config);
+ }));
+
+ context.enable(pluginMap, this);
+ this.pluginMaps[pluginMap.id] = pluginMap;
+ },
+
+ enable: function () {
+ this.enabled = true;
+
+ //Set flag mentioning that the module is enabling,
+ //so that immediate calls to the defined callbacks
+ //for dependencies do not trigger inadvertent load
+ //with the depCount still being zero.
+ this.enabling = true;
+
+ //Enable each dependency
+ each(this.depMaps, bind(this, function (depMap, i) {
+ var id, mod, handler;
+
+ if (typeof depMap === 'string') {
+ //Dependency needs to be converted to a depMap
+ //and wired up to this module.
+ depMap = makeModuleMap(depMap,
+ (this.map.isDefine ? this.map : this.map.parentMap),
+ false,
+ !this.skipMap);
+ this.depMaps[i] = depMap;
+
+ handler = getOwn(handlers, depMap.id);
+
+ if (handler) {
+ this.depExports[i] = handler(this);
+ return;
+ }
+
+ this.depCount += 1;
+
+ on(depMap, 'defined', bind(this, function (depExports) {
+ this.defineDep(i, depExports);
+ this.check();
+ }));
+
+ if (this.errback) {
+ on(depMap, 'error', this.errback);
+ }
+ }
+
+ id = depMap.id;
+ mod = registry[id];
+
+ //Skip special modules like 'require', 'exports', 'module'
+ //Also, don't call enable if it is already enabled,
+ //important in circular dependency cases.
+ if (!hasProp(handlers, id) && mod && !mod.enabled) {
+ context.enable(depMap, this);
+ }
+ }));
+
+ //Enable each plugin that is used in
+ //a dependency
+ eachProp(this.pluginMaps, bind(this, function (pluginMap) {
+ var mod = getOwn(registry, pluginMap.id);
+ if (mod && !mod.enabled) {
+ context.enable(pluginMap, this);
+ }
+ }));
+
+ this.enabling = false;
+
+ this.check();
+ },
+
+ on: function (name, cb) {
+ var cbs = this.events[name];
+ if (!cbs) {
+ cbs = this.events[name] = [];
+ }
+ cbs.push(cb);
+ },
+
+ emit: function (name, evt) {
+ each(this.events[name], function (cb) {
+ cb(evt);
+ });
+ if (name === 'error') {
+ //Now that the error handler was triggered, remove
+ //the listeners, since this broken Module instance
+ //can stay around for a while in the registry.
+ delete this.events[name];
+ }
+ }
+ };
+
+ function callGetModule(args) {
+ //Skip modules already defined.
+ if (!hasProp(defined, args[0])) {
+ getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
+ }
+ }
+
+ function removeListener(node, func, name, ieName) {
+ //Favor detachEvent because of IE9
+ //issue, see attachEvent/addEventListener comment elsewhere
+ //in this file.
+ if (node.detachEvent && !isOpera) {
+ //Probably IE. If not it will throw an error, which will be
+ //useful to know.
+ if (ieName) {
+ node.detachEvent(ieName, func);
+ }
+ } else {
+ node.removeEventListener(name, func, false);
+ }
+ }
+
+ /**
+ * Given an event from a script node, get the requirejs info from it,
+ * and then removes the event listeners on the node.
+ * @param {Event} evt
+ * @returns {Object}
+ */
+ function getScriptData(evt) {
+ //Using currentTarget instead of target for Firefox 2.0's sake. Not
+ //all old browsers will be supported, but this one was easy enough
+ //to support and still makes sense.
+ var node = evt.currentTarget || evt.srcElement;
+
+ //Remove the listeners once here.
+ removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange');
+ removeListener(node, context.onScriptError, 'error');
+
+ return {
+ node: node,
+ id: node && node.getAttribute('data-requiremodule')
+ };
+ }
+
+ function intakeDefines() {
+ var args;
+
+ //Any defined modules in the global queue, intake them now.
+ takeGlobalQueue();
+
+ //Make sure any remaining defQueue items get properly processed.
+ while (defQueue.length) {
+ args = defQueue.shift();
+ if (args[0] === null) {
+ return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1]));
+ } else {
+ //args are id, deps, factory. Should be normalized by the
+ //define() function.
+ callGetModule(args);
+ }
+ }
+ }
+
+ context = {
+ config: config,
+ contextName: contextName,
+ registry: registry,
+ defined: defined,
+ urlFetched: urlFetched,
+ defQueue: defQueue,
+ Module: Module,
+ makeModuleMap: makeModuleMap,
+ nextTick: req.nextTick,
+
+ /**
+ * Set a configuration for the context.
+ * @param {Object} cfg config object to integrate.
+ */
+ configure: function (cfg) {
+ //Make sure the baseUrl ends in a slash.
+ if (cfg.baseUrl) {
+ if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
+ cfg.baseUrl += '/';
+ }
+ }
+
+ //Save off the paths and packages since they require special processing,
+ //they are additive.
+ var pkgs = config.pkgs,
+ shim = config.shim,
+ objs = {
+ paths: true,
+ config: true,
+ map: true
+ };
+
+ eachProp(cfg, function (value, prop) {
+ if (objs[prop]) {
+ if (prop === 'map') {
+ mixin(config[prop], value, true, true);
+ } else {
+ mixin(config[prop], value, true);
+ }
+ } else {
+ config[prop] = value;
+ }
+ });
+
+ //Merge shim
+ if (cfg.shim) {
+ eachProp(cfg.shim, function (value, id) {
+ //Normalize the structure
+ if (isArray(value)) {
+ value = {
+ deps: value
+ };
+ }
+ if ((value.exports || value.init) && !value.exportsFn) {
+ value.exportsFn = context.makeShimExports(value);
+ }
+ shim[id] = value;
+ });
+ config.shim = shim;
+ }
+
+ //Adjust packages if necessary.
+ if (cfg.packages) {
+ each(cfg.packages, function (pkgObj) {
+ var location;
+
+ pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj;
+ location = pkgObj.location;
+
+ //Create a brand new object on pkgs, since currentPackages can
+ //be passed in again, and config.pkgs is the internal transformed
+ //state for all package configs.
+ pkgs[pkgObj.name] = {
+ name: pkgObj.name,
+ location: location || pkgObj.name,
+ //Remove leading dot in main, so main paths are normalized,
+ //and remove any trailing .js, since different package
+ //envs have different conventions: some use a module name,
+ //some use a file name.
+ main: (pkgObj.main || 'main')
+ .replace(currDirRegExp, '')
+ .replace(jsSuffixRegExp, '')
+ };
+ });
+
+ //Done with modifications, assing packages back to context config
+ config.pkgs = pkgs;
+ }
+
+ //If there are any "waiting to execute" modules in the registry,
+ //update the maps for them, since their info, like URLs to load,
+ //may have changed.
+ eachProp(registry, function (mod, id) {
+ //If module already has init called, since it is too
+ //late to modify them, and ignore unnormalized ones
+ //since they are transient.
+ if (!mod.inited && !mod.map.unnormalized) {
+ mod.map = makeModuleMap(id);
+ }
+ });
+
+ //If a deps array or a config callback is specified, then call
+ //require with those args. This is useful when require is defined as a
+ //config object before require.js is loaded.
+ if (cfg.deps || cfg.callback) {
+ context.require(cfg.deps || [], cfg.callback);
+ }
+ },
+
+ makeShimExports: function (value) {
+ function fn() {
+ var ret;
+ if (value.init) {
+ ret = value.init.apply(global, arguments);
+ }
+ return ret || (value.exports && getGlobal(value.exports));
+ }
+ return fn;
+ },
+
+ makeRequire: function (relMap, options) {
+ options = options || {};
+
+ function localRequire(deps, callback, errback) {
+ var id, map, requireMod;
+
+ if (options.enableBuildCallback && callback && isFunction(callback)) {
+ callback.__requireJsBuild = true;
+ }
+
+ if (typeof deps === 'string') {
+ if (isFunction(callback)) {
+ //Invalid call
+ return onError(makeError('requireargs', 'Invalid require call'), errback);
+ }
+
+ //If require|exports|module are requested, get the
+ //value for them from the special handlers. Caveat:
+ //this only works while module is being defined.
+ if (relMap && hasProp(handlers, deps)) {
+ return handlers[deps](registry[relMap.id]);
+ }
+
+ //Synchronous access to one module. If require.get is
+ //available (as in the Node adapter), prefer that.
+ if (req.get) {
+ return req.get(context, deps, relMap);
+ }
+
+ //Normalize module name, if it contains . or ..
+ map = makeModuleMap(deps, relMap, false, true);
+ id = map.id;
+
+ if (!hasProp(defined, id)) {
+ return onError(makeError('notloaded', 'Module name "' +
+ id +
+ '" has not been loaded yet for context: ' +
+ contextName +
+ (relMap ? '' : '. Use require([])')));
+ }
+ return defined[id];
+ }
+
+ //Grab defines waiting in the global queue.
+ intakeDefines();
+
+ //Mark all the dependencies as needing to be loaded.
+ context.nextTick(function () {
+ //Some defines could have been added since the
+ //require call, collect them.
+ intakeDefines();
+
+ requireMod = getModule(makeModuleMap(null, relMap));
+
+ //Store if map config should be applied to this require
+ //call for dependencies.
+ requireMod.skipMap = options.skipMap;
+
+ requireMod.init(deps, callback, errback, {
+ enabled: true
+ });
+
+ checkLoaded();
+ });
+
+ return localRequire;
+ }
+
+ mixin(localRequire, {
+ isBrowser: isBrowser,
+
+ /**
+ * Converts a module name + .extension into an URL path.
+ * *Requires* the use of a module name. It does not support using
+ * plain URLs like nameToUrl.
+ */
+ toUrl: function (moduleNamePlusExt) {
+ var ext, url,
+ index = moduleNamePlusExt.lastIndexOf('.'),
+ segment = moduleNamePlusExt.split('/')[0],
+ isRelative = segment === '.' || segment === '..';
+
+ //Have a file extension alias, and it is not the
+ //dots from a relative path.
+ if (index !== -1 && (!isRelative || index > 1)) {
+ ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length);
+ moduleNamePlusExt = moduleNamePlusExt.substring(0, index);
+ }
+
+ url = context.nameToUrl(normalize(moduleNamePlusExt,
+ relMap && relMap.id, true), ext || '.fake');
+ return ext ? url : url.substring(0, url.length - 5);
+ },
+
+ defined: function (id) {
+ return hasProp(defined, makeModuleMap(id, relMap, false, true).id);
+ },
+
+ specified: function (id) {
+ id = makeModuleMap(id, relMap, false, true).id;
+ return hasProp(defined, id) || hasProp(registry, id);
+ }
+ });
+
+ //Only allow undef on top level require calls
+ if (!relMap) {
+ localRequire.undef = function (id) {
+ //Bind any waiting define() calls to this context,
+ //fix for #408
+ takeGlobalQueue();
+
+ var map = makeModuleMap(id, relMap, true),
+ mod = getOwn(registry, id);
+
+ delete defined[id];
+ delete urlFetched[map.url];
+ delete undefEvents[id];
+
+ if (mod) {
+ //Hold on to listeners in case the
+ //module will be attempted to be reloaded
+ //using a different config.
+ if (mod.events.defined) {
+ undefEvents[id] = mod.events;
+ }
+
+ cleanRegistry(id);
+ }
+ };
+ }
+
+ return localRequire;
+ },
+
+ /**
+ * Called to enable a module if it is still in the registry
+ * awaiting enablement. A second arg, parent, the parent module,
+ * is passed in for context, when this method is overriden by
+ * the optimizer. Not shown here to keep code compact.
+ */
+ enable: function (depMap) {
+ var mod = getOwn(registry, depMap.id);
+ if (mod) {
+ getModule(depMap).enable();
+ }
+ },
+
+ /**
+ * Internal method used by environment adapters to complete a load event.
+ * A load event could be a script load or just a load pass from a synchronous
+ * load call.
+ * @param {String} moduleName the name of the module to potentially complete.
+ */
+ completeLoad: function (moduleName) {
+ var found, args, mod,
+ shim = getOwn(config.shim, moduleName) || {},
+ shExports = shim.exports;
+
+ takeGlobalQueue();
+
+ while (defQueue.length) {
+ args = defQueue.shift();
+ if (args[0] === null) {
+ args[0] = moduleName;
+ //If already found an anonymous module and bound it
+ //to this name, then this is some other anon module
+ //waiting for its completeLoad to fire.
+ if (found) {
+ break;
+ }
+ found = true;
+ } else if (args[0] === moduleName) {
+ //Found matching define call for this script!
+ found = true;
+ }
+
+ callGetModule(args);
+ }
+
+ //Do this after the cycle of callGetModule in case the result
+ //of those calls/init calls changes the registry.
+ mod = getOwn(registry, moduleName);
+
+ if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
+ if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
+ if (hasPathFallback(moduleName)) {
+ return;
+ } else {
+ return onError(makeError('nodefine',
+ 'No define call for ' + moduleName,
+ null,
+ [moduleName]));
+ }
+ } else {
+ //A script that does not call define(), so just simulate
+ //the call for it.
+ callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
+ }
+ }
+
+ checkLoaded();
+ },
+
+ /**
+ * Converts a module name to a file path. Supports cases where
+ * moduleName may actually be just an URL.
+ * Note that it **does not** call normalize on the moduleName,
+ * it is assumed to have already been normalized. This is an
+ * internal API, not a public one. Use toUrl for the public API.
+ */
+ nameToUrl: function (moduleName, ext) {
+ var paths, pkgs, pkg, pkgPath, syms, i, parentModule, url,
+ parentPath;
+
+ //If a colon is in the URL, it indicates a protocol is used and it is just
+ //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?)
+ //or ends with .js, then assume the user meant to use an url and not a module id.
+ //The slash is important for protocol-less URLs as well as full paths.
+ if (req.jsExtRegExp.test(moduleName)) {
+ //Just a plain path, not module name lookup, so just return it.
+ //Add extension if it is included. This is a bit wonky, only non-.js things pass
+ //an extension, this method probably needs to be reworked.
+ url = moduleName + (ext || '');
+ } else {
+ //A module that needs to be converted to a path.
+ paths = config.paths;
+ pkgs = config.pkgs;
+
+ syms = moduleName.split('/');
+ //For each module name segment, see if there is a path
+ //registered for it. Start with most specific name
+ //and work up from it.
+ for (i = syms.length; i > 0; i -= 1) {
+ parentModule = syms.slice(0, i).join('/');
+ pkg = getOwn(pkgs, parentModule);
+ parentPath = getOwn(paths, parentModule);
+ if (parentPath) {
+ //If an array, it means there are a few choices,
+ //Choose the one that is desired
+ if (isArray(parentPath)) {
+ parentPath = parentPath[0];
+ }
+ syms.splice(0, i, parentPath);
+ break;
+ } else if (pkg) {
+ //If module name is just the package name, then looking
+ //for the main module.
+ if (moduleName === pkg.name) {
+ pkgPath = pkg.location + '/' + pkg.main;
+ } else {
+ pkgPath = pkg.location;
+ }
+ syms.splice(0, i, pkgPath);
+ break;
+ }
+ }
+
+ //Join the path parts together, then figure out if baseUrl is needed.
+ url = syms.join('/');
+ url += (ext || (/\?/.test(url) ? '' : '.js'));
+ url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url;
+ }
+
+ return config.urlArgs ? url +
+ ((url.indexOf('?') === -1 ? '?' : '&') +
+ config.urlArgs) : url;
+ },
+
+ //Delegates to req.load. Broken out as a separate function to
+ //allow overriding in the optimizer.
+ load: function (id, url) {
+ req.load(context, id, url);
+ },
+
+ /**
+ * Executes a module callack function. Broken out as a separate function
+ * solely to allow the build system to sequence the files in the built
+ * layer in the right sequence.
+ *
+ * @private
+ */
+ execCb: function (name, callback, args, exports) {
+ return callback.apply(exports, args);
+ },
+
+ /**
+ * callback for script loads, used to check status of loading.
+ *
+ * @param {Event} evt the event from the browser for the script
+ * that was loaded.
+ */
+ onScriptLoad: function (evt) {
+ //Using currentTarget instead of target for Firefox 2.0's sake. Not
+ //all old browsers will be supported, but this one was easy enough
+ //to support and still makes sense.
+ if (evt.type === 'load' ||
+ (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
+ //Reset interactive script so a script node is not held onto for
+ //to long.
+ interactiveScript = null;
+
+ //Pull out the name of the module and the context.
+ var data = getScriptData(evt);
+ context.completeLoad(data.id);
+ }
+ },
+
+ /**
+ * Callback for script errors.
+ */
+ onScriptError: function (evt) {
+ var data = getScriptData(evt);
+ if (!hasPathFallback(data.id)) {
+ return onError(makeError('scripterror', 'Script error', evt, [data.id]));
+ }
+ }
+ };
+
+ context.require = context.makeRequire();
+ return context;
+ }
+
+ /**
+ * Main entry point.
+ *
+ * If the only argument to require is a string, then the module that
+ * is represented by that string is fetched for the appropriate context.
+ *
+ * If the first argument is an array, then it will be treated as an array
+ * of dependency string names to fetch. An optional function callback can
+ * be specified to execute when all of those dependencies are available.
+ *
+ * Make a local req variable to help Caja compliance (it assumes things
+ * on a require that are not standardized), and to give a short
+ * name for minification/local scope use.
+ */
+ req = requirejs = function (deps, callback, errback, optional) {
+
+ //Find the right context, use default
+ var context, config,
+ contextName = defContextName;
+
+ // Determine if have config object in the call.
+ if (!isArray(deps) && typeof deps !== 'string') {
+ // deps is a config object
+ config = deps;
+ if (isArray(callback)) {
+ // Adjust args if there are dependencies
+ deps = callback;
+ callback = errback;
+ errback = optional;
+ } else {
+ deps = [];
+ }
+ }
+
+ if (config && config.context) {
+ contextName = config.context;
+ }
+
+ context = getOwn(contexts, contextName);
+ if (!context) {
+ context = contexts[contextName] = req.s.newContext(contextName);
+ }
+
+ if (config) {
+ context.configure(config);
+ }
+
+ return context.require(deps, callback, errback);
+ };
+
+ /**
+ * Support require.config() to make it easier to cooperate with other
+ * AMD loaders on globally agreed names.
+ */
+ req.config = function (config) {
+ return req(config);
+ };
+
+ /**
+ * Execute something after the current tick
+ * of the event loop. Override for other envs
+ * that have a better solution than setTimeout.
+ * @param {Function} fn function to execute later.
+ */
+ req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
+ setTimeout(fn, 4);
+ } : function (fn) { fn(); };
+
+ /**
+ * Export require as a global, but only if it does not already exist.
+ */
+ if (!require) {
+ require = req;
+ }
+
+ req.version = version;
+
+ //Used to filter out dependencies that are already paths.
+ req.jsExtRegExp = /^\/|:|\?|\.js$/;
+ req.isBrowser = isBrowser;
+ s = req.s = {
+ contexts: contexts,
+ newContext: newContext
+ };
+
+ //Create default context.
+ req({});
+
+ //Exports some context-sensitive methods on global require.
+ each([
+ 'toUrl',
+ 'undef',
+ 'defined',
+ 'specified'
+ ], function (prop) {
+ //Reference from contexts instead of early binding to default context,
+ //so that during builds, the latest instance of the default context
+ //with its config gets used.
+ req[prop] = function () {
+ var ctx = contexts[defContextName];
+ return ctx.require[prop].apply(ctx, arguments);
+ };
+ });
+
+ if (isBrowser) {
+ head = s.head = document.getElementsByTagName('head')[0];
+ //If BASE tag is in play, using appendChild is a problem for IE6.
+ //When that browser dies, this can be removed. Details in this jQuery bug:
+ //http://dev.jquery.com/ticket/2709
+ baseElement = document.getElementsByTagName('base')[0];
+ if (baseElement) {
+ head = s.head = baseElement.parentNode;
+ }
+ }
+
+ /**
+ * Any errors that require explicitly generates will be passed to this
+ * function. Intercept/override it if you want custom error handling.
+ * @param {Error} err the error object.
+ */
+ req.onError = function (err) {
+ throw err;
+ };
+
+ /**
+ * Does the request to load a module for the browser case.
+ * Make this a separate function to allow other environments
+ * to override it.
+ *
+ * @param {Object} context the require context to find state.
+ * @param {String} moduleName the name of the module.
+ * @param {Object} url the URL to the module.
+ */
+ req.load = function (context, moduleName, url) {
+ var config = (context && context.config) || {},
+ node;
+ if (isBrowser) {
+ //In the browser so use a script tag
+ node = config.xhtml ?
+ document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
+ document.createElement('script');
+ node.type = config.scriptType || 'text/javascript';
+ node.charset = 'utf-8';
+ node.async = true;
+
+ node.setAttribute('data-requirecontext', context.contextName);
+ node.setAttribute('data-requiremodule', moduleName);
+
+ //Set up load listener. Test attachEvent first because IE9 has
+ //a subtle issue in its addEventListener and script onload firings
+ //that do not match the behavior of all other browsers with
+ //addEventListener support, which fire the onload event for a
+ //script right after the script execution. See:
+ //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
+ //UNFORTUNATELY Opera implements attachEvent but does not follow the script
+ //script execution mode.
+ if (node.attachEvent &&
+ //Check if node.attachEvent is artificially added by custom script or
+ //natively supported by browser
+ //read https://github.com/jrburke/requirejs/issues/187
+ //if we can NOT find [native code] then it must NOT natively supported.
+ //in IE8, node.attachEvent does not have toString()
+ //Note the test for "[native code" with no closing brace, see:
+ //https://github.com/jrburke/requirejs/issues/273
+ !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
+ !isOpera) {
+ //Probably IE. IE (at least 6-8) do not fire
+ //script onload right after executing the script, so
+ //we cannot tie the anonymous define call to a name.
+ //However, IE reports the script as being in 'interactive'
+ //readyState at the time of the define call.
+ useInteractive = true;
+
+ node.attachEvent('onreadystatechange', context.onScriptLoad);
+ //It would be great to add an error handler here to catch
+ //404s in IE9+. However, onreadystatechange will fire before
+ //the error handler, so that does not help. If addEvenListener
+ //is used, then IE will fire error before load, but we cannot
+ //use that pathway given the connect.microsoft.com issue
+ //mentioned above about not doing the 'script execute,
+ //then fire the script load event listener before execute
+ //next script' that other browsers do.
+ //Best hope: IE10 fixes the issues,
+ //and then destroys all installs of IE 6-9.
+ //node.attachEvent('onerror', context.onScriptError);
+ } else {
+ node.addEventListener('load', context.onScriptLoad, false);
+ node.addEventListener('error', context.onScriptError, false);
+ }
+ node.src = url;
+
+ //For some cache cases in IE 6-8, the script executes before the end
+ //of the appendChild execution, so to tie an anonymous define
+ //call to the module name (which is stored on the node), hold on
+ //to a reference to this node, but clear after the DOM insertion.
+ currentlyAddingScript = node;
+ if (baseElement) {
+ head.insertBefore(node, baseElement);
+ } else {
+ head.appendChild(node);
+ }
+ currentlyAddingScript = null;
+
+ return node;
+ } else if (isWebWorker) {
+ //In a web worker, use importScripts. This is not a very
+ //efficient use of importScripts, importScripts will block until
+ //its script is downloaded and evaluated. However, if web workers
+ //are in play, the expectation that a build has been done so that
+ //only one script needs to be loaded anyway. This may need to be
+ //reevaluated if other use cases become common.
+ importScripts(url);
+
+ //Account for anonymous modules
+ context.completeLoad(moduleName);
+ }
+ };
+
+ function getInteractiveScript() {
+ if (interactiveScript && interactiveScript.readyState === 'interactive') {
+ return interactiveScript;
+ }
+
+ eachReverse(scripts(), function (script) {
+ if (script.readyState === 'interactive') {
+ return (interactiveScript = script);
+ }
+ });
+ return interactiveScript;
+ }
+
+ //Look for a data-main script attribute, which could also adjust the baseUrl.
+ if (isBrowser) {
+ //Figure out baseUrl. Get it from the script tag with require.js in it.
+ eachReverse(scripts(), function (script) {
+ //Set the 'head' where we can append children by
+ //using the script's parent.
+ if (!head) {
+ head = script.parentNode;
+ }
+
+ //Look for a data-main attribute to set main script for the page
+ //to load. If it is there, the path to data main becomes the
+ //baseUrl, if it is not already set.
+ dataMain = script.getAttribute('data-main');
+ if (dataMain) {
+ //Set final baseUrl if there is not already an explicit one.
+ if (!cfg.baseUrl) {
+ //Pull off the directory of data-main for use as the
+ //baseUrl.
+ src = dataMain.split('/');
+ mainScript = src.pop();
+ subPath = src.length ? src.join('/') + '/' : './';
+
+ cfg.baseUrl = subPath;
+ dataMain = mainScript;
+ }
+
+ //Strip off any trailing .js since dataMain is now
+ //like a module name.
+ dataMain = dataMain.replace(jsSuffixRegExp, '');
+
+ //Put the data-main script in the files to load.
+ cfg.deps = cfg.deps ? cfg.deps.concat(dataMain) : [dataMain];
+
+ return true;
+ }
+ });
+ }
+
+ /**
+ * The function that handles definitions of modules. Differs from
+ * require() in that a string for the module should be the first argument,
+ * and the function to execute after dependencies are loaded should
+ * return a value to define the module corresponding to the first argument's
+ * name.
+ */
+ define = function (name, deps, callback) {
+ var node, context;
+
+ //Allow for anonymous modules
+ if (typeof name !== 'string') {
+ //Adjust args appropriately
+ callback = deps;
+ deps = name;
+ name = null;
+ }
+
+ //This module may not have dependencies
+ if (!isArray(deps)) {
+ callback = deps;
+ deps = [];
+ }
+
+ //If no name, and callback is a function, then figure out if it a
+ //CommonJS thing with dependencies.
+ if (!deps.length && isFunction(callback)) {
+ //Remove comments from the callback string,
+ //look for require calls, and pull them into the dependencies,
+ //but only if there are function args.
+ if (callback.length) {
+ callback
+ .toString()
+ .replace(commentRegExp, '')
+ .replace(cjsRequireRegExp, function (match, dep) {
+ deps.push(dep);
+ });
+
+ //May be a CommonJS thing even without require calls, but still
+ //could use exports, and module. Avoid doing exports and module
+ //work though if it just needs require.
+ //REQUIRES the function to expect the CommonJS variables in the
+ //order listed below.
+ deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
+ }
+ }
+
+ //If in IE 6-8 and hit an anonymous define() call, do the interactive
+ //work.
+ if (useInteractive) {
+ node = currentlyAddingScript || getInteractiveScript();
+ if (node) {
+ if (!name) {
+ name = node.getAttribute('data-requiremodule');
+ }
+ context = contexts[node.getAttribute('data-requirecontext')];
+ }
+ }
+
+ //Always save off evaluating the def call until the script onload handler.
+ //This allows multiple modules to be in a file without prematurely
+ //tracing dependencies, and allows for anonymous module support,
+ //where the module name is not known until the script onload event
+ //occurs. If no context, use the global queue, and get it processed
+ //in the onscript load callback.
+ (context ? context.defQueue : globalDefQueue).push([name, deps, callback]);
+ };
+
+ define.amd = {
+ jQuery: true
+ };
+
+
+ /**
+ * Executes the text. Normally just uses eval, but can be modified
+ * to use a better, environment-specific call. Only used for transpiling
+ * loader plugins, not for plain JS modules.
+ * @param {String} text the text to execute/evaluate.
+ */
+ req.exec = function (text) {
+ /*jslint evil: true */
+ return eval(text);
+ };
+
+ //Set up with config info.
+ req(cfg);
+}(this));
diff --git a/lib/sugar-web/LICENSE b/lib/sugar-web/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/lib/sugar-web/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/lib/sugar-web/README.md b/lib/sugar-web/README.md
new file mode 100644
index 0000000..de87a72
--- /dev/null
+++ b/lib/sugar-web/README.md
@@ -0,0 +1,7 @@
+Sugar Web
+=========
+
+These are the tools that a developer can use to make a web
+activity.
+
+For details see: http://developer.sugarlabs.org/
diff --git a/lib/sugar-web/activity/activity.js b/lib/sugar-web/activity/activity.js
new file mode 100644
index 0000000..9a407d9
--- /dev/null
+++ b/lib/sugar-web/activity/activity.js
@@ -0,0 +1,114 @@
+define(["webL10n",
+ "sugar-web/activity/shortcut",
+ "sugar-web/bus",
+ "sugar-web/env",
+ "sugar-web/datastore",
+ "sugar-web/graphics/icon",
+ "sugar-web/graphics/activitypalette"], function (
+ l10n, shortcut, bus, env, datastore, icon, activitypalette) {
+
+ var datastoreObject = null;
+
+ var activity = {};
+
+ activity.setup = function () {
+ bus.listen();
+
+ l10n.start();
+
+ function onPause() {
+ activity.write(function () {});
+ }
+
+ function onStop() {
+ function onDataStored(error, result) {
+ activity.close(function () {});
+ }
+ activity.write(onDataStored);
+ }
+
+ bus.onNotification("activity.pause", onPause);
+ bus.onNotification("activity.stop", onStop);
+
+ datastoreObject = new datastore.DatastoreObject();
+
+ var activityButton = document.getElementById("activity-button");
+
+ var activityPalette = new activitypalette.ActivityPalette(
+ activityButton, datastoreObject);
+
+ // Colorize the activity icon.
+ activity.getXOColor(function (error, colors) {
+ icon.colorize(activityButton, colors);
+ invokerElem =
+ document.querySelector("#activity-palette .palette-invoker");
+ icon.colorize(invokerElem, colors);
+ });
+
+ // Make the activity stop with the stop button.
+ var stopButton = document.getElementById("stop-button");
+ stopButton.addEventListener('click', onStop);
+
+ shortcut.add("Ctrl", "Q", this.close);
+
+ env.getEnvironment(function (error, environment) {
+ if (!environment.objectId) {
+ datastoreObject.setMetadata({
+ "title": environment.activityName + " Activity",
+ "title_set_by_user": "0",
+ "activity": environment.bundleId,
+ "activity_id": environment.activityId
+ });
+ }
+ datastoreObject.save(function () {
+ datastoreObject.getMetadata(function (error, metadata) {
+ activityPalette.setTitleDescription(metadata);
+ });
+ });
+ });
+ };
+
+ activity.getDatastoreObject = function () {
+ return datastoreObject;
+ };
+
+ activity.getXOColor = function (callback) {
+ function onResponseReceived(error, result) {
+ if (error === null) {
+ callback(null, {
+ stroke: result[0][0],
+ fill: result[0][1]
+ });
+ } else {
+ callback(null, {
+ stroke: "#00A0FF",
+ fill: "#8BFF7A"
+ });
+ }
+ }
+
+ bus.sendMessage("activity.get_xo_color", [], onResponseReceived);
+ };
+
+ // Activities should override this function in order to store
+ // data.
+ activity.write = function (callback) {
+ setTimeout(function () {
+ callback(null);
+ }, 0);
+ };
+
+ activity.close = function (callback) {
+ function onResponseReceived(error, result) {
+ if (error === null) {
+ callback(null);
+ } else {
+ console.log("activity.close called");
+ }
+ }
+
+ bus.sendMessage("activity.close", [], onResponseReceived);
+ };
+
+ return activity;
+});
diff --git a/lib/sugar-web/activity/shortcut.js b/lib/sugar-web/activity/shortcut.js
new file mode 100644
index 0000000..403af03
--- /dev/null
+++ b/lib/sugar-web/activity/shortcut.js
@@ -0,0 +1,57 @@
+define(function () {
+ var shortcut = {};
+
+ shortcut._allShortcuts = [];
+
+ shortcut.add = function (modifiersString, key, callback) {
+ // Parse the modifiers. For example "Ctrl+Alt" will become
+ // {'ctrlKey': true, 'altKey': true, 'shiftKey': false}
+ var modifiersList = modifiersString.toLowerCase().split("+");
+ var modifiers = {
+ 'ctrlKey': modifiersList.indexOf('ctrl') >= 0,
+ 'altKey': modifiersList.indexOf('alt') >= 0,
+ 'shiftKey': modifiersList.indexOf('shift') >= 0
+ };
+
+ this._allShortcuts.push({
+ 'modifiers': modifiers,
+ 'key': key.toLowerCase(),
+ 'callback': callback
+ });
+ };
+
+ document.onkeypress = function (e) {
+ e = e || window.event;
+
+ var modifiers = {
+ 'ctrlKey': e.ctrlKey,
+ 'altKey': e.altKey,
+ 'shiftKey': e.shiftKey
+ };
+
+ // Obtain the key
+ var charCode;
+ if (typeof e.which == "number") {
+ charCode = e.which;
+ } else {
+ charCode = e.keyCode;
+ }
+ var key = String.fromCharCode(charCode).toLowerCase();
+
+ // Search for a matching shortcut
+ for (i = 0; i < shortcut._allShortcuts.length; i += 1) {
+ var currentShortcut = shortcut._allShortcuts[i];
+
+ var match = currentShortcut.key == key &&
+ currentShortcut.modifiers.ctrlKey == modifiers.ctrlKey &&
+ currentShortcut.modifiers.altKey == modifiers.altKey &&
+ currentShortcut.modifiers.shiftKey == modifiers.shiftKey;
+ if (match) {
+ currentShortcut.callback();
+ return;
+ }
+ }
+ };
+
+ return shortcut;
+});
diff --git a/lib/sugar-web/bus.js b/lib/sugar-web/bus.js
new file mode 100644
index 0000000..79fe9dd
--- /dev/null
+++ b/lib/sugar-web/bus.js
@@ -0,0 +1,219 @@
+define(["sugar-web/env"], function (env) {
+ var lastId = 0;
+ var callbacks = {};
+ var notificationCallbacks = {};
+ var client = null;
+ var inputStreams = [];
+
+ function WebSocketClient(environment) {
+ this.queue = [];
+ this.socket = null;
+
+ var that = this;
+
+ env.getEnvironment(function (error, environment) {
+ var port = environment.apiSocketPort;
+ var socket = new WebSocket("ws://localhost:" + port);
+
+ socket.binaryType = "arraybuffer";
+
+ socket.onopen = function () {
+ var params = [environment.activityId,
+ environment.apiSocketKey];
+
+ socket.send(JSON.stringify({
+ "method": "authenticate",
+ "id": "authenticate",
+ "params": params
+ }));
+
+ while (that.queue.length > 0) {
+ socket.send(that.queue.shift());
+ }
+ };
+
+ socket.onmessage = function (message) {
+ that.onMessage(message);
+ };
+
+ that.socket = socket;
+ });
+ }
+
+ WebSocketClient.prototype.send = function (data) {
+ if (this.socket && this.socket.readyState == WebSocket.OPEN) {
+ this.socket.send(data);
+ } else {
+ this.queue.push(data);
+ }
+ };
+
+ WebSocketClient.prototype.close = function () {
+ this.socket.close();
+ };
+
+ var bus = {};
+
+ function InputStream() {
+ this.streamId = null;
+ this.readCallback = null;
+ }
+
+ InputStream.prototype.open = function (callback) {
+ var that = this;
+ bus.sendMessage("open_stream", [], function (error, result) {
+ that.streamId = result[0];
+ inputStreams[that.streamId] = that;
+ callback(error);
+ });
+ };
+
+ InputStream.prototype.read = function (count, callback) {
+ if (this.readCallback) {
+ throw Error("Read already in progress");
+ }
+
+ this.readCallback = callback;
+
+ var buffer = new ArrayBuffer(8);
+
+ var headerView = new Uint8Array(buffer, 0, 1);
+ headerView[0] = this.streamId;
+
+ var bodyView = new Uint32Array(buffer, 4, 1);
+ bodyView[0] = count;
+
+ bus.sendBinary(buffer);
+ };
+
+ InputStream.prototype.gotData = function (buffer) {
+ var callback = this.readCallback;
+
+ this.readCallback = null;
+
+ callback(null, buffer);
+ };
+
+ InputStream.prototype.close = function (callback) {
+ var that = this;
+
+ function onStreamClosed(error, result) {
+ if (callback) {
+ callback(error);
+ }
+ delete inputStreams[that.streamId];
+ }
+
+ bus.sendMessage("close_stream", [this.streamId], onStreamClosed);
+ };
+
+ function OutputStream() {
+ this.streamId = null;
+ }
+
+ OutputStream.prototype.open = function (callback) {
+ var that = this;
+ bus.sendMessage("open_stream", [], function (error, result) {
+ that.streamId = result[0];
+ callback(error);
+ });
+ };
+
+ OutputStream.prototype.write = function (data) {
+ var buffer = new ArrayBuffer(data.byteLength + 1);
+
+ var bufferView = new Uint8Array(buffer);
+ bufferView[0] = this.streamId;
+ bufferView.set(new Uint8Array(data), 1);
+
+ bus.sendBinary(buffer);
+ };
+
+ OutputStream.prototype.close = function (callback) {
+ bus.sendMessage("close_stream", [this.streamId], callback);
+ };
+
+ bus.createInputStream = function (callback) {
+ return new InputStream();
+ };
+
+ bus.createOutputStream = function (callback) {
+ return new OutputStream();
+ };
+
+ bus.sendMessage = function (method, params, callback) {
+ var message = {
+ "method": method,
+ "id": lastId,
+ "params": params
+ };
+
+ if (callback) {
+ callbacks[lastId] = callback;
+ }
+
+ client.send(JSON.stringify(message));
+
+ lastId++;
+ };
+
+ bus.onNotification = function (method, callback) {
+ notificationCallbacks[method] = callback;
+ };
+
+ bus.sendBinary = function (buffer, callback) {
+ client.send(buffer);
+ };
+
+ bus.listen = function (customClient) {
+ if (customClient) {
+ client = customClient;
+ } else {
+ client = new WebSocketClient();
+ }
+
+ client.onMessage = function (message) {
+ if (typeof message.data != "string") {
+ var dataView = new Uint8Array(message.data);
+ var streamId = dataView[0];
+
+ if (streamId in inputStreams) {
+ var inputStream = inputStreams[streamId];
+ inputStream.gotData(message.data.slice(1));
+ }
+
+ return;
+ }
+
+ var parsed = JSON.parse(message.data);
+ var responseId = parsed.id;
+
+ if (parsed.method) {
+ var notificationCallback = notificationCallbacks[parsed.method];
+ if (notificationCallback !== undefined) {
+ notificationCallback(parsed.params);
+ }
+ return;
+ }
+
+ if (responseId in callbacks) {
+ var callback = callbacks[responseId];
+
+ if (parsed.error === null) {
+ callback(null, parsed.result);
+ } else {
+ callback(new Error(parsed.error), null);
+ }
+
+ delete callbacks[responseId];
+ }
+ };
+ };
+
+ bus.close = function () {
+ client.close();
+ client = null;
+ };
+
+ return bus;
+});
diff --git a/lib/sugar-web/datastore.js b/lib/sugar-web/datastore.js
new file mode 100644
index 0000000..9291779
--- /dev/null
+++ b/lib/sugar-web/datastore.js
@@ -0,0 +1,221 @@
+define(["sugar-web/bus", "sugar-web/env"], function (bus, env) {
+ var datastore = {};
+
+ function DatastoreObject(objectId) {
+ this.objectId = objectId;
+ this.newMetadata = {};
+
+ this.ensureObjectId = function (callback) {
+ var that = this;
+
+ env.getEnvironment(function (error, environment) {
+ if (environment.objectId !== null) {
+ that.objectId = environment.objectId;
+ }
+ callback();
+ });
+ };
+
+ this.blobToText = function (blob, callback) {
+ var reader = new FileReader();
+ reader.onload = function (e) {
+ callback(e.target.result);
+ };
+ reader.readAsText(blob);
+ };
+
+ this.blobToArrayBuffer = function (blob, callback) {
+ var reader = new FileReader();
+ reader.onload = function (e) {
+ callback(e.target.result);
+ };
+ reader.readAsArrayBuffer(blob);
+ };
+
+ this.saveText = function (metadata, callback) {
+ var that = this;
+
+ function onSaved(error, outputStream) {
+ var blob = new Blob([that.newDataAsText]);
+
+ that.blobToArrayBuffer(blob, function (buffer) {
+ outputStream.write(buffer);
+ outputStream.close(callback);
+ });
+ }
+
+ datastore.save(this.objectId, metadata, onSaved);
+ };
+
+ this.applyChanges = function (metadata, callback) {
+ for (var key in this.newMetadata) {
+ metadata[key] = this.newMetadata[key];
+ }
+
+ if (this.newDataAsText !== undefined) {
+ this.saveText(metadata, callback);
+ } else {
+ datastore.setMetadata(this.objectId, metadata, callback);
+ }
+ };
+ }
+
+ DatastoreObject.prototype.getMetadata = function (callback) {
+ var that = this;
+
+ this.ensureObjectId(function () {
+ datastore.getMetadata(that.objectId, callback);
+ });
+ };
+
+ DatastoreObject.prototype.loadAsText = function (callback) {
+ var that = this;
+ var inputStream = null;
+ var arrayBuffers = [];
+ var metadata = null;
+
+ function onRead(error, data) {
+ if (data.byteLength === 0) {
+ var blob = new Blob(arrayBuffers);
+
+ that.blobToText(blob, function (text) {
+ callback(null, metadata, text);
+ });
+
+ inputStream.close();
+
+ return;
+ }
+
+ arrayBuffers.push(data);
+
+ inputStream.read(8192, onRead);
+ }
+
+ function onLoad(error, loadedMetadata, loadedInputStream) {
+ metadata = loadedMetadata;
+ inputStream = loadedInputStream;
+
+ inputStream.read(8192, onRead);
+ }
+
+ this.ensureObjectId(function () {
+ datastore.load(that.objectId, onLoad);
+ });
+ };
+
+ DatastoreObject.prototype.setMetadata = function (metadata) {
+ for (var key in metadata) {
+ this.newMetadata[key] = metadata[key];
+ }
+ };
+
+ DatastoreObject.prototype.setDataAsText = function (text) {
+ this.newDataAsText = text;
+ };
+
+ DatastoreObject.prototype.save = function (callback) {
+ if (callback === undefined) {
+ callback = function () {};
+ }
+
+ var that = this;
+
+ function onCreated(error, objectId) {
+ that.objectId = objectId;
+ that.applyChanges({}, callback);
+ }
+
+ function onGotMetadata(error, metadata) {
+ that.applyChanges(metadata, callback);
+ }
+
+ this.ensureObjectId(function () {
+ if (that.objectId === undefined) {
+ datastore.create(that.newMetadata, onCreated);
+ } else {
+ datastore.getMetadata(that.objectId, onGotMetadata);
+ }
+ });
+ };
+
+ datastore.DatastoreObject = DatastoreObject;
+
+
+ datastore.setMetadata = function (objectId, metadata, callback) {
+ function onResponseReceived(error, result) {
+ if (callback) {
+ if (error === null) {
+ callback(null);
+ } else {
+ callback(error);
+ }
+ }
+ }
+
+ var params = [objectId, metadata];
+ bus.sendMessage("datastore.set_metadata", params, onResponseReceived);
+ };
+
+ datastore.getMetadata = function (objectId, callback) {
+ function onResponseReceived(error, result) {
+ if (error === null) {
+ callback(null, result[0]);
+ } else {
+ callback(error, null);
+ }
+ }
+
+ var params = [objectId];
+ bus.sendMessage("datastore.get_metadata", params, onResponseReceived);
+ };
+
+ datastore.load = function (objectId, callback) {
+ inputStream = bus.createInputStream();
+
+ inputStream.open(function (error) {
+ function onResponseReceived(responseError, result) {
+ if (responseError === null) {
+ callback(null, result[0], inputStream);
+ } else {
+ callback(responseError, null, null);
+ }
+ }
+
+ var params = [objectId, inputStream.streamId];
+ bus.sendMessage("datastore.load", params, onResponseReceived);
+ });
+ };
+
+ datastore.create = function (metadata, callback) {
+ function onResponseReceived(responseError, result) {
+ if (responseError === null) {
+ callback(null, result[0]);
+ } else {
+ callback(responseError, null);
+ }
+ }
+
+ var params = [metadata];
+ bus.sendMessage("datastore.create", params, onResponseReceived);
+ };
+
+ datastore.save = function (objectId, metadata, callback) {
+ outputStream = bus.createOutputStream();
+
+ outputStream.open(function (error) {
+ function onResponseReceived(responseError, result) {
+ if (responseError === null) {
+ callback(null, outputStream);
+ } else {
+ callback(responseError, null);
+ }
+ }
+
+ var params = [objectId, metadata, outputStream.streamId];
+ bus.sendMessage("datastore.save", params, onResponseReceived);
+ });
+ };
+
+ return datastore;
+});
diff --git a/lib/sugar-web/env.js b/lib/sugar-web/env.js
new file mode 100644
index 0000000..1a948eb
--- /dev/null
+++ b/lib/sugar-web/env.js
@@ -0,0 +1,27 @@
+define(function () {
+
+ var env = {};
+
+ env.getEnvironment = function (callback) {
+ var sugar;
+
+ if (window.top.sugar) {
+ sugar = window.top.sugar;
+ } else {
+ sugar = {};
+ window.top.sugar = sugar;
+ }
+
+ if (sugar.environment) {
+ setTimeout(function () {
+ callback(null, sugar.environment);
+ }, 0);
+ } else {
+ sugar.onEnvironmentSet = function () {
+ callback(null, sugar.environment);
+ };
+ }
+ };
+
+ return env;
+});
diff --git a/lib/sugar-web/graphics/README.md b/lib/sugar-web/graphics/README.md
new file mode 100644
index 0000000..e3b7091
--- /dev/null
+++ b/lib/sugar-web/graphics/README.md
@@ -0,0 +1,39 @@
+Sugar Graphics
+==============
+
+Sugar widgets and graphics, implementing the [Sugar Interface
+Guidelines](http://wiki.sugarlabs.org/go/Human_Interface_Guidelines).
+
+Modifying the CSS
+-----------------
+
+We use [LESS](http://lesscss.org) and then compile the CSS. This is
+to be able to use calculations and variables for colors and measures.
+And to be able to output different CSS files for different screen
+resolutions, which is planned.
+
+To compile the CSS do:
+
+ lessc graphics/css/sugar.less graphics/css/sugar.css
+
+Be sure to compile it before commit.
+
+The grid helper
+---------------
+
+The grid is a visual debug tool that allows you to check if the
+elements have the correct size. To activate the grid, open the
+inspector console and paste the following.
+
+If RequireJS is not in the page head (ie. in the sugar-web-samples),
+load it:
+
+ var script = document.createElement('script');
+ script.src = 'lib/require.js';
+ document.head.appendChild(script);
+
+ requirejs.config({ baseUrl: "lib" });
+
+Then do:
+
+ require(["sugar-web/graphics/grid"], function (grid) { grid.addGrid(11) });
diff --git a/lib/sugar-web/graphics/activitypalette.js b/lib/sugar-web/graphics/activitypalette.js
new file mode 100644
index 0000000..aca036e
--- /dev/null
+++ b/lib/sugar-web/graphics/activitypalette.js
@@ -0,0 +1,70 @@
+define(["sugar-web/graphics/palette"], function (palette) {
+
+ var activitypalette = {};
+
+ activitypalette.ActivityPalette = function (activityButton,
+ datastoreObject) {
+
+ palette.Palette.call(this, activityButton);
+
+ var activityTitle;
+ var descriptionLabel;
+ var descriptionBox;
+
+ this.getPalette().id = "activity-palette";
+
+ this.template =
+ '<div class="row">' +
+ '<input type="text" id="title" class="expand">' +
+ '</div>' +
+ '<div class="row small">' +
+ '<label>Description:</label>' +
+ '</div>' +
+ '<div class="row expand">' +
+ '<textarea rows="8" id="description" class="expand"></textarea>' +
+ '</div>';
+
+ var containerElem = document.createElement('div');
+ containerElem.innerHTML = this.template;
+ this.setContent([containerElem]);
+
+ this.titleElem = containerElem.querySelector('#title');
+ this.descriptionElem = containerElem.querySelector('#description');
+
+ this.titleElem.onblur = function () {
+ datastoreObject.setMetadata({
+ "title": this.value,
+ "title_set_by_user": "1"
+ });
+ datastoreObject.save();
+ };
+
+ this.descriptionElem.onblur = function () {
+ datastoreObject.setMetadata({
+ "description": this.value
+ });
+ datastoreObject.save();
+ };
+ };
+
+ // Fill the text inputs with the received metadata.
+ var setTitleDescription = function (metadata) {
+ this.titleElem.value = metadata.title;
+
+ if (metadata.description !== undefined) {
+ this.descriptionElem.value = metadata.description;
+ }
+ };
+
+ activitypalette.ActivityPalette.prototype =
+ Object.create(palette.Palette.prototype, {
+ setTitleDescription: {
+ value: setTitleDescription,
+ enumerable: true,
+ configurable: true,
+ writable: true
+ }
+ });
+
+ return activitypalette;
+});
diff --git a/lib/sugar-web/graphics/css/sugar.css b/lib/sugar-web/graphics/css/sugar.css
new file mode 100644
index 0000000..cc1419d
--- /dev/null
+++ b/lib/sugar-web/graphics/css/sugar.css
@@ -0,0 +1,456 @@
+* {
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+html {
+ height: 100%;
+}
+body {
+ margin: 0;
+ height: 100%;
+ background-color: #c0c0c0;
+ position: relative;
+ font-family: sans-serif;
+ font-size: 10pt;
+}
+.unselectable {
+ -moz-user-select: none;
+ -webkit-user-select: none;
+}
+.pull-right {
+ float: right;
+}
+a {
+ color: #0076c3;
+ text-decoration: none;
+}
+/* Toolbar */
+.toolbar {
+ color: white;
+ background-color: #282828;
+ padding: 0 57px;
+ height: 55px;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+}
+/* Toolbar separator */
+.toolbar hr {
+ display: inline-block;
+ height: 33px;
+ border: 1px solid #808080;
+ margin: 2.5px;
+ margin-bottom: -17px;
+}
+/* Toolbar toolbutton */
+.toolbar .toolbutton {
+ background-color: transparent;
+ background-position: center;
+ background-size: contain;
+ background-repeat: no-repeat;
+ color: white;
+ color: transparent;
+ border: 0;
+ border-radius: 5.5px;
+ margin: 4px 2px;
+ width: 47px;
+ height: 47px;
+ position: relative;
+}
+.toolbar .toolbutton:hover {
+ background-color: black;
+}
+.toolbar .toolbutton:active,
+.toolbar .toolbutton.active {
+ background-color: #808080;
+}
+.toolbar .toolbutton img {
+ width: 100%;
+ height: 100%;
+}
+.toolbar .toolbutton:before {
+ content: "";
+ background-color: transparent;
+ position: absolute;
+ display: block;
+ width: 55px;
+ height: 11px;
+ bottom: -4px;
+ right: -4px;
+}
+.toolbar .toolbutton.invoker:before {
+ background-image: url('../icons/emblems/arrow-down.svg');
+}
+.toolbar #stop-button {
+ background-image: url('../icons/actions/activity-stop.svg');
+}
+/* Canvas */
+#canvas {
+ color: black;
+ background-color: #c0c0c0;
+ position: absolute;
+ bottom: 0;
+ top: 55px;
+ overflow-y: auto;
+ width: 100%;
+}
+/* Button */
+button {
+ background-color: #808080;
+ color: white;
+ border: 2px solid transparent;
+ border-radius: 22px;
+ line-height: 22px;
+ padding: 2px 4px;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+}
+button:hover {
+ background-color: #a2a2a2;
+ border-color: #a2a2a2;
+}
+button:active {
+ background-color: white;
+ color: black;
+ border-color: #808080;
+}
+button:focus {
+ border-color: white;
+}
+.toolbar button {
+ margin-top: 12.5px;
+}
+/* Button with icon */
+button.icon {
+ position: relative;
+ padding-left: 26px;
+}
+button.icon span.ok {
+ background-image: url(../icons/actions/dialog-ok.svg);
+}
+button.icon:active span.ok {
+ background-image: url(../icons/actions/dialog-ok-active.svg);
+}
+button.icon span.cancel {
+ background-image: url(../icons/actions/dialog-cancel.svg);
+}
+button.icon:active span.cancel {
+ background-image: url(../icons/actions/dialog-cancel-active.svg);
+}
+button.icon span {
+ display: inline-block;
+ width: 22px;
+ height: 22px;
+ background-color: transparent;
+ background-position: center;
+ background-size: 22px 22px;
+ background-repeat: no-repeat;
+ position: absolute;
+}
+button.icon span {
+ top: 2px;
+ left: 2px;
+}
+/* One line text input */
+input[type='text'] {
+ background-color: #e5e5e5;
+ border: 2px solid #e5e5e5;
+ border-radius: 22px;
+ padding: 4px;
+ width: 165px;
+ line-height: 22px;
+ outline: 0;
+}
+input[type='text']:focus {
+ background-color: white;
+}
+input[type='text']:disabled {
+ border-color: #808080;
+ background-color: #808080;
+}
+.toolbar input[type='text'],
+.palette .row input[type='text'] {
+ margin-top: 10.5px;
+}
+.palette .row input[type='text']:nth-last-child(1) {
+ margin-top: 8.5px;
+}
+input[type='text'].expand {
+ width: calc(100% - 12px);
+}
+/* One line text input with buttons inside */
+.icon-input {
+ display: inline-block;
+ position: relative;
+}
+.icon-input input[type='text'] {
+ width: 135px;
+ padding-right: 34px;
+}
+.icon-input.expand {
+ width: 100%;
+}
+.icon-input.expand input[type='text'] {
+ width: calc(100% - 42px);
+}
+.icon-input button {
+ width: 34px;
+ height: 34px;
+ padding: 0;
+ position: absolute;
+ background-size: 22px 22px;
+}
+.icon-input button.right {
+ margin: 0 0 0 -34px;
+ border-radius: 0 22px 22px 0;
+ right: 0;
+}
+.icon-input button {
+ background-color: transparent;
+ background-position: center;
+ background-repeat: no-repeat;
+ border: 0;
+}
+.icon-input button {
+ top: 2px;
+}
+.toolbar .icon-input button:hover {
+ background-color: transparent;
+}
+.toolbar .icon-input button {
+ top: 10.5px;
+}
+button.cancel {
+ background-image: url(../icons/actions/entry-cancel.svg);
+}
+button.cancel:active {
+ background-image: url(../icons/actions/entry-cancel-active.svg);
+}
+button.cancel:disabled {
+ background-image: url(../icons/actions/entry-cancel-disabled.svg);
+}
+/* Slider */
+/* FIXME this is not fully Sugarized yet */
+input[type='range'] {
+ -webkit-appearance: none !important;
+ background-color: #808080;
+ border-radius: 22px;
+ height: 11px;
+ cursor: pointer;
+}
+input[type='range']::-webkit-slider-thumb {
+ -webkit-appearance: none !important;
+ background-color: #c0c0c0;
+ border: 4px solid #808080;
+ border-radius: 11px;
+ height: 22px;
+ width: 22px;
+}
+input[type='range']::-webkit-slider-thumb:hover {
+ background-color: #e2e2e2;
+ border-color: #a2a2a2;
+}
+.toolbar input[type='range'] {
+ margin-top: 22px;
+}
+/* Label */
+label {
+ -moz-user-select: none;
+ -webkit-user-select: none;
+}
+/* Palette */
+.palette {
+ color: white;
+ background-color: transparent;
+ position: absolute;
+ pointer-events: none;
+}
+.palette-invoker {
+ width: 51px;
+ height: 53px;
+ background-color: black;
+ border: 2px solid #808080;
+ border-bottom: 0;
+ background-position: 2px 2px;
+ background-size: 47px;
+ background-repeat: no-repeat;
+}
+.palette-invoker:after {
+ content: "";
+ background-color: transparent;
+ position: absolute;
+ display: block;
+ width: 55px;
+ height: 11px;
+ top: 44px;
+ left: 0;
+ background-image: url('../icons/emblems/arrow-up.svg');
+}
+.palette-invoker:before {
+ content: "";
+ background-color: black;
+ position: absolute;
+ display: block;
+ top: 55px;
+ width: 51px;
+ left: 2px;
+ height: 2px;
+}
+.palette .wrapper {
+ background-color: black;
+ border: 2px solid #808080;
+ max-width: 271px;
+ min-width: 161px;
+ min-height: 51px;
+ pointer-events: auto;
+}
+.palette .header {
+ height: 51px;
+ line-height: 51px;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ margin: 0 5.5px;
+ font-weight: bold;
+}
+.palette hr {
+ border: 1px solid #808080;
+}
+.palette hr.header-separator {
+ margin-top: 0;
+}
+.palette .row {
+ height: 55px;
+ margin: 0 5.5px;
+}
+.palette .row:nth-last-child(1) {
+ height: 51px;
+}
+.palette .row.small {
+ height: 22px;
+}
+.palette .row.expand {
+ height: auto;
+}
+/* Palette menu */
+.palette .menu {
+ list-style-type: none;
+ padding: 0;
+ margin-top: 11px;
+ margin-bottom: 11px;
+}
+.palette .menu button {
+ background-color: transparent;
+ border-radius: 0;
+ border: 0;
+ width: 100%;
+ text-align: left;
+ line-height: 33px;
+ padding-top: 0;
+ padding-bottom: 0;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+.palette .menu button:hover {
+ background-color: #808080;
+ color: white;
+}
+.palette .menu button.icon {
+ padding-left: 37px;
+}
+.palette .menu button.icon span {
+ top: 0;
+ left: 0;
+ width: 33px;
+ height: 33px;
+}
+/* Scrollbar */
+::-webkit-scrollbar {
+ background-color: #808080;
+ width: 11px;
+}
+::-webkit-scrollbar-thumb {
+ background-color: white;
+ border: 2px solid #dddddd;
+ border-radius: 11px;
+}
+/* Grid for visual debugging and layout */
+.grid {
+ background-color: transparent;
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+/* Checkbox and radio */
+input[type='checkbox'],
+input[type='radio'] {
+ background-position: center;
+ background-size: contain;
+ width: 22px;
+ height: 22px;
+ margin: 2px 2px 4px 2px;
+ vertical-align: middle;
+ -moz-appearance: none;
+ appearance: none;
+ -webkit-appearance: none;
+}
+.toolbar input[type='checkbox'],
+.toolbar input[type='radio'] {
+ margin-top: 14.5px;
+ margin-bottom: 18.5px;
+}
+input[type='checkbox'] {
+ background-image: url(../icons/actions/checkbox-unchecked.svg);
+}
+input[type='checkbox']:active {
+ background-image: url(../icons/actions/checkbox-checked-selected.svg);
+}
+input[type='checkbox']:checked:active {
+ background-image: url(../icons/actions/checkbox-checked-selected.svg);
+}
+input[type='checkbox']:checked {
+ background-image: url(../icons/actions/checkbox-checked.svg);
+}
+input[type='radio'] {
+ background-image: url(../icons/actions/radio.svg);
+}
+input[type='radio']:active {
+ background-image: url(../icons/actions/radio-selected.svg);
+}
+input[type='radio']:checked:active {
+ background-image: url(../icons/actions/radio-active-selected.svg);
+}
+input[type='radio']:checked {
+ background-image: url(../icons/actions/radio-active.svg);
+}
+/* Textarea */
+textarea {
+ border: 2px solid #808080;
+ margin: 2px;
+}
+textarea.expand {
+ width: calc(100% - 12px);
+}
+/* Lists */
+ul.flat-list {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+ul.flat-list li {
+ border-bottom: 2px dotted #c0c0c0;
+ background-color: white;
+ height: 31px;
+ line-height: 31px;
+}
+ul.flat-list li:nth-last-child(1) {
+ border-bottom: none;
+}
+ul.flat-list.big li {
+ height: 42px;
+ line-height: 42px;
+}
+ul.flat-list.striped li:nth-child(odd) {
+ background-color: #e5e5e5;
+}
+/* ActivityPalette */
+#activity-palette .wrapper {
+ width: 271px;
+}
diff --git a/lib/sugar-web/graphics/css/sugar.less b/lib/sugar-web/graphics/css/sugar.less
new file mode 100644
index 0000000..8fa9766
--- /dev/null
+++ b/lib/sugar-web/graphics/css/sugar.less
@@ -0,0 +1,589 @@
+@toolbar-grey: #282828;
+@button-grey: #808080;
+@panel-grey: #C0C0C0;
+@text-field-grey: #E5E5E5;
+@link-blue: #0076C3;
+
+@line-width: 2px;
+@subcell-size: 11px;
+@cell-size: 5 * @subcell-size;
+@font-size: 10pt;
+
+@toolbar-height: @cell-size;
+@icon-small-size: 2 * @subcell-size;
+
+@toolbutton-size: @toolbar-height - (4 * @line-width);
+@toolbar-button-margin-top: (@toolbar-height / 2) - (2 * @line-width) -
+ (@icon-small-size / 2);
+
+@input-text-padding: (2 * @line-width);
+@input-text-line-height: 2 * @subcell-size;
+@input-text-width: 3 * @cell-size;
+@toolbar-input-height: (@toolbar-height / 2) - (@input-text-line-height / 2) -
+ (@input-text-padding + @line-width);
+
+@button-small-size: @input-text-line-height + (2 * @input-text-padding) +
+ (2 * @line-width);
+
+* {
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+html {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ height: 100%;
+ background-color: @panel-grey;
+ position: relative;
+ font-family: sans-serif;
+ font-size: @font-size;
+}
+
+.unselectable {
+ -moz-user-select: none;
+ -webkit-user-select: none;
+}
+
+.pull-right {
+ float: right;
+}
+
+a {
+ color: @link-blue;
+ text-decoration: none;
+}
+
+/* Toolbar */
+
+.toolbar {
+ color: white;
+ background-color: @toolbar-grey;
+ padding: 0 @toolbutton-size + (5 * @line-width);
+ height: @toolbar-height;
+ .unselectable;
+}
+
+/* Toolbar separator */
+
+.toolbar hr {
+ display: inline-block;
+ height: 3 * @subcell-size;
+ border: (@line-width / 2) solid @button-grey;
+ margin: (@subcell-size - (@line-width * 3)) / 2;
+ margin-bottom: -1 * (@subcell-size + (@line-width * 3));
+}
+
+/* Toolbar toolbutton */
+
+.toolbar .toolbutton {
+ background-color: transparent;
+ background-position: center;
+ background-size: contain;
+ background-repeat: no-repeat;
+ color: white;
+ color: transparent;
+ border: 0;
+ border-radius: @subcell-size / 2;
+ margin: (2 * @line-width) @line-width;
+ width: @toolbutton-size;
+ height: @toolbutton-size;
+ position: relative;
+}
+
+.toolbar .toolbutton:hover {
+ background-color: black;
+}
+
+.toolbar .toolbutton:active, .toolbar .toolbutton.active {
+ background-color: @button-grey;
+}
+
+.toolbar .toolbutton img {
+ width: 100%;
+ height: 100%;
+}
+
+.toolbar .toolbutton:before {
+ content: "";
+ background-color: transparent;
+ position: absolute;
+ display: block;
+ width: @cell-size;
+ height: @subcell-size;
+ bottom: -2 * @line-width;
+ right: -2 * @line-width;
+}
+
+.toolbar .toolbutton.invoker:before {
+ background-image: url('../icons/emblems/arrow-down.svg');
+}
+
+.toolbar #stop-button {
+ background-image: url('../icons/actions/activity-stop.svg');
+}
+
+/* Canvas */
+
+#canvas {
+ color: black;
+ background-color: @panel-grey;
+ position: absolute;
+ bottom: 0;
+ top: @toolbar-height;
+ overflow-y: auto;
+ width: 100%;
+}
+
+/* Button */
+
+button {
+ background-color: @button-grey;
+ color: white;
+ border: @line-width solid transparent;
+ border-radius: 2 * @subcell-size;
+ line-height: @icon-small-size;
+ padding: @line-width (2 * @line-width);
+ .unselectable;
+}
+
+button:hover {
+ background-color: @button-grey + #222;
+ border-color: @button-grey + #222;
+}
+
+button:active {
+ background-color: white;
+ color: black;
+ border-color: @button-grey;
+}
+
+button:focus {
+ border-color: white;
+}
+
+.toolbar button {
+ margin-top: @toolbar-button-margin-top;
+}
+
+/* Button with icon */
+
+button.icon {
+ position: relative;
+ padding-left: @icon-small-size + (2 * @line-width);
+}
+
+button.icon span.ok {
+ background-image: url(../icons/actions/dialog-ok.svg);
+}
+
+button.icon:active span.ok {
+ background-image: url(../icons/actions/dialog-ok-active.svg);
+}
+
+button.icon span.cancel {
+ background-image: url(../icons/actions/dialog-cancel.svg);
+}
+
+button.icon:active span.cancel {
+ background-image: url(../icons/actions/dialog-cancel-active.svg);
+}
+
+button.icon span {
+ display: inline-block;
+ width: @icon-small-size;
+ height: @icon-small-size;
+ background-color: transparent;
+ background-position: center;
+ background-size: @icon-small-size @icon-small-size;
+ background-repeat: no-repeat;
+ position: absolute;
+}
+
+button.icon span {
+ top: @line-width;
+ left: @line-width;
+}
+
+/* One line text input */
+
+input[type='text'] {
+ background-color: @text-field-grey;
+ border: @line-width solid @text-field-grey;
+ border-radius: 2 * @subcell-size;
+ padding: @input-text-padding;
+ width: @input-text-width;
+ line-height: @input-text-line-height;
+ outline: 0;
+}
+
+input[type='text']:focus {
+ background-color: white;
+}
+
+input[type='text']:disabled {
+ border-color: @button-grey;
+ background-color: @button-grey;
+}
+
+.toolbar input[type='text'], .palette .row input[type='text'] {
+ margin-top: @toolbar-input-height;
+}
+
+.palette .row input[type='text']:nth-last-child(1) {
+ margin-top: @toolbar-input-height - @line-width;
+}
+
+input[type='text'].expand {
+ width: calc(~"100%" - (2 * @input-text-padding + 2 * @line-width));
+}
+
+/* One line text input with buttons inside */
+
+.icon-input {
+ display: inline-block;
+ position: relative;
+}
+
+.icon-input input[type='text'] {
+ width: @input-text-width + @input-text-padding - @button-small-size;
+ padding-right: @button-small-size;
+}
+
+.icon-input.expand {
+ width: 100%;
+}
+
+.icon-input.expand input[type='text'] {
+ width: calc(~"100%" - (@input-text-padding + 2 * @line-width +
+ @button-small-size));
+}
+
+.icon-input button {
+ width: @button-small-size;
+ height: @button-small-size;
+ padding: 0;
+ position: absolute;
+ background-size: @icon-small-size @icon-small-size;
+}
+
+.icon-input button.right {
+ margin: 0 0 0 -@button-small-size;
+ border-radius: 0 (2 * @subcell-size) (2 * @subcell-size) 0;
+ right: 0;
+}
+
+.icon-input button {
+ background-color: transparent;
+ background-position: center;
+ background-repeat: no-repeat;
+ border: 0;
+}
+
+.icon-input button {
+ top: @line-width;
+}
+
+.toolbar .icon-input button:hover {
+ background-color: transparent;
+}
+
+.toolbar .icon-input button {
+ top: @toolbar-input-height;
+}
+
+button.cancel {
+ background-image: url(../icons/actions/entry-cancel.svg);
+}
+
+button.cancel:active {
+ background-image: url(../icons/actions/entry-cancel-active.svg);
+}
+
+button.cancel:disabled {
+ background-image: url(../icons/actions/entry-cancel-disabled.svg);
+}
+
+/* Slider */
+/* FIXME this is not fully Sugarized yet */
+
+input[type='range'] {
+ -webkit-appearance: none !important;
+ background-color: @button-grey;
+ border-radius: 2 * @subcell-size;
+ height: @subcell-size;
+ cursor: pointer;
+}
+
+input[type='range']::-webkit-slider-thumb {
+ -webkit-appearance: none !important;
+ background-color: @panel-grey;
+ border: (2 * @line-width) solid @button-grey;
+ border-radius: @subcell-size;
+ height: @icon-small-size;
+ width: @icon-small-size;
+}
+
+input[type='range']::-webkit-slider-thumb:hover {
+ background-color: @panel-grey + #222;
+ border-color: @button-grey + #222;
+}
+
+.toolbar input[type='range'] {
+ margin-top: (@toolbar-height / 2) - (@subcell-size / 2);
+}
+
+/* Label */
+
+label {
+ .unselectable;
+}
+
+/* Palette */
+
+.palette {
+ color: white;
+ background-color: transparent;
+ position: absolute;
+ pointer-events: none;
+}
+
+.palette-invoker {
+ width: @cell-size - 2 * @line-width;
+ height: @cell-size - @line-width;
+ background-color: black;
+ border: @line-width solid @button-grey;
+ border-bottom: 0;
+ background-position: @line-width @line-width;
+ background-size: @toolbutton-size;
+ background-repeat: no-repeat;
+}
+
+.palette-invoker:after {
+ content: "";
+ background-color: transparent;
+ position: absolute;
+ display: block;
+ width: @cell-size;
+ height: @subcell-size;
+ top: 4 * @subcell-size;
+ left: 0;
+ background-image: url('../icons/emblems/arrow-up.svg');
+}
+
+.palette-invoker:before {
+ content: "";
+ background-color: black;
+ position: absolute;
+ display: block;
+ top: @cell-size;
+ width: @cell-size - 2 * @line-width;
+ left: @line-width;
+ height: @line-width;
+}
+
+.palette .wrapper {
+ background-color: black;
+ border: @line-width solid @button-grey;
+ max-width: 5 * @cell-size - 2 * @line-width;
+ min-width: 3 * @cell-size - 2 * @line-width;
+ min-height: @cell-size - 2 * @line-width;
+ pointer-events: auto;
+}
+
+.palette .header {
+ height: @cell-size - 2 * @line-width;
+ line-height: @cell-size - 2 * @line-width;
+ .unselectable;
+ margin: 0 (@subcell-size / 2);
+ font-weight: bold;
+}
+
+.palette hr {
+ border: (@line-width / 2) solid @button-grey;
+}
+
+.palette hr.header-separator {
+ margin-top: 0;
+}
+
+.palette .row {
+ height: @cell-size;
+ margin: 0 (@subcell-size / 2);
+}
+
+.palette .row:nth-last-child(1) {
+ height: @cell-size - 2 * @line-width;
+}
+
+.palette .row.small {
+ height: 2 * @subcell-size;
+}
+
+.palette .row.expand {
+ height: auto;
+}
+
+/* Palette menu */
+
+.palette .menu {
+ list-style-type: none;
+ padding: 0;
+ margin-top: @subcell-size;
+ margin-bottom: @subcell-size;
+}
+
+.palette .menu button {
+ background-color: transparent;
+ border-radius: 0;
+ border: 0;
+ width: 100%;
+ text-align: left;
+ line-height: 3 * @subcell-size;
+ padding-top: 0;
+ padding-bottom: 0;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.palette .menu button:hover {
+ background-color: @button-grey;
+ color: white;
+}
+
+.palette .menu button.icon {
+ padding-left: 3 * @subcell-size + 2 * @line-width;
+}
+
+.palette .menu button.icon span {
+ top: 0;
+ left: 0;
+ width: 3 * @subcell-size;
+ height: 3 * @subcell-size;
+}
+
+/* Scrollbar */
+
+::-webkit-scrollbar {
+ background-color: @button-grey;
+ width: @subcell-size;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: white;
+ border: @line-width solid #ddd;
+ border-radius: @subcell-size;
+}
+
+/* Grid for visual debugging and layout */
+
+.grid {
+ background-color: transparent;
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.appearance(@value) {
+ -moz-appearance: @value;
+ appearance: @value;
+ -webkit-appearance: @value;
+}
+
+/* Checkbox and radio */
+
+input[type='checkbox'],
+input[type='radio'] {
+ background-position: center;
+ background-size: contain;
+ width: @icon-small-size;
+ height: @icon-small-size;
+ margin: @line-width @line-width (2 * @line-width) @line-width;
+ vertical-align: middle;
+ .appearance(none);
+}
+
+.toolbar input[type='checkbox'],
+.toolbar input[type='radio'] {
+ margin-top: (@toolbar-height - @icon-small-size) / 2 - @line-width;
+ margin-bottom: (@toolbar-height - @icon-small-size) / 2 + @line-width;
+}
+
+input[type='checkbox'] {
+ background-image: url(../icons/actions/checkbox-unchecked.svg);
+}
+
+input[type='checkbox']:active {
+ background-image: url(../icons/actions/checkbox-checked-selected.svg);
+}
+
+input[type='checkbox']:checked:active {
+ background-image: url(../icons/actions/checkbox-checked-selected.svg);
+}
+
+input[type='checkbox']:checked {
+ background-image: url(../icons/actions/checkbox-checked.svg);
+}
+
+input[type='radio'] {
+ background-image: url(../icons/actions/radio.svg);
+}
+
+input[type='radio']:active {
+ background-image: url(../icons/actions/radio-selected.svg);
+}
+
+input[type='radio']:checked:active {
+ background-image: url(../icons/actions/radio-active-selected.svg);
+}
+
+input[type='radio']:checked {
+ background-image: url(../icons/actions/radio-active.svg);
+}
+
+/* Textarea */
+
+textarea {
+ border: @line-width solid @button-grey;
+ margin: @line-width;
+}
+
+textarea.expand {
+ width: calc(~"100%" - (6 * (@line-width)));
+}
+
+/* Lists */
+
+ul.flat-list {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+ul.flat-list li {
+ border-bottom: @line-width dotted @panel-grey;
+ background-color: white;
+ height: 3 * @subcell-size - @line-width;
+ line-height: 3 * @subcell-size - @line-width;
+}
+
+ul.flat-list li:nth-last-child(1) {
+ border-bottom: none;
+}
+
+ul.flat-list.big li {
+ height: 4 * @subcell-size - @line-width;
+ line-height: 4 * @subcell-size - @line-width;
+}
+
+ul.flat-list.striped li:nth-child(odd) {
+ background-color: @text-field-grey;
+}
+
+/* ActivityPalette */
+
+#activity-palette .wrapper {
+ width: 5 * @cell-size - 2 * @line-width;
+} \ No newline at end of file
diff --git a/lib/sugar-web/graphics/grid.js b/lib/sugar-web/graphics/grid.js
new file mode 100644
index 0000000..503713a
--- /dev/null
+++ b/lib/sugar-web/graphics/grid.js
@@ -0,0 +1,54 @@
+define(function () {
+ var grid = {};
+
+ // Add a grid overlay with lines spaced by subcellSize, for visual
+ // debugging. This is useful while doing the activity layout or
+ // while developing widgets.
+ grid.addGrid = function (subcellSize) {
+ var canvas = document.createElement('canvas');
+ canvas.className = "grid";
+ document.body.appendChild(canvas);
+
+ var updateGrid = function () {
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+
+ var ctx = canvas.getContext("2d");
+ ctx.strokeStyle = "#00FFFF";
+
+ var subcellsVertical = window.innerHeight / subcellSize;
+ for (i = 0; i < subcellsVertical; i++) {
+ if ((i + 1) % 5 === 0) {
+ ctx.lineWidth = 1;
+ } else {
+ ctx.lineWidth = 0.5;
+ }
+ ctx.beginPath();
+ ctx.moveTo(0, subcellSize * (i + 1));
+ ctx.lineTo(canvas.width, subcellSize * (i + 1));
+ ctx.stroke();
+ }
+
+ var subcellsHorizontal = window.innerWidth / subcellSize;
+ for (i = 0; i < subcellsHorizontal; i++) {
+ if ((i + 1) % 5 === 0) {
+ ctx.lineWidth = 1;
+ } else {
+ ctx.lineWidth = 0.5;
+ }
+ ctx.beginPath();
+ ctx.moveTo(subcellSize * (i + 1), 0);
+ ctx.lineTo(subcellSize * (i + 1), canvas.height);
+ ctx.stroke();
+ }
+ };
+
+ updateGrid();
+
+ window.onresize = function (event) {
+ updateGrid();
+ };
+ };
+
+ return grid;
+});
diff --git a/lib/sugar-web/graphics/icon.js b/lib/sugar-web/graphics/icon.js
new file mode 100644
index 0000000..f2c0791
--- /dev/null
+++ b/lib/sugar-web/graphics/icon.js
@@ -0,0 +1,81 @@
+define(function () {
+ var icon = {};
+
+ function changeColors(iconData, fillColor, strokeColor) {
+ var re;
+
+ if (fillColor) {
+ re = /(<!ENTITY fill_color ")(.*)(">)/;
+ iconData = iconData.replace(re, "$1" + fillColor + "$3");
+ }
+
+ if (strokeColor) {
+ re = /(<!ENTITY stroke_color ")(.*)(">)/;
+ iconData = iconData.replace(re, "$1" + strokeColor + "$3");
+ }
+
+ return iconData;
+ }
+
+ icon.load = function (iconInfo, callback) {
+ var source;
+ var dataHeader = "data:image/svg+xml,";
+
+ if ("uri" in iconInfo) {
+ source = iconInfo.uri;
+ } else if ("name" in iconInfo) {
+ source = "lib/graphics/icons/" + iconInfo.name + ".svg";
+ }
+
+ var fillColor = iconInfo.fillColor;
+ var strokeColor = iconInfo.strokeColor;
+
+ // If source is already a data uri, read it instead of doing
+ // the XMLHttpRequest
+ if (source.substring(0, 4) == 'data') {
+ var iconData = unescape(source.slice(dataHeader.length));
+ var newData = changeColors(iconData, fillColor, strokeColor);
+ callback(dataHeader + escape(newData));
+ return;
+ }
+
+ var client = new XMLHttpRequest();
+
+ client.onload = function () {
+ var iconData = this.responseText;
+ var newData = changeColors(iconData, fillColor, strokeColor);
+ callback(dataHeader + escape(newData));
+ };
+
+ client.open("GET", source);
+ client.send();
+ };
+
+ function getBackgroundURL(elem) {
+ var style = elem.currentStyle || window.getComputedStyle(elem, '');
+ // Remove prefix 'url(' and suffix ')' before return
+ return style.backgroundImage.slice(4, -1);
+ }
+
+ function setBackgroundURL(elem, url) {
+ elem.style.backgroundImage = "url('" + url + "')";
+ }
+
+ icon.colorize = function (elem, colors, callback) {
+ var iconInfo = {
+ "uri": getBackgroundURL(elem),
+ "strokeColor": colors.stroke,
+ "fillColor": colors.fill
+ };
+
+ icon.load(iconInfo, function (url) {
+ setBackgroundURL(elem, url);
+ if (callback) {
+ callback();
+ }
+ });
+
+ };
+
+ return icon;
+});
diff --git a/lib/sugar-web/graphics/icons/actions/activity-stop.svg b/lib/sugar-web/graphics/icons/actions/activity-stop.svg
new file mode 100644
index 0000000..11b82e8
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/activity-stop.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="activity-stop">
+ <path d="M36.822,5H18.181L5.002,18.182V36.82L18.181,50h18.642l13.176-13.18V18.182L36.822,5z M35.75,35.414h-15.5v-15.5h15.5V35.414z" display="inline" fill="&fill_color;"/>
+</g></svg> \ No newline at end of file
diff --git a/lib/sugar-web/graphics/icons/actions/checkbox-checked-selected.svg b/lib/sugar-web/graphics/icons/actions/checkbox-checked-selected.svg
new file mode 100644
index 0000000..8ec1223
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/checkbox-checked-selected.svg
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#E5E5E5">
+ <!ENTITY stroke_color "#010101">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="26"
+ height="26"
+ id="svg814">
+ <g
+ transform="translate(0,-1026.3622)"
+ id="layer1">
+ <rect
+ width="23.999523"
+ height="23.999525"
+ x="1"
+ y="1027.3627"
+ id="rect3268"
+ style="color:&stroke_color;;fill:&fill_color;;fill-opacity:1;stroke:#808080;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 5.6810941,1039.239 4.7885489,4.7885 9.330512,-9.3305"
+ id="path3438"
+ style="fill:none;stroke:&stroke_color;;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ </g>
+</svg>
diff --git a/lib/sugar-web/graphics/icons/actions/checkbox-checked.svg b/lib/sugar-web/graphics/icons/actions/checkbox-checked.svg
new file mode 100644
index 0000000..3cfce18
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/checkbox-checked.svg
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#FFFFFF">
+ <!ENTITY stroke_color "#010101">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="26"
+ height="26"
+ id="svg814">
+ <g
+ transform="translate(0,-1026.3622)"
+ id="layer1">
+ <rect
+ width="23.999523"
+ height="23.999525"
+ x="1"
+ y="1027.3627"
+ id="rect3268"
+ style="color:&stroke_color;;fill:&fill_color;;fill-opacity:1;stroke:#808080;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 5.6810941,1039.239 4.7885489,4.7885 9.330512,-9.3305"
+ id="path3438"
+ style="fill:none;stroke:&stroke_color;;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ </g>
+</svg>
diff --git a/lib/sugar-web/graphics/icons/actions/checkbox-unchecked-selected.svg b/lib/sugar-web/graphics/icons/actions/checkbox-unchecked-selected.svg
new file mode 100644
index 0000000..2263279
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/checkbox-unchecked-selected.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#E5E5E5">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="26"
+ height="26"
+ id="svg814">
+ <g
+ transform="translate(0,-1026.3622)"
+ id="layer1">
+ <rect
+ width="23.999523"
+ height="23.999525"
+ x="1"
+ y="1027.3627"
+ id="rect3268"
+ style="color:#000000;fill:&fill_color;;fill-opacity:1;stroke:#808080;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/lib/sugar-web/graphics/icons/actions/checkbox-unchecked.svg b/lib/sugar-web/graphics/icons/actions/checkbox-unchecked.svg
new file mode 100644
index 0000000..e158782
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/checkbox-unchecked.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#FFFFFF">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="26"
+ height="26"
+ id="svg814">
+ <g
+ transform="translate(0,-1026.3622)"
+ id="layer1">
+ <rect
+ width="23.999523"
+ height="23.999525"
+ x="1"
+ y="1027.3627"
+ id="rect3268"
+ style="color:#000000;fill:&fill_color;;stroke:#808080;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/lib/sugar-web/graphics/icons/actions/dialog-cancel-active.svg b/lib/sugar-web/graphics/icons/actions/dialog-cancel-active.svg
new file mode 100644
index 0000000..dc0d688
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/dialog-cancel-active.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#FFFFFF">
+ <!ENTITY fill_color "#808080">
+]><svg enable-background="new 0 0 55.125 55" height="55px" version="1.1" viewBox="0 0 55.125 55" width="55.125px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="dialog-cancel">
+ <path d="M27.557,5.053c-12.43,0-22.5,10.076-22.5,22.497c0,12.432,10.07,22.503,22.5,22.503 c12.431,0,22.5-10.071,22.5-22.503C50.057,15.129,39.987,5.053,27.557,5.053z M37.756,33.212c1.254,1.256,1.257,3.291,0,4.545 c-0.628,0.629-1.451,0.943-2.274,0.943c-0.822,0-1.644-0.314-2.27-0.94l-5.76-5.761l-5.76,5.761 c-0.627,0.626-1.449,0.94-2.271,0.94c-0.823,0-1.647-0.314-2.275-0.943c-1.254-1.254-1.254-3.289,0.004-4.545l5.758-5.758 l-5.758-5.758c-1.258-1.254-1.258-3.292-0.004-4.546c1.255-1.254,3.292-1.259,4.546,0l5.76,5.759l5.76-5.759 c1.252-1.259,3.288-1.254,4.544,0c1.257,1.254,1.254,3.292,0,4.546l-5.758,5.758L37.756,33.212z" display="inline" fill="&fill_color;"/>
+</g></svg>
diff --git a/lib/sugar-web/graphics/icons/actions/dialog-cancel.svg b/lib/sugar-web/graphics/icons/actions/dialog-cancel.svg
new file mode 100644
index 0000000..dab4ae2
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/dialog-cancel.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55.125 55" height="55px" version="1.1" viewBox="0 0 55.125 55" width="55.125px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="dialog-cancel">
+ <path d="M27.557,5.053c-12.43,0-22.5,10.076-22.5,22.497c0,12.432,10.07,22.503,22.5,22.503 c12.431,0,22.5-10.071,22.5-22.503C50.057,15.129,39.987,5.053,27.557,5.053z M37.756,33.212c1.254,1.256,1.257,3.291,0,4.545 c-0.628,0.629-1.451,0.943-2.274,0.943c-0.822,0-1.644-0.314-2.27-0.94l-5.76-5.761l-5.76,5.761 c-0.627,0.626-1.449,0.94-2.271,0.94c-0.823,0-1.647-0.314-2.275-0.943c-1.254-1.254-1.254-3.289,0.004-4.545l5.758-5.758 l-5.758-5.758c-1.258-1.254-1.258-3.292-0.004-4.546c1.255-1.254,3.292-1.259,4.546,0l5.76,5.759l5.76-5.759 c1.252-1.259,3.288-1.254,4.544,0c1.257,1.254,1.254,3.292,0,4.546l-5.758,5.758L37.756,33.212z" display="inline" fill="&fill_color;"/>
+</g></svg> \ No newline at end of file
diff --git a/lib/sugar-web/graphics/icons/actions/dialog-ok-active.svg b/lib/sugar-web/graphics/icons/actions/dialog-ok-active.svg
new file mode 100644
index 0000000..45de840
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/dialog-ok-active.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#FFFFFF">
+ <!ENTITY fill_color "#808080">
+]><svg enable-background="new 0 0 55.125 55" height="55px" version="1.1" viewBox="0 0 55.125 55" width="55.125px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="dialog-ok">
+ <path d="M27.498,5C15.071,5,5,15.071,5,27.498C5,39.923,15.071,50,27.498,50 C39.927,50,50,39.923,50,27.498C50,15.071,39.927,5,27.498,5z M41.419,19.886L24.706,40.208L13.789,29.29 c-1.258-1.254-1.258-3.292-0.003-4.546c1.254-1.255,3.292-1.255,4.547,0l5.908,5.904L36.452,15.8 c1.127-1.368,3.153-1.568,4.525-0.438C42.347,16.489,42.546,18.515,41.419,19.886z" display="inline" fill="&fill_color;"/>
+</g></svg>
diff --git a/lib/sugar-web/graphics/icons/actions/dialog-ok.svg b/lib/sugar-web/graphics/icons/actions/dialog-ok.svg
new file mode 100644
index 0000000..69e5a2a
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/dialog-ok.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55.125 55" height="55px" version="1.1" viewBox="0 0 55.125 55" width="55.125px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="dialog-ok">
+ <path d="M27.498,5C15.071,5,5,15.071,5,27.498C5,39.923,15.071,50,27.498,50 C39.927,50,50,39.923,50,27.498C50,15.071,39.927,5,27.498,5z M41.419,19.886L24.706,40.208L13.789,29.29 c-1.258-1.254-1.258-3.292-0.003-4.546c1.254-1.255,3.292-1.255,4.547,0l5.908,5.904L36.452,15.8 c1.127-1.368,3.153-1.568,4.525-0.438C42.347,16.489,42.546,18.515,41.419,19.886z" display="inline" fill="&fill_color;"/>
+</g></svg> \ No newline at end of file
diff --git a/lib/sugar-web/graphics/icons/actions/entry-cancel-active.svg b/lib/sugar-web/graphics/icons/actions/entry-cancel-active.svg
new file mode 100644
index 0000000..467509e
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/entry-cancel-active.svg
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#010101">
+ <!ENTITY stroke_color "#FFFFFF">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="22"
+ height="22"
+ viewBox="0 0 22 22"
+ id="svg5047"
+ xml:space="preserve"><g
+ transform="matrix(0.48888889,0,0,0.48888889,-2.4723111,-2.4703556)"
+ id="dialog-cancel"><path
+ d="m 37.756,33.212 c 1.254,1.256 1.257,3.291 0,4.545 -0.628,0.629 -1.451,0.943 -2.274,0.943 -0.822,0 -1.644,-0.314 -2.27,-0.94 l -5.76,-5.761 -5.76,5.761 c -0.627,0.626 -1.449,0.94 -2.271,0.94 -0.823,0 -1.647,-0.314 -2.275,-0.943 -1.254,-1.254 -1.254,-3.289 0.004,-4.545 l 5.758,-5.758 -5.758,-5.758 c -1.258,-1.254 -1.258,-3.292 -0.004,-4.546 1.255,-1.254 3.292,-1.259 4.546,0 l 5.76,5.759 5.76,-5.759 c 1.252,-1.259 3.288,-1.254 4.544,0 1.257,1.254 1.254,3.292 0,4.546 l -5.758,5.758 z"
+ id="path5050" /></g><g
+ transform="matrix(0.48888889,0,0,0.48888889,-2.4723111,-2.4703556)"
+ id="g3084"
+ style="fill:#ffffff;stroke-width:4.090909;stroke-miterlimit:4;stroke-dasharray:none"><path
+ d="m 27.557,5.053 c -12.43,0 -22.5,10.076 -22.5,22.497 0,12.432 10.07,22.503 22.5,22.503 12.431,0 22.5,-10.071 22.5,-22.503 0,-12.421 -10.07,-22.497 -22.5,-22.497 z m 10.199,28.159 c 1.254,1.256 1.257,3.291 0,4.545 -0.628,0.629 -1.451,0.943 -2.274,0.943 -0.822,0 -1.644,-0.314 -2.27,-0.94 l -5.76,-5.761 -5.76,5.761 c -0.627,0.626 -1.449,0.94 -2.271,0.94 -0.823,0 -1.647,-0.314 -2.275,-0.943 -1.254,-1.254 -1.254,-3.289 0.004,-4.545 l 5.758,-5.758 -5.758,-5.758 c -1.258,-1.254 -1.258,-3.292 -0.004,-4.546 1.255,-1.254 3.292,-1.259 4.546,0 l 5.76,5.759 5.76,-5.759 c 1.252,-1.259 3.288,-1.254 4.544,0 1.257,1.254 1.254,3.292 0,4.546 l -5.758,5.758 5.758,5.758 z"
+ id="path3086"
+ style="fill:#ffffff;stroke-width:4.090909;stroke-miterlimit:4;stroke-dasharray:none" /></g></svg>
diff --git a/lib/sugar-web/graphics/icons/actions/entry-cancel-disabled.svg b/lib/sugar-web/graphics/icons/actions/entry-cancel-disabled.svg
new file mode 100644
index 0000000..55b4cdb
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/entry-cancel-disabled.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#C0C0C0">
+ <!ENTITY stroke_color "#FFFFFF">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="22"
+ height="22"
+ viewBox="0 0 22 22"
+ id="svg5047"
+ xml:space="preserve"><g
+ transform="matrix(0.48888889,0,0,0.48888889,-2.4723111,-2.4703556)"
+ id="dialog-cancel"
+ style="fill:&fill_color;;display:block">
+ <path
+ d="m 27.557,5.053 c -12.43,0 -22.5,10.076 -22.5,22.497 0,12.432 10.07,22.503 22.5,22.503 12.431,0 22.5,-10.071 22.5,-22.503 0,-12.421 -10.07,-22.497 -22.5,-22.497 z m 10.199,28.159 c 1.254,1.256 1.257,3.291 0,4.545 -0.628,0.629 -1.451,0.943 -2.274,0.943 -0.822,0 -1.644,-0.314 -2.27,-0.94 l -5.76,-5.761 -5.76,5.761 c -0.627,0.626 -1.449,0.94 -2.271,0.94 -0.823,0 -1.647,-0.314 -2.275,-0.943 -1.254,-1.254 -1.254,-3.289 0.004,-4.545 l 5.758,-5.758 -5.758,-5.758 c -1.258,-1.254 -1.258,-3.292 -0.004,-4.546 1.255,-1.254 3.292,-1.259 4.546,0 l 5.76,5.759 5.76,-5.759 c 1.252,-1.259 3.288,-1.254 4.544,0 1.257,1.254 1.254,3.292 0,4.546 l -5.758,5.758 5.758,5.758 z"
+ id="path5050"
+ style="fill:&fill_color;;display:inline" />
+</g></svg>
diff --git a/lib/sugar-web/graphics/icons/actions/entry-cancel.svg b/lib/sugar-web/graphics/icons/actions/entry-cancel.svg
new file mode 100644
index 0000000..5339a7e
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/entry-cancel.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#010101">
+ <!ENTITY stroke_color "#FFFFFF">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="22"
+ height="22"
+ viewBox="0 0 22 22"
+ id="svg5047"
+ xml:space="preserve"><g
+ transform="matrix(0.48888889,0,0,0.48888889,-2.4723111,-2.4703556)"
+ id="dialog-cancel"
+ style="fill:&fill_color;;display:block">
+ <path
+ d="m 27.557,5.053 c -12.43,0 -22.5,10.076 -22.5,22.497 0,12.432 10.07,22.503 22.5,22.503 12.431,0 22.5,-10.071 22.5,-22.503 0,-12.421 -10.07,-22.497 -22.5,-22.497 z m 10.199,28.159 c 1.254,1.256 1.257,3.291 0,4.545 -0.628,0.629 -1.451,0.943 -2.274,0.943 -0.822,0 -1.644,-0.314 -2.27,-0.94 l -5.76,-5.761 -5.76,5.761 c -0.627,0.626 -1.449,0.94 -2.271,0.94 -0.823,0 -1.647,-0.314 -2.275,-0.943 -1.254,-1.254 -1.254,-3.289 0.004,-4.545 l 5.758,-5.758 -5.758,-5.758 c -1.258,-1.254 -1.258,-3.292 -0.004,-4.546 1.255,-1.254 3.292,-1.259 4.546,0 l 5.76,5.759 5.76,-5.759 c 1.252,-1.259 3.288,-1.254 4.544,0 1.257,1.254 1.254,3.292 0,4.546 l -5.758,5.758 5.758,5.758 z"
+ id="path5050"
+ style="fill:&fill_color;;display:inline" />
+</g></svg>
diff --git a/lib/sugar-web/graphics/icons/actions/radio-active-selected.svg b/lib/sugar-web/graphics/icons/actions/radio-active-selected.svg
new file mode 100644
index 0000000..c1d1085
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/radio-active-selected.svg
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#E5E5E5">
+ <!ENTITY stroke_color "#808080">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="26"
+ height="26"
+ id="svg27352">
+ <g
+ transform="translate(0,10)"
+ id="layer1">
+ <path
+ d="M 13,0 C 5.8202983,0 0,5.8202983 0,13 0,20.179702 5.8202983,26 13,26 20.179702,26 26,20.179702 26,13 26,5.8202983 20.179702,0 13,0 z m 0,1 C 19.627417,1 25,6.372583 25,13 25,19.627417 19.627417,25 13,25 6.372583,25 1,19.627417 1,13 1,6.372583 6.372583,1 13,1 z"
+ transform="translate(0,-10)"
+ id="path3002"
+ style="color:#000000;fill:&stroke_color;;fill-opacity:1;stroke:none;stroke-width:0.82332432;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 11.276603,5.8201666 a 5.3516083,5.3516083 0 1 1 -10.70321633,0 5.3516083,5.3516083 0 1 1 10.70321633,0 z"
+ transform="matrix(2.2423166,0,0,2.2423166,-0.28571452,-10.050656)"
+ id="path4079"
+ style="color:#000000;fill:&fill_color;;fill-opacity:1;stroke:none;stroke-width:0.82325572;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 14.100023,7.3515821 a 5.9129128,5.9129128 0 1 1 -11.8258259,0 5.9129128,5.9129128 0 1 1 11.8258259,0 z"
+ transform="matrix(0.84560692,0,0,0.84560692,6.0769232,-3.2165487)"
+ id="path4754"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.84599996;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/lib/sugar-web/graphics/icons/actions/radio-active.svg b/lib/sugar-web/graphics/icons/actions/radio-active.svg
new file mode 100644
index 0000000..a5fe591
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/radio-active.svg
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#FFFFFF">
+ <!ENTITY stroke_color "#808080">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="26"
+ height="26"
+ id="svg27352">
+ <g
+ transform="translate(0,10)"
+ id="layer1">
+ <path
+ d="M 13,0 C 5.8202983,0 0,5.8202983 0,13 0,20.179702 5.8202983,26 13,26 20.179702,26 26,20.179702 26,13 26,5.8202983 20.179702,0 13,0 z m 0,1 C 19.627417,1 25,6.372583 25,13 25,19.627417 19.627417,25 13,25 6.372583,25 1,19.627417 1,13 1,6.372583 6.372583,1 13,1 z"
+ transform="translate(0,-10)"
+ id="path3002"
+ style="color:#000000;fill:&stroke_color;;fill-opacity:1;stroke:none;stroke-width:0.82332432;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 11.276603,5.8201666 a 5.3516083,5.3516083 0 1 1 -10.70321633,0 5.3516083,5.3516083 0 1 1 10.70321633,0 z"
+ transform="matrix(2.2423166,0,0,2.2423166,-0.28571452,-10.050656)"
+ id="path4079"
+ style="color:#000000;fill:&fill_color;;fill-opacity:1;stroke:none;stroke-width:0.82325572;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 14.100023,7.3515821 a 5.9129128,5.9129128 0 1 1 -11.8258259,0 5.9129128,5.9129128 0 1 1 11.8258259,0 z"
+ transform="matrix(0.84560692,0,0,0.84560692,6.0769232,-3.2165487)"
+ id="path4754"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.84599996;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/lib/sugar-web/graphics/icons/actions/radio-selected.svg b/lib/sugar-web/graphics/icons/actions/radio-selected.svg
new file mode 100644
index 0000000..a24b97e
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/radio-selected.svg
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#E5E5E5">
+ <!ENTITY stroke_color "#808080">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="26"
+ height="26"
+ id="svg27352">
+ <g
+ transform="translate(0,10)"
+ id="layer1">
+ <path
+ d="M 13,0 C 5.8202983,0 0,5.8202983 0,13 0,20.179702 5.8202983,26 13,26 20.179702,26 26,20.179702 26,13 26,5.8202983 20.179702,0 13,0 z m 0,1 C 19.627417,1 25,6.372583 25,13 25,19.627417 19.627417,25 13,25 6.372583,25 1,19.627417 1,13 1,6.372583 6.372583,1 13,1 z"
+ transform="translate(0,-10)"
+ id="path3002"
+ style="color:#000000;fill:&stroke_color;;fill-opacity:1;stroke:none;stroke-width:0.82332432;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 11.276603,5.8201666 a 5.3516083,5.3516083 0 1 1 -10.70321633,0 5.3516083,5.3516083 0 1 1 10.70321633,0 z"
+ transform="matrix(2.2423166,0,0,2.2423166,-0.28571452,-10.050656)"
+ id="path4079"
+ style="color:#000000;fill:&fill_color;;fill-opacity:1;stroke:none;stroke-width:0.82325572;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/lib/sugar-web/graphics/icons/actions/radio.svg b/lib/sugar-web/graphics/icons/actions/radio.svg
new file mode 100644
index 0000000..d250286
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/radio.svg
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#FFFFFF">
+ <!ENTITY stroke_color "#808080">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="26"
+ height="26"
+ id="svg27352">
+ <g
+ transform="translate(0,10)"
+ id="layer1">
+ <path
+ d="M 13,0 C 5.8202983,0 0,5.8202983 0,13 0,20.179702 5.8202983,26 13,26 20.179702,26 26,20.179702 26,13 26,5.8202983 20.179702,0 13,0 z m 0,1 C 19.627417,1 25,6.372583 25,13 25,19.627417 19.627417,25 13,25 6.372583,25 1,19.627417 1,13 1,6.372583 6.372583,1 13,1 z"
+ transform="translate(0,-10)"
+ id="path3002"
+ style="color:#000000;fill:&stroke_color;;fill-opacity:1;stroke:none;stroke-width:0.82332432;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 11.276603,5.8201666 a 5.3516083,5.3516083 0 1 1 -10.70321633,0 5.3516083,5.3516083 0 1 1 10.70321633,0 z"
+ transform="matrix(2.2423166,0,0,2.2423166,-0.28571452,-10.050656)"
+ id="path4079"
+ style="color:#000000;fill:&fill_color;;fill-opacity:1;stroke:none;stroke-width:0.82325572;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/lib/sugar-web/graphics/icons/actions/zoom-groups.svg b/lib/sugar-web/graphics/icons/actions/zoom-groups.svg
new file mode 100644
index 0000000..b88462f
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/zoom-groups.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="zoom-groups">
+ <path d="M27.5,5C15.074,5,5,15.074,5,27.5S15.074,50,27.5,50 S50,39.926,50,27.5S39.926,5,27.5,5z M18.759,37.955c-2.618,0-4.745-2.127-4.745-4.746c0-2.621,2.125-4.745,4.745-4.745 s4.748,2.124,4.748,4.745C23.506,35.828,21.379,37.955,18.759,37.955z M27.5,22.625c-2.621,0-4.746-2.125-4.746-4.747 c0-2.618,2.125-4.742,4.746-4.742c2.623,0,4.748,2.123,4.748,4.742C32.246,20.501,30.121,22.625,27.5,22.625z M36.238,37.953 c-2.619,0-4.744-2.125-4.744-4.744c0-2.621,2.125-4.746,4.744-4.746c2.623,0,4.748,2.125,4.748,4.746 C40.986,35.828,38.861,37.953,36.238,37.953z" display="inline" fill="&fill_color;" id="zoom-groups_2_"/>
+</g></svg> \ No newline at end of file
diff --git a/lib/sugar-web/graphics/icons/actions/zoom-home.svg b/lib/sugar-web/graphics/icons/actions/zoom-home.svg
new file mode 100644
index 0000000..5578fec
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/zoom-home.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="zoom-home">
+ <path d="M27.5,5C15.074,5,5,15.073,5,27.5C5,39.926,15.074,50,27.5,50 C39.928,50,50,39.926,50,27.5C50,15.073,39.926,5,27.5,5z M27.497,32.251c-2.621,0-4.749-2.131-4.749-4.75 c0-2.624,2.128-4.75,4.749-4.75c2.628,0,4.757,2.126,4.757,4.75C32.254,30.12,30.125,32.251,27.497,32.251z" display="inline" fill="&fill_color;" id="zoom-home_1_"/>
+</g></svg> \ No newline at end of file
diff --git a/lib/sugar-web/graphics/icons/actions/zoom-neighborhood.svg b/lib/sugar-web/graphics/icons/actions/zoom-neighborhood.svg
new file mode 100644
index 0000000..8d3f8d1
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/actions/zoom-neighborhood.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="zoom-neighborhood">
+ <path d="M27.5,5C15.074,5,5,15.074,5,27.5C5,39.927,15.074,50,27.5,50 C39.927,50,50,39.927,50,27.5C50,15.074,39.927,5,27.5,5z M37.604,13.904c2.096,0,3.795,1.699,3.795,3.794 c0,2.096-1.699,3.794-3.795,3.794s-3.794-1.699-3.794-3.794S35.51,13.904,37.604,13.904z M9.626,27.501 c0-2.099,1.7-3.799,3.793-3.799c2.097,0,3.796,1.7,3.796,3.799c0,2.095-1.7,3.793-3.796,3.793 C11.324,31.294,9.626,29.596,9.626,27.501z M17.391,41.096c-2.093,0-3.793-1.695-3.793-3.792c0-2.099,1.701-3.796,3.793-3.796 c2.099,0,3.799,1.696,3.799,3.796C21.189,39.399,19.489,41.096,17.391,41.096z M17.698,21.195c-2.094,0-3.795-1.7-3.795-3.799 c0-2.096,1.7-3.795,3.795-3.795c2.099,0,3.796,1.701,3.796,3.795C21.495,19.495,19.798,21.195,17.698,21.195z M27.499,45.374 c-2.1,0-3.797-1.7-3.797-3.794c0-2.098,1.696-3.797,3.797-3.797c2.095,0,3.794,1.699,3.794,3.797 C31.293,43.676,29.594,45.374,27.499,45.374z M27.501,17.217c-2.096,0-3.795-1.699-3.795-3.795s1.699-3.795,3.795-3.795 c2.097,0,3.795,1.699,3.795,3.795S29.598,17.217,27.501,17.217z M37.301,41.401c-2.096,0-3.795-1.699-3.795-3.795 s1.699-3.794,3.795-3.794s3.794,1.698,3.794,3.794S39.396,41.401,37.301,41.401z M41.581,31.298c-2.096,0-3.795-1.698-3.795-3.795 c0-2.096,1.699-3.793,3.799-3.793c2.094,0,3.791,1.697,3.791,3.793C45.376,29.6,43.679,31.298,41.581,31.298z" fill="&fill_color;" id="zoom-neighborhood_3_"/>
+</g></svg> \ No newline at end of file
diff --git a/lib/sugar-web/graphics/icons/emblems/arrow-down.svg b/lib/sugar-web/graphics/icons/emblems/arrow-down.svg
new file mode 100644
index 0000000..2de1a9e
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/emblems/arrow-down.svg
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#FFFFFF">
+ <!ENTITY stroke_color "#010101">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="11"
+ id="svg2">
+ <g
+ transform="translate(0,-1041.3622)"
+ id="layer1">
+ <path
+ d="m 29.756935,1046.8861 -2.256934,2.257 -2.256936,-2.257"
+ id="rect2998"
+ style="color:#000000;fill:none;stroke:&fill_color;;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/lib/sugar-web/graphics/icons/emblems/arrow-up.svg b/lib/sugar-web/graphics/icons/emblems/arrow-up.svg
new file mode 100644
index 0000000..a977f4a
--- /dev/null
+++ b/lib/sugar-web/graphics/icons/emblems/arrow-up.svg
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#FFFFFF">
+ <!ENTITY stroke_color "#010101">
+]>
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="11"
+ id="svg2">
+ <g
+ transform="translate(0,-1041.3622)"
+ id="layer1">
+ <path
+ d="m 29.756935,1049.1431 -2.256934,-2.257 -2.256936,2.257"
+ id="rect2998"
+ style="color:#000000;fill:none;stroke:&fill_color;;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/lib/sugar-web/graphics/menupalette.js b/lib/sugar-web/graphics/menupalette.js
new file mode 100644
index 0000000..df61cb2
--- /dev/null
+++ b/lib/sugar-web/graphics/menupalette.js
@@ -0,0 +1,64 @@
+define(["sugar-web/graphics/palette", "mustache"], function (palette, mustache) {
+
+ var menupalette = {};
+
+ menupalette.MenuPalette = function (invoker, primaryText, menuData) {
+ palette.Palette.call(this, invoker, primaryText);
+
+ this.selectItemEvent = new CustomEvent(
+ "selectItem", {
+ detail: {
+ item: undefined
+ },
+ bubbles: true,
+ cancelable: true
+ });
+
+ this.template =
+ '{{#.}}' +
+ '<li><button' +
+ ' {{ #icon }}class="icon"{{ /icon }}' +
+ ' {{ #id }}id="{{ id }}"{{ /id }}' +
+ '>' +
+ '{{ #icon }}<span></span>{{ /icon }}' +
+ '{{ label }}</button></li>' +
+ '{{/.}}';
+
+ var menuElem = document.createElement('ul');
+ menuElem.className = "menu";
+ menuElem.innerHTML = mustache.render(this.template, menuData);
+ this.setContent([menuElem]);
+
+ // Pop-down the palette when a item in the menu is clicked.
+
+ this.buttons = menuElem.querySelectorAll('button');
+
+ var that = this;
+
+ function popDownOnButtonClick(event) {
+ that.selectItemEvent.detail.target = event.target;
+ that.getPalette().dispatchEvent(that.selectItemEvent);
+ that.popDown();
+ }
+
+ for (var i = 0; i < this.buttons.length; i++) {
+ this.buttons[i].addEventListener('click', popDownOnButtonClick);
+ }
+ };
+
+ var addEventListener = function (type, listener, useCapture) {
+ return this.getPalette().addEventListener(type, listener, useCapture);
+ };
+
+ menupalette.MenuPalette.prototype =
+ Object.create(palette.Palette.prototype, {
+ addEventListener: {
+ value: addEventListener,
+ enumerable: true,
+ configurable: true,
+ writable: true
+ }
+ });
+
+ return menupalette;
+});
diff --git a/lib/sugar-web/graphics/palette.js b/lib/sugar-web/graphics/palette.js
new file mode 100644
index 0000000..9a194a1
--- /dev/null
+++ b/lib/sugar-web/graphics/palette.js
@@ -0,0 +1,179 @@
+define(function () {
+ var palettesGroup = [];
+
+ function getOffset(elem) {
+ // Ugly hack to consider the palette margin.
+ var style = elem.currentStyle || window.getComputedStyle(elem, '');
+
+ // Remove 'px' from the strings.
+ var x = -2 * style.marginLeft.slice(0, -2);
+ var y = -1 * style.marginTop.slice(0, -2);
+
+ var rect = elem.getBoundingClientRect();
+ x += rect.left;
+ y += rect.top;
+ return {
+ top: y,
+ left: x,
+ width: rect.width,
+ height: rect.height
+ };
+ }
+
+ var palette = {};
+
+ palette.Palette = function (invoker, primaryText) {
+ this.invoker = invoker;
+ if (this.invoker.classList.contains("toolbutton")) {
+ this.invoker.classList.add("invoker");
+ }
+ this.primaryText = primaryText;
+ var paletteElem;
+ var wrapperElem;
+ var headerElem;
+ var headerSeparatorElem;
+ var containerElem;
+ var that = this;
+ palettesGroup.push(this);
+
+ invoker.addEventListener('click', function (event) {
+ if (!that.invoker.classList.contains("toolbutton")) {
+ updatePosition(event.x, event.y);
+ }
+ that.toggle();
+ });
+
+ function updatePosition(clickX, clickY) {
+ var paletteX;
+ var paletteY;
+
+ if (typeof (clickX) !== 'undefined' &&
+ typeof (clickY) !== 'undefined') {
+ paletteX = clickX;
+ paletteY = clickY;
+ } else {
+ var invokerOffset = getOffset(that.invoker);
+ paletteX = invokerOffset.left;
+ paletteY = invokerOffset.top;
+ }
+
+ paletteElem.style.left = paletteX + "px";
+ paletteElem.style.top = paletteY + "px";
+ }
+
+ // A palette element can have a header, content, one or both.
+
+ function createPaletteElement() {
+ if (paletteElem !== undefined) {
+ return;
+ }
+ paletteElem = document.createElement('div');
+ paletteElem.className = "palette";
+ paletteElem.style.visibility = "hidden";
+ document.body.appendChild(paletteElem);
+
+ if (that.invoker.classList.contains("toolbutton")) {
+ invokerElem = document.createElement('div');
+ invokerElem.className = "palette-invoker";
+ var style = that.invoker.currentStyle ||
+ window.getComputedStyle(that.invoker, '');
+ invokerElem.style.backgroundImage = style.backgroundImage;
+
+ invokerElem.addEventListener('click', function (e) {
+ that.toggle();
+ });
+
+ paletteElem.appendChild(invokerElem);
+
+ }
+
+ wrapperElem = document.createElement('div');
+ wrapperElem.className = "wrapper";
+ paletteElem.appendChild(wrapperElem);
+
+ if (that.primaryText !== undefined) {
+ headerElem = document.createElement('div');
+ headerElem.className = "header";
+ headerElem.innerText = that.primaryText;
+ wrapperElem.appendChild(headerElem);
+ }
+
+ headerSeparatorElem = document.createElement('hr');
+ headerSeparatorElem.className = "header-separator";
+ headerSeparatorElem.style.display = "none";
+ wrapperElem.appendChild(headerSeparatorElem);
+
+ containerElem = document.createElement('div');
+ containerElem.className = "container";
+ wrapperElem.appendChild(containerElem);
+
+ updatePosition();
+ }
+
+ this.getPalette = function () {
+ if (paletteElem === undefined) {
+ createPaletteElement();
+ }
+ return paletteElem;
+ };
+
+ this.setContent = function (elementsList) {
+ if (paletteElem === undefined) {
+ createPaletteElement();
+ }
+
+ (function removePreviousContent() {
+ for (var i = 0; i < containerElem.children.length; i++) {
+ var child = containerElem.children[i];
+ containerElem.removeChild(child);
+ }
+ }());
+
+ (function addNewContent() {
+ for (var i = 0; i < elementsList.length; i++) {
+ var child = elementsList[i];
+ containerElem.appendChild(child);
+ }
+ }());
+
+ // The header separator will be visible only if there are
+ // both, header and content.
+ if (elementsList.length > 0 && this.primaryText !== undefined) {
+ headerSeparatorElem.style.display = "block";
+ } else {
+ headerSeparatorElem.style.display = "none";
+ }
+ };
+
+ this.isDown = function () {
+ return paletteElem === undefined ||
+ paletteElem.style.visibility == "hidden";
+ };
+
+ };
+
+ palette.Palette.prototype.popUp = function () {
+ for (var i = 0; i < palettesGroup.length; i++) {
+ otherPalette = palettesGroup[i];
+ if (otherPalette != this) {
+ otherPalette.popDown();
+ }
+ }
+ this.getPalette().style.visibility = "visible";
+ };
+
+ palette.Palette.prototype.popDown = function () {
+ this.getPalette().style.visibility = "hidden";
+ };
+
+ palette.Palette.prototype.toggle = function () {
+ if (this.isDown()) {
+ this.popUp();
+ } else {
+ this.popDown();
+ }
+ };
+
+ return palette;
+
+});
diff --git a/lib/sugar-web/graphics/radiobuttonsgroup.js b/lib/sugar-web/graphics/radiobuttonsgroup.js
new file mode 100644
index 0000000..712681b
--- /dev/null
+++ b/lib/sugar-web/graphics/radiobuttonsgroup.js
@@ -0,0 +1,59 @@
+define(function () {
+ var radioButtonsGroup = {};
+
+ // ## RadioButtonsGroup
+ //
+ // A group of elements where only one can be active at the same
+ // time.
+ //
+ // When an element is clicked, it becomes the active one. The
+ // active element gains the 'active' CSS class.
+ //
+ // Parameters:
+ //
+ // * **elems** Array of elements of the group.
+ radioButtonsGroup.RadioButtonsGroup = function (elems) {
+ this.elems = elems;
+ var active;
+
+ for (i = 0; i < elems.length; i++) {
+ var elem = elems[i];
+ elem.addEventListener("click", clickHandler);
+
+ // The first element that has 'active' CSS class is made
+ // the active of the group on startup.
+ if (active === undefined && elem.classList.contains('active')) {
+ active = elem;
+ }
+ }
+
+ // If no element has 'active' CSS class, the first element of
+ // the array is made the active.
+ if (active === undefined) {
+ active = elems[0];
+ updateClasses();
+ }
+
+ function clickHandler(evt) {
+ active = evt.target;
+ updateClasses();
+ }
+
+ function updateClasses() {
+ for (i = 0; i < elems.length; i++) {
+ var elem = elems[i];
+ elem.classList.remove('active');
+ }
+ active.classList.add('active');
+ }
+
+ // Get the active element.
+ this.getActive = function () {
+ return active;
+ };
+
+ };
+
+ return radioButtonsGroup;
+
+});
diff --git a/lib/sugar-web/graphics/xocolor.js b/lib/sugar-web/graphics/xocolor.js
new file mode 100644
index 0000000..1184ffa
--- /dev/null
+++ b/lib/sugar-web/graphics/xocolor.js
@@ -0,0 +1,729 @@
+define(function () {
+
+ var xocolor = {};
+
+ xocolor.colors = [
+ {
+ stroke: '#B20008',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#B20008'
+ },
+ {
+ stroke: '#E6000A',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#E6000A'
+ },
+ {
+ stroke: '#FFADCE',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#9A5200',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#9A5200'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FFC169',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#807500',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#807500'
+ },
+ {
+ stroke: '#BE9E00',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#BE9E00'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#008009',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#008009'
+ },
+ {
+ stroke: '#00B20D',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#00B20D'
+ },
+ {
+ stroke: '#8BFF7A',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#00588C',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#00588C'
+ },
+ {
+ stroke: '#005FE4',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#005FE4'
+ },
+ {
+ stroke: '#BCCDFF',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#5E008C',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#5E008C'
+ },
+ {
+ stroke: '#7F00BF',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#7F00BF'
+ },
+ {
+ stroke: '#D1A3FF',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#9A5200',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#9A5200'
+ },
+ {
+ stroke: '#C97E00',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#C97E00'
+ },
+ {
+ stroke: '#FFC169',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#807500',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#807500'
+ },
+ {
+ stroke: '#BE9E00',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#BE9E00'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#008009',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#008009'
+ },
+ {
+ stroke: '#00B20D',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#00B20D'
+ },
+ {
+ stroke: '#8BFF7A',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#00588C',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#00588C'
+ },
+ {
+ stroke: '#005FE4',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#005FE4'
+ },
+ {
+ stroke: '#BCCDFF',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#5E008C',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#5E008C'
+ },
+ {
+ stroke: '#A700FF',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#A700FF'
+ },
+ {
+ stroke: '#D1A3FF',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#B20008',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#B20008'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FFADCE',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#807500',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#807500'
+ },
+ {
+ stroke: '#BE9E00',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#BE9E00'
+ },
+ {
+ stroke: '#FFFA00',
+ fill: '#EDDE00'
+ },
+ {
+ stroke: '#008009',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#008009'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#8BFF7A',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#00588C',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#00588C'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#BCCEFF',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#5E008C',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#5E008C'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#D1A3FF',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#B20008',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#B20008'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FFADCE',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#9A5200',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#9A5200'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FFC169',
+ fill: '#F8E800'
+ },
+ {
+ stroke: '#008009',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#008009'
+ },
+ {
+ stroke: '#00B20D',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#00B20D'
+ },
+ {
+ stroke: '#8BFF7A',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00588C',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#00588C'
+ },
+ {
+ stroke: '#005FE4',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#005FE4'
+ },
+ {
+ stroke: '#BCCDFF',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#5E008C',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#5E008C'
+ },
+ {
+ stroke: '#7F00BF',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#7F00BF'
+ },
+ {
+ stroke: '#D1A3FF',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#B20008',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#B20008'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FFADCE',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#9A5200',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#9A5200'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FFC169',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#807500',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#807500'
+ },
+ {
+ stroke: '#BE9E00',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00EA11',
+ fill: '#BE9E00'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#00EA11'
+ },
+ {
+ stroke: '#00588C',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#00588C'
+ },
+ {
+ stroke: '#005FE4',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#005FE4'
+ },
+ {
+ stroke: '#BCCDFF',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#5E008C',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#5E008C'
+ },
+ {
+ stroke: '#9900E6',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#9900E6'
+ },
+ {
+ stroke: '#D1A3FF',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#B20008',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#B20008'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FFADCE',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#9A5200',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#9A5200'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FFC169',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#807500',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#807500'
+ },
+ {
+ stroke: '#BE9E00',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#BE9E00'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#008009',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#008009'
+ },
+ {
+ stroke: '#00B20D',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#00A0FF',
+ fill: '#00B20D'
+ },
+ {
+ stroke: '#8BFF7A',
+ fill: '#00A0FF'
+ },
+ {
+ stroke: '#5E008C',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#5E008C'
+ },
+ {
+ stroke: '#7F00BF',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#7F00BF'
+ },
+ {
+ stroke: '#D1A3FF',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#B20008',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#B20008'
+ },
+ {
+ stroke: '#FF2B34',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#FF2B34'
+ },
+ {
+ stroke: '#FFADCE',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#9A5200',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#9A5200'
+ },
+ {
+ stroke: '#FF8F00',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#FF8F00'
+ },
+ {
+ stroke: '#FFC169',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#807500',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#807500'
+ },
+ {
+ stroke: '#BE9E00',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#BE9E00'
+ },
+ {
+ stroke: '#F8E800',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#008009',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#008009'
+ },
+ {
+ stroke: '#00B20D',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#00B20D'
+ },
+ {
+ stroke: '#8BFF7A',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#00588C',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#00588C'
+ },
+ {
+ stroke: '#005FE4',
+ fill: '#AC32FF'
+ },
+ {
+ stroke: '#AC32FF',
+ fill: '#005FE4'
+ },
+ {
+ stroke: '#BCCDFF',
+ fill: '#AC32FF'
+ }
+ ];
+
+ return xocolor;
+});
diff --git a/lib/sugar-web/package.json b/lib/sugar-web/package.json
new file mode 100644
index 0000000..000de4e
--- /dev/null
+++ b/lib/sugar-web/package.json
@@ -0,0 +1,9 @@
+{
+ "volo": {
+ "baseUrl": "lib",
+ "dependencies": {
+ "webL10n": "github:sugarlabs/webL10n",
+ "mustache": "github:janl/mustache.js/0.7.2"
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/webL10n.js b/lib/webL10n.js
new file mode 100644
index 0000000..f76d66a
--- /dev/null
+++ b/lib/webL10n.js
@@ -0,0 +1,1029 @@
+/**
+ * Copyright (c) 2011-2013 Fabien Cazenave, Mozilla.
+ *
+ * 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.
+ */
+
+/*jshint browser: true, devel: true, es5: true, globalstrict: true */
+'use strict';
+
+define(function (require) {
+ var gL10nData = {};
+ var gTextData = '';
+ var gTextProp = 'textContent';
+ var gLanguage = '';
+ var gMacros = {};
+ var gReadyState = 'loading';
+
+ /**
+ * Synchronously loading l10n resources significantly minimizes flickering
+ * from displaying the app with non-localized strings and then updating the
+ * strings. Although this will block all script execution on this page, we
+ * expect that the l10n resources are available locally on flash-storage.
+ *
+ * As synchronous XHR is generally considered as a bad idea, we're still
+ * loading l10n resources asynchronously -- but we keep this in a setting,
+ * just in case... and applications using this library should hide their
+ * content until the `localized' event happens.
+ */
+
+ var gAsyncResourceLoading = true; // read-only
+
+
+ /**
+ * Debug helpers
+ *
+ * gDEBUG == 0: don't display any console message
+ * gDEBUG == 1: display only warnings, not logs
+ * gDEBUG == 2: display all console messages
+ */
+
+ var gDEBUG = 1;
+
+ function consoleLog(message) {
+ if (gDEBUG >= 2) {
+ console.log('[l10n] ' + message);
+ }
+ };
+
+ function consoleWarn(message) {
+ if (gDEBUG) {
+ console.warn('[l10n] ' + message);
+ }
+ };
+
+
+ /**
+ * DOM helpers for the so-called "HTML API".
+ *
+ * These functions are written for modern browsers. For old versions of IE,
+ * they're overridden in the 'startup' section at the end of this file.
+ */
+
+ function getL10nResourceLinks() {
+ return document.querySelectorAll('link[type="application/l10n"]');
+ }
+
+ function getL10nDictionary() {
+ var script = document.querySelector('script[type="application/l10n"]');
+ // TODO: support multiple and external JSON dictionaries
+ return script ? JSON.parse(script.innerHTML) : null;
+ }
+
+ function getTranslatableChildren(element) {
+ return element ? element.querySelectorAll('*[data-l10n-id]') : [];
+ }
+
+ function getL10nAttributes(element) {
+ if (!element)
+ return {};
+
+ var l10nId = element.getAttribute('data-l10n-id');
+ var l10nArgs = element.getAttribute('data-l10n-args');
+ var args = {};
+ if (l10nArgs) {
+ try {
+ args = JSON.parse(l10nArgs);
+ } catch (e) {
+ consoleWarn('could not parse arguments for #' + l10nId);
+ }
+ }
+ return { id: l10nId, args: args };
+ }
+
+ function fireL10nReadyEvent(lang) {
+ var evtObject = document.createEvent('Event');
+ evtObject.initEvent('localized', true, false);
+ evtObject.language = lang;
+ document.dispatchEvent(evtObject);
+ }
+
+ function xhrLoadText(url, onSuccess, onFailure, asynchronous) {
+ onSuccess = onSuccess || function _onSuccess(data) {};
+ onFailure = onFailure || function _onFailure() {
+ consoleWarn(url + ' not found.');
+ };
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url, asynchronous);
+ if (xhr.overrideMimeType) {
+ xhr.overrideMimeType('text/plain; charset=utf-8');
+ }
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200 || xhr.status === 0) {
+ onSuccess(xhr.responseText);
+ } else {
+ onFailure();
+ }
+ }
+ };
+ xhr.onerror = onFailure;
+ xhr.ontimeout = onFailure;
+
+ // in Firefox OS with the app:// protocol, trying to XHR a non-existing
+ // URL will raise an exception here -- hence this ugly try...catch.
+ try {
+ xhr.send(null);
+ } catch (e) {
+ onFailure();
+ }
+ }
+
+
+ /**
+ * l10n resource parser:
+ * - reads (async XHR) the l10n resource matching `lang';
+ * - imports linked resources (synchronously) when specified;
+ * - parses the text data (fills `gL10nData' and `gTextData');
+ * - triggers success/failure callbacks when done.
+ *
+ * @param {string} href
+ * URL of the l10n resource to parse.
+ *
+ * @param {string} lang
+ * locale (language) to parse.
+ *
+ * @param {Function} successCallback
+ * triggered when the l10n resource has been successully parsed.
+ *
+ * @param {Function} failureCallback
+ * triggered when the an error has occured.
+ *
+ * @return {void}
+ * uses the following global variables: gL10nData, gTextData, gTextProp.
+ */
+
+ function parseResource(href, lang, successCallback, failureCallback) {
+ var baseURL = href.replace(/[^\/]*$/, '') || './';
+
+ // handle escaped characters (backslashes) in a string
+ function evalString(text) {
+ if (text.lastIndexOf('\\') < 0)
+ return text;
+ return text.replace(/\\\\/g, '\\')
+ .replace(/\\n/g, '\n')
+ .replace(/\\r/g, '\r')
+ .replace(/\\t/g, '\t')
+ .replace(/\\b/g, '\b')
+ .replace(/\\f/g, '\f')
+ .replace(/\\{/g, '{')
+ .replace(/\\}/g, '}')
+ .replace(/\\"/g, '"')
+ .replace(/\\'/g, "'");
+ }
+
+ // parse *.properties text data into an l10n dictionary
+ function parseProperties(text) {
+ var dictionary = [];
+
+ // token expressions
+ var reBlank = /^\s*|\s*$/;
+ var reComment = /^\s*#|^\s*$/;
+ var reSection = /^\s*\[(.*)\]\s*$/;
+ var reImport = /^\s*@import\s+url\((.*)\)\s*$/i;
+ var reSplit = /^([^=\s]*)\s*=\s*(.+)$/; // TODO: escape EOLs with '\'
+
+ // parse the *.properties file into an associative array
+ function parseRawLines(rawText, extendedSyntax) {
+ var entries = rawText.replace(reBlank, '').split(/[\r\n]+/);
+ var currentLang = '*';
+ var genericLang = lang.replace(/-[a-z]+$/i, '');
+ var skipLang = false;
+ var match = '';
+
+ for (var i = 0; i < entries.length; i++) {
+ var line = entries[i];
+
+ // comment or blank line?
+ if (reComment.test(line))
+ continue;
+
+ // the extended syntax supports [lang] sections and @import rules
+ if (extendedSyntax) {
+ if (reSection.test(line)) { // section start?
+ match = reSection.exec(line);
+ currentLang = match[1];
+ skipLang = (currentLang !== '*') &&
+ (currentLang !== lang) && (currentLang !== genericLang);
+ continue;
+ } else if (skipLang) {
+ continue;
+ }
+ if (reImport.test(line)) { // @import rule?
+ match = reImport.exec(line);
+ loadImport(baseURL + match[1]); // load the resource synchronously
+ }
+ }
+
+ // key-value pair
+ var tmp = line.match(reSplit);
+ if (tmp && tmp.length == 3) {
+ dictionary[tmp[1]] = evalString(tmp[2]);
+ }
+ }
+ }
+
+ // import another *.properties file
+ function loadImport(url) {
+ xhrLoadText(url, function(content) {
+ parseRawLines(content, false); // don't allow recursive imports
+ }, null, false); // load synchronously
+ }
+
+ // fill the dictionary
+ parseRawLines(text, true);
+ return dictionary;
+ }
+
+ // load and parse l10n data (warning: global variables are used here)
+ xhrLoadText(href, function(response) {
+ gTextData += response; // mostly for debug
+
+ // parse *.properties text data into an l10n dictionary
+ var data = parseProperties(response);
+
+ // find attribute descriptions, if any
+ for (var key in data) {
+ var id, prop, index = key.lastIndexOf('.');
+ if (index > 0) { // an attribute has been specified
+ id = key.substring(0, index);
+ prop = key.substr(index + 1);
+ } else { // no attribute: assuming text content by default
+ id = key;
+ prop = gTextProp;
+ }
+ if (!gL10nData[id]) {
+ gL10nData[id] = {};
+ }
+ gL10nData[id][prop] = data[key];
+ }
+
+ // trigger callback
+ if (successCallback) {
+ successCallback();
+ }
+ }, failureCallback, gAsyncResourceLoading);
+ };
+
+ // load and parse all resources for the specified locale
+ function loadLocale(lang, callback) {
+ callback = callback || function _callback() {};
+
+ clear();
+ gLanguage = lang;
+
+ // check all <link type="application/l10n" href="..." /> nodes
+ // and load the resource files
+ var langLinks = getL10nResourceLinks();
+ var langCount = langLinks.length;
+ if (langCount == 0) {
+ // we might have a pre-compiled dictionary instead
+ var dict = getL10nDictionary();
+ if (dict && dict.locales && dict.default_locale) {
+ consoleLog('using the embedded JSON directory, early way out');
+ gL10nData = dict.locales[lang] || dict.locales[dict.default_locale];
+ callback();
+ } else {
+ consoleLog('no resource to load, early way out');
+ }
+ // early way out
+ fireL10nReadyEvent(lang);
+ gReadyState = 'complete';
+ return;
+ }
+
+ // start the callback when all resources are loaded
+ var onResourceLoaded = null;
+ var gResourceCount = 0;
+ onResourceLoaded = function() {
+ gResourceCount++;
+ if (gResourceCount >= langCount) {
+ callback();
+ fireL10nReadyEvent(lang);
+ gReadyState = 'complete';
+ }
+ };
+
+ // load all resource files
+ function l10nResourceLink(link) {
+ var href = link.href;
+ var type = link.type;
+ this.load = function(lang, callback) {
+ var applied = lang;
+ parseResource(href, lang, callback, function() {
+ consoleWarn(href + ' not found.');
+ applied = '';
+ });
+ return applied; // return lang if found, an empty string if not found
+ };
+ }
+
+ for (var i = 0; i < langCount; i++) {
+ var resource = new l10nResourceLink(langLinks[i]);
+ var rv = resource.load(lang, onResourceLoaded);
+ if (rv != lang) { // lang not found, used default resource instead
+ consoleWarn('"' + lang + '" resource not found');
+ gLanguage = '';
+ }
+ }
+ }
+
+ // clear all l10n data
+ function clear() {
+ gL10nData = {};
+ gTextData = '';
+ gLanguage = '';
+ // TODO: clear all non predefined macros.
+ // There's no such macro /yet/ but we're planning to have some...
+ }
+
+
+ /**
+ * Get rules for plural forms (shared with JetPack), see:
+ * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
+ * https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p
+ *
+ * @param {string} lang
+ * locale (language) used.
+ *
+ * @return {Function}
+ * returns a function that gives the plural form name for a given integer:
+ * var fun = getPluralRules('en');
+ * fun(1) -> 'one'
+ * fun(0) -> 'other'
+ * fun(1000) -> 'other'.
+ */
+
+ function getPluralRules(lang) {
+ var locales2rules = {
+ 'af': 3,
+ 'ak': 4,
+ 'am': 4,
+ 'ar': 1,
+ 'asa': 3,
+ 'az': 0,
+ 'be': 11,
+ 'bem': 3,
+ 'bez': 3,
+ 'bg': 3,
+ 'bh': 4,
+ 'bm': 0,
+ 'bn': 3,
+ 'bo': 0,
+ 'br': 20,
+ 'brx': 3,
+ 'bs': 11,
+ 'ca': 3,
+ 'cgg': 3,
+ 'chr': 3,
+ 'cs': 12,
+ 'cy': 17,
+ 'da': 3,
+ 'de': 3,
+ 'dv': 3,
+ 'dz': 0,
+ 'ee': 3,
+ 'el': 3,
+ 'en': 3,
+ 'eo': 3,
+ 'es': 3,
+ 'et': 3,
+ 'eu': 3,
+ 'fa': 0,
+ 'ff': 5,
+ 'fi': 3,
+ 'fil': 4,
+ 'fo': 3,
+ 'fr': 5,
+ 'fur': 3,
+ 'fy': 3,
+ 'ga': 8,
+ 'gd': 24,
+ 'gl': 3,
+ 'gsw': 3,
+ 'gu': 3,
+ 'guw': 4,
+ 'gv': 23,
+ 'ha': 3,
+ 'haw': 3,
+ 'he': 2,
+ 'hi': 4,
+ 'hr': 11,
+ 'hu': 0,
+ 'id': 0,
+ 'ig': 0,
+ 'ii': 0,
+ 'is': 3,
+ 'it': 3,
+ 'iu': 7,
+ 'ja': 0,
+ 'jmc': 3,
+ 'jv': 0,
+ 'ka': 0,
+ 'kab': 5,
+ 'kaj': 3,
+ 'kcg': 3,
+ 'kde': 0,
+ 'kea': 0,
+ 'kk': 3,
+ 'kl': 3,
+ 'km': 0,
+ 'kn': 0,
+ 'ko': 0,
+ 'ksb': 3,
+ 'ksh': 21,
+ 'ku': 3,
+ 'kw': 7,
+ 'lag': 18,
+ 'lb': 3,
+ 'lg': 3,
+ 'ln': 4,
+ 'lo': 0,
+ 'lt': 10,
+ 'lv': 6,
+ 'mas': 3,
+ 'mg': 4,
+ 'mk': 16,
+ 'ml': 3,
+ 'mn': 3,
+ 'mo': 9,
+ 'mr': 3,
+ 'ms': 0,
+ 'mt': 15,
+ 'my': 0,
+ 'nah': 3,
+ 'naq': 7,
+ 'nb': 3,
+ 'nd': 3,
+ 'ne': 3,
+ 'nl': 3,
+ 'nn': 3,
+ 'no': 3,
+ 'nr': 3,
+ 'nso': 4,
+ 'ny': 3,
+ 'nyn': 3,
+ 'om': 3,
+ 'or': 3,
+ 'pa': 3,
+ 'pap': 3,
+ 'pl': 13,
+ 'ps': 3,
+ 'pt': 3,
+ 'rm': 3,
+ 'ro': 9,
+ 'rof': 3,
+ 'ru': 11,
+ 'rwk': 3,
+ 'sah': 0,
+ 'saq': 3,
+ 'se': 7,
+ 'seh': 3,
+ 'ses': 0,
+ 'sg': 0,
+ 'sh': 11,
+ 'shi': 19,
+ 'sk': 12,
+ 'sl': 14,
+ 'sma': 7,
+ 'smi': 7,
+ 'smj': 7,
+ 'smn': 7,
+ 'sms': 7,
+ 'sn': 3,
+ 'so': 3,
+ 'sq': 3,
+ 'sr': 11,
+ 'ss': 3,
+ 'ssy': 3,
+ 'st': 3,
+ 'sv': 3,
+ 'sw': 3,
+ 'syr': 3,
+ 'ta': 3,
+ 'te': 3,
+ 'teo': 3,
+ 'th': 0,
+ 'ti': 4,
+ 'tig': 3,
+ 'tk': 3,
+ 'tl': 4,
+ 'tn': 3,
+ 'to': 0,
+ 'tr': 0,
+ 'ts': 3,
+ 'tzm': 22,
+ 'uk': 11,
+ 'ur': 3,
+ 've': 3,
+ 'vi': 0,
+ 'vun': 3,
+ 'wa': 4,
+ 'wae': 3,
+ 'wo': 0,
+ 'xh': 3,
+ 'xog': 3,
+ 'yo': 0,
+ 'zh': 0,
+ 'zu': 3
+ };
+
+ // utility functions for plural rules methods
+ function isIn(n, list) {
+ return list.indexOf(n) !== -1;
+ }
+ function isBetween(n, start, end) {
+ return start <= n && n <= end;
+ }
+
+ // list of all plural rules methods:
+ // map an integer to the plural form name to use
+ var pluralRules = {
+ '0': function(n) {
+ return 'other';
+ },
+ '1': function(n) {
+ if ((isBetween((n % 100), 3, 10)))
+ return 'few';
+ if (n === 0)
+ return 'zero';
+ if ((isBetween((n % 100), 11, 99)))
+ return 'many';
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '2': function(n) {
+ if (n !== 0 && (n % 10) === 0)
+ return 'many';
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '3': function(n) {
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '4': function(n) {
+ if ((isBetween(n, 0, 1)))
+ return 'one';
+ return 'other';
+ },
+ '5': function(n) {
+ if ((isBetween(n, 0, 2)) && n != 2)
+ return 'one';
+ return 'other';
+ },
+ '6': function(n) {
+ if (n === 0)
+ return 'zero';
+ if ((n % 10) == 1 && (n % 100) != 11)
+ return 'one';
+ return 'other';
+ },
+ '7': function(n) {
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '8': function(n) {
+ if ((isBetween(n, 3, 6)))
+ return 'few';
+ if ((isBetween(n, 7, 10)))
+ return 'many';
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '9': function(n) {
+ if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))
+ return 'few';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '10': function(n) {
+ if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))
+ return 'few';
+ if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
+ return 'one';
+ return 'other';
+ },
+ '11': function(n) {
+ if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
+ return 'few';
+ if ((n % 10) === 0 ||
+ (isBetween((n % 10), 5, 9)) ||
+ (isBetween((n % 100), 11, 14)))
+ return 'many';
+ if ((n % 10) == 1 && (n % 100) != 11)
+ return 'one';
+ return 'other';
+ },
+ '12': function(n) {
+ if ((isBetween(n, 2, 4)))
+ return 'few';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '13': function(n) {
+ if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
+ return 'few';
+ if (n != 1 && (isBetween((n % 10), 0, 1)) ||
+ (isBetween((n % 10), 5, 9)) ||
+ (isBetween((n % 100), 12, 14)))
+ return 'many';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '14': function(n) {
+ if ((isBetween((n % 100), 3, 4)))
+ return 'few';
+ if ((n % 100) == 2)
+ return 'two';
+ if ((n % 100) == 1)
+ return 'one';
+ return 'other';
+ },
+ '15': function(n) {
+ if (n === 0 || (isBetween((n % 100), 2, 10)))
+ return 'few';
+ if ((isBetween((n % 100), 11, 19)))
+ return 'many';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '16': function(n) {
+ if ((n % 10) == 1 && n != 11)
+ return 'one';
+ return 'other';
+ },
+ '17': function(n) {
+ if (n == 3)
+ return 'few';
+ if (n === 0)
+ return 'zero';
+ if (n == 6)
+ return 'many';
+ if (n == 2)
+ return 'two';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '18': function(n) {
+ if (n === 0)
+ return 'zero';
+ if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)
+ return 'one';
+ return 'other';
+ },
+ '19': function(n) {
+ if ((isBetween(n, 2, 10)))
+ return 'few';
+ if ((isBetween(n, 0, 1)))
+ return 'one';
+ return 'other';
+ },
+ '20': function(n) {
+ if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(
+ isBetween((n % 100), 10, 19) ||
+ isBetween((n % 100), 70, 79) ||
+ isBetween((n % 100), 90, 99)
+ ))
+ return 'few';
+ if ((n % 1000000) === 0 && n !== 0)
+ return 'many';
+ if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
+ return 'two';
+ if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
+ return 'one';
+ return 'other';
+ },
+ '21': function(n) {
+ if (n === 0)
+ return 'zero';
+ if (n == 1)
+ return 'one';
+ return 'other';
+ },
+ '22': function(n) {
+ if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
+ return 'one';
+ return 'other';
+ },
+ '23': function(n) {
+ if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)
+ return 'one';
+ return 'other';
+ },
+ '24': function(n) {
+ if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
+ return 'few';
+ if (isIn(n, [2, 12]))
+ return 'two';
+ if (isIn(n, [1, 11]))
+ return 'one';
+ return 'other';
+ }
+ };
+
+ // return a function that gives the plural form name for a given integer
+ var index = locales2rules[lang.replace(/-.*$/, '')];
+ if (!(index in pluralRules)) {
+ consoleWarn('plural form unknown for [' + lang + ']');
+ return function() { return 'other'; };
+ }
+ return pluralRules[index];
+ }
+
+ // pre-defined 'plural' macro
+ gMacros.plural = function(str, param, key, prop) {
+ var n = parseFloat(param);
+ if (isNaN(n))
+ return str;
+
+ // TODO: support other properties (l20n still doesn't...)
+ if (prop != gTextProp)
+ return str;
+
+ // initialize _pluralRules
+ if (!gMacros._pluralRules) {
+ gMacros._pluralRules = getPluralRules(gLanguage);
+ }
+ var index = '[' + gMacros._pluralRules(n) + ']';
+
+ // try to find a [zero|one|two] key if it's defined
+ if (n === 0 && (key + '[zero]') in gL10nData) {
+ str = gL10nData[key + '[zero]'][prop];
+ } else if (n == 1 && (key + '[one]') in gL10nData) {
+ str = gL10nData[key + '[one]'][prop];
+ } else if (n == 2 && (key + '[two]') in gL10nData) {
+ str = gL10nData[key + '[two]'][prop];
+ } else if ((key + index) in gL10nData) {
+ str = gL10nData[key + index][prop];
+ } else if ((key + '[other]') in gL10nData) {
+ str = gL10nData[key + '[other]'][prop];
+ }
+
+ return str;
+ };
+
+
+ /**
+ * l10n dictionary functions
+ */
+
+ // fetch an l10n object, warn if not found, apply `args' if possible
+ function getL10nData(key, args) {
+ var data = gL10nData[key];
+ if (!data) {
+ consoleWarn('#' + key + ' missing for [' + gLanguage + ']');
+ }
+
+ /** This is where l10n expressions should be processed.
+ * The plan is to support C-style expressions from the l20n project;
+ * until then, only two kinds of simple expressions are supported:
+ * {[ index ]} and {{ arguments }}.
+ */
+ var rv = {};
+ for (var prop in data) {
+ var str = data[prop];
+ str = substIndexes(str, args, key, prop);
+ str = substArguments(str, args);
+ rv[prop] = str;
+ }
+ return rv;
+ }
+
+ // replace {[macros]} with their values
+ function substIndexes(str, args, key, prop) {
+ var reIndex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)\s*\]\}/;
+ var reMatch = reIndex.exec(str);
+ if (!reMatch || !reMatch.length)
+ return str;
+
+ // an index/macro has been found
+ // Note: at the moment, only one parameter is supported
+ var macroName = reMatch[1];
+ var paramName = reMatch[2];
+ var param;
+ if (args && paramName in args) {
+ param = args[paramName];
+ } else if (paramName in gL10nData) {
+ param = gL10nData[paramName];
+ }
+
+ // there's no macro parser yet: it has to be defined in gMacros
+ if (macroName in gMacros) {
+ var macro = gMacros[macroName];
+ str = macro(str, param, key, prop);
+ }
+ return str;
+ }
+
+ // replace {{arguments}} with their values
+ function substArguments(str, args) {
+ var reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/;
+ var match = reArgs.exec(str);
+ while (match) {
+ if (!match || match.length < 2)
+ return str; // argument key not found
+
+ var arg = match[1];
+ var sub = '';
+ if (arg in args) {
+ sub = args[arg];
+ } else if (arg in gL10nData) {
+ sub = gL10nData[arg][gTextProp];
+ } else {
+ consoleWarn('could not find argument {{' + arg + '}}');
+ return str;
+ }
+
+ str = str.substring(0, match.index) + sub +
+ str.substr(match.index + match[0].length);
+ match = reArgs.exec(str);
+ }
+ return str;
+ }
+
+ // translate an HTML element
+ function translateElement(element) {
+ var l10n = getL10nAttributes(element);
+ if (!l10n.id)
+ return;
+
+ // get the related l10n object
+ var data = getL10nData(l10n.id, l10n.args);
+ if (!data) {
+ consoleWarn('#' + l10n.id + ' missing for [' + gLanguage + ']');
+ return;
+ }
+
+ // translate element (TODO: security checks?)
+ // for the node content, replace the content of the first child textNode
+ // and clear other child textNodes
+ if (data[gTextProp]) { // XXX
+ if (getChildElementCount(element) === 0) {
+ element[gTextProp] = data[gTextProp];
+ } else {
+ var children = element.childNodes,
+ found = false;
+ for (var i = 0, l = children.length; i < l; i++) {
+ if (children[i].nodeType === 3 &&
+ /\S/.test(children[i].textContent)) { // XXX
+ // using nodeValue seems cross-browser
+ if (found) {
+ children[i].nodeValue = '';
+ } else {
+ children[i].nodeValue = data[gTextProp];
+ found = true;
+ }
+ }
+ }
+ if (!found) {
+ consoleWarn('unexpected error, could not translate element content');
+ }
+ }
+ delete data[gTextProp];
+ }
+
+ for (var k in data) {
+ element[k] = data[k];
+ }
+ }
+
+ // webkit browsers don't currently support 'children' on SVG elements...
+ function getChildElementCount(element) {
+ if (element.children) {
+ return element.children.length;
+ }
+ if (typeof element.childElementCount !== 'undefined') {
+ return element.childElementCount;
+ }
+ var count = 0;
+ for (var i = 0; i < element.childNodes.length; i++) {
+ count += element.nodeType === 1 ? 1 : 0;
+ }
+ return count;
+ }
+
+ // translate an HTML subtree
+ function translateFragment(element) {
+ element = element || document.documentElement;
+
+ // check all translatable children (= w/ a `data-l10n-id' attribute)
+ var children = getTranslatableChildren(element);
+ var elementCount = children.length;
+ for (var i = 0; i < elementCount; i++) {
+ translateElement(children[i]);
+ }
+
+ // translate element itself if necessary
+ translateElement(element);
+ }
+
+ // Startup & Public API
+
+ function l10nStartup() {
+ gReadyState = 'interactive';
+ consoleLog('loading [' + navigator.language + '] resources, ' +
+ (gAsyncResourceLoading ? 'asynchronously.' : 'synchronously.'));
+
+ // load the default locale and translate the document if required
+ if (document.documentElement.lang === navigator.language) {
+ loadLocale(navigator.language);
+ } else {
+ loadLocale(navigator.language, translateFragment);
+ }
+ }
+
+ // public API
+ var l10n = {
+ start: function() {
+ if (document.readyState === 'complete' ||
+ document.readyState === 'interactive') {
+ window.setTimeout(l10nStartup);
+ } else {
+ document.addEventListener('DOMContentLoaded', l10nStartup);
+ }
+ },
+
+ // get a localized string
+ get: function l10n_get(key, args, fallback) {
+ var data = getL10nData(key, args) || fallback;
+ if (data) {
+ return 'textContent' in data ? data.textContent : '';
+ }
+ return '{{' + key + '}}';
+ },
+
+ // get|set the document language and direction
+ get language() {
+ return {
+ // get|set the document language (ISO-639-1)
+ get code() { return gLanguage; },
+ set code(lang) { loadLocale(lang, translateFragment); },
+
+ // get the direction (ltr|rtl) of the current language
+ get direction() {
+ // http://www.w3.org/International/questions/qa-scripts
+ // Arabic, Hebrew, Farsi, Pashto, Urdu
+ var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
+ return (rtlList.indexOf(gLanguage) >= 0) ? 'rtl' : 'ltr';
+ }
+ };
+ },
+
+ // translate an element or document fragment
+ translate: translateFragment,
+
+ // get (a clone of) the dictionary for the current locale
+ get dictionary() { return JSON.parse(JSON.stringify(gL10nData)); },
+
+ // this can be used to prevent race conditions
+ get readyState() { return gReadyState; },
+ ready: function l10n_ready(callback) {
+ if (!callback)
+ return;
+
+ if (gReadyState == 'complete') {
+ window.setTimeout(callback);
+ } else {
+ window.addEventListener('localized', callback);
+ }
+ }
+ };
+
+ return l10n;
+});