+
+
+
+
\ No newline at end of file
diff --git a/tests/UI/screenshot-diffs/style.css b/tests/UI/screenshot-diffs/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..367b01f0f876e7eb2bda2ca64384352289b2a584
--- /dev/null
+++ b/tests/UI/screenshot-diffs/style.css
@@ -0,0 +1,156 @@
+img {
+ max-width: 100%;
+ display: block;
+ margin: 0 auto;
+}
+
+#controls {
+ margin: 0 auto;
+ width: 100px;
+}
+
+.cd-image-container {
+ position: relative;
+ width: 90%;
+ max-width: 768px;
+ margin: 0em auto;
+}
+.cd-image-container img {
+ display: block;
+}
+
+.cd-image-label {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ color: #ffffff;
+ padding: 1em;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ opacity: 0;
+ -webkit-transform: translateY(20px);
+ -moz-transform: translateY(20px);
+ -ms-transform: translateY(20px);
+ -o-transform: translateY(20px);
+ transform: translateY(20px);
+ -webkit-transition: -webkit-transform 0.3s 0.7s, opacity 0.3s 0.7s;
+ -moz-transition: -moz-transform 0.3s 0.7s, opacity 0.3s 0.7s;
+ transition: transform 0.3s 0.7s, opacity 0.3s 0.7s;
+}
+.cd-image-label.is-hidden {
+ visibility: hidden;
+}
+.is-visible .cd-image-label {
+ opacity: 1;
+ -webkit-transform: translateY(0);
+ -moz-transform: translateY(0);
+ -ms-transform: translateY(0);
+ -o-transform: translateY(0);
+ transform: translateY(0);
+}
+
+.cd-resize-img {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 0;
+ height: 100%;
+ overflow: hidden;
+ /* Force Hardware Acceleration in WebKit */
+ -webkit-transform: translateZ(0);
+ -moz-transform: translateZ(0);
+ -ms-transform: translateZ(0);
+ -o-transform: translateZ(0);
+ transform: translateZ(0);
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+}
+.cd-resize-img img {
+ position: absolute;
+ left: 0;
+ top: 0;
+ display: block;
+ height: 100%;
+ width: auto;
+ max-width: none;
+}
+.cd-resize-img .cd-image-label {
+ right: auto;
+ left: 0;
+}
+.is-visible .cd-resize-img {
+ width: 50%;
+ /* bounce in animation of the modified image */
+ -webkit-animation: cd-bounce-in 0.7s;
+ -moz-animation: cd-bounce-in 0.7s;
+ animation: cd-bounce-in 0.7s;
+}
+
+@-webkit-keyframes cd-bounce-in {
+ 0% {
+ width: 0;
+ }
+ 60% {
+ width: 55%;
+ }
+ 100% {
+ width: 50%;
+ }
+}
+@-moz-keyframes cd-bounce-in {
+ 0% {
+ width: 0;
+ }
+ 60% {
+ width: 55%;
+ }
+ 100% {
+ width: 50%;
+ }
+}
+@keyframes cd-bounce-in {
+ 0% {
+ width: 0;
+ }
+ 60% {
+ width: 55%;
+ }
+ 100% {
+ width: 50%;
+ }
+}
+.cd-handle {
+ position: absolute;
+ height: 44px;
+ width: 44px;
+ /* center the element */
+ left: 50%;
+ top: 50%;
+ margin-left: -22px;
+ margin-top: -22px;
+ border-radius: 50%;
+ background: #dc717d no-repeat center center;
+ cursor: move;
+ box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.2), 0 0 10px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.3);
+ opacity: 0;
+ -webkit-transform: translate3d(0, 0, 0) scale(0);
+ -moz-transform: translate3d(0, 0, 0) scale(0);
+ -ms-transform: translate3d(0, 0, 0) scale(0);
+ -o-transform: translate3d(0, 0, 0) scale(0);
+ transform: translate3d(0, 0, 0) scale(0);
+}
+.cd-handle.draggable {
+ /* change background color when element is active */
+ background-color: #445b7c;
+}
+.is-visible .cd-handle {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0) scale(1);
+ -moz-transform: translate3d(0, 0, 0) scale(1);
+ -ms-transform: translate3d(0, 0, 0) scale(1);
+ -o-transform: translate3d(0, 0, 0) scale(1);
+ transform: translate3d(0, 0, 0) scale(1);
+ -webkit-transition: -webkit-transform 0.3s 0.7s, opacity 0s 0.7s;
+ -moz-transition: -moz-transform 0.3s 0.7s, opacity 0s 0.7s;
+ transition: transform 0.3s 0.7s, opacity 0s 0.7s;
+}
diff --git a/tests/UI/specs/BarGraph_spec.js b/tests/UI/specs/BarGraph_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..974335dfe4f22578affb86d84f9b18ca7bfbaf42
--- /dev/null
+++ b/tests/UI/specs/BarGraph_spec.js
@@ -0,0 +1,67 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Bar graph screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("BarGraph", function () {
+ var tokenAuth = "c4ca4238a0b923820dcc509a6f75849b",
+ viewTokenAuth = "a4ca4238a0b92382" + "0dcc509a6f75849f",
+ url = "?module=Widgetize&action=iframe&moduleToWidgetize=Referrers&idSite=1&period=year&date=2012-08-09&"
+ + "actionToWidgetize=getKeywords&viewDataTable=graphVerticalBar&isFooterExpandedInDashboard=1&";
+ const multiMetricUrl = url + "columns=nb_visits,nb_actions,bounce_rate&filter_add_columns_when_show_all_columns=0&token_auth=" + viewTokenAuth;
+
+ before(function () {
+ // use real auth + token auth to test that auth works when widgetizing reports in an iframe
+ testEnvironment.testUseMockAuth = 0;
+ testEnvironment.save();
+ });
+
+ it("should fail when admin token is used", async function () {
+ await page.goto(url + 'token_auth=' + tokenAuth);
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('load_fail_when_token_used');
+ });
+
+ it("should load correctly", async function () {
+ await page.goto(url + 'token_auth=' + viewTokenAuth);
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('load');
+ });
+
+ it("should display the metric picker when the metric picker button is clicked", async function () {
+ await page.click('.metrics-picker__toggle');
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('metric_picker_shown');
+ await page.keyboard.press('Escape');
+ });
+
+ it("should display multiple metrics when another metric picked", async function () {
+ await page.click('.metrics-picker__toggle');
+ await page.waitForSelector('.metrics-picker__options input');
+ var element = await page.jQuery('.metrics-picker__options input:not(:checked):first');
+ await element.click();
+ await page.waitForNetworkIdle();
+ await page.waitForTimeout(500);
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('other_metric');
+ });
+
+ it("should render the footer legend for bar graphs", async function () {
+ await page.goto(multiMetricUrl);
+ await page.waitForNetworkIdle();
+
+ const legendState = await page.evaluate(function () {
+ const footer = document.querySelector('.jqplot-legend-footer.has-legend');
+
+ return {
+ hasLegend: !!footer,
+ itemCount: footer ? footer.querySelectorAll('.jqplot-legend-item').length : 0,
+ swatchCount: footer ? footer.querySelectorAll('.jqplot-legend-swatch').length : 0,
+ };
+ });
+
+ expect(legendState.hasLegend).to.equal(true);
+ expect(legendState.itemCount).to.equal(3);
+ expect(legendState.swatchCount).to.equal(3);
+ });
+});
diff --git a/tests/UI/specs/Comparison_spec.js b/tests/UI/specs/Comparison_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..90254618db34650457108227aa32562d8338d297
--- /dev/null
+++ b/tests/UI/specs/Comparison_spec.js
@@ -0,0 +1,396 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Bar graph screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("Comparison", function () {
+ const generalParams = 'idSite=1&period=range&date=2012-01-12,2012-01-17',
+ urlBase = 'module=CoreHome&action=index&' + generalParams,
+ dashboardUrl = "?" + urlBase + "#?" + generalParams + "&category=Dashboard_Dashboard&subcategory=1",
+ comparePeriod = "&compareDates[]=2012-01-01,2012-01-31&comparePeriods[]=range",
+ compareSegment = "&compareSegments[]=continentCode%3D%3Deur",
+ compareParams = comparePeriod + compareSegment,
+ barGraphUrl = "?module=Widgetize&action=iframe&moduleToWidgetize=Referrers&idSite=1&period=year&date=2012-08-09&"
+ + "actionToWidgetize=getKeywords&viewDataTable=graphVerticalBar&isFooterExpandedInDashboard=1&"
+ + compareParams,
+ pieGraphUrl = "?module=Widgetize&action=iframe&moduleToWidgetize=Referrers&idSite=1&period=year&date=2012-08-09&"
+ + "actionToWidgetize=getKeywords&viewDataTable=graphPie&isFooterExpandedInDashboard=1&"
+ + compareParams,
+ goalsTableUrl = "?module=Widgetize&action=iframe&moduleToWidgetize=Referrers&idSite=1&period=year&date=2012-08-09&"
+ + "actionToWidgetize=getKeywords&viewDataTable=tableGoals&filter_limit=5&isFooterExpandedInDashboard=1" + compareParams,
+ htmlTableUrl = "?module=Widgetize&action=iframe&moduleToWidgetize=Referrers&idSite=1&period=year&date=2012-08-09&"
+ + "actionToWidgetize=getSearchEngines&viewDataTable=table&filter_limit=5&isFooterExpandedInDashboard=1" + compareParams,
+ htmlTableUrlNoPeriods = "?module=Widgetize&action=iframe&moduleToWidgetize=Referrers&idSite=1&period=year&date=2012-08-09&"
+ + "actionToWidgetize=getSearchEngines&viewDataTable=table&filter_limit=5&isFooterExpandedInDashboard=1" + compareSegment,
+ htmlTableUrlNoSegments = "?module=Widgetize&action=iframe&moduleToWidgetize=Referrers&idSite=1&period=year&date=2012-08-09&"
+ + "actionToWidgetize=getSearchEngines&viewDataTable=table&filter_limit=5&isFooterExpandedInDashboard=1" + comparePeriod,
+ visitOverviewWidget = "?module=Widgetize&action=iframe&containerId=VisitOverviewWithGraph&disableLink=0&widget=1&" +
+ "moduleToWidgetize=CoreHome&actionToWidgetize=renderWidgetContainer&disableLink=1&widget=1&" + generalParams + "&" +
+ compareParams,
+ visitOverviewSparklines = "?module=Widgetize&action=iframe&disableLink=1&widget=1&" +
+ "moduleToWidgetize=VisitsSummary&actionToWidgetize=get&forceView=1&viewDataTable=sparklines&" + generalParams + "&" +
+ compareParams,
+ visitOverviewWidgetComparePeriods = "?module=Widgetize&action=iframe&containerId=VisitOverviewWithGraph&disableLink=0&widget=1&" +
+ "moduleToWidgetize=CoreHome&actionToWidgetize=renderWidgetContainer&disableLink=1&widget=1&idSite=1&period=year&date=2012-01-12" + compareSegment,
+ visitOverviewWidgetCompareYear = "?module=Widgetize&action=iframe&containerId=VisitOverviewWithGraph&disableLink=0&widget=1&" +
+ "moduleToWidgetize=CoreHome&actionToWidgetize=renderWidgetContainer&disableLink=1&widget=1&idSite=1&period=year&date=2012-01-12&compareDates[]=2011-01-31&comparePeriods[]=year",
+ visitOverviewWidgetCompareWeekSmallRange = "?module=Widgetize&action=iframe&containerId=VisitOverviewWithGraph&disableLink=0&widget=1&" +
+ "moduleToWidgetize=CoreHome&actionToWidgetize=renderWidgetContainer&disableLink=1&widget=1&idSite=1&period=week&date=2012-01-12&compareDates[]=2012-01-20,2012-01-31&comparePeriods[]=range",
+ visitOverviewWidgetCompareLargeRange = "?module=Widgetize&action=iframe&containerId=VisitOverviewWithGraph&disableLink=0&widget=1&" +
+ "moduleToWidgetize=CoreHome&actionToWidgetize=renderWidgetContainer&disableLink=1&widget=1&idSite=1&period=range&date=2012-01-12,2014-02-12&compareDates[]=2011-01-31,2013-02-31&comparePeriods[]=range"
+ ;
+
+ async function getSparklineEvolutionForMetric(metricText) {
+ await page.waitForSelector('.sparkline .metricEvolution');
+
+ return page.evaluate(function (text) {
+ function normalize(value) {
+ return value.replace(/\s+/g, ' ').trim().toLowerCase();
+ }
+
+ var textToFind = text.toLowerCase();
+ var sparklines = Array.prototype.slice.call(document.querySelectorAll('.sparkline'));
+
+ for (var i = 0; i < sparklines.length; i++) {
+ var metrics = Array.prototype.slice.call(sparklines[i].querySelectorAll('.sparkline-metrics'));
+
+ for (var j = 0; j < metrics.length; j++) {
+ if (normalize(metrics[j].textContent).indexOf(textToFind) === -1) {
+ continue;
+ }
+
+ var evolution = metrics[j].nextElementSibling;
+ if (!evolution || !evolution.classList.contains('metricEvolution')) {
+ continue;
+ }
+
+ var value = evolution.querySelector('strong');
+ var image = evolution.querySelector('img');
+ if (!value || !image) {
+ continue;
+ }
+
+ return {
+ className: value.className,
+ image: image.getAttribute('src'),
+ text: value.textContent.trim(),
+ };
+ }
+ }
+
+ return null;
+ }, metricText);
+ }
+
+ function expectEvolutionPolarity(evolution, isLowerValueBetter) {
+ expect(evolution).to.not.equal(null);
+
+ var isPositive = evolution.className.indexOf('positive-evolution') !== -1;
+ var isNegative = evolution.className.indexOf('negative-evolution') !== -1;
+ expect(isPositive || isNegative).to.equal(true);
+
+ var isUp = evolution.image.indexOf('arrow_up') !== -1;
+ var isDown = evolution.image.indexOf('arrow_down') !== -1;
+ expect(isUp || isDown).to.equal(true);
+
+ if (isUp) {
+ if (isLowerValueBetter) {
+ expect(evolution.className).to.contain('negative-evolution');
+ expect(evolution.image).to.contain('arrow_up_red.svg');
+ } else {
+ expect(evolution.className).to.contain('positive-evolution');
+ expect(evolution.image).to.contain('arrow_up.svg');
+ expect(evolution.image).to.not.contain('arrow_up_red.svg');
+ }
+ }
+
+ if (isDown) {
+ if (isLowerValueBetter) {
+ expect(evolution.className).to.contain('positive-evolution');
+ expect(evolution.image).to.contain('arrow_down_green.svg');
+ } else {
+ expect(evolution.className).to.contain('negative-evolution');
+ expect(evolution.image).to.contain('arrow_down.svg');
+ expect(evolution.image).to.not.contain('arrow_down_green.svg');
+ }
+ }
+ }
+
+ it('should compare periods correctly when comparing the last period', async () => {
+ await page.goto(dashboardUrl);
+ await page.waitForNetworkIdle();
+
+ await page.click('#periodString #date');
+
+ await page.waitForSelector('input#comparePeriodTo', { visible: true });
+ await page.click('input#comparePeriodTo + span');
+
+ await page.click('#calendarApply');
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('.widget');
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('.piwik-graph');
+ await page.waitForNetworkIdle();
+
+ const pageWrap = await page.$('.pageWrap');
+ expect(await pageWrap.screenshot()).to.matchImage('dashboard_last_period');
+ });
+
+ it('should add a segment comparison when the compare icon in the segment list is clicked', async () => {
+ await page.click('.segmentationContainer');
+ await (await page.jQuery('li[data-idsegment=2] .compareSegment', { waitFor: true })).click();
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('.widget');
+ await page.waitForNetworkIdle();
+
+ const pageWrap = await page.$('.pageWrap');
+ expect(await pageWrap.screenshot()).to.matchImage('dashboard_last_period_and_segment');
+ });
+
+ it('should not show comparisons for pages that do not support it', async () => {
+ await (await page.jQuery('li.menuTab:contains(Behaviour) > a')).click();
+ await page.waitForTimeout(100);
+ await (await page.jQuery('a.item:contains(Transitions)')).click();
+ await page.waitForNetworkIdle();
+
+ const pageWrap = await page.$('.pageWrap');
+ expect(await pageWrap.screenshot()).to.matchImage('transitions');
+ });
+
+ it('should show extra serieses when comparing in evolution graphs and sparklines', async () => {
+ await (await page.jQuery('li.menuTab:contains(Visitors) > a')).click();
+ await page.waitForTimeout(100);
+ await (await page.jQuery('li.menuTab:contains(Visitors) a.item:contains(Overview)')).click();
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('.piwik-graph');
+ await page.waitForSelector('.matomo-comparisons');
+
+ const pageWrap = await page.$('.pageWrap');
+ expect(await pageWrap.screenshot()).to.matchImage('visitors_overview');
+ });
+
+ it('should change the evolution series when the sparkline is clicked', async () => {
+ await (await page.jQuery('.sparkline:contains(pageviews):eq(0)')).click();
+ await page.waitForNetworkIdle();
+
+ await page.mouse.move(-10, -10);
+
+ const pageWrap = await page.$('.pageWrap');
+ expect(await pageWrap.screenshot()).to.matchImage('visitors_overview_switched');
+ });
+
+ it('should show the tooltip correctly in an evolution graph', async () => {
+ await page.hover('.piwik-graph');
+ await page.waitForTimeout(250);
+
+ const element = await page.$('.ui-tooltip');
+ expect(await element.screenshot()).to.matchImage('visitors_overview_tooltip');
+ });
+
+ it('should remove segment comparison when the x button is clicked', async () => {
+ await page.click('.card.comparison .remove-button');
+ await page.waitForNetworkIdle();
+
+ await page.mouse.move(-10, -10);
+
+ const pageWrap = await page.$('.pageWrap');
+ expect(await pageWrap.screenshot()).to.matchImage('visitors_overview_segment_removed');
+ });
+
+ it('should remove period comparison if period is selected w/o compare set', async () => {
+ await page.click('#periodString .periodSelector');
+ await page.waitForSelector('input#comparePeriodTo', { visible: true });
+ await page.click('input#comparePeriodTo + span');
+
+ await page.click('#calendarApply');
+ await page.waitForNetworkIdle();
+
+ await page.mouse.move(-10, -10);
+
+ const pageWrap = await page.$('.pageWrap');
+ expect(await pageWrap.screenshot()).to.matchImage('visitors_overview_no_compare');
+ });
+
+ it('should show the bar graph correctly when comparing segments and period', async () => {
+ await page.goto(barGraphUrl);
+ await page.waitForNetworkIdle();
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('bar_graph');
+ });
+
+ it('should show the pie graph correctly when comparing segments and period', async () => {
+ await page.goto(pieGraphUrl);
+ await page.waitForNetworkIdle();
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('pie_graph');
+ });
+
+ it('should show the normal html table correctly when comparing segments and periods', async () => {
+ await page.goto(htmlTableUrl);
+ await page.waitForNetworkIdle();
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('normal_table');
+ });
+
+ it('should show the correct percentages and tooltip during comparison', async () => {
+ const element = await page.jQuery('span.ratio:visible:eq(1)');
+ await element.hover();
+ const tooltip = await page.waitForSelector('.ui-tooltip', { visible: true });
+ expect(await tooltip.screenshot()).to.matchImage('totals_tooltip');
+ });
+
+ it('should show the normal html table correctly when comparing segments but not periods', async () => {
+ await page.goto(htmlTableUrlNoPeriods);
+ await page.mouse.move(-10, -10); // mae sure no row is highlighted
+ await page.waitForNetworkIdle();
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('normal_table_no_periods');
+ });
+
+ it('should show the normal html table correctly when comparing periods but not segments', async () => {
+ await page.goto(htmlTableUrlNoSegments);
+ await page.waitForNetworkIdle();
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('normal_table_no_segments');
+ });
+
+ it('should expand subtables correctly when comparing', async () => {
+ (await page.$$('tr.subDataTable'))[0].click();
+ await page.waitForNetworkIdle();
+
+ await page.waitForFunction(function () {
+ return $('.cellSubDataTable > .dataTable').length === 1;
+ });
+
+ await page.mouse.move(-10, -10); // make sure no row is highlighted
+ await page.waitForTimeout(250);
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('subtables_loaded');
+ });
+
+ it('should advance to the next page when paginating the subtable', async () => {
+ await page.click('.cellSubDataTable .dataTableNext');
+ await page.waitForNetworkIdle();
+
+ await page.mouse.move(-10, -10); // make sure no row is highlighted
+ await page.waitForTimeout(250);
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('subtables_paginate');
+ });
+
+ it('should show the row evolution popup for the compared row/segment/period when clicked', async () => {
+ const row = await page.jQuery('tbody tr.comparisonRow:visible:eq(1)');
+ await row.hover();
+
+ const icon = await page.jQuery('tbody tr.comparisonRow:visible:eq(1) a.actionRowEvolution');
+ await icon.click();
+
+ await page.waitForSelector('.ui-dialog');
+ await page.waitForNetworkIdle();
+
+ await page.mouse.move(-10, -10);
+
+ const dialog = await page.$('.ui-dialog');
+ expect(await dialog.screenshot()).to.matchImage('row_evolution');
+ });
+
+ it('should show the multirow evolution popup for another comparison series', async () => {
+ await page.click('.rowevolution-startmulti');
+ await page.waitForTimeout(250);
+
+ const row = await page.jQuery('tbody tr.comparisonRow:visible:eq(0)');
+ await row.hover();
+
+ const icon = await page.jQuery('tbody tr.comparisonRow:visible:eq(0) a.actionRowEvolution');
+ await icon.click();
+
+ await page.waitForSelector('.ui-dialog');
+ await page.waitForNetworkIdle();
+
+ await page.mouse.move(-10, -10);
+
+ const dialog = await page.$('.ui-dialog');
+ expect(await dialog.screenshot()).to.matchImage('multi_row_evolution');
+ });
+
+ it('should show the segmented visitor log popup for the compared row/segment/period when clicked', async () => {
+ await page.click('.ui-dialog-titlebar-close');
+
+ const row = await page.jQuery('tbody tr.comparisonRow:eq(1)');
+ await row.hover();
+
+ const icon = await page.jQuery('tbody tr.comparisonRow:eq(1) a.actionSegmentVisitorLog');
+ await icon.click();
+
+ await page.waitForSelector('.ui-dialog');
+ await page.waitForNetworkIdle();
+
+ await page.mouse.move(-10, -10);
+
+ const dialog = await page.$('.ui-dialog');
+ expect(await dialog.screenshot()).to.matchImage('segmented_visitorlog');
+ });
+
+ it('should show the goals overview table correctly when comparing segments and period', async () => {
+ await page.goto(goalsTableUrl);
+ await page.waitForNetworkIdle();
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('goals_table');
+ });
+
+ it('should show a specific goals table correctly when comparing segments and period', async () => {
+ await page.goto(goalsTableUrl + '&idGoal=1');
+ await page.waitForNetworkIdle();
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('goals_table_specific');
+ });
+
+ it('should load a widgetized sparklines visualization correctly', async () => {
+ await page.goto(visitOverviewWidget);
+ await page.waitForNetworkIdle();
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('visits_overview_widget');
+ });
+
+ it('should load a widgetized sparklines visualization correctly when comparing two years', async () => {
+ await page.goto(visitOverviewWidgetCompareYear);
+ await page.waitForNetworkIdle();
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('visits_overview_widget_year');
+ });
+
+ it('should load a widgetized sparklines visualization correctly when comparing two segments over a year', async () => {
+ await page.goto(visitOverviewWidgetComparePeriods);
+ await page.waitForNetworkIdle();
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('visits_overview_widget_compareperiod_year');
+ });
+
+ it('should load a widgetized sparklines visualization correctly when comparing a week with a small range', async () => {
+ await page.goto(visitOverviewWidgetCompareWeekSmallRange);
+ await page.waitForNetworkIdle();
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('visits_overview_widget_week_smallrange');
+ });
+
+ it('should load a widgetized sparklines visualization correctly when comparing large ranges', async () => {
+ await page.goto(visitOverviewWidgetCompareLargeRange);
+ await page.waitForNetworkIdle();
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('visits_overview_widget_largerange');
+ });
+
+ it('should apply lower-is-better polarity to comparison sparkline evolution', async () => {
+ await page.goto(visitOverviewSparklines);
+ await page.waitForNetworkIdle();
+
+ var visitsEvolution = await getSparklineEvolutionForMetric('visits');
+ expectEvolutionPolarity(visitsEvolution, false);
+
+ var bounceEvolution = await getSparklineEvolutionForMetric('bounced');
+ expectEvolutionPolarity(bounceEvolution, true);
+ });
+
+ it('should show evolution metrics correctly formatted in other language', async () => {
+ await page.goto(visitOverviewSparklines + '&language=sv');
+ await page.waitForNetworkIdle();
+ await page.evaluate(function(){
+ // replace all metric names with `metric name` to avoid test failures when metric translation changes
+ $('.sparkline-metrics').each(function(){ $(this).html($(this).find('strong').prop('outerHTML') + ' metric name') });
+ });
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('visits_overview_widget_sv');
+ });
+});
diff --git a/tests/UI/specs/DisablePluginArchive_spec.js b/tests/UI/specs/DisablePluginArchive_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..1ec9034014cf8794e0449e510f77aeaedadd3089
--- /dev/null
+++ b/tests/UI/specs/DisablePluginArchive_spec.js
@@ -0,0 +1,31 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Tests that theming works.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("DisablePluginArchive", function () {
+
+ this.fixture = 'Piwik\\Tests\\Fixtures\\DisablePluginArchive';
+
+ before(function () {
+ const firefoxUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 11.2; rv:85.0) Gecko/20100101 Firefox/85.0";
+ page.setUserAgent(firefoxUserAgent);
+ });
+
+ after(async () => {
+ await page.setUserAgent(page.originalUserAgent);
+ });
+
+ var url = "?module=Widgetize&action=iframe&moduleToWidgetize=Referrers&idSite=1&period=year&date=2012-08-09&"
+ + "actionToWidgetize=getKeywords&viewDataTable=table&filter_limit=5&isFooterExpandedInDashboard=1&segment=actions%3D%3D1";
+
+
+ it("should show plugin disable text", async function () {
+ await page.goto(url);
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('referrer_disabled');
+ });
+});
diff --git a/tests/UI/specs/EmptySite_spec.js b/tests/UI/specs/EmptySite_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..e220d57a854b82520ee42a4af149d0da30a44166
--- /dev/null
+++ b/tests/UI/specs/EmptySite_spec.js
@@ -0,0 +1,257 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Screenshot integration tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("EmptySite", function () {
+
+ this.fixture = "Piwik\\Tests\\Fixtures\\EmptySite";
+
+ const generalParams = 'idSite=1&period=day&date=2010-01-03';
+ const urlToTest = "?" + generalParams + "&module=CoreHome&action=index";
+
+ before(function () {
+ testEnvironment.detectedContentDetections = [];
+ testEnvironment.connectedConsentManagers = [];
+ testEnvironment.save();
+ });
+
+ after(function () {
+ // unset all detections so fake class is no longer used
+ delete testEnvironment.detectedContentDetections;
+ delete testEnvironment.connectedConsentManagers;
+ testEnvironment.save();
+ });
+
+ async function makeTrackingCodeStatic() {
+ await page.waitForNetworkIdle();
+ await page.evaluate(function () {
+ // ensure hostname and container id is always the same
+ var selector = $('.codeblock, [vue-directive="CoreHome.CopyToClipboard"]');
+ selector.each(function(){
+ let $elem = $(this);
+ $elem.text($elem.text().replace(/u="\/\/(.*)";/g, 'u="//localhost/tests/PHPUnit/proxy/";'));
+ $elem.text($elem.text().replace(/host='http\/\/(.*)',/g, 'host=\'http://localhost/tests/PHPUnit/proxy/\','));
+ $elem.text($elem.text().replace(/http(.*)container_(.*).js/g, 'http://localhost/js/container_test123.js'));
+ $elem.text($elem.text().replace(new RegExp('https?://' + document.location.host + '/'), 'http://localhost/'));
+ });
+ });
+ }
+
+ it('should initially show the no data page overview', async function () {
+ await page.goto(urlToTest);
+ await page.waitForSelector('#start-tracking-method-list'); // wait till list is shown
+
+ const pageElement = await page.$('.page');
+ expect(await pageElement.screenshot()).to.matchImage('emptySiteDashboard');
+ });
+
+ it('should show the tracking code when selected', async function () {
+ await page.evaluate(() => $('#start-tracking-detection a[href="#matomo"]')[0].click());
+
+ // wait till url check field is filled with data, which means loading has finished.
+ await page.waitForFunction(() => $('#baseUrl').val());
+ await makeTrackingCodeStatic();
+
+ const pageElement = await page.$('.page');
+ expect(await pageElement.screenshot()).to.matchImage('trackingCode');
+ });
+
+ it('should show the advanced tracking options when clicked', async function () {
+ await page.evaluate(() => $('.advance-option a').click());
+
+ // wait till checkbox isn't disabled anymore, which means loading has finished.
+ await page.waitForFunction(() => !$('#javascript-tracking-all-subdomains').is(':disabled'));
+
+ const pageElement = await page.$('.page');
+ expect(await pageElement.screenshot()).to.matchImage('showAdvancedTrackingOptions');
+ });
+
+ it('should hide the advanced tracking options when clicked', async function () {
+ await page.evaluate(() => $('.advance-option a').click());
+
+ await page.waitForSelector('#javascript-advanced-options', {hidden: true, timeout: 1000});
+ });
+
+ it('should show SPA/PWA details when clicked', async function () {
+ await page.click('#start-tracking-back');
+ await page.evaluate(() => $('#start-tracking-method-list a[href="#spapwa"]')[0].click());
+
+ await makeTrackingCodeStatic();
+
+ const pageElement = await page.$('.page');
+ expect(await pageElement.screenshot()).to.matchImage('spa_pwa');
+ });
+
+ it('should show the Other methods when clicked', async function () {
+ await page.click('#start-tracking-back');
+ await page.evaluate(() => $('#start-tracking-method-list a[href="#other"]')[0].click());
+ await makeTrackingCodeStatic();
+
+ const pageElement = await page.$('.page');
+ expect(await pageElement.screenshot()).to.matchImage('other');
+ });
+
+ it('should show the google tag manager details when clicked', async function () {
+ await page.click('#start-tracking-back');
+ await page.evaluate(() => $('#start-tracking-method-list a[href="#googletagmanager"]')[0].click());
+ await makeTrackingCodeStatic();
+
+ const pageElement = await page.$('.page');
+ expect(await pageElement.screenshot()).to.matchImage('gtm');
+ });
+
+ it('should show the wordpress details when clicked', async function () {
+ await page.click('#start-tracking-back');
+ await page.evaluate(() => $('#start-tracking-method-list a[href="#wordpress"]')[0].click());
+
+ // wait till url check field is filled with data, which means loading has finished.
+ await page.waitForFunction(() => $('#baseUrl').val());
+ await makeTrackingCodeStatic();
+
+ const pageElement = await page.$('.page');
+ expect(await pageElement.screenshot()).to.matchImage('wordpress');
+ });
+
+ it('should show the vue js details when clicked', async function () {
+ await page.click('#start-tracking-back');
+ await page.evaluate(() => $('#start-tracking-method-list a[href="#vuejs"]')[0].click());
+ await makeTrackingCodeStatic();
+
+ const pageElement = await page.$('.page');
+ expect(await pageElement.screenshot()).to.matchImage('vuejs');
+ });
+
+ it('should directly show details when provided as url param', async function () {
+ testEnvironment.detectedContentDetections = ['GoogleTagManager', 'WordPress'];
+ testEnvironment.connectedConsentManagers = [];
+ testEnvironment.save();
+
+ await page.goto(urlToTest + "#?" + generalParams + "&activeTab=integrations");
+ await page.waitForSelector('#start-tracking-details'); // wait till details ar shown
+ await makeTrackingCodeStatic();
+
+ const pageElement = await page.$('.page');
+ expect(await pageElement.screenshot()).to.matchImage('integrations');
+ });
+
+ // take one full screenshot when a detected method is shown
+ it('should suggest wordpress method when detected, other detections should be shown first', async function () {
+ testEnvironment.detectedContentDetections = ['WordPress', 'VueJs'];
+ testEnvironment.connectedConsentManagers = [];
+ testEnvironment.save();
+
+ await page.goto('about:blank');
+ await page.goto(urlToTest);
+ await page.waitForSelector('#start-tracking-method-list'); // wait till list is shown
+
+ const pageElement = await page.$('.page');
+ expect(await pageElement.screenshot()).to.matchImage('detected_wordpress');
+ });
+
+ // only take shots from recommended method for other detections
+ it('should prefer gtm method over others when detected', async function () {
+ testEnvironment.detectedContentDetections = ['GoogleTagManager', 'WordPress', 'VueJs'];
+ testEnvironment.connectedConsentManagers = [];
+ testEnvironment.save();
+
+ await page.goto('about:blank');
+ await page.goto(urlToTest);
+ await page.waitForSelector('#start-tracking-method-list'); // wait till list is shown
+
+ const pageElement = await page.$('#start-tracking-detection');
+ expect(await pageElement.screenshot()).to.matchImage('detected_gtm');
+ });
+
+ it('should should show a notification on the tracking code screen when a consent manager is detected', async function () {
+ testEnvironment.detectedContentDetections = ['Osano'];
+ testEnvironment.connectedConsentManagers = ['Osano'];
+ testEnvironment.save();
+
+ await page.goto('about:blank');
+ await page.goto(urlToTest);
+ await page.waitForSelector('#start-tracking-method-list'); // wait till list is shown
+
+ await page.evaluate(() => $('#start-tracking-detection a[href="#matomo"]')[0].click());
+
+ // wait till url check field is filled with data, which means loading has finished.
+ await page.waitForFunction(() => $('#baseUrl').val());
+ await makeTrackingCodeStatic();
+
+ const pageElement = await page.$('.page');
+ expect(await pageElement.screenshot()).to.matchImage('detected_osano');
+ });
+
+
+ it.skip('should have button to send tracking code to developer', async function () {
+ var mailtoLink = await page.$eval('.emailTrackingCode', link => link.getAttribute('href'));
+
+ // Check that it's a mailto link with correct subject line
+ expect(mailtoLink).to.include('mailto:?subject=Matomo%20Analytics%20Tracking%20Code&body');
+ // Check that template rendered and only contains chars that are OK in all mail clients (e.g. no HTML at all)
+ expect(mailtoLink).to.match(/^mailto:\?[a-zA-Z0-9&%=.,_*()\[\]'"-]*$/);
+ });
+
+ // switch reporting groups the way the top-menu links do: only the group is set in the hash,
+ // the section's category/subcategory are then resolved client-side by the reporting menu.
+ async function switchReportingGroup(group) {
+ await page.evaluate((targetGroup) => {
+ const hash = { ...window.CoreHome.MatomoUrl.hashParsed.value };
+ delete hash.category;
+ delete hash.subcategory;
+ hash.group = targetGroup;
+ window.CoreHome.MatomoUrl.updateHash(hash);
+ }, group);
+ }
+
+ it('should reload the tracker-setup screen when returning to it from another section', async function () {
+ await page.goto('about:blank');
+ await page.goto(urlToTest);
+ await page.waitForSelector('#start-tracking-method-list', { visible: true });
+
+ // AI Insights is exempt from the tracker-setup gate, so its reporting loads normally
+ await switchReportingGroup('CoreHome_AIInsights');
+ await page.waitForFunction(() => document.body && document.body.id !== 'site-without-data');
+
+ // a transient notification raised while in the other section must not leak back
+ await page.evaluate(() => window.CoreHome.NotificationsStore.show({
+ id: 'EmptySiteRegressionNotification',
+ message: 'notification from another section',
+ context: 'info',
+ type: 'transient',
+ }));
+ await page.waitForFunction(
+ () => document.body.innerText.indexOf('notification from another section') !== -1
+ );
+
+ // back to the default (Analytics) group: the gate must return with its methods reloaded
+ await switchReportingGroup('');
+ await page.waitForFunction(() => document.body && document.body.id === 'site-without-data');
+ await page.waitForSelector('#start-tracking-method-list', { visible: true });
+
+ const methodCount = await page.evaluate(() => $('#start-tracking-method-list .list-entry').length);
+ expect(methodCount).to.be.greaterThan(0);
+
+ // and the transient notification from the other section must have been cleared
+ await page.waitForFunction(
+ () => document.body.innerText.indexOf('notification from another section') === -1
+ );
+ });
+
+ it('should be possible to ignore this screen for one hour', async function () {
+ await page.goto('about:blank');
+ await page.goto(urlToTest);
+
+ await page.click('.ignoreSitesWithoutData');
+ await page.waitForSelector('#dashboardWidgetsArea');
+ await page.waitForNetworkIdle();
+
+ // ensure dashboard widgets are loaded
+ const widgetsCount = await page.evaluate(() => $('.widget').length);
+ expect(widgetsCount).to.be.greaterThan(1);
+ });
+});
diff --git a/tests/UI/specs/EvolutionGraph_spec.js b/tests/UI/specs/EvolutionGraph_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..553ff1431c2563fa775439242628cc64637e873d
--- /dev/null
+++ b/tests/UI/specs/EvolutionGraph_spec.js
@@ -0,0 +1,402 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * evolution graph screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("EvolutionGraph", function () {
+ const url = "?module=Widgetize&action=iframe&idSite=1&period=day&date=2012-01-31&evolution_day_last_n=30"
+ + "&moduleToWidgetize=UserCountry&actionToWidgetize=getCountry&viewDataTable=graphEvolution"
+ + "&isFooterExpandedInDashboard=1";
+ const multiMetricColumns = "nb_visits,nb_actions,avg_time_on_site,bounce_rate";
+ const multiMetricUrl = url + "&columns=" + multiMetricColumns + "&filter_add_columns_when_show_all_columns=0";
+ const setThemeMode = async function (themeMode) {
+ await page.evaluate((mode) => {
+ window.piwik.setThemeMode(mode);
+ }, themeMode);
+ await page.waitForFunction((mode) => window.piwik.getThemeMode() === mode, {}, themeMode);
+ };
+ const getResolvedBackgroundColor = async function (selector) {
+ return page.evaluate(function (targetSelector) {
+ var target = document.querySelector(targetSelector);
+ var dataTable = target && target.closest('.dataTable');
+ var uiControlObject = dataTable ? $(dataTable).data('uiControlObject') : null;
+ var configuredColor = (uiControlObject
+ && uiControlObject.jqplotParams
+ && uiControlObject.jqplotParams.grid
+ && uiControlObject.jqplotParams.grid.background)
+ || '#ffffff';
+ var colorProbe = document.createElement('div');
+
+ colorProbe.style.display = 'none';
+ colorProbe.style.color = configuredColor;
+ document.body.appendChild(colorProbe);
+
+ var resolvedColor = window.getComputedStyle(colorProbe).color;
+ colorProbe.remove();
+
+ return resolvedColor || 'rgb(255, 255, 255)';
+ }, selector);
+ };
+ const getImagePixelColor = async function (selector, offset) {
+ return page.evaluate(async function (targetSelector, pixelOffset) {
+ const exportImage = document.querySelector(targetSelector);
+ const image = new Image();
+ image.src = exportImage.src;
+
+ await new Promise((resolve, reject) => {
+ image.onload = resolve;
+ image.onerror = reject;
+ });
+
+ const canvas = document.createElement('canvas');
+ canvas.width = image.width;
+ canvas.height = image.height;
+
+ const context = canvas.getContext('2d');
+ context.drawImage(image, 0, 0);
+
+ const pixel = context.getImageData(
+ image.width - pixelOffset,
+ image.height - pixelOffset,
+ 1,
+ 1
+ ).data;
+
+ return 'rgb(' + pixel[0] + ', ' + pixel[1] + ', ' + pixel[2] + ')';
+ }, selector, offset);
+ };
+ const getFooterLegendState = async function () {
+ return page.evaluate(function () {
+ const footer = document.querySelector('.jqplot-legend-footer.has-legend');
+ const items = footer ? footer.querySelectorAll('.jqplot-legend-item') : [];
+ const hiddenItems = footer ? footer.querySelectorAll('.jqplot-legend-item-hidden') : [];
+ const overflowItem = footer ? footer.querySelector('.jqplot-legend-item-overflow .jqplot-legend-label') : null;
+ const labels = footer
+ ? Array.prototype.map.call(
+ footer.querySelectorAll('.jqplot-legend-item .jqplot-legend-label'),
+ function (label) { return label.textContent.trim(); }
+ )
+ : [];
+
+ return {
+ hasLegend: !!footer,
+ itemCount: items.length,
+ hiddenItemCount: hiddenItems.length,
+ visibleItemCount: footer ? footer.querySelectorAll('.jqplot-legend-item:not(.jqplot-legend-item-hidden)').length : 0,
+ overflowLabel: overflowItem ? overflowItem.textContent.trim() : null,
+ labels: labels,
+ };
+ });
+ };
+
+ before(function () {
+ return testEnvironment.callApi("Annotations.deleteAll", {idSite: 3});
+ });
+
+ afterEach(async function () {
+ await setThemeMode('light');
+ });
+
+ it("should load correctly", async function () {
+ await page.goto(url);
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('initial');
+ });
+
+ it("should show percent metrics like bounce rate correctly", async function () {
+ await page.goto(url + "&columns=nb_visits,bounce_rate,avg_time_on_site&filter_add_columns_when_show_all_columns=0");
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('bounce_rate');
+ });
+
+ it("should show only one series when a label is specified", async function () {
+ await page.goto(url + "&label=Canada");
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('one_series');
+ });
+
+ it("should display the metric picker when the metric picker button is clicked", async function () {
+ await page.click('.metrics-picker__toggle');
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('metric_picker_shown');
+ await page.keyboard.press('Escape');
+ });
+
+ it("should show multiple metrics when another metric picked", async function () {
+ await page.click('.metrics-picker__toggle');
+ await page.waitForSelector('.metrics-picker__options input');
+ const element = await page.jQuery('.metrics-picker__options input:not(:checked):first');
+ await element.click();
+ await page.waitForNetworkIdle();
+ await page.waitForTimeout(250);
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('two_metrics');
+ });
+
+ it("should show graph as image when export as image icon clicked", async function () {
+ await page.click('#dataTableFooterExportAsImageIcon');
+ await page.waitForNetworkIdle();
+
+ const dialog = await page.$('.ui-dialog');
+ expect(await dialog.screenshot()).to.matchImage('export_image');
+ });
+
+ it("should display more periods when limit selection changed", async function () {
+ const element = await page.jQuery('.ui-dialog .ui-widget-header button:visible');
+ await element.click();
+
+ await page.click('.limitSelection input');
+ await page.evaluate(function () {
+ $('.limitSelection ul li:contains(60) span').click();
+ });
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('limit_changed');
+ });
+
+ // annotations tests
+ it("should show annotations when annotation icon on x-axis clicked", async function () {
+ await page.click('.limitSelection input');
+ await page.evaluate(function () {
+ $('.limitSelection ul li:contains(30) span').click(); // change limit back
+ });
+ await page.waitForNetworkIdle();
+
+ const element = await page.jQuery('.evolution-annotations>span[data-count!=0]');
+ await element.click();
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('annotations_single_period');
+ });
+
+ it("should show all annotations when annotations footer link clicked", async function () { // TODO: fails
+ await page.click('.annotationView');
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('annotations_all');
+ });
+
+ it("should show no annotations message when no annotations for site", async function () {
+ await page.goto(page.url().replace(/idSite=[^&]*/, "idSite=3") + "&columns=nb_visits");
+ await page.click('.annotationView');
+ await page.waitForNetworkIdle();
+ await page.mouse.move(-10, -10);
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('annotations_none');
+ });
+
+ it("should show add annotation form when create annotation clicked", async function () {
+ await page.click('.add-annotation');
+ await page.click('.annotation-period-edit>a');
+ await page.evaluate(function () {
+ $('.datepicker').datepicker("setDate", new Date(2012,0,2) );
+ $(".ui-datepicker-current-day").trigger("click"); // this triggers onSelect event which sets .annotation-period-edit>a
+ });
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('new_annotation_form');
+ });
+
+ it("should add new annotation when create annotation submitted", async function () {
+ await page.focus('.new-annotation-edit');
+ await page.keyboard.type('new annotation');
+ await page.click('.annotation-period-edit>a');
+ await page.evaluate(function () {
+ $('.ui-datepicker-calendar td a:contains(15)').click();
+ });
+ await page.waitForNetworkIdle();
+ await page.click('.annotation-list-range');
+ await page.click('input.new-annotation-save');
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('new_annotation_submit');
+ });
+
+ it("should star annotation when star image clicked", async function () {
+ await page.click('.annotation-star');
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('annotation_starred');
+ });
+
+ it("should show edit annotation form", async function () {
+ await page.click('.edit-annotation');
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('annotation_edit_form');
+ });
+
+ it("should edit annotation when edit form submitted", async function () {
+ await page.focus('.annotation-edit');
+ await page.keyboard.type('edited annotation');
+ await page.click('.annotation-period-edit>a');
+ await page.evaluate(function () {
+ $('.annotation-meta .ui-datepicker-calendar td a:contains(16)').click();
+ });
+ await page.waitForNetworkIdle();
+ await page.click('.annotation-list-range');
+ await page.click('input.annotation-save');
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('annotation_edit_submit');
+ });
+
+ it("should delete annotation when delete link clicked", async function () {
+ await page.click('.edit-annotation');
+ await page.waitForFunction("$('.delete-annotation:visible').length > 0");
+ await page.evaluate(function () {
+ $('.delete-annotation').click();
+ });
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('annotations_none');
+ });
+
+ it("should cutout two labels so all can fit on screen", async function () {
+ await page.webpage.setViewport({ width: 320, height: 320 });
+ await page.goto(url.replace(/idSite=[^&]*/, "idSite=3") + "&columns=nb_visits");
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('label_ticks_cutout');
+ });
+
+ it("should show available periods", async function () {
+ await page.webpage.setViewport({
+ width: 1350,
+ height: 768,
+ });
+ await page.reload();
+ await page.waitForNetworkIdle();
+ await (await page.jQuery('.activatePeriodsSelection:last')).click();
+
+ await page.mouse.move(-10, -10);
+ await page.waitForTimeout(500); // wait for animation
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('periods_list');
+ });
+
+ it("should be possible to change period", async function () {
+ await (await page.jQuery('[data-period=month]:last')).click();
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('periods_selected');
+ });
+
+ it("should not show add annotation form for user with view access", async function () {
+ testEnvironment.idSitesViewAccess = [1];
+ testEnvironment.testUseMockAuth = 1;
+ testEnvironment.save();
+
+ await page.goto(url);
+ await page.waitForNetworkIdle();
+ await page.click('.annotationView');
+ await page.waitForNetworkIdle();
+
+ // check that add annotation link is not shown
+ const element = await page.$('.add-annotation');
+ expect(element).to.be.not.ok;
+ });
+
+ describe("footer legend", function () {
+ before(function () {
+ delete testEnvironment.idSitesViewAccess;
+ testEnvironment.testUseMockAuth = 1;
+ testEnvironment.save();
+ });
+
+ it("should render the evolution graph footer legend with all selected metrics", async function () {
+ await page.webpage.setViewport({ width: 1350, height: 768 });
+ await page.goto(multiMetricUrl);
+ await page.waitForNetworkIdle();
+
+ const legendState = await getFooterLegendState();
+ expect(legendState.hasLegend).to.equal(true);
+
+ // getCountry plots every row (country) for each selected metric, so the legend
+ // labels look like "United States (Visits)". Assert that every selected metric is
+ // represented, regardless of how many rows the fixture happens to contain.
+ const selectedMetricLabels = ['Visits', 'Actions', 'Avg. Time on Website', 'Bounce Rate'];
+ selectedMetricLabels.forEach(function (metricLabel) {
+ const isPresent = legendState.labels.some(function (label) {
+ return label.indexOf('(' + metricLabel + ')') !== -1;
+ });
+ expect(isPresent, 'legend should include a "' + metricLabel + '" series').to.equal(true);
+ });
+ });
+
+ it("should export the graph image using the active dark theme background", async function () {
+ await page.goto(multiMetricUrl);
+ await page.waitForNetworkIdle();
+ await setThemeMode('dark');
+ await page.waitForTimeout(250);
+ await page.click('#dataTableFooterExportAsImageIcon');
+ await page.waitForSelector('.ui-dialog img');
+
+ expect(await getImagePixelColor('.ui-dialog img', 5))
+ .to.equal(await getResolvedBackgroundColor('.jqplot-target'));
+ });
+
+ it("should overflow footer legend labels cleanly in a narrow viewport", async function () {
+ await page.webpage.setViewport({ width: 320, height: 480 });
+ await page.goto(multiMetricUrl);
+ await page.waitForNetworkIdle();
+
+ const legendState = await getFooterLegendState();
+ expect(legendState.hasLegend).to.equal(true);
+ expect(legendState.itemCount).to.be.above(legendState.visibleItemCount);
+ expect(legendState.visibleItemCount).to.be.at.least(1);
+ expect(legendState.hiddenItemCount).to.be.above(0);
+ expect(legendState.overflowLabel).to.equal('…');
+ });
+
+ it("should use the active dark theme background for the graph loading overlay", async function () {
+ await page.webpage.setViewport({ width: 1350, height: 768 });
+ await page.goto(multiMetricUrl);
+ await page.waitForNetworkIdle();
+ await setThemeMode('dark');
+ await page.waitForTimeout(250);
+
+ await page.evaluate(function () {
+ var dataTable = $('.dataTable').data('uiControlObject');
+ var originalReloadAjaxDataTable = dataTable.reloadAjaxDataTable.bind(dataTable);
+
+ dataTable.reloadAjaxDataTable = function () {
+ return null;
+ };
+
+ dataTable.__restoreReloadAjaxDataTable = function () {
+ dataTable.reloadAjaxDataTable = originalReloadAjaxDataTable;
+ };
+ });
+
+ await page.click('.metrics-picker__toggle');
+ await page.waitForSelector('.metrics-picker__options input');
+ const element = await page.jQuery('.metrics-picker__options input:not(:checked):first');
+ await element.click();
+ await page.waitForSelector('.jqplot-loading');
+
+ expect(await page.evaluate(function () {
+ return window.getComputedStyle(document.querySelector('.jqplot-loading')).backgroundColor;
+ })).to.equal(await getResolvedBackgroundColor('.jqplot-loading'));
+ expect(await page.evaluate(function () {
+ return !!document.querySelector('.jqplot-loading .matomo-loader');
+ })).to.equal(true);
+ expect(await page.evaluate(function () {
+ return window.getComputedStyle(document.querySelector('.jqplot-loading')).opacity;
+ })).to.equal('0.7');
+
+ await page.evaluate(function () {
+ var dataTable = $('.dataTable').data('uiControlObject');
+ if (dataTable && dataTable.__restoreReloadAjaxDataTable) {
+ dataTable.__restoreReloadAjaxDataTable();
+ delete dataTable.__restoreReloadAjaxDataTable;
+ }
+ $('.jqplot-loading').remove();
+ });
+ });
+ });
+});
diff --git a/tests/UI/specs/IncompletePeriodVisualisation_spec.js b/tests/UI/specs/IncompletePeriodVisualisation_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..42b04bcc6f21f8d2ee7f9083d19d329da0c57528
--- /dev/null
+++ b/tests/UI/specs/IncompletePeriodVisualisation_spec.js
@@ -0,0 +1,37 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Incomplete Period Visualisation Test
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe('IncompletePeriodVisualisation', function () {
+ this.fixture = 'Piwik\\Tests\\Fixtures\\SomeVisitsLastYearAndThisYear';
+
+ const generalParams = 'idSite=1&period=year&date=today';
+ const pageUrl = '?module=CoreHome&action=index&' + generalParams;
+
+ it('should load visitors > overview page and show incomplete period', async function () {
+ await page.goto(pageUrl + generalParams + '&segment=&category=General_Visitors&subcategory=General_Overview#?idSite=1&period=year&date=today&segment=&category=General_Visitors&subcategory=General_Overview');
+ await page.waitForNetworkIdle();
+ const pageWrap = await page.$('.pageWrap');
+
+ expect(await pageWrap.screenshot()).to.matchImage('visitors_overview');
+ });
+
+ it('tooltip for incomplete period should say "incomplete period"', async function () {
+ const graph = await page.$('.piwik-graph');
+ const boundingBox = await graph.boundingBox();
+
+ await page.mouse.move(
+ boundingBox.x + boundingBox.width - 50,
+ boundingBox.y + boundingBox.height - 50
+ );
+
+ const tooltipContent = await page.evaluate(() => $('.ui-tooltip').text().trim());
+
+ expect(tooltipContent).to.contain('Incomplete Period');
+ });
+});
diff --git a/tests/UI/specs/InvalidatedPeriodVisualisation_spec.js b/tests/UI/specs/InvalidatedPeriodVisualisation_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..8e3da98ac85b5025cc68810c7a0cb80e6777134f
--- /dev/null
+++ b/tests/UI/specs/InvalidatedPeriodVisualisation_spec.js
@@ -0,0 +1,59 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Invalidated Period Visualisation Test
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe('InvalidatedPeriodVisualisation', function () {
+ const url = '?module=Widgetize&action=iframe&idSite=1&evolution_day_last_n=30'
+ + '&moduleToWidgetize=UserCountry&actionToWidgetize=getCountry&viewDataTable=graphEvolution'
+ + '&isFooterExpandedInDashboard=1';
+
+ before(() => {
+ testEnvironment.forceDataStates = {
+ '2012-01-08': 'invalidated',
+ '2012-01-09': 'invalidated',
+ '2012-01-10': 'invalidated',
+ // center point used for tooltip check
+ '2012-01-16': 'invalidated',
+ };
+
+ testEnvironment.save();
+ });
+
+ after(() => {
+ delete testEnvironment.forceDataStates;
+ testEnvironment.save();
+ });
+
+ it('should show invalidated data points', async function () {
+ await page.goto(url + '&period=day&date=2012-01-31');
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('graph');
+ });
+
+ it('tooltip for invalidated period should say "invalidated period"', async function () {
+ const graph = await page.$('.piwik-graph');
+ const boundingBox = await graph.boundingBox();
+
+ await page.mouse.move(
+ boundingBox.x + boundingBox.width / 2,
+ boundingBox.y + boundingBox.height / 2
+ );
+
+ const tooltipContent = await page.evaluate(() => $('.ui-tooltip').text().trim());
+
+ expect(tooltipContent).to.contain('Invalidated Period');
+ });
+
+ it('should show invalidated data points', async function () {
+ await page.goto(url + '&period=range&date=2012-01-01,2012-01-31&comparePeriods[]=range&compareDates[]=2011-12-01,2011-12-31');
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('comparison');
+ });
+});
diff --git a/tests/UI/specs/JSTracker_spec.js b/tests/UI/specs/JSTracker_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..63764fafae8fafd0a7c58e64614e3a99fcdf0f41
--- /dev/null
+++ b/tests/UI/specs/JSTracker_spec.js
@@ -0,0 +1,43 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * JS tracker UI tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("JSTracker", function () {
+ this.fixture = 'Piwik\\Tests\\Fixtures\\JSTrackingUIFixture';
+
+ before(function () {
+ testEnvironment.pluginsToLoad = ['CustomJsTracker', 'ExampleTracker'];
+ testEnvironment.save();
+ });
+
+ var testWebsiteUrl = 'tests/resources/overlay-test-site-real/index.html';
+ var generalParams = 'idSite=1&period=day&date=today',
+ widgetizeParams = "module=Widgetize&action=iframe",
+ visitorLogUrl = "?" + widgetizeParams + "&" + generalParams + "&moduleToWidgetize=Live&actionToWidgetize=getVisitorLog";
+
+ it("run correctly on a website and correctly track visits in the visitor log", async function () {
+ await page.goto(testWebsiteUrl);
+
+ // view another page
+ await page.evaluate(() => $('a:contains(Page 3)')[0].click());
+ await page.waitForNetworkIdle();
+ await page.waitForTimeout(500);
+ await page.waitForNetworkIdle();
+
+ // visit visitor log for correct date
+ await page.goto(visitorLogUrl);
+
+ await page.evaluate(function () {
+ $('.visitor-log-datetime').html('REMOVED');
+ var $e = $('.dataTableWrapper>.row>.column>strong');
+ $e.text($e.text().replace(/\d+s/, 'Ns'));
+ });
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('visitor_log');
+ });
+});
diff --git a/tests/UI/specs/MeasurableManager_spec.js b/tests/UI/specs/MeasurableManager_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f4c7e301f0a02604dcc42003e3a84b1575baa08e
--- /dev/null
+++ b/tests/UI/specs/MeasurableManager_spec.js
@@ -0,0 +1,59 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Site selector screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("MeasurableManager", function () {
+ this.fixture = "Piwik\\Plugins\\SitesManager\\tests\\Fixtures\\ManySites";
+
+ const url = "?module=SitesManager&action=index&idSite=1&period=day&date=yesterday&showaddsite=false";
+
+ before(function () {
+ testEnvironment.pluginsToLoad = ['MobileAppMeasurable'];
+ testEnvironment.save();
+ });
+
+ async function assertScreenshotEquals(screenshotName, selector) {
+ const element = await page.jQuery(selector);
+ expect(await element.screenshot()).to.matchImage(screenshotName);
+ }
+
+ it("should load correctly and should not use SitesManager wording as another type is enabled", async function () {
+ await page.goto(url);
+ await assertScreenshotEquals("loaded", '#content.admin');
+ });
+
+ it("should use measurable wording in menu", async function () {
+ const element = await page.jQuery('#secondNavBar li:contains(Tracking Code):first');
+ expect(await element.screenshot()).to.matchImage('measurable_menu_item');
+ });
+
+ // '.sitesManagerList,.sitesButtonBar,.sites-manager-header,.ui-dialog.ui-widget,.modal.open'
+ it("should show selection of available types when adding a type", async function () {
+ const element = await page.jQuery('.SitesManager .addSite:first');
+ await element.click();
+ await page.waitForSelector('.modal.open');
+ await page.waitForTimeout(350); // wait for modal animation
+ await assertScreenshotEquals("add_new_dialog", '.modal.open');
+ });
+
+ it("should load mobile app specific fields", async function () {
+ const element = await page.jQuery('.modal.open .btn:contains(Mobile App)');
+ await element.click();
+ await page.mouse.move(-10, -10);
+
+ await page.waitForSelector('input.btn[value=Save]');
+ await page.waitForNetworkIdle();
+ await page.evaluate(function () {
+ $('.form-help:contains(UTC time is)').hide();
+ });
+ await page.waitForTimeout(250);
+
+ await assertScreenshotEquals("add_measurable_view", '#content.admin');
+ });
+
+});
diff --git a/tests/UI/specs/Menus_spec.js b/tests/UI/specs/Menus_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ace36c030536f714f42c814e6035f8bb6e7d05f0
--- /dev/null
+++ b/tests/UI/specs/Menus_spec.js
@@ -0,0 +1,165 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Screenshot tests for main, top and admin menus.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("Menus", function () {
+ this.fixture = "Piwik\\Tests\\Fixtures\\ThreeGoalsOnePageview";
+
+ const generalParams = 'idSite=1&period=year&date=2009-01-04',
+ urlBase = 'module=CoreHome&action=index&' + generalParams;
+
+ async function openMenuItem(page, menuItem) {
+ const element = await page.jQuery('#secondNavBar .navbar a:contains(' + menuItem + '):visible:first');
+ await element.click();
+ await page.mouse.move(-10, -10);
+ await page.waitForTimeout(250);
+ }
+
+ async function screenshotClippedSelector(selector, maxHeight, screenshotName) {
+ const element = await page.$(selector);
+ const box = await element.boundingBox();
+
+ expect(await page.screenshot({
+ clip: {
+ x: Math.floor(box.x),
+ y: Math.floor(box.y),
+ width: Math.ceil(box.width),
+ height: Math.min(maxHeight, Math.ceil(box.height)),
+ },
+ })).to.matchImage(screenshotName);
+ }
+
+ beforeEach(function() {
+ if (testEnvironment.enableProfessionalSupportAdsForUITests) {
+ delete testEnvironment.enableProfessionalSupportAdsForUITests;
+ testEnvironment.save();
+ }
+ });
+
+ // main menu tests
+ it('should load the main reporting menu correctly', async function() {
+ await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Actions&subcategory=General_Pages");
+ await page.waitForSelector('#secondNavBar', { visible: true });
+
+ const element = await page.jQuery('#secondNavBar');
+ expect(await element.screenshot()).to.matchImage('mainmenu_loaded');
+ });
+
+ // main menu with plugin promos (reloads the previous test's page with new config)
+ it('should load the main reporting menu with plugin promos correctly', async function() {
+ testEnvironment.enableProfessionalSupportAdsForUITests = true;
+ await testEnvironment.save();
+
+ await page.reload(); // use URL from the previous test and reload to apply the config changes
+ await page.waitForSelector('#secondNavBar', { visible: true });
+
+ const element = await page.jQuery('#secondNavBar');
+ expect(await element.screenshot()).to.matchImage('mainmenu_loaded_withpromos');
+ });
+
+ it('should change the menu when a upper menu item is clicked in the main menu', async function() {
+ // reload to remove config override set by previous tests
+ await page.reload(); // use URL from the previous test and reload to apply the config changes
+ await page.waitForSelector('#secondNavBar', { visible: true });
+
+ await openMenuItem(page, 'Visitors');
+
+ const element = await page.jQuery('#secondNavBar');
+ expect(await element.screenshot()).to.matchImage('mainmenu_upper_clicked');
+ });
+
+ it('should change the menu when a lower menu item is clicked in the main menu', async function() {
+ await openMenuItem(page, 'Custom Variables');
+
+ const element = await page.jQuery('#secondNavBar');
+ expect(await element.screenshot()).to.matchImage('mainmenu_lower_clicked');
+ });
+
+ // AI Insights section
+ it('should switch to the AI Insights section when its top menu entry is clicked', async function() {
+ await page.webpage.setViewport({ width: 1500, height: 750 });
+ await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Visitors&subcategory=General_Overview");
+ await page.waitForSelector('#secondNavBar', { visible: true });
+
+ // the "AI Insights" top menu entry is tagged with its reporting group
+ await page.click('.nav-wrapper .right a[data-reporting-group="CoreHome_AIInsights"]');
+ await page.waitForNetworkIdle();
+ await page.waitForTimeout(500);
+
+ const element = await page.jQuery('#secondNavBar');
+ expect(await element.screenshot()).to.matchImage('ai_insights_menu');
+ });
+
+ // admin menu tests
+ it('should load the admin reporting menu correctly', async function() {
+ await page.webpage.setViewport({width: 1500, height: 750 });
+ await page.goto("?" + generalParams + "&module=CoreAdminHome&action=generalSettings");
+ await page.waitForSelector('#secondNavBar');
+
+ await screenshotClippedSelector('#secondNavBar', 620, 'admin_loaded');
+ });
+
+ it('should toggle the submenu visibility when main item is clicked', async function() {
+ await openMenuItem(page, 'Website');
+ await page.waitForTimeout(500); // wait for animation
+
+ await screenshotClippedSelector('#secondNavBar', 620, 'admin_websites');
+ });
+
+ it('should change the admin page correctly when an admin menu item is clicked', async function() {
+ await openMenuItem(page, 'Manage');
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('#secondNavBar');
+
+ const element = await page.jQuery('#secondNavBar');
+ expect(await element.screenshot()).to.matchImage('admin_changed');
+ });
+
+ it('should load the admin left menu correctly on mobile', async function() {
+ await page.webpage.setViewport({ width: 815, height: 1650 });
+ await page.goto("?module=CoreAdminHome&action=home");
+ await page.waitForNetworkIdle();
+ await page.click('[data-target="mobile-left-menu"]');
+ await page.waitForTimeout(150);
+ await page.click('ul#mobile-left-menu > li:nth-child(1) a');
+ await page.click('ul#mobile-left-menu > li:nth-child(2) a');
+ await page.click('ul#mobile-left-menu > li:nth-child(3) a');
+ await page.click('ul#mobile-left-menu > li:nth-child(4) a');
+ await page.click('ul#mobile-left-menu > li:nth-child(5) a');
+ await page.click('ul#mobile-left-menu > li:nth-child(6) a');
+ await page.waitForTimeout(500);
+
+ expect(await page.screenshotSelector('#mobile-left-menu', false)).to.matchImage('mobile_left_admin');
+ });
+
+ it('should load the admin top menu correctly on mobile', async function() {
+ await page.webpage.setViewport({ width: 768, height: 512 });
+ await page.reload();
+ await page.waitForNetworkIdle();
+ await page.evaluate(function(){
+ $('.activateTopMenu>span').click();
+ });
+ await page.waitForTimeout(250);
+
+ expect(await page.screenshotSelector('#mobile-top-menu', false)).to.matchImage('mobile_top');
+ });
+
+ it('should load the left reporting menu correctly on mobile', async function() {
+ await page.webpage.setViewport({ width: 768, height: 1000 });
+ await page.goto("?" + generalParams + "&module=CoreHome&action=index#?category=General_Visitors&subcategory=General_Overview");
+ await page.waitForNetworkIdle();
+ await page.evaluate(function(){
+ $('.activateLeftMenu>span').click();
+ });
+ await page.waitForTimeout(250);
+ await (await page.jQuery('#mobile-left-menu>li>ul:contains(Goals)')).click();
+ await page.waitForTimeout(500);
+
+ expect(await page.screenshotSelector('#mobile-left-menu', false)).to.matchImage('mobile_left');
+ });
+});
diff --git a/tests/UI/specs/OneClickLastForcedUpdate_spec.js b/tests/UI/specs/OneClickLastForcedUpdate_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..290a2e5de77c4890fefccce0e809405e5caa53de
--- /dev/null
+++ b/tests/UI/specs/OneClickLastForcedUpdate_spec.js
@@ -0,0 +1,150 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * OneClickUpdate screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+var fs = require('fs'),
+ path = require('../../lib/screenshot-testing/support/path');
+
+const request = require('request-promise');
+const exec = require('child_process').exec;
+
+describe("OneClickLastForcedUpdate", function () {
+ this.fixture = "Piwik\\Tests\\Fixtures\\LastForcedInstall";
+
+ var latestStableUrl = config.piwikUrl + '/latestStableInstall/index.php';
+ var settingsUrl = latestStableUrl + '?module=CoreAdminHome&action=home&idSite=1&period=day&date=yesterday';
+ var pluginsUrl = latestStableUrl + '?module=CorePluginsAdmin&action=plugins';
+
+ it('should show the new version available button in the admin screen', async function () {
+ await page.goto(latestStableUrl);
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('#login_form_login', { visible: true });
+
+ await page.type('#login_form_login', superUserLogin);
+ await page.type('#login_form_password', superUserPassword);
+ await page.evaluate(function(){
+ $('#login_form_submit').click();
+ });
+
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('.pageWrap');
+
+ await page.goto(settingsUrl);
+ await page.waitForNetworkIdle();
+
+ const element = await page.waitForSelector('#header_message', { visible: true });
+
+ expect(await element.screenshot()).to.matchImage('latest_version_available');
+ });
+
+ it('should show the one click update screen when the update button is clicked', async function () {
+ await page.click('#header_message');
+
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('.content');
+
+ await page.evaluate(() => {
+ $('.content').html($('.content').html().replace(/requires Matomo <([0-9])\.[0-9]+\.[0-9]+(?:-(?:rc|b)[0-9]+)?/i, 'requires Matomo <$1.99.99'));
+ });
+
+ await page.waitForTimeout(100);
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('update_screen');
+ });
+
+ it('should fail to automatically update when trying to update over https fails', async function () {
+ await page.click('#updateAutomatically');
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('.content');
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('update_fail');
+ });
+
+ it('should update successfully and show the finished update screen', async function () {
+ await page.click('#updateUsingHttp');
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('.content');
+
+ // redact specific version changes
+ page.evaluate(() => {
+ const updateLog = $('.row code').html();
+ const redacted = updateLog.replace(/\bv?\d+(?:\.\d+){1,}\b/gi, 'x.x.x');
+ $('.row code').text(redacted);
+ });
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('update_success');
+ });
+
+ it('should login successfully after the update', async function () {
+ await page.click('.footer a');
+ await page.waitForNetworkIdle();
+
+ // in case a db upgrade is required
+ while (true) {
+ const submitButton = await page.$('.content input[type=submit]');
+ if (submitButton) {
+ await submitButton.click();
+ await page.waitForNetworkIdle();
+ await page.waitForTimeout(250);
+
+ const continueButton = await page.$('.footer a');
+ if (continueButton) { // finish page might not be displayed if only one query is executed
+ await continueButton.click();
+ await page.waitForNetworkIdle();
+ }
+ } else {
+ break;
+ }
+ }
+
+ // avoid taking an unnecessary screenshot, as knowing we land on #site-without-data is enough
+ await page.waitForSelector('#site-without-data', { visible: true });
+ await page.evaluate(() => window.stop()); // stop ongoing requests
+ });
+
+ it('should have TreemapVisualization plugin active after the update', async function () {
+ await page.goto(pluginsUrl);
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('#plugins', { visible: true });
+
+ const activatedPluginRow = page.$('tr.active-plugin:has(td.name a[name="TreemapVisualization"])');
+ expect(activatedPluginRow).to.be.ok;
+ });
+
+ it('should have a working cron archiving process', async function () {
+ // track one action
+ const trackerUrl = config.piwikUrl + "latestStableInstall/piwik.php?";
+
+ await request({
+ method: 'POST',
+ uri: trackerUrl,
+ form: {
+ idsite: 1,
+ url: 'http://piwik.net/test/url',
+ action_name: 'test page',
+ },
+ });
+
+ // run cron archiving
+ const output = await new Promise((resolve, reject) => {
+ exec(`${config.php} ${PIWIK_INCLUDE_PATH}/latestStableInstall/console --matomo-domain=${config.phpServer.HTTP_HOST} core:archive`, (error, stdout, stderr) => {
+ const output = stdout.toString() + "\n" + stderr.toString();
+
+ if (error) {
+ console.log(`core:archive failed, output: ${output}`);
+ reject(error);
+ return;
+ }
+
+ resolve(output);
+ });
+ });
+
+ // check output has no errors
+ expect(output).to.not.match(/ERROR|WARN/g);
+ });
+});
diff --git a/tests/UI/specs/OneClickUpdate_spec.js b/tests/UI/specs/OneClickUpdate_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..60f1df46c99fb2d66afc1512f464c284b1294dfc
--- /dev/null
+++ b/tests/UI/specs/OneClickUpdate_spec.js
@@ -0,0 +1,180 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * OneClickUpdate screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+var fs = require('fs'),
+ path = require('../../lib/screenshot-testing/support/path');
+
+const request = require('request-promise');
+const exec = require('child_process').exec;
+
+describe("OneClickUpdate", function () {
+ this.fixture = "Piwik\\Tests\\Fixtures\\LatestStableInstall";
+
+ const latestStableUrl = config.piwikUrl + '/latestStableInstall/index.php';
+ const settingsUrl = latestStableUrl + '?module=CoreAdminHome&action=home&idSite=1&period=day&date=yesterday';
+
+ async function openHttpsFailureScreen() {
+ // Recreate the HTTPS failure state directly so the rest of the test
+ // does not depend on browser history or transport-specific behavior.
+ await page.evaluate((oneClickResultsUrl) => {
+ const form = document.createElement('form');
+ form.method = 'POST';
+ form.action = oneClickResultsUrl;
+
+ [
+ ['httpsFail', '1'],
+ ['error', 'Simulated SSL certificate failure'],
+ ['messages', 'a:0:{}'],
+ ].forEach(([name, value]) => {
+ const input = document.createElement('input');
+ input.type = 'hidden';
+ input.name = name;
+ input.value = value;
+ form.appendChild(input);
+ });
+
+ document.body.appendChild(form);
+ form.submit();
+ }, latestStableUrl + '?module=CoreUpdater&action=oneClickResults');
+
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('#updateUsingHttp', { visible: true });
+ }
+
+ before(async function () {
+ // The updater page loads helper code that may register shortcuts before
+ // Mousetrap is available in this screenshot-test environment.
+ await page.evaluateOnNewDocument(() => {
+ window.Mousetrap = window.Mousetrap || {
+ bind: () => {},
+ trigger: () => {},
+ };
+ });
+ });
+
+ it('should show the new version available button in the admin screen', async function () {
+ await page.goto(latestStableUrl);
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('#login_form_login', { visible: true });
+
+ await page.type('#login_form_login', superUserLogin);
+ await page.type('#login_form_password', superUserPassword);
+ await page.evaluate(function(){
+ $('#login_form_submit').click();
+ });
+
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('.pageWrap');
+
+ await page.goto(settingsUrl);
+ await page.waitForNetworkIdle();
+
+ const element = await page.waitForSelector('#header_message', { visible: true });
+ expect(await element.screenshot()).to.matchImage('latest_version_available');
+ });
+
+ it('should show the one click update screen when the update button is clicked', async function () {
+ await page.click('#header_message');
+
+ await page.waitForNetworkIdle();
+ await page.waitForSelector('.content');
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('update_screen');
+ });
+
+ it('should fail to automatically update when trying to update over https fails', async function () {
+ await openHttpsFailureScreen();
+ expect(await page.$('#updateUsingHttp')).to.be.ok;
+ expect(await page.$('#updateUsingHttps')).to.be.ok;
+ expect(await page.$('.alert-warning')).to.be.ok;
+ });
+
+ it('should fail when a directory is not writable', async function () {
+ await openHttpsFailureScreen();
+ // Force the updater to hit the writable-directory error path.
+ fs.chmodSync(path.join(PIWIK_INCLUDE_PATH, '/latestStableInstall/core'), 0o555);
+ await page.click('#updateUsingHttp');
+ await page.waitForSelector('.alert-danger', { visible: true });
+ const heading = await page.$eval('.header h1', node => node.textContent);
+ expect(heading).to.match(/update error/i);
+ expect(await page.$('.alert-danger')).to.be.ok;
+ expect(await page.$('.footer a')).to.be.ok;
+ });
+
+ it('should update successfully and show the finished update screen', async function () {
+ await openHttpsFailureScreen();
+ // Restore permissions so the same flow can complete successfully.
+ fs.chmodSync(path.join(PIWIK_INCLUDE_PATH, '/latestStableInstall/core'), 0o777);
+ await page.click('#updateUsingHttp');
+ await page.waitForSelector('.footer a', { visible: true });
+ const heading = await page.$eval('.header h1', node => node.textContent);
+ expect(heading).to.match(/successfully updated/i);
+ expect(await page.$('.footer a')).to.be.ok;
+ });
+
+ it('should login successfully after the update', async function () {
+ await page.click('.footer a');
+ await page.waitForNetworkIdle();
+
+ // in case a db upgrade is required
+ while (true) {
+ const submitButton = await page.$('.content input[type=submit]');
+ if (submitButton) {
+ await submitButton.click();
+ await page.waitForNetworkIdle();
+ await page.waitForTimeout(250);
+
+ const continueButton = await page.$('.footer a');
+ if (continueButton) { // finish page might not be displayed if only one query is executed
+ await continueButton.click();
+ await page.waitForNetworkIdle();
+ }
+ } else {
+ break;
+ }
+ }
+
+ // avoid taking an unnecessary screenshot, as knowing we land on #site-without-data is enough
+ await page.waitForSelector('#site-without-data', { visible: true });
+ await page.evaluate(() => window.stop()); // stop ongoing requests
+ });
+
+ it('should have a working cron archiving process', async function () {
+ // track one action
+ const trackerUrl = config.piwikUrl + "latestStableInstall/piwik.php?";
+
+ await request({
+ method: 'POST',
+ uri: trackerUrl,
+ form: {
+ idsite: 1,
+ url: 'http://piwik.net/test/url',
+ action_name: 'test page',
+ },
+ });
+
+ // run cron archiving
+ const output = await new Promise((resolve, reject) => {
+ exec(`${config.php} ${PIWIK_INCLUDE_PATH}/latestStableInstall/console --matomo-domain=${config.phpServer.HTTP_HOST} core:archive`, (error, stdout, stderr) => {
+ const output = stdout.toString() + "\n" + stderr.toString();
+
+ if (error) {
+ console.log(`core:archive failed, output: ${output}`);
+ reject(error);
+ return;
+ }
+
+ resolve(output);
+ });
+ });
+
+ // check output has no errors
+ expect(output).to.not.match(/ERROR|WARN/g);
+ });
+});
diff --git a/tests/UI/specs/OnlyRawDataNotification_spec.js b/tests/UI/specs/OnlyRawDataNotification_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..1e7ace751696558cea7e26368aa7190bf5b5a1b8
--- /dev/null
+++ b/tests/UI/specs/OnlyRawDataNotification_spec.js
@@ -0,0 +1,44 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Only Raw Data Notification screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("OnlyRawDataNotification", function () {
+ this.fixture = "Piwik\\Tests\\Fixtures\\OneVisit";
+
+ const generalParams = 'idSite=1&period=range&date=2021-01-01,today';
+ const pageUrl = '?module=CoreHome&action=index&' + generalParams;
+
+ before(function () {
+ testEnvironment.testUseMockAuth = 1;
+ testEnvironment.configOverride.General = {
+ browser_archiving_disabled_enforce: '1',
+ enable_browser_archiving_triggering: '0',
+ };
+ testEnvironment.optionsOverride = {
+ enableBrowserTriggerArchiving: '0',
+ };
+ testEnvironment.save();
+ });
+
+ it("should show notification when only raw data exists", async function () {
+ await page.goto(pageUrl);
+ await page.waitForSelector('.widget');
+ const notificationContainer = await page.$('#notificationContainer');
+ expect(await notificationContainer.screenshot()).to.matchImage('show_notification_when_only_raw_data_exists');
+ });
+
+ it("should show notification when only raw data exists and visits log is disabled", async function () {
+ testEnvironment.overrideConfig('Live', 'disable_visitor_log', 1);
+ testEnvironment.save();
+ await page.goto('about:blank');
+ await page.goto(pageUrl);
+ await page.waitForSelector('.widget');
+ const notificationContainer = await page.$('#notificationContainer');
+ expect(await notificationContainer.screenshot()).to.matchImage('show_notification_when_only_raw_data_exists_no_visits_log');
+ });
+});
diff --git a/tests/UI/specs/OptOutIframe_spec.js b/tests/UI/specs/OptOutIframe_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..495deba636be87f5c9c8c92a99ceeeb7149105d6
--- /dev/null
+++ b/tests/UI/specs/OptOutIframe_spec.js
@@ -0,0 +1,122 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Opt-out form tests
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("OptOutIframe", function () {
+ const siteUrl = "/tests/resources/overlay-test-site-real/opt-out.php?implementation=iframe",
+ safariUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A",
+ chromeUserAgent = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36";
+
+ function expandIframe() {
+ return page.evaluate(() => {
+ const $iframe = $('iframe#optOutIframe');
+ $iframe.contents().find('#textError_https').hide();
+ $iframe.width(350);
+ $iframe.height($iframe.contents().outerHeight());
+ });
+ }
+
+ after(async () => {
+ await page.setUserAgent(page.originalUserAgent);
+ });
+
+ it("should display correctly when embedded in another site", async function () {
+ await page.clearCookies();
+
+ page.setUserAgent(chromeUserAgent);
+ await page.goto(siteUrl);
+
+ await expandIframe();
+
+ const element = await page.jQuery('iframe#optOutIframe');
+ expect(await element.screenshot()).to.matchImage('loaded');
+ });
+
+ it("should reload the iframe when clicking the opt out checkbox and display an empty checkbox", async function () {
+ await page.evaluate(function () {
+ $('iframe#optOutIframe').contents().find('input#trackVisits').click();
+ });
+
+ await page.waitForTimeout(5000); // opt out iframe creates a new page, so we can't wait on it that easily
+ await page.waitForNetworkIdle(); // safety
+
+ await expandIframe();
+
+ const element = await page.jQuery('iframe#optOutIframe');
+ expect(await element.screenshot()).to.matchImage('opted-out');
+ });
+
+ it("should correctly show the checkbox unchecked after reloading after opting-out", async function () {
+ page.setUserAgent(chromeUserAgent);
+ await page.goto(siteUrl);
+
+ await expandIframe();
+
+ const element = await page.jQuery('iframe#optOutIframe');
+ expect(await element.screenshot()).to.matchImage('opted-out_reloaded');
+ });
+
+ it("using opt out twice should work correctly", async function () {
+ page.setUserAgent(chromeUserAgent);
+ await page.goto(siteUrl);
+
+ await page.evaluate(function () {
+ $('iframe#optOutIframe').contents().find('input#trackVisits').click();
+ });
+
+ await page.waitForTimeout(5000);
+
+ await expandIframe();
+
+ // check the box has opted in state after clicking once
+ var element = await page.jQuery('iframe#optOutIframe');
+ expect(await element.screenshot()).to.matchImage('clicked_once');
+
+ await page.evaluate(function () {
+ $('iframe#optOutIframe').contents().find('input#trackVisits').click();
+ });
+
+ await page.waitForTimeout(5000);
+
+ // check the box has outed out state after click another time
+ await page.reload();
+
+ await expandIframe();
+
+ var element = await page.jQuery('iframe#optOutIframe');
+ expect(await element.screenshot()).to.matchImage('clicked_twice');
+ });
+
+ it("should correctly show display opted-in form when cookies are cleared", async function () {
+ await page.clearCookies();
+
+ page.setUserAgent(safariUserAgent);
+ await page.goto(siteUrl);
+
+ await expandIframe();
+
+ const element = await page.jQuery('iframe#optOutIframe');
+ expect(await element.screenshot()).to.matchImage('safari-loaded');
+ });
+
+ it("should correctly set opt-out cookie on safari", async function () {
+ await page.evaluate(function () {
+ $('iframe#optOutIframe').contents().find('input#trackVisits').click();
+ });
+
+ await page.waitForTimeout(5000); // opt out iframe creates a new page, so we can't wait on it that easily
+ await page.waitForNetworkIdle(); // safety
+
+ await page.goto(siteUrl); // reload to check that cookie was set
+
+ await expandIframe();
+
+ const element = await page.jQuery('iframe#optOutIframe');
+ expect(await element.screenshot()).to.matchImage('safari-opted-out');
+ });
+});
diff --git a/tests/UI/specs/OptOutJS_spec.js b/tests/UI/specs/OptOutJS_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..154c356eadb0bcc4778a1b1fb3d5b1e0da496a59
--- /dev/null
+++ b/tests/UI/specs/OptOutJS_spec.js
@@ -0,0 +1,130 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Opt-out form tests
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe('OptOutJS', function () {
+ const parentSuite = this;
+
+ const siteUrl = '/tests/resources/overlay-test-site-real/opt-out.php?implementation=js';
+
+ async function expectHasConsentToBe(useTracker, expectedState) {
+ let hasConsent;
+
+ if (useTracker) {
+ hasConsent = await page.evaluate(() => window.Matomo.getTracker().hasConsent());
+ } else {
+ hasConsent = await page.evaluate(() => !!window.MatomoConsent.hasConsent());
+ }
+
+ expect(hasConsent).to.equal(expectedState);
+ }
+
+ async function hideHTTPSWarning() {
+ await page.evaluate(() => $('#matomo-opt-out p:contains("not loaded over HTTPS")').hide());
+ }
+
+ async function screenshotOptOut(screenshotName) {
+ expect(await page.screenshotSelector('#matomo-opt-out')).to.matchImage(screenshotName);
+ }
+
+ [true, false].forEach(function (loadTracker) {
+ const optOutUrl = siteUrl + '&loadTracker=' + (loadTracker ? '1' : '0');
+ const testSuffix = loadTracker ? ' (with tracker)' : ' (without tracker)';
+
+ it('should display correctly when integrated' + testSuffix, async function () {
+ await page.clearCookies();
+ await page.goto(optOutUrl);
+ await page.waitForSelector('#trackVisits');
+ await hideHTTPSWarning();
+
+ await expectHasConsentToBe(loadTracker, true);
+
+ if (loadTracker) {
+ await screenshotOptOut('loaded');
+ }
+ });
+
+ it('should register consent withdrawal and display an empty checkbox' + testSuffix, async function () {
+ await page.click('#trackVisits');
+ await hideHTTPSWarning();
+
+ await expectHasConsentToBe(loadTracker, false);
+
+ if (loadTracker) {
+ await screenshotOptOut('opted-out');
+ }
+ });
+
+ it('should correctly show the checkbox unchecked after reloading after opting-out' + testSuffix, async function () {
+ await page.goto(optOutUrl);
+ await page.waitForSelector('#trackVisits');
+ await hideHTTPSWarning();
+
+ await expectHasConsentToBe(loadTracker, false);
+ });
+
+ it('should allow granting consent again and display a filled checkbox' + testSuffix, async function () {
+ await page.click('#trackVisits');
+ await hideHTTPSWarning();
+
+ await expectHasConsentToBe(loadTracker, true);
+
+ if (loadTracker) {
+ await screenshotOptOut('opted-in');
+ }
+ });
+
+ it('should work correctly when using opt out twice' + testSuffix, async function () {
+ await page.clearCookies();
+ await page.goto(optOutUrl);
+ await page.waitForSelector('#trackVisits');
+ await hideHTTPSWarning();
+
+ await expectHasConsentToBe(loadTracker, true);
+
+ await page.click('#trackVisits');
+ await hideHTTPSWarning();
+ await expectHasConsentToBe(loadTracker, false);
+
+ await page.click('#trackVisits');
+ await hideHTTPSWarning();
+ await expectHasConsentToBe(loadTracker, true);
+
+ if (loadTracker) {
+ await screenshotOptOut('clicked_twice');
+ }
+ });
+ });
+
+ it('should show a warning if the divId is missing', async function () {
+ await page.goto(siteUrl + '&divId=missingDivId');
+ await page.waitForSelector('#missingDivId-warning');
+ await page.evaluate(() => $('#missingDivId-warning').width(640));
+
+ expect(await page.screenshotSelector('#missingDivId-warning')).to.matchImage('missing-divId');
+ });
+
+ describe('with disabled browser cookies', function () {
+ this.title = parentSuite.title; // to make sure the screenshot prefix is the same
+
+ before(async function (){
+ await page.webpage._client.send('Emulation.setDocumentCookieDisabled', {'disabled': true});
+ });
+
+ after(async function () {
+ await page.webpage._client.send('Emulation.setDocumentCookieDisabled', {'disabled': false});
+ });
+
+ it('should show a warning', async function () {
+ await page.goto(siteUrl);
+ await page.waitForSelector('#matomo-opt-out p');
+
+ await screenshotOptOut('cookies-disabled');
+ });
+ })
+});
diff --git a/tests/UI/specs/PeriodSelector_spec.js b/tests/UI/specs/PeriodSelector_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..3e7e24923b9c6d4e7a766fcc7570d28bad3929e0
--- /dev/null
+++ b/tests/UI/specs/PeriodSelector_spec.js
@@ -0,0 +1,449 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Period selector screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("PeriodSelector", function () {
+ const parentSuite = this;
+
+ const generalParams = 'idSite=1&period=day&date=2012-01-01';
+ const url = '?module=CoreHome&action=index&' + generalParams + '#?' + generalParams + '&category=General_Actions&subcategory=General_Pages';
+
+ const selector = '#periodString,#periodString .dropdown';
+ const waitForPeriodChecked = async function (periodId) {
+ await page.waitForFunction((id) => window.$(id).is(':checked'), {}, periodId);
+ };
+ it("should load correctly", async function() {
+ await page.goto(url);
+
+ // disable broadcast.propagateNewPage & remove loading gif
+ await page.evaluate(function () {
+ piwikHelper.isReportingPage = function () {
+ return false;
+ };
+
+ broadcast.propagateNewPage = function () {};
+
+ // hide ajaxLoadingCalendar via CSS (can't just remove it since it's managed by vue)
+ $('head').append('');
+ });
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('loaded');
+ });
+
+ it("should expand when clicked", async function() {
+ await page.click('.periodSelector .title');
+ expect(await page.screenshotSelector(selector)).to.matchImage('expanded');
+ });
+
+ it("should select a date when a date is clicked in day-period mode", async function() {
+ const element = await page.jQuery('.period-date .ui-datepicker-calendar a:contains(12)');
+ await element.click();
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('day_selected');
+ });
+
+ it("should change the month displayed when a month is selected in the month dropdown", async function() {
+ await page.evaluate(function () {
+ $('.ui-datepicker-month').val(1).trigger('change');
+ });
+ await page.mouse.move(-10, -10);
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('month_changed');
+ });
+
+ it("should change the year displayed when a year is selected in the year dropdown", async function() {
+ await page.evaluate(function () {
+ $('.ui-datepicker-year').val(2013).trigger('change');
+ });
+ await page.mouse.move(-10, -10);
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('year_changed');
+ });
+
+ it("should change the date when a date is clicked in week-period mode", async function() {
+ await page.click('#period_id_week');
+ await waitForPeriodChecked('#period_id_week');
+
+ const element = await page.jQuery('.period-date .ui-datepicker-calendar a:contains(13)');
+ await element.click();
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('week_selected');
+ });
+
+ it('should activate a period option via Enter key', async function () {
+ await page.focus('#period_id_week');
+ await page.keyboard.press('Enter');
+ await waitForPeriodChecked('#period_id_week');
+
+ const selectedState = await page.evaluate(function () {
+ return {
+ weekChecked: $('#period_id_week').is(':checked'),
+ dayChecked: $('#period_id_day').is(':checked'),
+ };
+ });
+
+ expect(selectedState.weekChecked).to.equal(true);
+ expect(selectedState.dayChecked).to.equal(false);
+ });
+
+ it('should activate a period option via Space key', async function () {
+ await page.focus('#period_id_month');
+ await page.keyboard.press('Space');
+ await waitForPeriodChecked('#period_id_month');
+
+ const selectedState = await page.evaluate(function () {
+ return {
+ monthChecked: $('#period_id_month').is(':checked'),
+ dayChecked: $('#period_id_day').is(':checked'),
+ };
+ });
+
+ expect(selectedState.monthChecked).to.equal(true);
+ expect(selectedState.dayChecked).to.equal(false);
+ });
+
+ it("should change the date when a date is clicked in month-period mode", async function() {
+ await page.click('#period_id_month');
+ await waitForPeriodChecked('#period_id_month');
+
+ const element = await page.jQuery('.period-date .ui-datepicker-calendar a:contains(14)');
+ await element.click();
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('month_selected');
+ });
+
+ it("should change the date when a date is clicked in year-period mode", async function() {
+ await page.click('#period_id_year');
+ await waitForPeriodChecked('#period_id_year');
+
+ const element = await page.jQuery('.period-date .ui-datepicker-calendar a:contains(15)');
+ await element.click();
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('year_selected');
+ });
+
+ it("should display the range picker when the range radio button is clicked", async function() {
+ await page.click('#period_id_range');
+ await waitForPeriodChecked('#period_id_range');
+ await page.waitForSelector('#calendarFrom .ui-datepicker-calendar');
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('range_picker_displayed');
+ });
+
+ it("should change from & to dates when range picker calendar dates are clicked", async function() {
+ let element = await page.jQuery('#calendarFrom .ui-datepicker-calendar a:contains(10)');
+ await element.click();
+
+ element = await page.jQuery('#calendarTo .ui-datepicker-calendar a:contains(18)');
+ await element.click();
+
+ await page.hover('#calendarApply');
+ await page.waitForFunction(() => !!document.querySelector('#calendarApply:hover'));
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('date_range_selected');
+ });
+
+ it("should enable the comparison dropdown when 'compare' is checked", async function () {
+ await page.click('#comparePeriodTo + span');
+ await page.waitForFunction(() => window.$('#comparePeriodTo').is(':checked'));
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('comparison_checked');
+ });
+
+ it('should show range inputs when custom date range compare is selected', async function () {
+ await page.evaluate(function () {
+ $('#comparePeriodToDropdown select').val('string:custom').trigger('change');
+ });
+ await page.waitForFunction(
+ () => window.$('#comparePeriodToDropdown select').val() === 'string:custom',
+ );
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('custom_comparison');
+ });
+
+ it("should close on click if previously opened", async function () {
+ await page.click('.periodSelector .title');
+ expect(await page.screenshotSelector(selector)).to.matchImage('closed');
+ });
+
+
+ it('should apply non-range period selection only after calendar click', async function () {
+ await page.goto(url);
+ await page.click('.periodSelector .title');
+ await page.evaluate(function () {
+ piwikHelper.isReportingPage = function () {
+ return true;
+ };
+ });
+
+ const initialUrl = await page.url();
+
+ await page.click('#period_id_week');
+ await waitForPeriodChecked('#period_id_week');
+
+ const stateAfterPeriodClick = await page.evaluate(function () {
+ return {
+ expanded: $('.periodSelector').hasClass('expanded'),
+ applyVisible: $('#calendarApply').is(':visible'),
+ selectedCells: $('.period-date td.ui-datepicker-current-period').length,
+ };
+ });
+
+ expect(stateAfterPeriodClick.expanded).to.equal(true);
+ expect(stateAfterPeriodClick.applyVisible).to.equal(true);
+ expect(stateAfterPeriodClick.selectedCells).to.equal(0);
+ expect(await page.url()).to.equal(initialUrl);
+
+ const dateCell = await page.jQuery('.period-date .ui-datepicker-calendar a:contains(13)');
+ await dateCell.click();
+ await page.waitForFunction((initial) => window.location.href !== initial, {}, initialUrl);
+
+ const appliedUrl = await page.url();
+ expect(appliedUrl).to.contain('period=week');
+ expect(appliedUrl).to.not.equal(initialUrl);
+ });
+
+ it('should keep the calendar interactive after selecting a preset', async function () {
+ await page.goto('about:blank');
+ await page.evaluateOnNewDocument(() => {
+ Date.now = () => new Date('2012-01-09T12:00:00Z').getTime();
+ });
+
+ await page.goto(url);
+ await page.click('.periodSelector .title');
+ await page.evaluate(function () {
+ piwikHelper.isReportingPage = function () {
+ return true;
+ };
+ });
+
+ await page.click('#preset_date_yesterday');
+ await waitForPeriodChecked('#period_id_day');
+ await page.waitForFunction(() => window.$('#preset_date_yesterday').is(':checked'));
+
+ const dayToSelect = await page.evaluate(function () {
+ const selectedDay = $('.period-date td.ui-datepicker-current-period')
+ .find('a, span')
+ .first()
+ .text()
+ .trim();
+
+ const clickableDays = $('.period-date .ui-datepicker-calendar a')
+ .map(function () {
+ return $(this).text().trim();
+ })
+ .get();
+
+ return clickableDays.find((dayText) => dayText !== selectedDay) || null;
+ });
+
+ expect(dayToSelect).to.not.equal(null);
+
+ const initialUrl = await page.url();
+
+ await page.evaluate(function (dayText) {
+ const link = $('.period-date .ui-datepicker-calendar a').filter(function () {
+ return $(this).text().trim() === String(dayText);
+ }).get(0);
+
+ if (!link) {
+ throw new Error(`Could not find clickable calendar day "${dayText}"`);
+ }
+
+ link.click();
+ }, dayToSelect);
+
+ await page.waitForFunction((initial) => window.location.href !== initial, {}, initialUrl);
+
+ const stateAfterCalendarClick = await page.evaluate(function () {
+ return {
+ dayChecked: $('#period_id_day').is(':checked'),
+ presetChecked: $('#preset_date_yesterday').is(':checked'),
+ };
+ });
+
+ expect(stateAfterCalendarClick.dayChecked).to.equal(true);
+ expect(stateAfterCalendarClick.presetChecked).to.equal(false);
+ });
+
+ it('should keep range selection pending until apply', async function () {
+ await page.goto(url);
+ await page.click('.periodSelector .title');
+ await page.evaluate(function () {
+ piwikHelper.isReportingPage = function () {
+ return true;
+ };
+ });
+
+ const initialUrl = await page.url();
+
+ await page.click('#period_id_range');
+ await waitForPeriodChecked('#period_id_range');
+ expect(await page.url()).to.equal(initialUrl);
+
+ await page.waitForSelector('#calendarApply', {visible: true, timeout: 250});
+ await page.click('#calendarApply');
+ await page.waitForFunction((initial) => window.location.href !== initial, {}, initialUrl);
+
+ const appliedUrl = await page.url();
+ expect(appliedUrl).to.contain('period=range');
+ expect(appliedUrl).to.not.equal(initialUrl);
+ });
+
+ it('should keep legacy double-click immediate apply behavior for non-range periods', async function () {
+ await page.goto(url);
+ await page.click('.periodSelector .title');
+ await page.evaluate(function () {
+ piwikHelper.isReportingPage = function () {
+ return true;
+ };
+ });
+
+ await page.click('#period_id_month', { clickCount: 2 });
+ await page.waitForFunction(() => window.location.href.includes('period=month'));
+
+ const currentUrl = await page.url();
+ expect(currentUrl).to.contain('period=month');
+ });
+
+ it('should keep rolling last7 token after reload and apply', async function () {
+ const tokenUrl = '?module=CoreHome&action=index&idSite=1&period=range&date=last7#?idSite=1&period=range&date=last7&category=General_Actions&subcategory=General_Pages';
+ await page.goto(tokenUrl);
+ await page.click('.periodSelector .title');
+ await page.evaluate(function () {
+ piwikHelper.isReportingPage = function () {
+ return true;
+ };
+ });
+ await page.click('#calendarApply');
+ await page.waitForNetworkIdle();
+
+ const currentUrl = await page.url();
+ expect(currentUrl).to.contain('period=range');
+ expect(currentUrl).to.contain('date=last7');
+ expect(currentUrl).to.not.match(/date=\d{4}-\d{2}-\d{2}%2C\d{4}-\d{2}-\d{2}/);
+ });
+
+ it("should move forward two days when next period selector is clicked twice", async function () {
+ await page.goto(url);
+
+ await page.click('.periodSelector .move-period-next');
+ await page.waitForNetworkIdle();
+ await page.click('.periodSelector .move-period-next');
+
+ await page.waitForNetworkIdle();
+ await page.mouse.move(-10, -10);
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('two_days_forward');
+ });
+
+ it("should move back one days when previous period selector is clicked once", async function () {
+ await page.click('.periodSelector .move-period-prev');
+
+ await page.waitForNetworkIdle();
+ await page.mouse.move(-10, -10);
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('one_day_back');
+ });
+
+ it("should display disabled previous period button when at the start of site tracking", async function () {
+ const generalParams = 'idSite=1&period=day&date=2011-01-01';
+ const url = '?module=CoreHome&action=index&' + generalParams + '#?' + generalParams + '&category=General_Actions&subcategory=General_Pages';
+
+ await page.goto(url);
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('disabled_previous_period');
+ });
+
+ it("should hide prev/next buttons when dates range selection", async function () {
+ const generalParams = 'idSite=1&period=range&date=2011-01-01,2011-02-01';
+ const url = '?module=CoreHome&action=index&' + generalParams + '#?' + generalParams + '&category=General_Actions&subcategory=General_Pages';
+
+ await page.goto(url);
+
+ await page.evaluate(function () {
+ // disable page propagation again for further tests
+ broadcast.propagateNewPage = function () {};
+ });
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('hide_prevnext_for_range');
+ });
+
+ describe('match selected compare settings with URL', async function() {
+ this.title = parentSuite.title; // to make sure the screenshot prefix is the same
+
+ const getSelectedPeriodType = async function () {
+ const compareToTypeInput = await page.$('#comparePeriodToDropdown input');
+ const compareToTypeValue = await compareToTypeInput.getProperty('value');
+
+ return await compareToTypeValue.jsonValue();
+ };
+
+ it('should select "previous period" from URL', async function () {
+ await page.goto(url + '&comparePeriods[]=day&comparePeriodType=previousPeriod&compareDates[]=2011-12-31');
+ await page.waitForNetworkIdle();
+
+ expect(await getSelectedPeriodType()).to.match(/Period/);
+ });
+
+ it('should select "previous year" from URL', async function () {
+ await page.goto(url + '&comparePeriods[]=day&comparePeriodType=previousYear&compareDates[]=2011-01-01');
+ await page.waitForNetworkIdle();
+
+ expect(await getSelectedPeriodType()).to.match(/Year/);
+ });
+
+ it ('should select "custom" from URL', async function() {
+ await page.goto(url + '&comparePeriods[]=range&comparePeriodType=custom&compareDates[]=2013-01-01,2013-01-02');
+ await page.waitForNetworkIdle();
+
+ expect(await getSelectedPeriodType()).to.match(/Custom/);
+
+ // ensure inputs are properly filled
+ await page.click('.periodSelector .title');
+ await page.waitForSelector('#calendarApply', {visible: true, timeout: 250});
+
+ expect(await page.screenshotSelector(selector)).to.matchImage('custom_comparison_url');
+ });
+ });
+
+ it('should show an error when invalid date/period combination is given', async function () {
+ await page.goto('about:blank');
+ await page.goto(url.replace(/date=[^]+&/, 'date=2020-08-08,2020-08-09&'));
+ await page.waitForFunction(
+ () => window.$('.periodSelector .title').text().trim().length > 0,
+ );
+
+ expect(await page.screenshotSelector(selector + ',#notificationContainer')).to.matchImage('invalid');
+ });
+
+ it('should rediscover the last week preset for explicit week dates after navigation', async function () {
+ await page.goto('about:blank');
+ await page.evaluateOnNewDocument(() => {
+ Date.now = () => new Date('2012-01-09T12:00:00Z').getTime();
+ });
+
+ const explicitWeekUrl = '?module=CoreHome&action=index&idSite=1&period=week&date=2012-01-03'
+ + '#?idSite=1&period=week&date=2012-01-03&category=General_Actions&subcategory=General_Pages';
+
+ await page.goto(explicitWeekUrl);
+ await page.click('.periodSelector .title');
+ await waitForPeriodChecked('#period_id_week');
+ await page.waitForFunction(() => window.$('#preset_date_lastWeekMonSun').is(':checked'));
+
+ const selectedState = await page.evaluate(function () {
+ return {
+ weekChecked: $('#period_id_week').is(':checked'),
+ lastWeekChecked: $('#preset_date_lastWeekMonSun').is(':checked'),
+ };
+ });
+
+ expect(selectedState.weekChecked).to.equal(true);
+ expect(selectedState.lastWeekChecked).to.equal(true);
+ });
+});
diff --git a/tests/UI/specs/PieGraph_spec.js b/tests/UI/specs/PieGraph_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..2716222647e0c7825ed66d0742c22ac98ed567fe
--- /dev/null
+++ b/tests/UI/specs/PieGraph_spec.js
@@ -0,0 +1,66 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Pie graph screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("PieGraph", function () {
+ const url = "?module=Widgetize&action=iframe&moduleToWidgetize=Referrers&idSite=1&period=year&date=2012-08-09&"
+ + "actionToWidgetize=getKeywords&viewDataTable=graphPie&isFooterExpandedInDashboard=1";
+ const multiMetricUrl = url + "&columns=nb_visits,nb_actions,bounce_rate&filter_add_columns_when_show_all_columns=0";
+
+ it("should load correctly", async function () {
+ await page.goto(url);
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('load');
+ });
+
+ it("should show tooltip on hover", async function () {
+ await page.hover('.piwik-graph');
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('pie_segment_tooltip');
+ });
+
+ it("should display the metric picker when the metric picker button is clicked", async function () {
+ await page.click('.metrics-picker__toggle');
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('metric_picker_shown');
+ await page.keyboard.press('Escape');
+ });
+
+ it("should change displayed metric when another metric picked", async function () {
+ await page.click('.metrics-picker__toggle');
+ await page.waitForSelector('.metrics-picker__options input');
+ var element = await page.jQuery('.metrics-picker__options input:not(:checked):eq(0)');
+ await element.click();
+
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('other_metric');
+ });
+
+ it("should render the footer legend for pie graphs", async function () {
+ await page.goto(multiMetricUrl);
+ await page.waitForNetworkIdle();
+
+ const legendState = await page.evaluate(function () {
+ const footer = document.querySelector('.jqplot-legend-footer.has-legend');
+ const labels = footer ? Array.from(footer.querySelectorAll('.jqplot-legend-label')).map(function (label) {
+ return label.textContent.trim();
+ }).filter(Boolean) : [];
+
+ return {
+ hasLegend: !!footer,
+ itemCount: labels.length,
+ labels: labels,
+ };
+ });
+
+ expect(legendState.hasLegend).to.equal(true);
+ expect(legendState.itemCount).to.be.above(0);
+ expect(legendState.labels.length).to.equal(legendState.itemCount);
+ });
+});
diff --git a/tests/UI/specs/PivotByDimension_spec.js b/tests/UI/specs/PivotByDimension_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..32436e012f1d0ca89188def13e1826bbaa2a0f8d
--- /dev/null
+++ b/tests/UI/specs/PivotByDimension_spec.js
@@ -0,0 +1,56 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * PivotByDimension UI tests
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("PivotByDimension", function () {
+ var eventsUrl = "?module=CoreHome&action=index&idSite=1&period=year&date=2012-08-09#?idSite=1&period=year&date=2012-08-09&category=General_Actions&subcategory=Events_Events",
+ actionsUrl = "?module=CoreHome&action=index&idSite=1&period=year&date=2012-08-09#?idSite=1&period=year&date=2012-08-09&category=General_Actions&subcategory=General_Pages",
+ cvarsUrl = "?module=CoreHome&action=index&idSite=1&period=year&date=2012-08-09#?idSite=1&period=year&date=2012-08-09&category=General_Visitors&subcategory=CustomVariables_CustomVariables"
+ ;
+
+ it("should pivot a report correctly when the pivot cog option is selected", async function () {
+ await page.goto(eventsUrl);
+
+ let element = await page.jQuery('.dimension:contains(Event Names)');
+ await element.click();
+ await page.waitForNetworkIdle();
+
+ await page.click('.dropdownConfigureIcon');
+ await page.waitForTimeout(100);
+ await page.click('.dataTablePivotBySubtable');
+ await page.waitForNetworkIdle();
+
+ await page.mouse.move(-150, -150); // make sure nothing is highlighted
+
+ element = await page.$('.theWidgetContent');
+ expect(await element.screenshot()).to.matchImage('pivoted');
+ });
+
+ it("should not display the pivot option on actions reports", async function () {
+ await page.goto(actionsUrl);
+
+ const element = await page.$('.dataTablePivotBySubtable');
+ expect(element).to.be.not.ok;
+ });
+
+ it("should display the pivot option on reports that set a custom columns_to_display", async function () {
+ await page.goto(cvarsUrl);
+ await page.waitForNetworkIdle();
+ await page.click('.dropdownConfigureIcon');
+ await page.click('.dataTablePivotBySubtable');
+ await page.waitForNetworkIdle();
+
+ await page.mouse.move(-15, -15); // make sure nothing is highlighted
+ await page.waitForTimeout(100);
+
+ expect(await page.screenshotSelector('.dataTable,.expandDataTableFooterDrawer')).to.matchImage({
+ imageName: 'pivoted_columns_report',
+ comparisonThreshold: 0.01,
+ });
+ });
+});
diff --git a/tests/UI/specs/QuickAccess_spec.js b/tests/UI/specs/QuickAccess_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b4f7e50a8bfac0a514edbd8375c82cf8ea175a93
--- /dev/null
+++ b/tests/UI/specs/QuickAccess_spec.js
@@ -0,0 +1,202 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * ActionsDataTable screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("QuickAccess", function () {
+ const selectorToCapture = ".quick-access,.quick-access .dropdown";
+ const url = "?module=CoreHome&action=index&idSite=1&period=year&date=2012-08-09";
+ const pagesResultSelector = '.quick-access .result a';
+
+ async function enterSearchTerm(searchTermToAdd) {
+ await page.evaluate(function () {
+ $('.quick-access input').val('');
+ });
+
+ await page.focus(".quick-access input");
+ await page.keyboard.type(searchTermToAdd);
+ await page.waitForTimeout(100);
+ await page.waitForNetworkIdle();
+ await page.waitForTimeout(100);
+
+ await page.evaluate(function () {
+ $('.quick-access input').blur();
+ });
+ }
+
+ async function searchForPages() {
+ await page.focus('.quick-access input');
+ await page.keyboard.type('page');
+ await page.waitForTimeout(200);
+ await page.waitForFunction(
+ (selector) => Array.from(document.querySelectorAll(selector)).some((element) => element.textContent.includes('Pages')),
+ {},
+ pagesResultSelector,
+ );
+ }
+
+ // Switches to the AI Insights section the way its top-menu entry does (hash-only, no reload) and
+ // waits for the SPA to finish switching: it rewrites the hash with the section's first page (the
+ // AI Assistants category) once that section's menu is ready, then the report loads (network idle).
+ // Waiting for this deterministic state - rather than a fixed sleep - means the result set is
+ // already complete when the caller searches and asserts.
+ async function switchToAiInsightsSection() {
+ await page.evaluate(() => {
+ window.location.hash = '#?idSite=1&period=year&date=2012-08-09&group=CoreHome_AIInsights';
+ });
+ await page.waitForFunction(() => window.location.href.includes('category=General_AIAssistants'));
+ await page.waitForNetworkIdle();
+ }
+
+ it("should be displayed", async function () {
+ await page.goto(url);
+ expect(await page.screenshotSelector(selectorToCapture)).to.matchImage('initially');
+ });
+
+ it('should display a styled tooltip on hover', async function () {
+ await page.hover('.quick-access input');
+ const content = await page.waitForFunction(() => $('.ui-tooltip:visible').text());
+ expect(content).to.match(/Search for Menu entries, Segments, and Websites\. Use the arrow keys to navigate through search results\. Shortcut: Press 'f' to search\./);
+ });
+
+ it("should search for something and update view", async function () {
+ await page.mouse.move(0,0);
+ await enterSearchTerm('s');
+ await page.waitForTimeout(100);
+ expect(await page.screenshotSelector(selectorToCapture)).to.matchImage('search_1');
+ });
+
+ it("should search again when typing another letter", async function () {
+ await enterSearchTerm('as');
+ await page.waitForTimeout(100);
+ expect(await page.screenshotSelector(selectorToCapture)).to.matchImage('search_2');
+ });
+
+ it("should show message if no results", async function () {
+ await enterSearchTerm('alaskdjfs');
+ await page.waitForTimeout(100);
+ expect(await page.screenshotSelector(selectorToCapture)).to.matchImage('search_no_result');
+ });
+
+ it("should be possible to activate via shortcut", async function () {
+ await page.goto(url);
+ await page.focus('body');
+ await page.keyboard.type('f');
+
+ await page.evaluate(function () {
+ $('.quick-access input').blur();
+ });
+
+ expect(await page.screenshotSelector(selectorToCapture)).to.matchImage('shortcut');
+ });
+
+ it("should search for websites", async function () {
+ await enterSearchTerm('si');
+ expect(await page.screenshotSelector(selectorToCapture)).to.matchImage('search_sites');
+ });
+
+ it("clicking on a category should show all items that belong to that category", async function () {
+ const element = await page.jQuery('.quick-access-category:first');
+ await element.click();
+ await page.waitForNetworkIdle();
+ expect(await page.screenshotSelector(selectorToCapture)).to.matchImage('search_category');
+ });
+
+ it("should close the mobile side menu when selecting a search result", async function () {
+ await page.webpage.setViewport({ width: 768, height: 1200 });
+ await page.goto('?module=CoreHome&action=index&idSite=1&period=year&date=2009-01-04#?idSite=1&period=year&date=2009-01-04&category=General_Visitors&subcategory=General_Overview');
+ await page.waitForNetworkIdle();
+
+ await page.evaluate(function () {
+ $('.activateLeftMenu>span').click();
+ });
+ await page.waitForFunction(() => $('#secondNavBar').hasClass('mobileLeftMenuOpen'));
+
+ await searchForPages();
+ await page.click(pagesResultSelector);
+
+ await page.waitForFunction(() => !$('#secondNavBar').hasClass('mobileLeftMenuOpen'));
+ await page.waitForFunction(() => window.location.href.includes('subcategory=General_Pages'));
+ });
+ it("should clear the active state when tabbing away", async function () {
+ await page.goto(url);
+ await searchForPages();
+
+ await page.keyboard.press('Tab');
+
+ await page.waitForFunction(() => !$('.quick-access').hasClass('active'));
+ await page.waitForFunction(() => !$('.quick-access .dropdown').is(':visible'));
+ });
+
+ it("should navigate when clicking a search result", async function () {
+ await page.goto(url);
+ await searchForPages();
+
+ await page.click(pagesResultSelector);
+
+ await page.waitForFunction(() => window.location.href.includes('subcategory=General_Pages'));
+ });
+
+ it("should clear the search field when switching reporting section", async function () {
+ // Switching section only changes the hash (no remount), so the typed text used to persist here.
+ await page.goto(url + '#?idSite=1&period=year&date=2012-08-09&category=General_Visitors&subcategory=General_Overview');
+ await page.waitForSelector('#secondNavBar', { visible: true });
+ await page.waitForNetworkIdle();
+
+ await enterSearchTerm('visitor');
+ await page.waitForFunction((selector) => $(selector).length > 0, {}, pagesResultSelector);
+
+ await switchToAiInsightsSection();
+
+ const searchValue = await page.evaluate(() => document.querySelector('.quick-access input').value);
+ expect(searchValue).to.equal('');
+ });
+
+ it("should not show duplicate results after switching reporting section", async function () {
+ // Regression: a hash-only section switch left this component's scraped menu cache pointed at the
+ // previous section, duplicating its entries. Start from a hashed URL so the switch stays hash-only
+ // (a full reload would remount the component and hide the bug).
+ await page.goto(url + '#?idSite=1&period=year&date=2012-08-09&category=General_Visitors&subcategory=General_Overview');
+ await page.waitForSelector('#secondNavBar', { visible: true });
+ await page.waitForNetworkIdle();
+
+ const hasResults = (selector) => $(selector).length > 0;
+
+ // prime the quick search menu cache while in the default (Analytics) section
+ await enterSearchTerm('visitor');
+ await page.waitForFunction(hasResults, {}, pagesResultSelector);
+
+ // switch sections and wait for the new section to fully load before searching, so the result
+ // set is already complete when asserted (a stale cache surfaces the previous section's entries
+ // alongside the live cross-section ones)
+ await switchToAiInsightsSection();
+
+ // The dropdown still holds the pre-switch search results, and searchMenu is debounced, so force
+ // a known-empty state first (a non-matching term, wait for zero results); otherwise the assertion
+ // below could read the stale results before the post-switch search has re-run.
+ await page.focus('.quick-access input');
+ await page.keyboard.type('zzqqxxnomatch');
+ await page.waitForFunction((selector) => $(selector).length === 0, {}, pagesResultSelector);
+
+ // now search for an entry that only exists in the Analytics section
+ await page.evaluate(() => {
+ const input = document.querySelector('.quick-access input');
+ input.value = '';
+ input.dispatchEvent(new Event('input', { bubbles: true }));
+ });
+ await page.keyboard.type('visitor');
+ await page.waitForFunction(hasResults, {}, pagesResultSelector);
+
+ // menu results only (exclude the website results that share the same .result class)
+ const resultTexts = await page.evaluate((selector) => Array.from(document.querySelectorAll(selector))
+ .filter((a) => !a.closest('.quickAccessMatomoSearch'))
+ .map((el) => el.textContent.trim()), pagesResultSelector);
+
+ expect(resultTexts.length).to.be.above(0);
+ expect(resultTexts.length).to.equal(new Set(resultTexts).size);
+ });
+});
diff --git a/tests/UI/specs/ReportExporting_spec.js b/tests/UI/specs/ReportExporting_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..712d7b3af79f81a77ba38a2ed8dd3980ec2a347a
--- /dev/null
+++ b/tests/UI/specs/ReportExporting_spec.js
@@ -0,0 +1,107 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Export link screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("ReportExporting", function () {
+ var baseUrl = "?module=Widgetize&action=iframe&idSite=1&period=year&date=2012-08-09&isFooterExpandedInDashboard=1",
+ referrersGetWebsitesUrl = baseUrl + "&moduleToWidgetize=Referrers&actionToWidgetize=getWebsites&filter_limit=5",
+ visitsSummaryGetUrl = baseUrl + "&moduleToWidgetize=VisitsSummary&actionToWidgetize=get&forceView=1&viewDataTable=graphEvolution";
+
+ function normalReportTest(format) {
+ it(`should export a normal report correctly when the ${format} export is chosen`, async function () {
+ if (await page.url() !== referrersGetWebsitesUrl) {
+ await page.goto(referrersGetWebsitesUrl);
+ await page.click('.activateExportSelection');
+ }
+
+ await page.waitForSelector('[name="format"] input[value="'+format+'"]');
+
+ await page.click('[name="format"] input[value="' + format + '"]');
+ await page.click('[name="filter_limit_all"] input[value="no"]');
+ await page.click('input[name="option_format_metrics"]');
+ await page.evaluate(function () {
+ $('[name=filter_limit] input').val(100).trigger('change');
+ });
+ await page.waitForTimeout(250);
+
+ var url = await page.evaluate(function () {
+ return $('#reportExport a.btn').attr('href');
+ });
+ var pageContents = await page.downloadUrl(url);
+
+ expect.fileMatchesContent('Referrers.getWebsites_exported.' + format.toLowerCase() + '.txt', pageContents);
+ });
+ }
+
+ function evolutionReportTest(format) {
+ it(`should export an evolution graph report correctly when the ${format} export is chosen`, async function () {
+ if (await page.url() !== visitsSummaryGetUrl) {
+ await page.goto(visitsSummaryGetUrl);
+ await page.click('.activateExportSelection');
+ }
+
+ await page.waitForSelector('[name="format"] input[value="'+format+'"]');
+
+ await page.click('[name="format"] input[value="'+format+'"]');
+ await page.click('[name="filter_limit_all"] input[value="no"]');
+ await page.click('input[name="option_format_metrics"]');
+ await page.evaluate(function(){
+ $('[name=filter_limit] input').val(100).trigger('change');
+ });
+ await page.waitForTimeout(250);
+
+ var url = await page.evaluate(function() {
+ return $('#reportExport a.btn').attr('href');
+ });
+ var pageContents = await page.downloadUrl(url);
+
+ expect.fileMatchesContent('VisitsSummary.get_exported.' + format.toLowerCase() + '.txt', pageContents);
+ });
+ }
+
+ function rowEvolutionReportTest(format) {
+ it(`should export an row evolution graph report correctly when the ${format} export link is clicked`, async function () {
+ if (!page.url() || page.url().indexOf('popover') === -1) {
+ await page.goto(referrersGetWebsitesUrl);
+
+ const row = await page.waitForSelector('tbody tr:first-child');
+ await row.hover();
+
+ const icon = await page.waitForSelector('tbody tr:first-child a.actionRowEvolution');
+ await icon.click();
+
+ await page.waitForSelector('.ui-dialog');
+ await page.waitForNetworkIdle();
+
+ await page.click('.ui-dialog .activateExportSelection');
+ }
+
+ await page.waitForSelector('[name="format"] input[value="'+format+'"]');
+
+ await page.click('[name="format"] input[value="'+format+'"]');
+ await page.click('[name="filter_limit_all"] input[value="no"]');
+ await page.click('input[name="option_format_metrics"]');
+ await page.evaluate(function(){
+ $('[name=filter_limit] input').val(100).trigger('change');
+ });
+ await page.waitForTimeout(250);
+
+ var url = await page.evaluate(function() {
+ return $('#reportExport a.btn').attr('href');
+ });
+ var pageContents = await page.downloadUrl(url);
+
+ expect.fileMatchesContent('RowEvolution_exported.' + format.toLowerCase() + '.txt', pageContents);
+ });
+ }
+
+ var formats = ['CSV', 'TSV', 'XML', 'JSON'];
+ formats.forEach(normalReportTest);
+ formats.forEach(evolutionReportTest);
+ formats.forEach(rowEvolutionReportTest);
+});
diff --git a/tests/UI/specs/RowEvolution_spec.js b/tests/UI/specs/RowEvolution_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..0d42778c505bf18b5b3e866774c643d177e506ba
--- /dev/null
+++ b/tests/UI/specs/RowEvolution_spec.js
@@ -0,0 +1,205 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * row evolution screenshot tests
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("RowEvolution", function () {
+ const viewDataTableUrl = "?module=Widgetize&action=iframe&moduleToWidgetize=Referrers&idSite=1&period=week&date=2012-02-09&"
+ + "actionToWidgetize=getKeywords&viewDataTable=table&filter_limit=5";
+
+ const ecommerceItemReportWidgetized = "?module=Widgetize&action=iframe&moduleToWidgetize=Goals&actionToWidgetize=getItemsSku&idGoal=ecommerceAbandonedCart"
+ + "&idSite=1&period=year&date=2012-02-09&viewDataTable=ecommerceAbandonedCart&filter_limit=-1";
+
+ const waitForRowEvolutionAnnotations = async () => {
+ await page.waitForFunction("$('.ui-dialog .evolution-annotations > span').length > 0");
+ };
+
+ const setThemeMode = async (themeMode) => {
+ await page.evaluate((mode) => {
+ window.piwik.setThemeMode(mode);
+ }, themeMode);
+ await page.waitForFunction((mode) => window.piwik.getThemeMode() === mode, {}, themeMode);
+ };
+
+ it('should load when icon clicked in ViewDataTable', async function() {
+ await page.goto(viewDataTableUrl);
+ await page.waitForSelector('tbody tr:first-child')
+ const row = await page.jQuery('tbody tr:contains("corruption")');
+ await row.hover();
+
+ const icon = await page.jQuery('tbody tr:contains("corruption") a.actionRowEvolution');
+ await icon.click();
+
+ await page.waitForSelector('.ui-dialog');
+ await page.waitForNetworkIdle();
+ await waitForRowEvolutionAnnotations();
+
+ const dialog = await page.$('.ui-dialog');
+ expect(await dialog.screenshot()).to.matchImage('row_evolution');
+ });
+
+ it('should change the metric shown when a metric sparkline row is clicked', async function() {
+ await page.click('table.metrics tr[data-i="1"]');
+ await page.waitForNetworkIdle();
+
+ const dialog = await page.$('.ui-dialog');
+ expect(await dialog.screenshot()).to.matchImage('row_evolution_other_metric');
+ });
+
+ it('should show two serieses when a metric sparkline row is shift+clicked', async function() {
+ await page.keyboard.down('Shift');
+ await page.click('table.metrics tr[data-i="2"]', ['shift']);
+ await page.keyboard.up('Shift');
+ await page.waitForNetworkIdle();
+
+ const dialog = await page.$('.ui-dialog');
+ expect(await dialog.screenshot()).to.matchImage('row_evolution_multiple_series');
+ });
+
+ it('should load multi-row evolution correctly', async function() {
+ await page.evaluate(function() {
+ $('.rowevolution-startmulti').click();
+ });
+ await page.waitForFunction("$('.ui-dialog').length === 0");
+
+ const row = await page.waitForSelector('tbody tr:nth-child(2)');
+ await row.hover();
+
+ const icon = await page.waitForSelector('tbody tr:nth-child(2) a.actionRowEvolution');
+ await icon.click();
+
+ await page.waitForSelector('.ui-dialog');
+ await page.waitForNetworkIdle();
+
+ const dialog = await page.$('.ui-dialog');
+ await page.waitForNetworkIdle();
+ expect(await dialog.screenshot()).to.matchImage('multirow_evolution');
+ });
+
+ it('should display a different row evolution metric when the metric selection is changed', async function() {
+ await page.evaluate(function () {
+ $('select.multirowevoltion-metric').val($('select.multirowevoltion-metric option:nth-child(3)').val()).change();
+ });
+
+ await page.waitForSelector('.ui-dialog');
+ await page.waitForNetworkIdle();
+
+ const dialog = await page.$('.ui-dialog');
+ expect(await dialog.screenshot()).to.matchImage('multirow_evolution_other_metric');
+ });
+
+ it('should load row evolution for goals view', async function() {
+ await page.goto(viewDataTableUrl + '&forceView=1&viewDataTable=tableGoals');
+
+ await page.waitForSelector('tbody tr:first-child')
+ const row = await page.jQuery('tbody tr:contains("corruption")');
+ await row.hover();
+
+ const icon = await page.jQuery('tbody tr:contains("corruption") a.actionRowEvolution');
+ await icon.click();
+
+ const dialog = await page.waitForSelector('.ui-dialog');
+ await page.waitForNetworkIdle();
+ expect(await dialog.screenshot()).to.matchImage('row_evolution_goal_view');
+ });
+
+ it('should load row evolution with goal metrics again when reloading the page url', async function() {
+ // page.reload() won't work with url hashes
+ const url = await page.evaluate('location.href');
+ await page.goto('about:blank');
+ await page.goto(url);
+
+ await page.waitForSelector('.ui-dialog');
+ await page.waitForNetworkIdle();
+
+ const dialog = await page.$('.ui-dialog');
+ expect(await dialog.screenshot()).to.matchImage('row_evolution_goal_view_reload');
+ });
+
+ it('should display row evolution for an ecommerce item report correctly', async function() {
+ await page.goto(ecommerceItemReportWidgetized);
+ const row = await page.waitForSelector('tbody tr:first-child');
+ await row.hover();
+
+ const icon = await page.waitForSelector('tbody tr:first-child a.actionRowEvolution');
+ await icon.click();
+
+ await page.waitForSelector('.ui-dialog', { visible: true });
+ await page.waitForNetworkIdle();
+ await waitForRowEvolutionAnnotations();
+
+ const dialog = await page.$('.ui-dialog');
+ expect(await dialog.screenshot()).to.matchImage('row_evolution_ecommerce_item');
+ });
+
+ it('should keep row evolution open when the theme changes live', async function() {
+ await page.goto(viewDataTableUrl);
+ await setThemeMode('light');
+
+ const row = await page.jQuery('tbody tr:contains("corruption")');
+ await row.hover();
+ const icon = await page.jQuery('tbody tr:contains("corruption") a.actionRowEvolution');
+ await icon.click();
+
+ await setThemeMode('dark');
+ await waitForRowEvolutionAnnotations();
+
+ const dialogCount = await page.evaluate(() => $('.ui-dialog').length);
+ const hasRowEvolution = await page.evaluate(() => $('.ui-dialog .rowevolution').length);
+ expect(dialogCount).to.be.equal(1);
+ expect(hasRowEvolution).to.be.equal(1);
+
+ await setThemeMode('light');
+ });
+
+ it('refuses to steer the popover request to a different module/action', async function() {
+ const attackerJson = JSON.stringify({
+ module: 'CoreAdminHome',
+ action: 'setMailSettings',
+ mailHost: 'attacker.example',
+ force_api_session: 0,
+ format: 'json'
+ });
+ const popoverPayload = 'RowAction:RowEvolution:Actions.getPageUrls:'
+ + encodeURIComponent(attackerJson) + ':attacker-label';
+ const hashValue = encodeURIComponent(popoverPayload).replace(/%/g, '$');
+ const attackUrl = '?module=CoreHome&action=index&idSite=1&period=day&date=yesterday'
+ + '#?popover=' + hashValue;
+
+ const popoverRequests = [];
+ let listening = true;
+ const onRequest = (req) => {
+ if (!listening) {
+ return;
+ }
+ const url = req.url();
+ if (url.indexOf('apiMethod=Actions.getPageUrls') !== -1) {
+ popoverRequests.push(url);
+ }
+ };
+ page.on('request', onRequest);
+
+ try {
+ await page.goto('about:blank');
+ await page.goto(attackUrl);
+ await page.waitForNetworkIdle();
+ } finally {
+ listening = false;
+ }
+
+ expect(popoverRequests.length).to.be.above(0);
+ popoverRequests.forEach((url) => {
+ expect(url).to.contain('module=CoreHome');
+ expect(url).to.contain('action=getRowEvolutionPopover');
+ expect(url).to.not.contain('module=CoreAdminHome');
+ expect(url).to.not.contain('action=setMailSettings');
+ expect(url).to.not.contain('force_api_session=0');
+ expect(url).to.not.match(/[?&]format=json(&|$)/);
+ expect(url).to.not.contain('mailHost=');
+ });
+ });
+});
diff --git a/tests/UI/specs/SearchFiltersPersistence_spec.js b/tests/UI/specs/SearchFiltersPersistence_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ba74cc1cf7af1c155816ccdc389fb4d515d97051
--- /dev/null
+++ b/tests/UI/specs/SearchFiltersPersistence_spec.js
@@ -0,0 +1,75 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * ViewDataTable screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("SearchFilterPersistenceTest", function () {
+ var baseUrl = "?module=CoreHome&action=index&idSite=1&period=day&date=2012-08-09&category=General_Actions";
+ var url1 = "&subcategory=Actions_SubmenuPagesEntry";
+ var url2 = "&subcategory=Actions_SubmenuPagesExit";
+
+ it("should load correctly", async function () {
+ await page.goto(baseUrl + url1);
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('load_ok');
+ });
+
+ it("should search the table when a search string is entered and the search button clicked", async function () {
+ await page.click('.dataTableAction.searchAction');
+ await page.focus('.searchAction .dataTableSearchInput');
+ await page.keyboard.type('lo');
+ await page.click('.searchAction .icon-search');
+ await page.waitForNetworkIdle();
+ await page.evaluate(() => document.activeElement.blur());
+ await page.mouse.move(-10, -10);
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('search_results');
+ });
+
+ it("should persist the search term when date is moved", async function () {
+ await page.click('.periodSelector .move-period-next');
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('persisted_date');
+ });
+
+ it("should persist column sorting when date is moved", async function () {
+ // click twice to sort ascending
+ await(await page.jQuery('th#sum_bandwidth', { waitFor: true })).click();
+ await page.waitForNetworkIdle();
+ await page.waitForTimeout(200);
+ await(await page.jQuery('th#sum_bandwidth', { waitFor: true })).click();
+ await page.waitForNetworkIdle();
+ await page.waitForTimeout(200);
+
+ await page.click('.periodSelector .move-period-prev');
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('persisted_sorting');
+ });
+
+ it("should persist the search term when segment is changed", async function () {
+ await page.click('.segmentationContainer');
+ await(await page.jQuery('li[data-idsegment=3] .segname', { waitFor: true })).click();
+ await page.waitForNetworkIdle();
+ await page.evaluate(() => document.activeElement.blur());
+ await page.mouse.move(-10, -10);
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('persisted_segment');
+ });
+
+ it("should reset the search term when page is changed there and back", async function () {
+ await page.goto(baseUrl + url1);
+ await page.waitForNetworkIdle();
+ await page.goto(baseUrl + url2);
+ await page.waitForNetworkIdle();
+ await page.goto(baseUrl + url1);
+ await page.waitForNetworkIdle();
+
+ expect(await page.screenshot({ fullPage: true })).to.matchImage('load_ok');
+ });
+});
diff --git a/tests/UI/specs/SiteSelector_spec.js b/tests/UI/specs/SiteSelector_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..4301f706db4cba0d74c4bdd0161d1a66c15c20a8
--- /dev/null
+++ b/tests/UI/specs/SiteSelector_spec.js
@@ -0,0 +1,74 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Site selector screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("SiteSelector", function () {
+ const selectorToCapture = '.siteSelector,.siteSelector .dropdown';
+ const url = "?module=UsersManager&action=userSettings&idSite=1&period=day&date=yesterday";
+
+ it("should load correctly", async function() {
+ await page.goto(url);
+ await page.waitForNetworkIdle();
+
+ dialog = await page.$(selectorToCapture);
+ await page.waitForTimeout(500);
+ expect(await dialog.screenshot()).to.matchImage('loaded');
+ });
+
+ it("should show tooltip when hovering the element", async function() {
+ await page.hover('.sites_autocomplete .title');
+ const tooltip = await page.waitForSelector('.ui-tooltip', { visible: true })
+
+ expect(await tooltip.getProperty('textContent')).to.match(/Choose a website, currently selected website:
+ // |
+ d._modulePrefixes[module] = { name: module, value: prefix };
+ }
+
+ dojo.requireLocalization = function(/*String*/moduleName, /*String*/bundleName, /*String?*/locale, /*String?*/availableFlatLocales){
+ // summary:
+ // Declares translated resources and loads them if necessary, in the
+ // same style as dojo.require. Contents of the resource bundle are
+ // typically strings, but may be any name/value pair, represented in
+ // JSON format. See also dojo.i18n.getLocalization.
+ // moduleName:
+ // name of the package containing the "nls" directory in which the
+ // bundle is found
+ // bundleName:
+ // bundle name, i.e. the filename without the '.js' suffix
+ // locale:
+ // the locale to load (optional) By default, the browser's user
+ // locale as defined by dojo.locale
+ // availableFlatLocales:
+ // A comma-separated list of the available, flattened locales for this
+ // bundle. This argument should only be set by the build process.
+ // description:
+ // Load translated resource bundles provided underneath the "nls"
+ // directory within a package. Translated resources may be located in
+ // different packages throughout the source tree. For example, a
+ // particular widget may define one or more resource bundles,
+ // structured in a program as follows, where moduleName is
+ // mycode.mywidget and bundleNames available include bundleone and
+ // bundletwo:
+ //
+ // | ...
+ // | mycode/
+ // | mywidget/
+ // | nls/
+ // | bundleone.js (the fallback translation, English in this example)
+ // | bundletwo.js (also a fallback translation)
+ // | de/
+ // | bundleone.js
+ // | bundletwo.js
+ // | de-at/
+ // | bundleone.js
+ // | en/
+ // | (empty; use the fallback translation)
+ // | en-us/
+ // | bundleone.js
+ // | en-gb/
+ // | bundleone.js
+ // | es/
+ // | bundleone.js
+ // | bundletwo.js
+ // | ...etc
+ // | ...
+ //
+ // Each directory is named for a locale as specified by RFC 3066,
+ // (http://www.ietf.org/rfc/rfc3066.txt), normalized in lowercase.
+ // Note that the two bundles in the example do not define all the
+ // same variants. For a given locale, bundles will be loaded for
+ // that locale and all more general locales above it, including a
+ // fallback at the root directory. For example, a declaration for
+ // the "de-at" locale will first load `nls/de-at/bundleone.js`,
+ // then `nls/de/bundleone.js` and finally `nls/bundleone.js`. The
+ // data will be flattened into a single Object so that lookups
+ // will follow this cascading pattern. An optional build step can
+ // preload the bundles to avoid data redundancy and the multiple
+ // network hits normally required to load these resources.
+
+ d.require("dojo.i18n");
+ d.i18n._requireLocalization.apply(d.hostenv, arguments);
+ };
+
+ var ore = new RegExp("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?$");
+ var ire = new RegExp("^((([^:]+:)?([^@]+))@)?([^:]*)(:([0-9]+))?$");
+
+ dojo._Url = function(/*dojo._Url||String...*/){
+ // summary:
+ // Constructor to create an object representing a URL.
+ // It is marked as private, since we might consider removing
+ // or simplifying it.
+ // description:
+ // Each argument is evaluated in order relative to the next until
+ // a canonical uri is produced. To get an absolute Uri relative to
+ // the current document use:
+ // new dojo._Url(document.baseURI, url)
+
+ var n = null;
+
+ // TODO: support for IPv6, see RFC 2732
+ var _a = arguments;
+ var uri = [_a[0]];
+ // resolve uri components relative to each other
+ for(var i = 1; i<_a.length; i++){
+ if(!_a[i]){ continue; }
+
+ // Safari doesn't support this.constructor so we have to be explicit
+ // FIXME: Tracked (and fixed) in Webkit bug 3537.
+ // http://bugs.webkit.org/show_bug.cgi?id=3537
+ var relobj = new d._Url(_a[i]+"");
+ var uriobj = new d._Url(uri[0]+"");
+
+ if(
+ relobj.path == "" &&
+ !relobj.scheme &&
+ !relobj.authority &&
+ !relobj.query
+ ){
+ if(relobj.fragment != n){
+ uriobj.fragment = relobj.fragment;
+ }
+ relobj = uriobj;
+ }else if(!relobj.scheme){
+ relobj.scheme = uriobj.scheme;
+
+ if(!relobj.authority){
+ relobj.authority = uriobj.authority;
+
+ if(relobj.path.charAt(0) != "/"){
+ var path = uriobj.path.substring(0,
+ uriobj.path.lastIndexOf("/") + 1) + relobj.path;
+
+ var segs = path.split("/");
+ for(var j = 0; j < segs.length; j++){
+ if(segs[j] == "."){
+ // flatten "./" references
+ if(j == segs.length - 1){
+ segs[j] = "";
+ }else{
+ segs.splice(j, 1);
+ j--;
+ }
+ }else if(j > 0 && !(j == 1 && segs[0] == "") &&
+ segs[j] == ".." && segs[j-1] != ".."){
+ // flatten "../" references
+ if(j == (segs.length - 1)){
+ segs.splice(j, 1);
+ segs[j - 1] = "";
+ }else{
+ segs.splice(j - 1, 2);
+ j -= 2;
+ }
+ }
+ }
+ relobj.path = segs.join("/");
+ }
+ }
+ }
+
+ uri = [];
+ if(relobj.scheme){
+ uri.push(relobj.scheme, ":");
+ }
+ if(relobj.authority){
+ uri.push("//", relobj.authority);
+ }
+ uri.push(relobj.path);
+ if(relobj.query){
+ uri.push("?", relobj.query);
+ }
+ if(relobj.fragment){
+ uri.push("#", relobj.fragment);
+ }
+ }
+
+ this.uri = uri.join("");
+
+ // break the uri into its main components
+ var r = this.uri.match(ore);
+
+ this.scheme = r[2] || (r[1] ? "" : n);
+ this.authority = r[4] || (r[3] ? "" : n);
+ this.path = r[5]; // can never be undefined
+ this.query = r[7] || (r[6] ? "" : n);
+ this.fragment = r[9] || (r[8] ? "" : n);
+
+ if(this.authority != n){
+ // server based naming authority
+ r = this.authority.match(ire);
+
+ this.user = r[3] || n;
+ this.password = r[4] || n;
+ this.host = r[5];
+ this.port = r[7] || n;
+ }
+ }
+
+ dojo._Url.prototype.toString = function(){ return this.uri; };
+
+ dojo.moduleUrl = function(/*String*/module, /*dojo._Url||String*/url){
+ // summary:
+ // Returns a `dojo._Url` object relative to a module.
+ // example:
+ // | var pngPath = dojo.moduleUrl("acme","images/small.png");
+ // | console.dir(pngPath); // list the object properties
+ // | // create an image and set it's source to pngPath's value:
+ // | var img = document.createElement("img");
+ // | // NOTE: we assign the string representation of the url object
+ // | img.src = pngPath.toString();
+ // | // add our image to the document
+ // | dojo.body().appendChild(img);
+ // example:
+ // you may de-reference as far as you like down the package
+ // hierarchy. This is sometimes handy to avoid lenghty relative
+ // urls or for building portable sub-packages. In this example,
+ // the `acme.widget` and `acme.util` directories may be located
+ // under different roots (see `dojo.registerModulePath`) but the
+ // the modules which reference them can be unaware of their
+ // relative locations on the filesystem:
+ // | // somewhere in a configuration block
+ // | dojo.registerModulePath("acme.widget", "../../acme/widget");
+ // | dojo.registerModulePath("acme.util", "../../util");
+ // |
+ // | // ...
+ // |
+ // | // code in a module using acme resources
+ // | var tmpltPath = dojo.moduleUrl("acme.widget","templates/template.html");
+ // | var dataPath = dojo.moduleUrl("acme.util","resources/data.json");
+
+ var loc = d._getModuleSymbols(module).join('/');
+ if(!loc){ return null; }
+ if(loc.lastIndexOf("/") != loc.length-1){
+ loc += "/";
+ }
+
+ //If the path is an absolute path (starts with a / or is on another
+ //domain/xdomain) then don't add the baseUrl.
+ var colonIndex = loc.indexOf(":");
+ if(loc.charAt(0) != "/" && (colonIndex == -1 || colonIndex > loc.indexOf("/"))){
+ loc = d.baseUrl + loc;
+ }
+
+ return new d._Url(loc, url); // String
+ }
+})();
+
+/*=====
+dojo.isBrowser = {
+ // example:
+ // | if(dojo.isBrowser){ ... }
+};
+
+dojo.isFF = {
+ // example:
+ // | if(dojo.isFF > 1){ ... }
+};
+
+dojo.isIE = {
+ // example:
+ // | if(dojo.isIE > 6){
+ // | // we are IE7
+ // | }
+};
+
+dojo.isSafari = {
+ // example:
+ // | if(dojo.isSafari){ ... }
+ // example:
+ // Detect iPhone:
+ // | if(dojo.isSafari && (navigator.userAgent.indexOf("iPhone") < 0)){
+ // | // we are iPhone. iPod touch reports "iPod" above
+ // | }
+};
+
+dojo = {
+ // isBrowser: Boolean
+ // True if the client is a web-browser
+ isBrowser: true,
+ // isFF: Number
+ // Greater than zero if client is FireFox. 0 otherwise. Corresponds to
+ // major detected FireFox version (1.5, 2, 3, etc.)
+ isFF: 2,
+ // isIE: Number
+ // Greater than zero if client is MSIE(PC). 0 otherwise. Corresponds to
+ // major detected IE version (6, 7, 8, etc.)
+ isIE: 6,
+ // isKhtml: Number
+ // Greater than zero if client is a KTHML-derived browser (Konqueror,
+ // Safari, etc.). 0 otherwise. Corresponds to major detected version.
+ isKhtml: 0,
+ // isMozilla: Number
+ // Greater than zero if client is a Mozilla-based browser (Firefox,
+ // SeaMonkey). 0 otherwise. Corresponds to major detected version.
+ isMozilla: 0,
+ // isOpera: Number
+ // Greater than zero if client is Opera. 0 otherwise. Corresponds to
+ // major detected version.
+ isOpera: 0,
+ // isSafari: Number
+ // Greater than zero if client is Safari or iPhone. 0 otherwise.
+ isSafari: 0
+}
+=====*/
+
+if(typeof window != 'undefined'){
+ dojo.isBrowser = true;
+ dojo._name = "browser";
+
+ // attempt to figure out the path to dojo if it isn't set in the config
+ (function(){
+ var d = dojo;
+ // this is a scope protection closure. We set browser versions and grab
+ // the URL we were loaded from here.
+
+ // grab the node we were loaded from
+ if(document && document.getElementsByTagName){
+ var scripts = document.getElementsByTagName("script");
+ var rePkg = /dojo(\.xd)?\.js(\W|$)/i;
+ for(var i = 0; i < scripts.length; i++){
+ var src = scripts[i].getAttribute("src");
+ if(!src){ continue; }
+ var m = src.match(rePkg);
+ if(m){
+ // find out where we came from
+ if(!d.config.baseUrl){
+ d.config.baseUrl = src.substring(0, m.index);
+ }
+ // and find out if we need to modify our behavior
+ var cfg = scripts[i].getAttribute("djConfig");
+ if(cfg){
+ var cfgo = eval("({ "+cfg+" })");
+ for(var x in cfgo){
+ dojo.config[x] = cfgo[x];
+ }
+ }
+ break; // "first Dojo wins"
+ }
+ }
+ }
+ d.baseUrl = d.config.baseUrl;
+
+ // fill in the rendering support information in dojo.render.*
+ var n = navigator;
+ var dua = n.userAgent;
+ var dav = n.appVersion;
+ var tv = parseFloat(dav);
+
+ d.isOpera = (dua.indexOf("Opera") >= 0) ? tv : 0;
+ // safari detection derived from:
+ // http://developer.apple.com/internet/safari/faq.html#anchor2
+ // http://developer.apple.com/internet/safari/uamatrix.html
+ var idx = Math.max(dav.indexOf("WebKit"), dav.indexOf("Safari"), 0);
+ if(idx){
+ // try to grab the explicit Safari version first. If we don't get
+ // one, look for 419.3+ as the indication that we're on something
+ // "Safari 3-ish". Lastly, default to "Safari 2" handling.
+ d.isSafari = parseFloat(dav.split("Version/")[1]) || ( ( parseFloat(dav.substr(idx+7)) >= 419.3 ) ? 3 : 2 ) || 2;
+ }
+ d.isAIR = (dua.indexOf("AdobeAIR") >= 0) ? 1 : 0;
+ d.isKhtml = (dav.indexOf("Konqueror") >= 0 || d.isSafari) ? tv : 0;
+ d.isMozilla = d.isMoz = (dua.indexOf("Gecko") >= 0 && !d.isKhtml) ? tv : 0;
+ d.isFF = d.isIE = 0;
+ if(d.isMoz){
+ d.isFF = parseFloat(dua.split("Firefox/")[1]) || 0;
+ }
+ if(document.all && !d.isOpera){
+ d.isIE = parseFloat(dav.split("MSIE ")[1]) || 0;
+ }
+
+ //Workaround to get local file loads of dojo to work on IE 7
+ //by forcing to not use native xhr.
+ if(dojo.isIE && window.location.protocol === "file:"){
+ dojo.config.ieForceActiveXXhr=true;
+ }
+
+ var cm = document.compatMode;
+ d.isQuirks = cm == "BackCompat" || cm == "QuirksMode" || d.isIE < 6;
+
+ // TODO: is the HTML LANG attribute relevant?
+ d.locale = dojo.config.locale || (d.isIE ? n.userLanguage : n.language).toLowerCase();
+
+ // These are in order of decreasing likelihood; this will change in time.
+ d._XMLHTTP_PROGIDS = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'];
+
+ d._xhrObj = function(){
+ // summary:
+ // does the work of portably generating a new XMLHTTPRequest
+ // object.
+ var http = null;
+ var last_e = null;
+ if(!dojo.isIE || !dojo.config.ieForceActiveXXhr){
+ try{ http = new XMLHttpRequest(); }catch(e){}
+ }
+ if(!http){
+ for(var i=0; i<3; ++i){
+ var progid = d._XMLHTTP_PROGIDS[i];
+ try{
+ http = new ActiveXObject(progid);
+ }catch(e){
+ last_e = e;
+ }
+
+ if(http){
+ d._XMLHTTP_PROGIDS = [progid]; // so faster next time
+ break;
+ }
+ }
+ }
+
+ if(!http){
+ throw new Error("XMLHTTP not available: "+last_e);
+ }
+
+ return http; // XMLHTTPRequest instance
+ }
+
+ d._isDocumentOk = function(http){
+ var stat = http.status || 0;
+ return (stat >= 200 && stat < 300) || // Boolean
+ stat == 304 || // allow any 2XX response code
+ stat == 1223 || // get it out of the cache
+ (!stat && (location.protocol=="file:" || location.protocol=="chrome:") ); // Internet Explorer mangled the status code
+ }
+
+ //See if base tag is in use.
+ //This is to fix http://trac.dojotoolkit.org/ticket/3973,
+ //but really, we need to find out how to get rid of the dojo._Url reference
+ //below and still have DOH work with the dojo.i18n test following some other
+ //test that uses the test frame to load a document (trac #2757).
+ //Opera still has problems, but perhaps a larger issue of base tag support
+ //with XHR requests (hasBase is true, but the request is still made to document
+ //path, not base path).
+ var owloc = window.location+"";
+ var base = document.getElementsByTagName("base");
+ var hasBase = (base && base.length > 0);
+
+ d._getText = function(/*URI*/ uri, /*Boolean*/ fail_ok){
+ // summary: Read the contents of the specified uri and return those contents.
+ // uri:
+ // A relative or absolute uri. If absolute, it still must be in
+ // the same "domain" as we are.
+ // fail_ok:
+ // Default false. If fail_ok and loading fails, return null
+ // instead of throwing.
+ // returns: The response text. null is returned when there is a
+ // failure and failure is okay (an exception otherwise)
+
+ // alert("_getText: " + uri);
+
+ // NOTE: must be declared before scope switches ie. this._xhrObj()
+ var http = this._xhrObj();
+
+ if(!hasBase && dojo._Url){
+ uri = (new dojo._Url(owloc, uri)).toString();
+ }
+ /*
+ console.debug("_getText:", uri);
+ console.debug(window.location+"");
+ alert(uri);
+ */
+
+ if(d.config.cacheBust){
+ uri += (uri.indexOf("?") == -1 ? "?" : "&") + String(d.config.cacheBust).replace(/\W+/g,"");
+ }
+
+ http.open('GET', uri, false);
+ try{
+ http.send(null);
+ // alert(http);
+ if(!d._isDocumentOk(http)){
+ var err = Error("Unable to load "+uri+" status:"+ http.status);
+ err.status = http.status;
+ err.responseText = http.responseText;
+ throw err;
+ }
+ }catch(e){
+ if(fail_ok){ return null; } // null
+ // rethrow the exception
+ throw e;
+ }
+ return http.responseText; // String
+ }
+ })();
+
+ dojo._initFired = false;
+ // BEGIN DOMContentLoaded, from Dean Edwards (http://dean.edwards.name/weblog/2006/06/again/)
+ dojo._loadInit = function(e){
+ dojo._initFired = true;
+ // allow multiple calls, only first one will take effect
+ // A bug in khtml calls events callbacks for document for event which isnt supported
+ // for example a created contextmenu event calls DOMContentLoaded, workaround
+ var type = (e && e.type) ? e.type.toLowerCase() : "load";
+ if(arguments.callee.initialized || (type != "domcontentloaded" && type != "load")){ return; }
+ arguments.callee.initialized = true;
+ if("_khtmlTimer" in dojo){
+ clearInterval(dojo._khtmlTimer);
+ delete dojo._khtmlTimer;
+ }
+
+ if(dojo._inFlightCount == 0){
+ dojo._modulesLoaded();
+ }
+ }
+
+ dojo._fakeLoadInit = function(){
+ dojo._loadInit({type: "load"});
+ }
+
+ if(!dojo.config.afterOnLoad){
+ // START DOMContentLoaded
+ // Mozilla and Opera 9 expose the event we could use
+ if(document.addEventListener){
+ // NOTE:
+ // due to a threading issue in Firefox 2.0, we can't enable
+ // DOMContentLoaded on that platform. For more information, see:
+ // http://trac.dojotoolkit.org/ticket/1704
+ if(dojo.isOpera || dojo.isFF >= 3 || (dojo.isMoz && dojo.config.enableMozDomContentLoaded === true)){
+ document.addEventListener("DOMContentLoaded", dojo._loadInit, null);
+ }
+
+ // mainly for Opera 8.5, won't be fired if DOMContentLoaded fired already.
+ // also used for Mozilla because of trac #1640
+ window.addEventListener("load", dojo._loadInit, null);
+ }
+
+ if(dojo.isAIR){
+ window.addEventListener("load", dojo._loadInit, null);
+ }else if(/(WebKit|khtml)/i.test(navigator.userAgent)){ // sniff
+ dojo._khtmlTimer = setInterval(function(){
+ if(/loaded|complete/.test(document.readyState)){
+ dojo._loadInit(); // call the onload handler
+ }
+ }, 10);
+ }
+ // END DOMContentLoaded
+ }
+
+ (function(){
+ var _w = window;
+ var _handleNodeEvent = function(/*String*/evtName, /*Function*/fp){
+ // summary:
+ // non-destructively adds the specified function to the node's
+ // evtName handler.
+ // evtName: should be in the form "onclick" for "onclick" handlers.
+ // Make sure you pass in the "on" part.
+ var oldHandler = _w[evtName] || function(){};
+ _w[evtName] = function(){
+ fp.apply(_w, arguments);
+ oldHandler.apply(_w, arguments);
+ };
+ };
+
+ if(dojo.isIE){
+ // for Internet Explorer. readyState will not be achieved on init
+ // call, but dojo doesn't need it however, we'll include it
+ // because we don't know if there are other functions added that
+ // might. Note that this has changed because the build process
+ // strips all comments -- including conditional ones.
+ if(!dojo.config.afterOnLoad){
+ document.write(''
+ + ''
+ );
+ }
+
+ // IE WebControl hosted in an application can fire "beforeunload" and "unload"
+ // events when control visibility changes, causing Dojo to unload too soon. The
+ // following code fixes the problem
+ // Reference: http://support.microsoft.com/default.aspx?scid=kb;en-us;199155
+ var _unloading = true;
+ _handleNodeEvent("onbeforeunload", function(){
+ _w.setTimeout(function(){ _unloading = false; }, 0);
+ });
+ _handleNodeEvent("onunload", function(){
+ if(_unloading){ dojo.unloaded(); }
+ });
+
+ try{
+ document.namespaces.add("v","urn:schemas-microsoft-com:vml");
+ document.createStyleSheet().addRule("v\\:*", "behavior:url(#default#VML)");
+ }catch(e){}
+ }else{
+ // FIXME: dojo.unloaded requires dojo scope, so using anon function wrapper.
+ _handleNodeEvent("onbeforeunload", function() { dojo.unloaded(); });
+ }
+
+ })();
+
+ /*
+ OpenAjax.subscribe("OpenAjax", "onload", function(){
+ if(dojo._inFlightCount == 0){
+ dojo._modulesLoaded();
+ }
+ });
+
+ OpenAjax.subscribe("OpenAjax", "onunload", function(){
+ dojo.unloaded();
+ });
+ */
+} //if (typeof window != 'undefined')
+
+//Register any module paths set up in djConfig. Need to do this
+//in the hostenvs since hostenv_browser can read djConfig from a
+//script tag's attribute.
+(function(){
+ var mp = dojo.config["modulePaths"];
+ if(mp){
+ for(var param in mp){
+ dojo.registerModulePath(param, mp[param]);
+ }
+ }
+})();
+
+//Load debug code if necessary.
+if(dojo.config.isDebug){
+ dojo.require("dojo._firebug.firebug");
+}
+
+if(dojo.config.debugAtAllCosts){
+ dojo.config.useXDomain = true;
+ dojo.require("dojo._base._loader.loader_xd");
+ dojo.require("dojo._base._loader.loader_debug");
+ dojo.require("dojo.i18n");
+}
+
+if(!dojo._hasResource["dojo._base.lang"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.lang"] = true;
+dojo.provide("dojo._base.lang");
+
+// Crockford (ish) functions
+
+dojo.isString = function(/*anything*/ it){
+ // summary:
+ // Return true if it is a String
+ return !!arguments.length && it != null && (typeof it == "string" || it instanceof String); // Boolean
+}
+
+dojo.isArray = function(/*anything*/ it){
+ // summary:
+ // Return true if it is an Array
+ return it && (it instanceof Array || typeof it == "array"); // Boolean
+}
+
+/*=====
+dojo.isFunction = function(it){
+ // summary: Return true if it is a Function
+ // it: anything
+ // return: Boolean
+}
+=====*/
+
+dojo.isFunction = (function(){
+ var _isFunction = function(/*anything*/ it){
+ return it && (typeof it == "function" || it instanceof Function); // Boolean
+ };
+
+ return dojo.isSafari ?
+ // only slow this down w/ gratuitious casting in Safari since it's what's b0rken
+ function(/*anything*/ it){
+ if(typeof it == "function" && it == "[object NodeList]"){ return false; }
+ return _isFunction(it); // Boolean
+ } : _isFunction;
+})();
+
+dojo.isObject = function(/*anything*/ it){
+ // summary:
+ // Returns true if it is a JavaScript object (or an Array, a Function
+ // or null)
+ return it !== undefined &&
+ (it === null || typeof it == "object" || dojo.isArray(it) || dojo.isFunction(it)); // Boolean
+}
+
+dojo.isArrayLike = function(/*anything*/ it){
+ // summary:
+ // similar to dojo.isArray() but more permissive
+ // description:
+ // Doesn't strongly test for "arrayness". Instead, settles for "isn't
+ // a string or number and has a length property". Arguments objects
+ // and DOM collections will return true when passed to
+ // dojo.isArrayLike(), but will return false when passed to
+ // dojo.isArray().
+ // return:
+ // If it walks like a duck and quicks like a duck, return `true`
+ var d = dojo;
+ return it && it !== undefined &&
+ // keep out built-in constructors (Number, String, ...) which have length
+ // properties
+ !d.isString(it) && !d.isFunction(it) &&
+ !(it.tagName && it.tagName.toLowerCase() == 'form') &&
+ (d.isArray(it) || isFinite(it.length)); // Boolean
+}
+
+dojo.isAlien = function(/*anything*/ it){
+ // summary:
+ // Returns true if it is a built-in function or some other kind of
+ // oddball that *should* report as a function but doesn't
+ return it && !dojo.isFunction(it) && /\{\s*\[native code\]\s*\}/.test(String(it)); // Boolean
+}
+
+dojo.extend = function(/*Object*/ constructor, /*Object...*/ props){
+ // summary:
+ // Adds all properties and methods of props to constructor's
+ // prototype, making them available to all instances created with
+ // constructor.
+ for(var i=1, l=arguments.length; i 2){
+ return dojo._hitchArgs.apply(dojo, arguments); // Function
+ }
+ if(!method){
+ method = scope;
+ scope = null;
+ }
+ if(dojo.isString(method)){
+ scope = scope || dojo.global;
+ if(!scope[method]){ throw(['dojo.hitch: scope["', method, '"] is null (scope="', scope, '")'].join('')); }
+ return function(){ return scope[method].apply(scope, arguments || []); }; // Function
+ }
+ return !scope ? method : function(){ return method.apply(scope, arguments || []); }; // Function
+}
+
+/*=====
+dojo.delegate = function(obj, props){
+ // summary:
+ // returns a new object which "looks" to obj for properties which it
+ // does not have a value for. Optionally takes a bag of properties to
+ // seed the returned object with initially.
+ // description:
+ // This is a small implementaton of the Boodman/Crockford delegation
+ // pattern in JavaScript. An intermediate object constructor mediates
+ // the prototype chain for the returned object, using it to delegate
+ // down to obj for property lookup when object-local lookup fails.
+ // This can be thought of similarly to ES4's "wrap", save that it does
+ // not act on types but rather on pure objects.
+ // obj:
+ // The object to delegate to for properties not found directly on the
+ // return object or in props.
+ // props:
+ // an object containing properties to assign to the returned object
+ // returns:
+ // an Object of anonymous type
+ // example:
+ // | var foo = { bar: "baz" };
+ // | var thinger = dojo.delegate(foo, { thud: "xyzzy"});
+ // | thinger.bar == "baz"; // delegated to foo
+ // | foo.thud == undefined; // by definition
+ // | thinger.thud == "xyzzy"; // mixed in from props
+ // | foo.bar = "thonk";
+ // | thinger.bar == "thonk"; // still delegated to foo's bar
+}
+=====*/
+
+dojo.delegate = dojo._delegate = function(obj, props){
+
+ // boodman/crockford delegation
+ function TMP(){};
+ TMP.prototype = obj;
+ var tmp = new TMP();
+ if(props){
+ dojo.mixin(tmp, props);
+ }
+ return tmp; // Object
+}
+
+dojo.partial = function(/*Function|String*/method /*, ...*/){
+ // summary:
+ // similar to hitch() except that the scope object is left to be
+ // whatever the execution context eventually becomes.
+ // description:
+ // Calling dojo.partial is the functional equivalent of calling:
+ // | dojo.hitch(null, funcName, ...);
+ var arr = [ null ];
+ return dojo.hitch.apply(dojo, arr.concat(dojo._toArray(arguments))); // Function
+}
+
+dojo._toArray = function(/*Object*/obj, /*Number?*/offset, /*Array?*/ startWith){
+ // summary:
+ // Converts an array-like object (i.e. arguments, DOMCollection) to an
+ // array. Returns a new Array with the elements of obj.
+ // obj:
+ // the object to "arrayify". We expect the object to have, at a
+ // minimum, a length property which corresponds to integer-indexed
+ // properties.
+ // offset:
+ // the location in obj to start iterating from. Defaults to 0.
+ // Optional.
+ // startWith:
+ // An array to pack with the properties of obj. If provided,
+ // properties in obj are appended at the end of startWith and
+ // startWith is the returned array.
+ var arr = startWith||[];
+ for(var x = offset || 0; x < obj.length; x++){
+ arr.push(obj[x]);
+ }
+ return arr; // Array
+}
+
+dojo.clone = function(/*anything*/ o){
+ // summary:
+ // Clones objects (including DOM nodes) and all children.
+ // Warning: do not clone cyclic structures.
+ if(!o){ return o; }
+ if(dojo.isArray(o)){
+ var r = [];
+ for(var i = 0; i < o.length; ++i){
+ r.push(dojo.clone(o[i]));
+ }
+ return r; // Array
+ }
+ if(!dojo.isObject(o)){
+ return o; /*anything*/
+ }
+ if(o.nodeType && o.cloneNode){ // isNode
+ return o.cloneNode(true); // Node
+ }
+ if(o instanceof Date){
+ return new Date(o.getTime()); // Date
+ }
+ // Generic objects
+ var r = new o.constructor(); // specific to dojo.declare()'d classes!
+ for(var i in o){
+ if(!(i in r) || r[i] != o[i]){
+ r[i] = dojo.clone(o[i]);
+ }
+ }
+ return r; // Object
+}
+
+dojo.trim = function(/*String*/ str){
+ // summary:
+ // trims whitespaces from both sides of the string
+ // description:
+ // This version of trim() was selected for inclusion into the base due
+ // to its compact size and relatively good performance (see Steven
+ // Levithan's blog:
+ // http://blog.stevenlevithan.com/archives/faster-trim-javascript).
+ // The fastest but longest version of this function is located at
+ // dojo.string.trim()
+ return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); // String
+}
+
+}
+
+if(!dojo._hasResource["dojo._base.declare"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.declare"] = true;
+dojo.provide("dojo._base.declare");
+
+// this file courtesy of the TurboAjax group, licensed under a Dojo CLA
+
+dojo.declare = function(/*String*/ className, /*Function|Function[]*/ superclass, /*Object*/ props){
+ // summary:
+ // Create a feature-rich constructor from compact notation
+ // className:
+ // The name of the constructor (loosely, a "class")
+ // stored in the "declaredClass" property in the created prototype
+ // superclass:
+ // May be null, a Function, or an Array of Functions. If an array,
+ // the first element is used as the prototypical ancestor and
+ // any following Functions become mixin ancestors.
+ // props:
+ // An object whose properties are copied to the
+ // created prototype.
+ // Add an instance-initialization function by making it a property
+ // named "constructor".
+ // description:
+ // Create a constructor using a compact notation for inheritance and
+ // prototype extension.
+ //
+ // All superclasses (including mixins) must be Functions (not simple Objects).
+ //
+ // Mixin ancestors provide a type of multiple inheritance. Prototypes of mixin
+ // ancestors are copied to the new class: changes to mixin prototypes will
+ // not affect classes to which they have been mixed in.
+ //
+ // "className" is cached in "declaredClass" property of the new class.
+ //
+ // example:
+ // | dojo.declare("my.classes.bar", my.classes.foo, {
+ // | // properties to be added to the class prototype
+ // | someValue: 2,
+ // | // initialization function
+ // | constructor: function(){
+ // | this.myComplicatedObject = new ReallyComplicatedObject();
+ // | },
+ // | // other functions
+ // | someMethod: function(){
+ // | doStuff();
+ // | }
+ // | );
+
+ // process superclass argument
+ // var dd=dojo.declare, mixins=null;
+ var dd = arguments.callee, mixins;
+ if(dojo.isArray(superclass)){
+ mixins = superclass;
+ superclass = mixins.shift();
+ }
+ // construct intermediate classes for mixins
+ if(mixins){
+ dojo.forEach(mixins, function(m){
+ if(!m){ throw(className + ": mixin #" + i + " is null"); } // It's likely a required module is not loaded
+ superclass = dd._delegate(superclass, m);
+ });
+ }
+ // prepare values
+ var init = (props||0).constructor, ctor = dd._delegate(superclass), fn;
+ // name methods (experimental)
+ for(var i in props){ if(dojo.isFunction(fn = props[i]) && !0[i]){fn.nom = i;} } // 0[i] checks Object.prototype
+ // decorate prototype
+ dojo.extend(ctor, {declaredClass: className, _constructor: init, preamble: null}, props || 0);
+ // special help for IE
+ ctor.prototype.constructor = ctor;
+ // create named reference
+ return dojo.setObject(className, ctor); // Function
+};
+
+dojo.mixin(dojo.declare, {
+ _delegate: function(base, mixin){
+ var bp = (base||0).prototype, mp = (mixin||0).prototype;
+ // fresh constructor, fresh prototype
+ var ctor = dojo.declare._makeCtor();
+ // cache ancestry
+ dojo.mixin(ctor, {superclass: bp, mixin: mp, extend: dojo.declare._extend});
+ // chain prototypes
+ if(base){ctor.prototype = dojo._delegate(bp);}
+ // add mixin and core
+ dojo.extend(ctor, dojo.declare._core, mp||0, {_constructor: null, preamble: null});
+ // special help for IE
+ ctor.prototype.constructor = ctor;
+ // name this class for debugging
+ ctor.prototype.declaredClass = (bp||0).declaredClass + '_' + (mp||0).declaredClass;
+ return ctor;
+ },
+ _extend: function(props){
+ for(var i in props){ if(dojo.isFunction(fn=props[i]) && !0[i]){fn.nom=i;} }
+ dojo.extend(this, props);
+ },
+ _makeCtor: function(){
+ // we have to make a function, but don't want to close over anything
+ return function(){ this._construct(arguments); };
+ },
+ _core: {
+ _construct: function(args){
+ var c=args.callee, s=c.superclass, ct=s&&s.constructor, m=c.mixin, mct=m&&m.constructor, a=args, ii, fn;
+ // side-effect of = used on purpose here, lint may complain, don't try this at home
+ if(a[0]){
+ // FIXME: preambles for each mixin should be allowed
+ // FIXME:
+ // should we allow the preamble here NOT to modify the
+ // default args, but instead to act on each mixin
+ // independently of the class instance being constructed
+ // (for impedence matching)?
+
+ // allow any first argument w/ a "preamble" property to act as a
+ // class preamble (not exclusive of the prototype preamble)
+ if(/*dojo.isFunction*/((fn = a[0].preamble))){
+ a = fn.apply(this, a) || a;
+ }
+ }
+ // prototype preamble
+ if((fn = c.prototype.preamble)){a = fn.apply(this, a) || a;}
+ // FIXME:
+ // need to provide an optional prototype-settable
+ // "_explicitSuper" property which disables this
+ // initialize superclass
+ if(ct&&ct.apply){ct.apply(this, a);}
+ // initialize mixin
+ if(mct&&mct.apply){mct.apply(this, a);}
+ // initialize self
+ if((ii=c.prototype._constructor)){ii.apply(this, args);}
+ // post construction
+ if(this.constructor.prototype==c.prototype && (ct=this.postscript)){ ct.apply(this, args); }
+ },
+ _findMixin: function(mixin){
+ var c = this.constructor, p, m;
+ while(c){
+ p = c.superclass;
+ m = c.mixin;
+ if(m==mixin || (m instanceof mixin.constructor)){return p;}
+ if(m && (m=m._findMixin(mixin))){return m;}
+ c = p && p.constructor;
+ }
+ },
+ _findMethod: function(name, method, ptype, has){
+ // consciously trading readability for bytes and speed in this low-level method
+ var p=ptype, c, m, f;
+ do{
+ c = p.constructor;
+ m = c.mixin;
+ // find method by name in our mixin ancestor
+ if(m && (m=this._findMethod(name, method, m, has))){return m;}
+ // if we found a named method that either exactly-is or exactly-is-not 'method'
+ if((f=p[name])&&(has==(f==method))){return p;}
+ // ascend chain
+ p = c.superclass;
+ }while(p);
+ // if we couldn't find an ancestor in our primary chain, try a mixin chain
+ return !has && (p=this._findMixin(ptype)) && this._findMethod(name, method, p, has);
+ },
+ inherited: function(name, args, newArgs){
+ // optionalize name argument (experimental)
+ var a = arguments;
+ if(!dojo.isString(a[0])){newArgs=args; args=name; name=args.callee.nom;}
+ a = newArgs||args;
+ var c = args.callee, p = this.constructor.prototype, fn, mp;
+ // if not an instance override
+ if(this[name] != c || p[name] == c){
+ mp = this._findMethod(name, c, p, true);
+ if(!mp){throw(this.declaredClass + ': inherited method "' + name + '" mismatch');}
+ p = this._findMethod(name, c, mp, false);
+ }
+ fn = p && p[name];
+ if(!fn){throw(mp.declaredClass + ': inherited method "' + name + '" not found');}
+ // if the function exists, invoke it in our scope
+ return fn.apply(this, a);
+ }
+ }
+});
+
+}
+
+if(!dojo._hasResource["dojo._base.connect"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.connect"] = true;
+dojo.provide("dojo._base.connect");
+
+// this file courtesy of the TurboAjax Group, licensed under a Dojo CLA
+
+// low-level delegation machinery
+dojo._listener = {
+ // create a dispatcher function
+ getDispatcher: function(){
+ // following comments pulled out-of-line to prevent cloning them
+ // in the returned function.
+ // - indices (i) that are really in the array of listeners (ls) will
+ // not be in Array.prototype. This is the 'sparse array' trick
+ // that keeps us safe from libs that take liberties with built-in
+ // objects
+ // - listener is invoked with current scope (this)
+ return function(){
+ var ap=Array.prototype, c=arguments.callee, ls=c._listeners, t=c.target;
+ // return value comes from original target function
+ var r=t && t.apply(this, arguments);
+ // invoke listeners after target function
+ for(var i in ls){
+ if(!(i in ap)){
+ ls[i].apply(this, arguments);
+ }
+ }
+ // return value comes from original target function
+ return r;
+ }
+ },
+ // add a listener to an object
+ add: function(/*Object*/ source, /*String*/ method, /*Function*/ listener){
+ // Whenever 'method' is invoked, 'listener' will have the same scope.
+ // Trying to supporting a context object for the listener led to
+ // complexity.
+ // Non trivial to provide 'once' functionality here
+ // because listener could be the result of a dojo.hitch call,
+ // in which case two references to the same hitch target would not
+ // be equivalent.
+ source = source || dojo.global;
+ // The source method is either null, a dispatcher, or some other function
+ var f = source[method];
+ // Ensure a dispatcher
+ if(!f||!f._listeners){
+ var d = dojo._listener.getDispatcher();
+ // original target function is special
+ d.target = f;
+ // dispatcher holds a list of listeners
+ d._listeners = [];
+ // redirect source to dispatcher
+ f = source[method] = d;
+ }
+ // The contract is that a handle is returned that can
+ // identify this listener for disconnect.
+ //
+ // The type of the handle is private. Here is it implemented as Integer.
+ // DOM event code has this same contract but handle is Function
+ // in non-IE browsers.
+ //
+ // We could have separate lists of before and after listeners.
+ return f._listeners.push(listener) ; /*Handle*/
+ },
+ // remove a listener from an object
+ remove: function(/*Object*/ source, /*String*/ method, /*Handle*/ handle){
+ var f = (source||dojo.global)[method];
+ // remember that handle is the index+1 (0 is not a valid handle)
+ if(f && f._listeners && handle--){
+ delete f._listeners[handle];
+ }
+ }
+};
+
+// Multiple delegation for arbitrary methods.
+
+// This unit knows nothing about DOM,
+// but we include DOM aware
+// documentation and dontFix
+// argument here to help the autodocs.
+// Actual DOM aware code is in event.js.
+
+dojo.connect = function(/*Object|null*/ obj,
+ /*String*/ event,
+ /*Object|null*/ context,
+ /*String|Function*/ method,
+ /*Boolean*/ dontFix){
+ // summary:
+ // Create a link that calls one function when another executes.
+ //
+ // description:
+ // Connects method to event, so that after event fires, method
+ // does too. All connected functions are passed the same arguments as
+ // the event function was initially called with. You may connect as
+ // many methods to event as needed.
+ //
+ // event must be a string. If obj is null, dojo.global is used.
+ //
+ // null arguments may simply be omitted.
+ //
+ // obj[event] can resolve to a function or undefined (null).
+ // If obj[event] is null, it is assigned a function.
+ //
+ // The return value is a handle that is needed to
+ // remove this connection with dojo.disconnect.
+ //
+ // obj:
+ // The source object for the event function.
+ // Defaults to dojo.global if null.
+ // If obj is a DOM node, the connection is delegated
+ // to the DOM event manager (unless dontFix is true).
+ //
+ // event:
+ // String name of the event function in obj.
+ // I.e. identifies a property obj[event].
+ //
+ // context:
+ // The object that method will receive as "this".
+ //
+ // If context is null and method is a function, then method
+ // inherits the context of event.
+ //
+ // If method is a string then context must be the source
+ // object object for method (context[method]). If context is null,
+ // dojo.global is used.
+ //
+ // method:
+ // A function reference, or name of a function in context.
+ // The function identified by method fires after event does.
+ // method receives the same arguments as the event.
+ // See context argument comments for information on method's scope.
+ //
+ // dontFix:
+ // If obj is a DOM node, set dontFix to true to prevent delegation
+ // of this connection to the DOM event manager.
+ //
+ // example:
+ // When obj.onchange(), do ui.update():
+ // | dojo.connect(obj, "onchange", ui, "update");
+ // | dojo.connect(obj, "onchange", ui, ui.update); // same
+ //
+ // example:
+ // Using return value for disconnect:
+ // | var link = dojo.connect(obj, "onchange", ui, "update");
+ // | ...
+ // | dojo.disconnect(link);
+ //
+ // example:
+ // When onglobalevent executes, watcher.handler is invoked:
+ // | dojo.connect(null, "onglobalevent", watcher, "handler");
+ //
+ // example:
+ // When ob.onCustomEvent executes, customEventHandler is invoked:
+ // | dojo.connect(ob, "onCustomEvent", null, "customEventHandler");
+ // | dojo.connect(ob, "onCustomEvent", "customEventHandler"); // same
+ //
+ // example:
+ // When ob.onCustomEvent executes, customEventHandler is invoked
+ // with the same scope (this):
+ // | dojo.connect(ob, "onCustomEvent", null, customEventHandler);
+ // | dojo.connect(ob, "onCustomEvent", customEventHandler); // same
+ //
+ // example:
+ // When globalEvent executes, globalHandler is invoked
+ // with the same scope (this):
+ // | dojo.connect(null, "globalEvent", null, globalHandler);
+ // | dojo.connect("globalEvent", globalHandler); // same
+
+ // normalize arguments
+ var a=arguments, args=[], i=0;
+ // if a[0] is a String, obj was ommited
+ args.push(dojo.isString(a[0]) ? null : a[i++], a[i++]);
+ // if the arg-after-next is a String or Function, context was NOT omitted
+ var a1 = a[i+1];
+ args.push(dojo.isString(a1)||dojo.isFunction(a1) ? a[i++] : null, a[i++]);
+ // absorb any additional arguments
+ for(var l=a.length; i.
+ // description:
+ // JavaScript has no threads, and even if it did, threads are hard.
+ // Deferreds are a way of abstracting non-blocking events, such as the
+ // final response to an XMLHttpRequest. Deferreds create a promise to
+ // return a response a some point in the future and an easy way to
+ // register your interest in receiving that response.
+ //
+ // The most important methods for Deffered users are:
+ //
+ // * addCallback(handler)
+ // * addErrback(handler)
+ // * callback(result)
+ // * errback(result)
+ //
+ // In general, when a function returns a Deferred, users then "fill
+ // in" the second half of the contract by registering callbacks and
+ // error handlers. You may register as many callback and errback
+ // handlers as you like and they will be executed in the order
+ // registered when a result is provided. Usually this result is
+ // provided as the result of an asynchronous operation. The code
+ // "managing" the Deferred (the code that made the promise to provide
+ // an answer later) will use the callback() and errback() methods to
+ // communicate with registered listeners about the result of the
+ // operation. At this time, all registered result handlers are called
+ // *with the most recent result value*.
+ //
+ // Deferred callback handlers are treated as a chain, and each item in
+ // the chain is required to return a value that will be fed into
+ // successive handlers. The most minimal callback may be registered
+ // like this:
+ //
+ // | var d = new dojo.Deferred();
+ // | d.addCallback(function(result){ return result; });
+ //
+ // Perhaps the most common mistake when first using Deferreds is to
+ // forget to return a value (in most cases, the value you were
+ // passed).
+ //
+ // The sequence of callbacks is internally represented as a list of
+ // 2-tuples containing the callback/errback pair. For example, the
+ // following call sequence:
+ //
+ // | var d = new dojo.Deferred();
+ // | d.addCallback(myCallback);
+ // | d.addErrback(myErrback);
+ // | d.addBoth(myBoth);
+ // | d.addCallbacks(myCallback, myErrback);
+ //
+ // is translated into a Deferred with the following internal
+ // representation:
+ //
+ // | [
+ // | [myCallback, null],
+ // | [null, myErrback],
+ // | [myBoth, myBoth],
+ // | [myCallback, myErrback]
+ // | ]
+ //
+ // The Deferred also keeps track of its current status (fired). Its
+ // status may be one of three things:
+ //
+ // * -1: no value yet (initial condition)
+ // * 0: success
+ // * 1: error
+ //
+ // A Deferred will be in the error state if one of the following three
+ // conditions are met:
+ //
+ // 1. The result given to callback or errback is "instanceof" Error
+ // 2. The previous callback or errback raised an exception while
+ // executing
+ // 3. The previous callback or errback returned a value
+ // "instanceof" Error
+ //
+ // Otherwise, the Deferred will be in the success state. The state of
+ // the Deferred determines the next element in the callback sequence
+ // to run.
+ //
+ // When a callback or errback occurs with the example deferred chain,
+ // something equivalent to the following will happen (imagine
+ // that exceptions are caught and returned):
+ //
+ // | // d.callback(result) or d.errback(result)
+ // | if(!(result instanceof Error)){
+ // | result = myCallback(result);
+ // | }
+ // | if(result instanceof Error){
+ // | result = myErrback(result);
+ // | }
+ // | result = myBoth(result);
+ // | if(result instanceof Error){
+ // | result = myErrback(result);
+ // | }else{
+ // | result = myCallback(result);
+ // | }
+ //
+ // The result is then stored away in case another step is added to the
+ // callback sequence. Since the Deferred already has a value
+ // available, any new callbacks added will be called immediately.
+ //
+ // There are two other "advanced" details about this implementation
+ // that are useful:
+ //
+ // Callbacks are allowed to return Deferred instances themselves, so
+ // you can build complicated sequences of events with ease.
+ //
+ // The creator of the Deferred may specify a canceller. The canceller
+ // is a function that will be called if Deferred.cancel is called
+ // before the Deferred fires. You can use this to implement clean
+ // aborting of an XMLHttpRequest, etc. Note that cancel will fire the
+ // deferred with a CancelledError (unless your canceller returns
+ // another kind of error), so the errbacks should be prepared to
+ // handle that error for cancellable Deferreds.
+ // example:
+ // | var deferred = new dojo.Deferred();
+ // | setTimeout(function(){ deferred.callback({success: true}); }, 1000);
+ // | return deferred;
+ // example:
+ // Deferred objects are often used when making code asynchronous. It
+ // may be easiest to write functions in a synchronous manner and then
+ // split code using a deferred to trigger a response to a long-lived
+ // operation. For example, instead of register a callback function to
+ // denote when a rendering operation completes, the function can
+ // simply return a deferred:
+ //
+ // | // callback style:
+ // | function renderLotsOfData(data, callback){
+ // | var success = false
+ // | try{
+ // | for(var x in data){
+ // | renderDataitem(data[x]);
+ // | }
+ // | success = true;
+ // | }catch(e){ }
+ // | if(callback){
+ // | callback(success);
+ // | }
+ // | }
+ //
+ // | // using callback style
+ // | renderLotsOfData(someDataObj, function(success){
+ // | // handles success or failure
+ // | if(!success){
+ // | promptUserToRecover();
+ // | }
+ // | });
+ // | // NOTE: no way to add another callback here!!
+ // example:
+ // Using a Deferred doesn't simplify the sending code any, but it
+ // provides a standard interface for callers and senders alike,
+ // providing both with a simple way to service multiple callbacks for
+ // an operation and freeing both sides from worrying about details
+ // such as "did this get called already?". With Deferreds, new
+ // callbacks can be added at any time.
+ //
+ // | // Deferred style:
+ // | function renderLotsOfData(data){
+ // | var d = new dojo.Deferred();
+ // | try{
+ // | for(var x in data){
+ // | renderDataitem(data[x]);
+ // | }
+ // | d.callback(true);
+ // | }catch(e){
+ // | d.errback(new Error("rendering failed"));
+ // | }
+ // | return d;
+ // | }
+ //
+ // | // using Deferred style
+ // | renderLotsOfData(someDataObj).addErrback(function(){
+ // | promptUserToRecover();
+ // | });
+ // | // NOTE: addErrback and addCallback both return the Deferred
+ // | // again, so we could chain adding callbacks or save the
+ // | // deferred for later should we need to be notified again.
+ // example:
+ // In this example, renderLotsOfData is syncrhonous and so both
+ // versions are pretty artificial. Putting the data display on a
+ // timeout helps show why Deferreds rock:
+ //
+ // | // Deferred style and async func
+ // | function renderLotsOfData(data){
+ // | var d = new dojo.Deferred();
+ // | setTimeout(function(){
+ // | try{
+ // | for(var x in data){
+ // | renderDataitem(data[x]);
+ // | }
+ // | d.callback(true);
+ // | }catch(e){
+ // | d.errback(new Error("rendering failed"));
+ // | }
+ // | }, 100);
+ // | return d;
+ // | }
+ //
+ // | // using Deferred style
+ // | renderLotsOfData(someDataObj).addErrback(function(){
+ // | promptUserToRecover();
+ // | });
+ //
+ // Note that the caller doesn't have to change his code at all to
+ // handle the asynchronous case.
+
+ this.chain = [];
+ this.id = this._nextId();
+ this.fired = -1;
+ this.paused = 0;
+ this.results = [null, null];
+ this.canceller = canceller;
+ this.silentlyCancelled = false;
+};
+
+dojo.extend(dojo.Deferred, {
+ /*
+ makeCalled: function(){
+ // summary:
+ // returns a new, empty deferred, which is already in the called
+ // state. Calling callback() or errback() on this deferred will
+ // yeild an error and adding new handlers to it will result in
+ // them being called immediately.
+ var deferred = new dojo.Deferred();
+ deferred.callback();
+ return deferred;
+ },
+
+ toString: function(){
+ var state;
+ if(this.fired == -1){
+ state = 'unfired';
+ }else{
+ state = this.fired ? 'success' : 'error';
+ }
+ return 'Deferred(' + this.id + ', ' + state + ')';
+ },
+ */
+
+ _nextId: (function(){
+ var n = 1;
+ return function(){ return n++; };
+ })(),
+
+ cancel: function(){
+ // summary:
+ // Cancels a Deferred that has not yet received a value, or is
+ // waiting on another Deferred as its value.
+ // description:
+ // If a canceller is defined, the canceller is called. If the
+ // canceller did not return an error, or there was no canceller,
+ // then the errback chain is started.
+ var err;
+ if(this.fired == -1){
+ if(this.canceller){
+ err = this.canceller(this);
+ }else{
+ this.silentlyCancelled = true;
+ }
+ if(this.fired == -1){
+ if(!(err instanceof Error)){
+ var res = err;
+ err = new Error("Deferred Cancelled");
+ err.dojoType = "cancel";
+ err.cancelResult = res;
+ }
+ this.errback(err);
+ }
+ }else if( (this.fired == 0) &&
+ (this.results[0] instanceof dojo.Deferred)
+ ){
+ this.results[0].cancel();
+ }
+ },
+
+ _resback: function(res){
+ // summary:
+ // The private primitive that means either callback or errback
+ this.fired = ((res instanceof Error) ? 1 : 0);
+ this.results[this.fired] = res;
+ this._fire();
+ },
+
+ _check: function(){
+ if(this.fired != -1){
+ if(!this.silentlyCancelled){
+ throw new Error("already called!");
+ }
+ this.silentlyCancelled = false;
+ return;
+ }
+ },
+
+ callback: function(res){
+ // summary:
+ // Begin the callback sequence with a non-error value.
+
+ /*
+ callback or errback should only be called once on a given
+ Deferred.
+ */
+ this._check();
+ this._resback(res);
+ },
+
+ errback: function(/*Error*/res){
+ // summary:
+ // Begin the callback sequence with an error result.
+ this._check();
+ if(!(res instanceof Error)){
+ res = new Error(res);
+ }
+ this._resback(res);
+ },
+
+ addBoth: function(/*Function|Object*/cb, /*String?*/cbfn){
+ // summary:
+ // Add the same function as both a callback and an errback as the
+ // next element on the callback sequence.This is useful for code
+ // that you want to guarantee to run, e.g. a finalizer.
+ var enclosed = dojo.hitch.apply(dojo, arguments);
+ return this.addCallbacks(enclosed, enclosed);
+ },
+
+ addCallback: function(/*Function|Object*/cb, /*String?*/cbfn /*...*/){
+ // summary:
+ // Add a single callback to the end of the callback sequence.
+ return this.addCallbacks(dojo.hitch.apply(dojo, arguments));
+ },
+
+ addErrback: function(cb, cbfn){
+ // summary:
+ // Add a single callback to the end of the callback sequence.
+ return this.addCallbacks(null, dojo.hitch.apply(dojo, arguments));
+ },
+
+ addCallbacks: function(cb, eb){
+ // summary:
+ // Add separate callback and errback to the end of the callback
+ // sequence.
+ this.chain.push([cb, eb])
+ if(this.fired >= 0){
+ this._fire();
+ }
+ return this;
+ },
+
+ _fire: function(){
+ // summary:
+ // Used internally to exhaust the callback sequence when a result
+ // is available.
+ var chain = this.chain;
+ var fired = this.fired;
+ var res = this.results[fired];
+ var self = this;
+ var cb = null;
+ while(
+ (chain.length > 0) &&
+ (this.paused == 0)
+ ){
+ // Array
+ var f = chain.shift()[fired];
+ if(!f){ continue; }
+ try{
+ res = f(res);
+ fired = ((res instanceof Error) ? 1 : 0);
+ if(res instanceof dojo.Deferred){
+ cb = function(res){
+ self._resback(res);
+ // inlined from _pause()
+ self.paused--;
+ if(
+ (self.paused == 0) &&
+ (self.fired >= 0)
+ ){
+ self._fire();
+ }
+ }
+ // inlined from _unpause
+ this.paused++;
+ }
+ }catch(err){
+ console.debug(err);
+ fired = 1;
+ res = err;
+ }
+ }
+ this.fired = fired;
+ this.results[fired] = res;
+ if((cb)&&(this.paused)){
+ // this is for "tail recursion" in case the dependent
+ // deferred is already fired
+ res.addBoth(cb);
+ }
+ }
+});
+
+}
+
+if(!dojo._hasResource["dojo._base.json"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.json"] = true;
+dojo.provide("dojo._base.json");
+
+dojo.fromJson = function(/*String*/ json){
+ // summary:
+ // Parses a [JSON](http://json.org) string to return a JavaScript object.
+ // json:
+ // a string literal of a JSON item, for instance:
+ // `'{ "foo": [ "bar", 1, { "baz": "thud" } ] }'`
+
+ return eval("(" + json + ")"); // Object
+}
+
+dojo._escapeString = function(/*String*/str){
+ //summary:
+ // Adds escape sequences for non-visual characters, double quote and
+ // backslash and surrounds with double quotes to form a valid string
+ // literal.
+ return ('"' + str.replace(/(["\\])/g, '\\$1') + '"').
+ replace(/[\f]/g, "\\f").replace(/[\b]/g, "\\b").replace(/[\n]/g, "\\n").
+ replace(/[\t]/g, "\\t").replace(/[\r]/g, "\\r"); // string
+}
+
+dojo.toJsonIndentStr = "\t";
+dojo.toJson = function(/*Object*/ it, /*Boolean?*/ prettyPrint, /*String?*/ _indentStr){
+ // summary:
+ // Returns a [JSON](http://json.org) serialization of an object.
+ //
+ // description:
+ // Returns a [JSON](http://json.org) serialization of an object.
+ // Note that this doesn't check for infinite recursion, so don't do that!
+ //
+ // it:
+ // an object to be serialized. Objects may define their own
+ // serialization via a special "__json__" or "json" function
+ // property. If a specialized serializer has been defined, it will
+ // be used as a fallback.
+ //
+ // prettyPrint:
+ // if true, we indent objects and arrays to make the output prettier.
+ // The variable dojo.toJsonIndentStr is used as the indent string
+ // -- to use something other than the default (tab),
+ // change that variable before calling dojo.toJson().
+ //
+ // _indentStr:
+ // private variable for recursive calls when pretty printing, do not use.
+
+ if(it === undefined){
+ return "undefined";
+ }
+ var objtype = typeof it;
+ if(objtype == "number" || objtype == "boolean"){
+ return it + "";
+ }
+ if(it === null){
+ return "null";
+ }
+ if(dojo.isString(it)){
+ return dojo._escapeString(it);
+ }
+ if(it.nodeType && it.cloneNode){ // isNode
+ return ""; // FIXME: would something like outerHTML be better here?
+ }
+ // recurse
+ var recurse = arguments.callee;
+ // short-circuit for objects that support "json" serialization
+ // if they return "self" then just pass-through...
+ var newObj;
+ _indentStr = _indentStr || "";
+ var nextIndent = prettyPrint ? _indentStr + dojo.toJsonIndentStr : "";
+ if(typeof it.__json__ == "function"){
+ newObj = it.__json__();
+ if(it !== newObj){
+ return recurse(newObj, prettyPrint, nextIndent);
+ }
+ }
+ if(typeof it.json == "function"){
+ newObj = it.json();
+ if(it !== newObj){
+ return recurse(newObj, prettyPrint, nextIndent);
+ }
+ }
+
+ var sep = prettyPrint ? " " : "";
+ var newLine = prettyPrint ? "\n" : "";
+
+ // array
+ if(dojo.isArray(it)){
+ var res = dojo.map(it, function(obj){
+ var val = recurse(obj, prettyPrint, nextIndent);
+ if(typeof val != "string"){
+ val = "undefined";
+ }
+ return newLine + nextIndent + val;
+ });
+ return "[" + res.join("," + sep) + newLine + _indentStr + "]";
+ }
+ /*
+ // look in the registry
+ try {
+ window.o = it;
+ newObj = dojo.json.jsonRegistry.match(it);
+ return recurse(newObj, prettyPrint, nextIndent);
+ }catch(e){
+ // console.debug(e);
+ }
+ // it's a function with no adapter, skip it
+ */
+ if(objtype == "function"){
+ return null; // null
+ }
+ // generic object code path
+ var output = [];
+ for(var key in it){
+ var keyStr;
+ if(typeof key == "number"){
+ keyStr = '"' + key + '"';
+ }else if(typeof key == "string"){
+ keyStr = dojo._escapeString(key);
+ }else{
+ // skip non-string or number keys
+ continue;
+ }
+ val = recurse(it[key], prettyPrint, nextIndent);
+ if(typeof val != "string"){
+ // skip non-serializable values
+ continue;
+ }
+ // FIXME: use += on Moz!!
+ // MOW NOTE: using += is a pain because you have to account for the dangling comma...
+ output.push(newLine + nextIndent + keyStr + ":" + sep + val);
+ }
+ return "{" + output.join("," + sep) + newLine + _indentStr + "}"; // String
+}
+
+}
+
+if(!dojo._hasResource["dojo._base.array"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.array"] = true;
+
+dojo.provide("dojo._base.array");
+
+(function(){
+ var _getParts = function(arr, obj, cb){
+ return [
+ dojo.isString(arr) ? arr.split("") : arr,
+ obj || dojo.global,
+ // FIXME: cache the anonymous functions we create here?
+ dojo.isString(cb) ? new Function("item", "index", "array", cb) : cb
+ ];
+ };
+
+ dojo.mixin(dojo, {
+ indexOf: function( /*Array*/ array,
+ /*Object*/ value,
+ /*Integer?*/ fromIndex,
+ /*Boolean?*/ findLast){
+ // summary:
+ // locates the first index of the provided value in the
+ // passed array. If the value is not found, -1 is returned.
+ // description:
+ // For details on this method, see:
+ //
+
+ var step = 1, end = array.length || 0, i = 0;
+ if(findLast){
+ i = end - 1;
+ step = end = -1;
+ }
+ if(fromIndex != undefined){ i = fromIndex; }
+ if((findLast && i > end) || i < end){
+ for(; i != end; i += step){
+ if(array[i] == value){ return i; }
+ }
+ }
+ return -1; // Number
+ },
+
+ lastIndexOf: function(/*Array*/array, /*Object*/value, /*Integer?*/fromIndex){
+ // summary:
+ // locates the last index of the provided value in the passed array.
+ // If the value is not found, -1 is returned.
+ // description:
+ // For details on this method, see:
+ //
+ return dojo.indexOf(array, value, fromIndex, true); // Number
+ },
+
+ forEach: function(/*Array|String*/arr, /*Function|String*/callback, /*Object?*/thisObject){
+ // summary:
+ // for every item in arr, callback is invoked. Return values are ignored.
+ // arr: the array to iterate on. If a string, operates on individual characters.
+ // callback: a function is invoked with three arguments: item, index, and array
+ // thisObject: may be used to scope the call to callback
+ // description:
+ // This function corresponds to the JavaScript 1.6 Array.forEach() method.
+ // In environments that support JavaScript 1.6, this function is a passthrough to the built-in method.
+ // For more details, see:
+ //
+
+ // match the behavior of the built-in forEach WRT empty arrs
+ if(!arr || !arr.length){ return; }
+
+ // FIXME: there are several ways of handilng thisObject. Is
+ // dojo.global always the default context?
+ var _p = _getParts(arr, thisObject, callback); arr = _p[0];
+ for(var i=0,l=_p[0].length; i
+ // example:
+ // | dojo.every([1, 2, 3, 4], function(item){ return item>1; });
+ // returns false
+ // example:
+ // | dojo.every([1, 2, 3, 4], function(item){ return item>0; });
+ // returns true
+ return this._everyOrSome(true, arr, callback, thisObject); // Boolean
+ },
+
+ some: function(/*Array|String*/arr, /*Function|String*/callback, /*Object?*/thisObject){
+ // summary:
+ // Determines whether or not any item in arr satisfies the
+ // condition implemented by callback.
+ // arr: the array to iterate on. If a string, operates on individual characters.
+ // callback: a function is invoked with three arguments: item, index, and array and returns true
+ // if the condition is met.
+ // thisObject: may be used to scope the call to callback
+ // description:
+ // This function corresponds to the JavaScript 1.6 Array.some() method.
+ // In environments that support JavaScript 1.6, this function is a passthrough to the built-in method.
+ // For more details, see:
+ //
+ // example:
+ // | dojo.some([1, 2, 3, 4], function(item){ return item>1; });
+ // returns true
+ // example:
+ // | dojo.some([1, 2, 3, 4], function(item){ return item<1; });
+ // returns false
+ return this._everyOrSome(false, arr, callback, thisObject); // Boolean
+ },
+
+ map: function(/*Array|String*/arr, /*Function|String*/callback, /*Function?*/thisObject){
+ // summary:
+ // applies callback to each element of arr and returns
+ // an Array with the results
+ // arr: the array to iterate on. If a string, operates on individual characters.
+ // callback: a function is invoked with three arguments: item, index, and array and returns a value
+ // thisObject: may be used to scope the call to callback
+ // description:
+ // This function corresponds to the JavaScript 1.6 Array.map() method.
+ // In environments that support JavaScript 1.6, this function is a passthrough to the built-in method.
+ // For more details, see:
+ //
+ // example:
+ // | dojo.map([1, 2, 3, 4], function(item){ return item+1 });
+ // returns [2, 3, 4, 5]
+ var _p = _getParts(arr, thisObject, callback); arr = _p[0];
+ var outArr = (arguments[3] ? (new arguments[3]()) : []);
+ for(var i=0;i
+ // example:
+ // | dojo.filter([1, 2, 3, 4], function(item){ return item>1; });
+ // returns [2, 3, 4]
+
+ var _p = _getParts(arr, thisObject, callback); arr = _p[0];
+ var outArr = [];
+ for(var i = 0; i < arr.length; i++){
+ if(_p[2].call(_p[1], arr[i], i, arr)){
+ outArr.push(arr[i]);
+ }
+ }
+ return outArr; // Array
+ }
+ });
+})();
+
+}
+
+if(!dojo._hasResource["dojo._base.Color"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.Color"] = true;
+dojo.provide("dojo._base.Color");
+
+dojo.Color = function(/*Array|String|Object*/ color){
+ // summary:
+ // takes a named string, hex string, array of rgb or rgba values,
+ // an object with r, g, b, and a properties, or another dojo.Color object
+ if(color){ this.setColor(color); }
+};
+
+// FIXME: there's got to be a more space-efficient way to encode or discover these!! Use hex?
+dojo.Color.named = {
+ black: [0,0,0],
+ silver: [192,192,192],
+ gray: [128,128,128],
+ white: [255,255,255],
+ maroon: [128,0,0],
+ red: [255,0,0],
+ purple: [128,0,128],
+ fuchsia: [255,0,255],
+ green: [0,128,0],
+ lime: [0,255,0],
+ olive: [128,128,0],
+ yellow: [255,255,0],
+ navy: [0,0,128],
+ blue: [0,0,255],
+ teal: [0,128,128],
+ aqua: [0,255,255]
+};
+
+dojo.extend(dojo.Color, {
+ r: 255, g: 255, b: 255, a: 1,
+ _set: function(r, g, b, a){
+ var t = this; t.r = r; t.g = g; t.b = b; t.a = a;
+ },
+ setColor: function(/*Array|String|Object*/ color){
+ // summary:
+ // takes a named string, hex string, array of rgb or rgba values,
+ // an object with r, g, b, and a properties, or another dojo.Color object
+ var d = dojo;
+ if(d.isString(color)){
+ d.colorFromString(color, this);
+ }else if(d.isArray(color)){
+ d.colorFromArray(color, this);
+ }else{
+ this._set(color.r, color.g, color.b, color.a);
+ if(!(color instanceof d.Color)){ this.sanitize(); }
+ }
+ return this; // dojo.Color
+ },
+ sanitize: function(){
+ // summary:
+ // makes sure that the object has correct attributes
+ // description:
+ // the default implementation does nothing, include dojo.colors to
+ // augment it to real checks
+ return this; // dojo.Color
+ },
+ toRgb: function(){
+ // summary: returns 3 component array of rgb values
+ var t = this;
+ return [t.r, t.g, t.b]; // Array
+ },
+ toRgba: function(){
+ // summary: returns a 4 component array of rgba values
+ var t = this;
+ return [t.r, t.g, t.b, t.a]; // Array
+ },
+ toHex: function(){
+ // summary: returns a css color string in hexadecimal representation
+ var arr = dojo.map(["r", "g", "b"], function(x){
+ var s = this[x].toString(16);
+ return s.length < 2 ? "0" + s : s;
+ }, this);
+ return "#" + arr.join(""); // String
+ },
+ toCss: function(/*Boolean?*/ includeAlpha){
+ // summary: returns a css color string in rgb(a) representation
+ var t = this, rgb = t.r + ", " + t.g + ", " + t.b;
+ return (includeAlpha ? "rgba(" + rgb + ", " + t.a : "rgb(" + rgb) + ")"; // String
+ },
+ toString: function(){
+ // summary: returns a visual representation of the color
+ return this.toCss(true); // String
+ }
+});
+
+dojo.blendColors = function(
+ /*dojo.Color*/ start,
+ /*dojo.Color*/ end,
+ /*Number*/ weight,
+ /*dojo.Color?*/ obj
+){
+ // summary:
+ // blend colors end and start with weight from 0 to 1, 0.5 being a 50/50 blend,
+ // can reuse a previously allocated dojo.Color object for the result
+ var d = dojo, t = obj || new dojo.Color();
+ d.forEach(["r", "g", "b", "a"], function(x){
+ t[x] = start[x] + (end[x] - start[x]) * weight;
+ if(x != "a"){ t[x] = Math.round(t[x]); }
+ });
+ return t.sanitize(); // dojo.Color
+};
+
+dojo.colorFromRgb = function(/*String*/ color, /*dojo.Color?*/ obj){
+ // summary: get rgb(a) array from css-style color declarations
+ var m = color.toLowerCase().match(/^rgba?\(([\s\.,0-9]+)\)/);
+ return m && dojo.colorFromArray(m[1].split(/\s*,\s*/), obj); // dojo.Color
+};
+
+dojo.colorFromHex = function(/*String*/ color, /*dojo.Color?*/ obj){
+ // summary: converts a hex string with a '#' prefix to a color object.
+ // Supports 12-bit #rgb shorthand.
+ var d = dojo, t = obj || new d.Color(),
+ bits = (color.length == 4) ? 4 : 8,
+ mask = (1 << bits) - 1;
+ color = Number("0x" + color.substr(1));
+ if(isNaN(color)){
+ return null; // dojo.Color
+ }
+ d.forEach(["b", "g", "r"], function(x){
+ var c = color & mask;
+ color >>= bits;
+ t[x] = bits == 4 ? 17 * c : c;
+ });
+ t.a = 1;
+ return t; // dojo.Color
+};
+
+dojo.colorFromArray = function(/*Array*/ a, /*dojo.Color?*/ obj){
+ // summary: builds a color from 1, 2, 3, or 4 element array
+ var t = obj || new dojo.Color();
+ t._set(Number(a[0]), Number(a[1]), Number(a[2]), Number(a[3]));
+ if(isNaN(t.a)){ t.a = 1; }
+ return t.sanitize(); // dojo.Color
+};
+
+dojo.colorFromString = function(/*String*/ str, /*dojo.Color?*/ obj){
+ // summary:
+ // parses str for a color value.
+ // description:
+ // Acceptable input values for str may include arrays of any form
+ // accepted by dojo.colorFromArray, hex strings such as "#aaaaaa", or
+ // rgb or rgba strings such as "rgb(133, 200, 16)" or "rgba(10, 10,
+ // 10, 50)"
+ // returns:
+ // a dojo.Color object. If obj is passed, it will be the return value.
+ var a = dojo.Color.named[str];
+ return a && dojo.colorFromArray(a, obj) || dojo.colorFromRgb(str, obj) || dojo.colorFromHex(str, obj);
+};
+
+}
+
+if(!dojo._hasResource["dojo._base"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base"] = true;
+dojo.provide("dojo._base");
+
+}
+
+if(!dojo._hasResource["dojo._base.window"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.window"] = true;
+dojo.provide("dojo._base.window");
+
+dojo._gearsObject = function(){
+ // summary:
+ // factory method to get a Google Gears plugin instance to
+ // expose in the browser runtime environment, if present
+ var factory;
+ var results;
+
+ var gearsObj = dojo.getObject("google.gears");
+ if(gearsObj){ return gearsObj; } // already defined elsewhere
+
+ if(typeof GearsFactory != "undefined"){ // Firefox
+ factory = new GearsFactory();
+ }else{
+ if(dojo.isIE){
+ // IE
+ try{
+ factory = new ActiveXObject("Gears.Factory");
+ }catch(e){
+ // ok to squelch; there's no gears factory. move on.
+ }
+ }else if(navigator.mimeTypes["application/x-googlegears"]){
+ // Safari?
+ factory = document.createElement("object");
+ factory.setAttribute("type", "application/x-googlegears");
+ factory.setAttribute("width", 0);
+ factory.setAttribute("height", 0);
+ factory.style.display = "none";
+ document.documentElement.appendChild(factory);
+ }
+ }
+
+ // still nothing?
+ if(!factory){ return null; }
+
+ // define the global objects now; don't overwrite them though if they
+ // were somehow set internally by the Gears plugin, which is on their
+ // dev roadmap for the future
+ dojo.setObject("google.gears.factory", factory);
+ return dojo.getObject("google.gears");
+};
+
+/*=====
+dojo.isGears = {
+ // summary: True if client is using Google Gears
+};
+=====*/
+// see if we have Google Gears installed, and if
+// so, make it available in the runtime environment
+// and in the Google standard 'google.gears' global object
+dojo.isGears = (!!dojo._gearsObject())||0;
+
+/*=====
+dojo.doc = {
+ // summary:
+ // Alias for the current document. 'dojo.doc' can be modified
+ // for temporary context shifting. Also see dojo.withDoc().
+ // description:
+ // Refer to dojo.doc rather
+ // than referring to 'window.document' to ensure your code runs
+ // correctly in managed contexts.
+ // example:
+ // | n.appendChild(dojo.doc.createElement('div'));
+}
+=====*/
+dojo.doc = window["document"] || null;
+
+dojo.body = function(){
+ // summary:
+ // Return the body element of the document
+ // return the body object associated with dojo.doc
+ // example:
+ // | dojo.body().appendChild(dojo.doc.createElement('div'));
+
+ // Note: document.body is not defined for a strict xhtml document
+ // Would like to memoize this, but dojo.doc can change vi dojo.withDoc().
+ return dojo.doc.body || dojo.doc.getElementsByTagName("body")[0]; // Node
+}
+
+dojo.setContext = function(/*Object*/globalObject, /*DocumentElement*/globalDocument){
+ // summary:
+ // changes the behavior of many core Dojo functions that deal with
+ // namespace and DOM lookup, changing them to work in a new global
+ // context (e.g., an iframe). The varibles dojo.global and dojo.doc
+ // are modified as a result of calling this function and the result of
+ // `dojo.body()` likewise differs.
+ dojo.global = globalObject;
+ dojo.doc = globalDocument;
+};
+
+dojo._fireCallback = function(callback, context, cbArguments){
+ if(context && dojo.isString(callback)){
+ callback = context[callback];
+ }
+ return callback.apply(context, cbArguments || [ ]);
+}
+
+dojo.withGlobal = function( /*Object*/globalObject,
+ /*Function*/callback,
+ /*Object?*/thisObject,
+ /*Array?*/cbArguments){
+ // summary:
+ // Call callback with globalObject as dojo.global and
+ // globalObject.document as dojo.doc. If provided, globalObject
+ // will be executed in the context of object thisObject
+ // description:
+ // When callback() returns or throws an error, the dojo.global
+ // and dojo.doc will be restored to its previous state.
+ var rval;
+ var oldGlob = dojo.global;
+ var oldDoc = dojo.doc;
+ try{
+ dojo.setContext(globalObject, globalObject.document);
+ rval = dojo._fireCallback(callback, thisObject, cbArguments);
+ }finally{
+ dojo.setContext(oldGlob, oldDoc);
+ }
+ return rval;
+}
+
+dojo.withDoc = function( /*Object*/documentObject,
+ /*Function*/callback,
+ /*Object?*/thisObject,
+ /*Array?*/cbArguments){
+ // summary:
+ // Call callback with documentObject as dojo.doc. If provided,
+ // callback will be executed in the context of object thisObject
+ // description:
+ // When callback() returns or throws an error, the dojo.doc will
+ // be restored to its previous state.
+ var rval;
+ var oldDoc = dojo.doc;
+ try{
+ dojo.doc = documentObject;
+ rval = dojo._fireCallback(callback, thisObject, cbArguments);
+ }finally{
+ dojo.doc = oldDoc;
+ }
+ return rval;
+};
+
+}
+
+if(!dojo._hasResource["dojo._base.event"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.event"] = true;
+dojo.provide("dojo._base.event");
+
+// this file courtesy of the TurboAjax Group, licensed under a Dojo CLA
+
+(function(){
+ // DOM event listener machinery
+ var del = (dojo._event_listener = {
+ add: function(/*DOMNode*/node, /*String*/name, /*Function*/fp){
+ if(!node){return;}
+ name = del._normalizeEventName(name);
+ fp = del._fixCallback(name, fp);
+ var oname = name;
+ if(!dojo.isIE && (name == "mouseenter" || name == "mouseleave")){
+ var ofp = fp;
+ //oname = name;
+ name = (name == "mouseenter") ? "mouseover" : "mouseout";
+ fp = function(e){
+ // thanks ben!
+ if(!dojo.isDescendant(e.relatedTarget, node)){
+ // e.type = oname; // FIXME: doesn't take? SJM: event.type is generally immutable.
+ return ofp.call(this, e);
+ }
+ }
+ }
+ node.addEventListener(name, fp, false);
+ return fp; /*Handle*/
+ },
+ remove: function(/*DOMNode*/node, /*String*/event, /*Handle*/handle){
+ // summary:
+ // clobbers the listener from the node
+ // node:
+ // DOM node to attach the event to
+ // event:
+ // the name of the handler to remove the function from
+ // handle:
+ // the handle returned from add
+ if (node){
+ node.removeEventListener(del._normalizeEventName(event), handle, false);
+ }
+ },
+ _normalizeEventName: function(/*String*/name){
+ // Generally, name should be lower case, unless it is special
+ // somehow (e.g. a Mozilla DOM event).
+ // Remove 'on'.
+ return name.slice(0,2) =="on" ? name.slice(2) : name;
+ },
+ _fixCallback: function(/*String*/name, fp){
+ // By default, we only invoke _fixEvent for 'keypress'
+ // If code is added to _fixEvent for other events, we have
+ // to revisit this optimization.
+ // This also applies to _fixEvent overrides for Safari and Opera
+ // below.
+ return name != "keypress" ? fp : function(e){ return fp.call(this, del._fixEvent(e, this)); };
+ },
+ _fixEvent: function(evt, sender){
+ // _fixCallback only attaches us to keypress.
+ // Switch on evt.type anyway because we might
+ // be called directly from dojo.fixEvent.
+ switch(evt.type){
+ case "keypress":
+ del._setKeyChar(evt);
+ break;
+ }
+ return evt;
+ },
+ _setKeyChar: function(evt){
+ evt.keyChar = evt.charCode ? String.fromCharCode(evt.charCode) : '';
+ }
+ });
+
+ // DOM events
+
+ dojo.fixEvent = function(/*Event*/evt, /*DOMNode*/sender){
+ // summary:
+ // normalizes properties on the event object including event
+ // bubbling methods, keystroke normalization, and x/y positions
+ // evt: Event
+ // native event object
+ // sender: DOMNode
+ // node to treat as "currentTarget"
+ return del._fixEvent(evt, sender);
+ }
+
+ dojo.stopEvent = function(/*Event*/evt){
+ // summary:
+ // prevents propagation and clobbers the default action of the
+ // passed event
+ // evt: Event
+ // The event object. If omitted, window.event is used on IE.
+ evt.preventDefault();
+ evt.stopPropagation();
+ // NOTE: below, this method is overridden for IE
+ }
+
+ // the default listener to use on dontFix nodes, overriden for IE
+ var node_listener = dojo._listener;
+
+ // Unify connect and event listeners
+ dojo._connect = function(obj, event, context, method, dontFix){
+ // FIXME: need a more strict test
+ var isNode = obj && (obj.nodeType||obj.attachEvent||obj.addEventListener);
+ // choose one of three listener options: raw (connect.js), DOM event on a Node, custom event on a Node
+ // we need the third option to provide leak prevention on broken browsers (IE)
+ var lid = !isNode ? 0 : (!dontFix ? 1 : 2), l = [dojo._listener, del, node_listener][lid];
+ // create a listener
+ var h = l.add(obj, event, dojo.hitch(context, method));
+ // formerly, the disconnect package contained "l" directly, but if client code
+ // leaks the disconnect package (by connecting it to a node), referencing "l"
+ // compounds the problem.
+ // instead we return a listener id, which requires custom _disconnect below.
+ // return disconnect package
+ return [ obj, event, h, lid ];
+ }
+
+ dojo._disconnect = function(obj, event, handle, listener){
+ ([dojo._listener, del, node_listener][listener]).remove(obj, event, handle);
+ }
+
+ // Constants
+
+ // Public: client code should test
+ // keyCode against these named constants, as the
+ // actual codes can vary by browser.
+ dojo.keys = {
+ // summary: definitions for common key values
+ BACKSPACE: 8,
+ TAB: 9,
+ CLEAR: 12,
+ ENTER: 13,
+ SHIFT: 16,
+ CTRL: 17,
+ ALT: 18,
+ PAUSE: 19,
+ CAPS_LOCK: 20,
+ ESCAPE: 27,
+ SPACE: 32,
+ PAGE_UP: 33,
+ PAGE_DOWN: 34,
+ END: 35,
+ HOME: 36,
+ LEFT_ARROW: 37,
+ UP_ARROW: 38,
+ RIGHT_ARROW: 39,
+ DOWN_ARROW: 40,
+ INSERT: 45,
+ DELETE: 46,
+ HELP: 47,
+ LEFT_WINDOW: 91,
+ RIGHT_WINDOW: 92,
+ SELECT: 93,
+ NUMPAD_0: 96,
+ NUMPAD_1: 97,
+ NUMPAD_2: 98,
+ NUMPAD_3: 99,
+ NUMPAD_4: 100,
+ NUMPAD_5: 101,
+ NUMPAD_6: 102,
+ NUMPAD_7: 103,
+ NUMPAD_8: 104,
+ NUMPAD_9: 105,
+ NUMPAD_MULTIPLY: 106,
+ NUMPAD_PLUS: 107,
+ NUMPAD_ENTER: 108,
+ NUMPAD_MINUS: 109,
+ NUMPAD_PERIOD: 110,
+ NUMPAD_DIVIDE: 111,
+ F1: 112,
+ F2: 113,
+ F3: 114,
+ F4: 115,
+ F5: 116,
+ F6: 117,
+ F7: 118,
+ F8: 119,
+ F9: 120,
+ F10: 121,
+ F11: 122,
+ F12: 123,
+ F13: 124,
+ F14: 125,
+ F15: 126,
+ NUM_LOCK: 144,
+ SCROLL_LOCK: 145
+ };
+
+ // IE event normalization
+ if(dojo.isIE){
+ var _trySetKeyCode = function(e, code){
+ try{
+ // squelch errors when keyCode is read-only
+ // (e.g. if keyCode is ctrl or shift)
+ return (e.keyCode = code);
+ }catch(e){
+ return 0;
+ }
+ }
+
+ // by default, use the standard listener
+ var iel = dojo._listener;
+ // dispatcher tracking property
+ if(!dojo.config._allow_leaks){
+ // custom listener that handles leak protection for DOM events
+ node_listener = iel = dojo._ie_listener = {
+ // support handler indirection: event handler functions are
+ // referenced here. Event dispatchers hold only indices.
+ handlers: [],
+ // add a listener to an object
+ add: function(/*Object*/ source, /*String*/ method, /*Function*/ listener){
+ source = source || dojo.global;
+ var f = source[method];
+ if(!f||!f._listeners){
+ var d = dojo._getIeDispatcher();
+ // original target function is special
+ d.target = f && (ieh.push(f) - 1);
+ // dispatcher holds a list of indices into handlers table
+ d._listeners = [];
+ // redirect source to dispatcher
+ f = source[method] = d;
+ }
+ return f._listeners.push(ieh.push(listener) - 1) ; /*Handle*/
+ },
+ // remove a listener from an object
+ remove: function(/*Object*/ source, /*String*/ method, /*Handle*/ handle){
+ var f = (source||dojo.global)[method], l = f && f._listeners;
+ if(f && l && handle--){
+ delete ieh[l[handle]];
+ delete l[handle];
+ }
+ }
+ };
+ // alias used above
+ var ieh = iel.handlers;
+ }
+
+ dojo.mixin(del, {
+ add: function(/*DOMNode*/node, /*String*/event, /*Function*/fp){
+ if(!node){return;} // undefined
+ event = del._normalizeEventName(event);
+ if(event=="onkeypress"){
+ // we need to listen to onkeydown to synthesize
+ // keypress events that otherwise won't fire
+ // on IE
+ var kd = node.onkeydown;
+ if(!kd || !kd._listeners || !kd._stealthKeydownHandle){
+ var h = del.add(node, "onkeydown", del._stealthKeyDown);
+ kd = node.onkeydown;
+ kd._stealthKeydownHandle = h;
+ kd._stealthKeydownRefs = 1;
+ }else{
+ kd._stealthKeydownRefs++;
+ }
+ }
+ return iel.add(node, event, del._fixCallback(fp));
+ },
+ remove: function(/*DOMNode*/node, /*String*/event, /*Handle*/handle){
+ event = del._normalizeEventName(event);
+ iel.remove(node, event, handle);
+ if(event=="onkeypress"){
+ var kd = node.onkeydown;
+ if(--kd._stealthKeydownRefs <= 0){
+ iel.remove(node, "onkeydown", kd._stealthKeydownHandle);
+ delete kd._stealthKeydownHandle;
+ }
+ }
+ },
+ _normalizeEventName: function(/*String*/eventName){
+ // Generally, eventName should be lower case, unless it is
+ // special somehow (e.g. a Mozilla event)
+ // ensure 'on'
+ return eventName.slice(0,2) != "on" ? "on" + eventName : eventName;
+ },
+ _nop: function(){},
+ _fixEvent: function(/*Event*/evt, /*DOMNode*/sender){
+ // summary:
+ // normalizes properties on the event object including event
+ // bubbling methods, keystroke normalization, and x/y positions
+ // evt: native event object
+ // sender: node to treat as "currentTarget"
+ if(!evt){
+ var w = sender && (sender.ownerDocument || sender.document || sender).parentWindow || window;
+ evt = w.event;
+ }
+ if(!evt){return(evt);}
+ evt.target = evt.srcElement;
+ evt.currentTarget = (sender || evt.srcElement);
+ evt.layerX = evt.offsetX;
+ evt.layerY = evt.offsetY;
+ // FIXME: scroll position query is duped from dojo.html to
+ // avoid dependency on that entire module. Now that HTML is in
+ // Base, we should convert back to something similar there.
+ var se = evt.srcElement, doc = (se && se.ownerDocument) || document;
+ // DO NOT replace the following to use dojo.body(), in IE, document.documentElement should be used
+ // here rather than document.body
+ var docBody = ((dojo.isIE < 6) || (doc["compatMode"] == "BackCompat")) ? doc.body : doc.documentElement;
+ var offset = dojo._getIeDocumentElementOffset();
+ evt.pageX = evt.clientX + dojo._fixIeBiDiScrollLeft(docBody.scrollLeft || 0) - offset.x;
+ evt.pageY = evt.clientY + (docBody.scrollTop || 0) - offset.y;
+ if(evt.type == "mouseover"){
+ evt.relatedTarget = evt.fromElement;
+ }
+ if(evt.type == "mouseout"){
+ evt.relatedTarget = evt.toElement;
+ }
+ evt.stopPropagation = del._stopPropagation;
+ evt.preventDefault = del._preventDefault;
+ return del._fixKeys(evt);
+ },
+ _fixKeys: function(evt){
+ switch(evt.type){
+ case "keypress":
+ var c = ("charCode" in evt ? evt.charCode : evt.keyCode);
+ if (c==10){
+ // CTRL-ENTER is CTRL-ASCII(10) on IE, but CTRL-ENTER on Mozilla
+ c=0;
+ evt.keyCode = 13;
+ }else if(c==13||c==27){
+ c=0; // Mozilla considers ENTER and ESC non-printable
+ }else if(c==3){
+ c=99; // Mozilla maps CTRL-BREAK to CTRL-c
+ }
+ // Mozilla sets keyCode to 0 when there is a charCode
+ // but that stops the event on IE.
+ evt.charCode = c;
+ del._setKeyChar(evt);
+ break;
+ }
+ return evt;
+ },
+ // some ctrl-key combinations (mostly w/punctuation) do not emit a char code in IE
+ // we map those virtual key codes to ascii here
+ // not valid for all (non-US) keyboards, so maybe we shouldn't bother
+ _punctMap: {
+ 106:42,
+ 111:47,
+ 186:59,
+ 187:43,
+ 188:44,
+ 189:45,
+ 190:46,
+ 191:47,
+ 192:96,
+ 219:91,
+ 220:92,
+ 221:93,
+ 222:39
+ },
+ _stealthKeyDown: function(evt){
+ // IE doesn't fire keypress for most non-printable characters.
+ // other browsers do, we simulate it here.
+ var kp = evt.currentTarget.onkeypress;
+ // only works if kp exists and is a dispatcher
+ if(!kp || !kp._listeners){ return; }
+ // munge key/charCode
+ var k=evt.keyCode;
+ // These are Windows Virtual Key Codes
+ // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/WinUI/WindowsUserInterface/UserInput/VirtualKeyCodes.asp
+ var unprintable = (k!=13)&&(k!=32)&&(k!=27)&&(k<48||k>90)&&(k<96||k>111)&&(k<186||k>192)&&(k<219||k>222);
+ // synthesize keypress for most unprintables and CTRL-keys
+ if(unprintable||evt.ctrlKey){
+ var c = unprintable ? 0 : k;
+ if(evt.ctrlKey){
+ if(k==3 || k==13){
+ return; // IE will post CTRL-BREAK, CTRL-ENTER as keypress natively
+ }else if(c>95 && c<106){
+ c -= 48; // map CTRL-[numpad 0-9] to ASCII
+ }else if((!evt.shiftKey)&&(c>=65&&c<=90)){
+ c += 32; // map CTRL-[A-Z] to lowercase
+ }else{
+ c = del._punctMap[c] || c; // map other problematic CTRL combinations to ASCII
+ }
+ }
+ // simulate a keypress event
+ var faux = del._synthesizeEvent(evt, {type: 'keypress', faux: true, charCode: c});
+ kp.call(evt.currentTarget, faux);
+ evt.cancelBubble = faux.cancelBubble;
+ evt.returnValue = faux.returnValue;
+ _trySetKeyCode(evt, faux.keyCode);
+ }
+ },
+ // Called in Event scope
+ _stopPropagation: function(){
+ this.cancelBubble = true;
+ },
+ _preventDefault: function(){
+ // Setting keyCode to 0 is the only way to prevent certain keypresses (namely
+ // ctrl-combinations that correspond to menu accelerator keys).
+ // Otoh, it prevents upstream listeners from getting this information
+ // Try to split the difference here by clobbering keyCode only for ctrl
+ // combinations. If you still need to access the key upstream, bubbledKeyCode is
+ // provided as a workaround.
+ this.bubbledKeyCode = this.keyCode;
+ if(this.ctrlKey){_trySetKeyCode(this, 0);}
+ this.returnValue = false;
+ }
+ });
+
+ // override stopEvent for IE
+ dojo.stopEvent = function(evt){
+ evt = evt || window.event;
+ del._stopPropagation.call(evt);
+ del._preventDefault.call(evt);
+ }
+ }
+
+ del._synthesizeEvent = function(evt, props){
+ var faux = dojo.mixin({}, evt, props);
+ del._setKeyChar(faux);
+ // FIXME: would prefer to use dojo.hitch: dojo.hitch(evt, evt.preventDefault);
+ // but it throws an error when preventDefault is invoked on Safari
+ // does Event.preventDefault not support "apply" on Safari?
+ faux.preventDefault = function(){ evt.preventDefault(); };
+ faux.stopPropagation = function(){ evt.stopPropagation(); };
+ return faux;
+ }
+
+ // Opera event normalization
+ if(dojo.isOpera){
+ dojo.mixin(del, {
+ _fixEvent: function(evt, sender){
+ switch(evt.type){
+ case "keypress":
+ var c = evt.which;
+ if(c==3){
+ c=99; // Mozilla maps CTRL-BREAK to CTRL-c
+ }
+ // can't trap some keys at all, like INSERT and DELETE
+ // there is no differentiating info between DELETE and ".", or INSERT and "-"
+ c = ((c<41)&&(!evt.shiftKey) ? 0 : c);
+ if((evt.ctrlKey)&&(!evt.shiftKey)&&(c>=65)&&(c<=90)){
+ // lowercase CTRL-[A-Z] keys
+ c += 32;
+ }
+ return del._synthesizeEvent(evt, { charCode: c });
+ }
+ return evt;
+ }
+ });
+ }
+
+ // Safari event normalization
+ if(dojo.isSafari){
+ dojo.mixin(del, {
+ _fixEvent: function(evt, sender){
+ switch(evt.type){
+ case "keypress":
+ var c = evt.charCode, s = evt.shiftKey, k = evt.keyCode;
+ // FIXME: This is a hack, suggest we rethink keyboard strategy.
+ // Arrow and page keys have 0 "keyCode" in keypress events.on Safari for Windows
+ k = k || identifierMap[evt.keyIdentifier] || 0;
+ if(evt.keyIdentifier=="Enter"){
+ c = 0; // differentiate Enter from CTRL-m (both code 13)
+ }else if((evt.ctrlKey)&&(c>0)&&(c<27)){
+ c += 96; // map CTRL-[A-Z] codes to ASCII
+ } else if (c==dojo.keys.SHIFT_TAB) {
+ c = dojo.keys.TAB; // morph SHIFT_TAB into TAB + shiftKey: true
+ s = true;
+ } else {
+ c = (c>=32 && c<63232 ? c : 0); // avoid generating keyChar for non-printables
+ }
+ return del._synthesizeEvent(evt, {charCode: c, shiftKey: s, keyCode: k});
+ }
+ return evt;
+ }
+ });
+
+ dojo.mixin(dojo.keys, {
+ SHIFT_TAB: 25,
+ UP_ARROW: 63232,
+ DOWN_ARROW: 63233,
+ LEFT_ARROW: 63234,
+ RIGHT_ARROW: 63235,
+ F1: 63236,
+ F2: 63237,
+ F3: 63238,
+ F4: 63239,
+ F5: 63240,
+ F6: 63241,
+ F7: 63242,
+ F8: 63243,
+ F9: 63244,
+ F10: 63245,
+ F11: 63246,
+ F12: 63247,
+ PAUSE: 63250,
+ DELETE: 63272,
+ HOME: 63273,
+ END: 63275,
+ PAGE_UP: 63276,
+ PAGE_DOWN: 63277,
+ INSERT: 63302,
+ PRINT_SCREEN: 63248,
+ SCROLL_LOCK: 63249,
+ NUM_LOCK: 63289
+ });
+ var dk = dojo.keys, identifierMap = { "Up": dk.UP_ARROW, "Down": dk.DOWN_ARROW, "Left": dk.LEFT_ARROW, "Right": dk.RIGHT_ARROW, "PageUp": dk.PAGE_UP, "PageDown": dk.PAGE_DOWN };
+ }
+})();
+
+if(dojo.isIE){
+ // keep this out of the closure
+ // closing over 'iel' or 'ieh' b0rks leak prevention
+ // ls[i] is an index into the master handler array
+ dojo._ieDispatcher = function(args, sender){
+ var ap=Array.prototype, h=dojo._ie_listener.handlers, c=args.callee, ls=c._listeners, t=h[c.target];
+ // return value comes from original target function
+ var r = t && t.apply(sender, args);
+ // invoke listeners after target function
+ for(var i in ls){
+ if(!(i in ap)){
+ h[ls[i]].apply(sender, args);
+ }
+ }
+ return r;
+ }
+ dojo._getIeDispatcher = function(){
+ // ensure the returned function closes over nothing
+ return new Function(dojo._scopeName + "._ieDispatcher(arguments, this)"); // function
+ }
+ // keep this out of the closure to reduce RAM allocation
+ dojo._event_listener._fixCallback = function(fp){
+ var f = dojo._event_listener._fixEvent;
+ return function(e){ return fp.call(this, f(e, this)); };
+ }
+}
+
+}
+
+if(!dojo._hasResource["dojo._base.html"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.html"] = true;
+
+dojo.provide("dojo._base.html");
+
+// FIXME: need to add unit tests for all the semi-public methods
+
+try{
+ document.execCommand("BackgroundImageCache", false, true);
+}catch(e){
+ // sane browsers don't have cache "issues"
+}
+
+// =============================
+// DOM Functions
+// =============================
+
+/*=====
+dojo.byId = function(id, doc){
+ // summary:
+ // Returns DOM node with matching `id` attribute or `null`
+ // if not found, similar to "$" function in another library.
+ // If `id` is a DomNode, this function is a no-op.
+ //
+ // id: String|DOMNode
+ // A string to match an HTML id attribute or a reference to a DOM Node
+ //
+ // doc: Document?
+ // Document to work in. Defaults to the current value of
+ // dojo.doc. Can be used to retrieve
+ // node references from other documents.
+=====*/
+if(dojo.isIE || dojo.isOpera){
+ dojo.byId = function(id, doc){
+ if(dojo.isString(id)){
+ var _d = doc || dojo.doc;
+ var te = _d.getElementById(id);
+ // attributes.id.value is better than just id in case the
+ // user has a name=id inside a form
+ if(te && te.attributes.id.value == id){
+ return te;
+ }else{
+ var eles = _d.all[id];
+ if(!eles || !eles.length){ return eles; }
+ // if more than 1, choose first with the correct id
+ var i=0;
+ while((te=eles[i++])){
+ if(te.attributes.id.value == id){ return te; }
+ }
+ }
+ }else{
+ return id; // DomNode
+ }
+ }
+}else{
+ dojo.byId = function(id, doc){
+ return dojo.isString(id) ? (doc || dojo.doc).getElementById(id) : id; // DomNode
+ }
+}
+/*=====
+}
+=====*/
+
+(function(){
+ /*
+ dojo.createElement = function(obj, parent, position){
+ // TODO: need to finish this!
+ }
+ */
+
+ var d = dojo;
+
+ var _destroyContainer = null;
+ dojo.addOnUnload(function(){
+ _destroyContainer=null; //prevent IE leak
+ });
+ dojo._destroyElement = function(/*String||DomNode*/node){
+ // summary:
+ // removes node from its parent, clobbers it and all of its
+ // children.
+ // node:
+ // the element to be destroyed, either as an ID or a reference
+
+ node = d.byId(node);
+ try{
+ if(!_destroyContainer){
+ _destroyContainer = document.createElement("div");
+ }
+ _destroyContainer.appendChild(node.parentNode ? node.parentNode.removeChild(node) : node);
+ // NOTE: see http://trac.dojotoolkit.org/ticket/2931. This may be a bug and not a feature
+ _destroyContainer.innerHTML = "";
+ }catch(e){
+ /* squelch */
+ }
+ };
+
+ dojo.isDescendant = function(/*DomNode|String*/node, /*DomNode|String*/ancestor){
+ // summary:
+ // Returns true if node is a descendant of ancestor
+ // node: id or node reference to test
+ // ancestor: id or node reference of potential parent to test against
+ try{
+ node = d.byId(node);
+ ancestor = d.byId(ancestor);
+ while(node){
+ if(node === ancestor){
+ return true; // Boolean
+ }
+ node = node.parentNode;
+ }
+ }catch(e){ /* squelch, return false */ }
+ return false; // Boolean
+ };
+
+ dojo.setSelectable = function(/*DomNode|String*/node, /*Boolean*/selectable){
+ // summary: enable or disable selection on a node
+ // node:
+ // id or reference to node
+ // selectable:
+ node = d.byId(node);
+ if(d.isMozilla){
+ node.style.MozUserSelect = selectable ? "" : "none";
+ }else if(d.isKhtml){
+ node.style.KhtmlUserSelect = selectable ? "auto" : "none";
+ }else if(d.isIE){
+ node.unselectable = selectable ? "" : "on";
+ d.query("*", node).forEach(function(descendant){
+ descendant.unselectable = selectable ? "" : "on";
+ });
+ }
+ //FIXME: else? Opera?
+ };
+
+ var _insertBefore = function(/*Node*/node, /*Node*/ref){
+ ref.parentNode.insertBefore(node, ref);
+ return true; // boolean
+ }
+
+ var _insertAfter = function(/*Node*/node, /*Node*/ref){
+ // summary:
+ // Try to insert node after ref
+ var pn = ref.parentNode;
+ if(ref == pn.lastChild){
+ pn.appendChild(node);
+ }else{
+ return _insertBefore(node, ref.nextSibling); // boolean
+ }
+ return true; // boolean
+ }
+
+ dojo.place = function(/*String|DomNode*/node, /*String|DomNode*/refNode, /*String|Number*/position){
+ // summary:
+ // Attempt to insert node into the DOM, choosing from various positioning options.
+ // Returns true if successful, false otherwise.
+ // node:
+ // id or node reference to place relative to refNode
+ // refNode:
+ // id or node reference to use as basis for placement
+ // position:
+ // string noting the position of node relative to refNode or a
+ // number indicating the location in the childNodes collection of
+ // refNode. Accepted string values are:
+ //
+ // * before
+ // * after
+ // * first
+ // * last
+ //
+ // "first" and "last" indicate positions as children of refNode.
+
+ // FIXME: need to write tests for this!!!!
+ if(!node || !refNode || position === undefined){
+ return false; // boolean
+ }
+ node = d.byId(node);
+ refNode = d.byId(refNode);
+ if(typeof position == "number"){
+ var cn = refNode.childNodes;
+ if((position == 0 && cn.length == 0) ||
+ cn.length == position){
+ refNode.appendChild(node); return true;
+ }
+ if(position == 0){
+ return _insertBefore(node, refNode.firstChild);
+ }
+ return _insertAfter(node, cn[position-1]);
+ }
+ switch(position.toLowerCase()){
+ case "before":
+ return _insertBefore(node, refNode); // boolean
+ case "after":
+ return _insertAfter(node, refNode); // boolean
+ case "first":
+ if(refNode.firstChild){
+ return _insertBefore(node, refNode.firstChild); // boolean
+ }
+ // else fallthrough...
+ default: // aka: last
+ refNode.appendChild(node);
+ return true; // boolean
+ }
+ }
+
+ // Box functions will assume this model.
+ // On IE/Opera, BORDER_BOX will be set if the primary document is in quirks mode.
+ // Can be set to change behavior of box setters.
+
+ // can be either:
+ // "border-box"
+ // "content-box" (default)
+ dojo.boxModel = "content-box";
+
+ // We punt per-node box mode testing completely.
+ // If anybody cares, we can provide an additional (optional) unit
+ // that overrides existing code to include per-node box sensitivity.
+
+ // Opera documentation claims that Opera 9 uses border-box in BackCompat mode.
+ // but experiments (Opera 9.10.8679 on Windows Vista) indicate that it actually continues to use content-box.
+ // IIRC, earlier versions of Opera did in fact use border-box.
+ // Opera guys, this is really confusing. Opera being broken in quirks mode is not our fault.
+
+ if(d.isIE /*|| dojo.isOpera*/){
+ var _dcm = document.compatMode;
+ // client code may have to adjust if compatMode varies across iframes
+ d.boxModel = _dcm == "BackCompat" || _dcm == "QuirksMode" || d.isIE<6 ? "border-box" : "content-box"; // FIXME: remove IE < 6 support?
+ }
+
+ // =============================
+ // Style Functions
+ // =============================
+
+ // getComputedStyle drives most of the style code.
+ // Wherever possible, reuse the returned object.
+ //
+ // API functions below that need to access computed styles accept an
+ // optional computedStyle parameter.
+ // If this parameter is omitted, the functions will call getComputedStyle themselves.
+ // This way, calling code can access computedStyle once, and then pass the reference to
+ // multiple API functions.
+
+/*=====
+ dojo.getComputedStyle = function(node){
+ // summary:
+ // Returns a "computed style" object.
+ //
+ // description:
+ // Gets a "computed style" object which can be used to gather
+ // information about the current state of the rendered node.
+ //
+ // Note that this may behave differently on different browsers.
+ // Values may have different formats and value encodings across
+ // browsers.
+ //
+ // Note also that this method is expensive. Wherever possible,
+ // reuse the returned object.
+ //
+ // Use the dojo.style() method for more consistent (pixelized)
+ // return values.
+ //
+ // node: DOMNode
+ // A reference to a DOM node. Does NOT support taking an
+ // ID string for speed reasons.
+ // example:
+ // | dojo.getComputedStyle(dojo.byId('foo')).borderWidth;
+ return; // CSS2Properties
+ }
+=====*/
+
+ var gcs, dv = document.defaultView;
+ if(d.isSafari){
+ gcs = function(/*DomNode*/node){
+ var s = dv.getComputedStyle(node, null);
+ if(!s && node.style){
+ node.style.display = "";
+ s = dv.getComputedStyle(node, null);
+ }
+ return s || {};
+ };
+ }else if(d.isIE){
+ gcs = function(node){
+ return node.currentStyle;
+ };
+ }else{
+ gcs = function(node){
+ return dv.getComputedStyle(node, null);
+ };
+ }
+ dojo.getComputedStyle = gcs;
+
+ if(!d.isIE){
+ dojo._toPixelValue = function(element, value){
+ // style values can be floats, client code may want
+ // to round for integer pixels.
+ return parseFloat(value) || 0;
+ }
+ }else{
+ dojo._toPixelValue = function(element, avalue){
+ if(!avalue){ return 0; }
+ // on IE7, medium is usually 4 pixels
+ if(avalue=="medium"){ return 4; }
+ // style values can be floats, client code may
+ // want to round this value for integer pixels.
+ if(avalue.slice && (avalue.slice(-2)=='px')){ return parseFloat(avalue); }
+ with(element){
+ var sLeft = style.left;
+ var rsLeft = runtimeStyle.left;
+ runtimeStyle.left = currentStyle.left;
+ try{
+ // 'avalue' may be incompatible with style.left, which can cause IE to throw
+ // this has been observed for border widths using "thin", "medium", "thick" constants
+ // those particular constants could be trapped by a lookup
+ // but perhaps there are more
+ style.left = avalue;
+ avalue = style.pixelLeft;
+ }catch(e){
+ avalue = 0;
+ }
+ style.left = sLeft;
+ runtimeStyle.left = rsLeft;
+ }
+ return avalue;
+ }
+ }
+ var px = d._toPixelValue;
+
+ // FIXME: there opacity quirks on FF that we haven't ported over. Hrm.
+ /*=====
+ dojo._getOpacity = function(node){
+ // summary:
+ // Returns the current opacity of the passed node as a
+ // floating-point value between 0 and 1.
+ // node: DomNode
+ // a reference to a DOM node. Does NOT support taking an
+ // ID string for speed reasons.
+ // return: Number between 0 and 1
+ }
+ =====*/
+
+ dojo._getOpacity = d.isIE ? function(node){
+ try{
+ return node.filters.alpha.opacity / 100; // Number
+ }catch(e){
+ return 1; // Number
+ }
+ } : function(node){
+ return gcs(node).opacity;
+ };
+
+ /*=====
+ dojo._setOpacity = function(node, opacity){
+ // summary:
+ // set the opacity of the passed node portably. Returns the
+ // new opacity of the node.
+ // node: DOMNode
+ // a reference to a DOM node. Does NOT support taking an
+ // ID string for performance reasons.
+ // opacity: Number
+ // A Number between 0 and 1. 0 specifies transparent.
+ // return: Number between 0 and 1
+ }
+ =====*/
+
+ dojo._setOpacity = d.isIE ? function(/*DomNode*/node, /*Number*/opacity){
+ if(opacity == 1){
+ // on IE7 Alpha(Filter opacity=100) makes text look fuzzy so remove it altogether (bug #2661)
+ var filterRE = /FILTER:[^;]*;?/i;
+ node.style.cssText = node.style.cssText.replace(filterRE, "");
+ if(node.nodeName.toLowerCase() == "tr"){
+ d.query("> td", node).forEach(function(i){
+ i.style.cssText = i.style.cssText.replace(filterRE, "");
+ });
+ }
+ }else{
+ var o = "Alpha(Opacity="+ opacity * 100 +")";
+ node.style.filter = o;
+ }
+ if(node.nodeName.toLowerCase() == "tr"){
+ d.query("> td", node).forEach(function(i){
+ i.style.filter = o;
+ });
+ }
+ return opacity;
+ } : function(node, opacity){
+ return node.style.opacity = opacity;
+ };
+
+ var _pixelNamesCache = {
+ left: true, top: true
+ };
+ var _pixelRegExp = /margin|padding|width|height|max|min|offset/; // |border
+ var _toStyleValue = function(node, type, value){
+ type = type.toLowerCase();
+ if(d.isIE && value == "auto"){
+ if(type == "height"){ return node.offsetHeight; }
+ if(type == "width"){ return node.offsetWidth; }
+ }
+ if(!(type in _pixelNamesCache)){
+ // if(dojo.isOpera && type == "cssText"){
+ // FIXME: add workaround for #2855 here
+ // }
+ _pixelNamesCache[type] = _pixelRegExp.test(type);
+ }
+ return _pixelNamesCache[type] ? px(node, value) : value;
+ }
+
+ var _floatStyle = d.isIE ? "styleFloat" : "cssFloat";
+ var _floatAliases = { "cssFloat": _floatStyle, "styleFloat": _floatStyle, "float": _floatStyle };
+
+ // public API
+
+ dojo.style = function( /*DomNode|String*/ node,
+ /*String?|Object?*/ style,
+ /*String?*/ value){
+ // summary:
+ // Accesses styles on a node. If 2 arguments are
+ // passed, acts as a getter. If 3 arguments are passed, acts
+ // as a setter.
+ // node:
+ // id or reference to node to get/set style for
+ // style:
+ // the style property to set in DOM-accessor format
+ // ("borderWidth", not "border-width") or an object with key/value
+ // pairs suitable for setting each property.
+ // value:
+ // If passed, sets value on the node for style, handling
+ // cross-browser concerns.
+ // example:
+ // Passing only an ID or node returns the computed style object of
+ // the node:
+ // | dojo.style("thinger");
+ // example:
+ // Passing a node and a style property returns the current
+ // normalized, computed value for that property:
+ // | dojo.style("thinger", "opacity"); // 1 by default
+ //
+ // example:
+ // Passing a node, a style property, and a value changes the
+ // current display of the node and returns the new computed value
+ // | dojo.style("thinger", "opacity", 0.5); // == 0.5
+ //
+ // example:
+ // Passing a node, an object-style style property sets each of the values in turn and returns the computed style object of the node:
+ // | dojo.style("thinger", {
+ // | "opacity": 0.5,
+ // | "border": "3px solid black",
+ // | "height": 300
+ // | });
+ //
+ // example:
+ // When the CSS style property is hyphenated, the JavaScript property is camelCased.
+ // font-size becomes fontSize, and so on.
+ // | dojo.style("thinger",{
+ // | fontSize:"14pt",
+ // | letterSpacing:"1.2em"
+ // | });
+ //
+ // example:
+ // dojo.NodeList implements .style() using the same syntax, omitting the "node" parameter, calling
+ // dojo.style() on every element of the list. See: dojo.query and dojo.NodeList
+ // | dojo.query(".someClassName").style("visibility","hidden");
+ // | // or
+ // | dojo.query("#baz > div").style({
+ // | opacity:0.75,
+ // | fontSize:"13pt"
+ // | });
+
+ var n = d.byId(node), args = arguments.length, op = (style=="opacity");
+ style = _floatAliases[style] || style;
+ if(args == 3){
+ return op ? d._setOpacity(n, value) : n.style[style] = value; /*Number*/
+ }
+ if(args == 2 && op){
+ return d._getOpacity(n);
+ }
+ var s = gcs(n);
+ if(args == 2 && !d.isString(style)){
+ for(var x in style){
+ d.style(node, x, style[x]);
+ }
+ return s;
+ }
+ return (args == 1) ? s : _toStyleValue(n, style, s[style]); /* CSS2Properties||String||Number */
+ }
+
+ // =============================
+ // Box Functions
+ // =============================
+
+ dojo._getPadExtents = function(/*DomNode*/n, /*Object*/computedStyle){
+ // summary:
+ // Returns object with special values specifically useful for node
+ // fitting.
+ //
+ // * l/t = left/top padding (respectively)
+ // * w = the total of the left and right padding
+ // * h = the total of the top and bottom padding
+ //
+ // If 'node' has position, l/t forms the origin for child nodes.
+ // The w/h are used for calculating boxes.
+ // Normally application code will not need to invoke this
+ // directly, and will use the ...box... functions instead.
+ var
+ s = computedStyle||gcs(n),
+ l = px(n, s.paddingLeft),
+ t = px(n, s.paddingTop);
+ return {
+ l: l,
+ t: t,
+ w: l+px(n, s.paddingRight),
+ h: t+px(n, s.paddingBottom)
+ };
+ }
+
+ dojo._getBorderExtents = function(/*DomNode*/n, /*Object*/computedStyle){
+ // summary:
+ // returns an object with properties useful for noting the border
+ // dimensions.
+ //
+ // * l/t = the sum of left/top border (respectively)
+ // * w = the sum of the left and right border
+ // * h = the sum of the top and bottom border
+ //
+ // The w/h are used for calculating boxes.
+ // Normally application code will not need to invoke this
+ // directly, and will use the ...box... functions instead.
+ var
+ ne = "none",
+ s = computedStyle||gcs(n),
+ bl = (s.borderLeftStyle != ne ? px(n, s.borderLeftWidth) : 0),
+ bt = (s.borderTopStyle != ne ? px(n, s.borderTopWidth) : 0);
+ return {
+ l: bl,
+ t: bt,
+ w: bl + (s.borderRightStyle!=ne ? px(n, s.borderRightWidth) : 0),
+ h: bt + (s.borderBottomStyle!=ne ? px(n, s.borderBottomWidth) : 0)
+ };
+ }
+
+ dojo._getPadBorderExtents = function(/*DomNode*/n, /*Object*/computedStyle){
+ // summary:
+ // returns object with properties useful for box fitting with
+ // regards to padding.
+ //
+ // * l/t = the sum of left/top padding and left/top border (respectively)
+ // * w = the sum of the left and right padding and border
+ // * h = the sum of the top and bottom padding and border
+ //
+ // The w/h are used for calculating boxes.
+ // Normally application code will not need to invoke this
+ // directly, and will use the ...box... functions instead.
+ var
+ s = computedStyle||gcs(n),
+ p = d._getPadExtents(n, s),
+ b = d._getBorderExtents(n, s);
+ return {
+ l: p.l + b.l,
+ t: p.t + b.t,
+ w: p.w + b.w,
+ h: p.h + b.h
+ };
+ }
+
+ dojo._getMarginExtents = function(n, computedStyle){
+ // summary:
+ // returns object with properties useful for box fitting with
+ // regards to box margins (i.e., the outer-box).
+ //
+ // * l/t = marginLeft, marginTop, respectively
+ // * w = total width, margin inclusive
+ // * h = total height, margin inclusive
+ //
+ // The w/h are used for calculating boxes.
+ // Normally application code will not need to invoke this
+ // directly, and will use the ...box... functions instead.
+ var
+ s = computedStyle||gcs(n),
+ l = px(n, s.marginLeft),
+ t = px(n, s.marginTop),
+ r = px(n, s.marginRight),
+ b = px(n, s.marginBottom);
+ if(d.isSafari && (s.position != "absolute")){
+ // FIXME: Safari's version of the computed right margin
+ // is the space between our right edge and the right edge
+ // of our offsetParent.
+ // What we are looking for is the actual margin value as
+ // determined by CSS.
+ // Hack solution is to assume left/right margins are the same.
+ r = l;
+ }
+ return {
+ l: l,
+ t: t,
+ w: l+r,
+ h: t+b
+ };
+ }
+
+ // Box getters work in any box context because offsetWidth/clientWidth
+ // are invariant wrt box context
+ //
+ // They do *not* work for display: inline objects that have padding styles
+ // because the user agent ignores padding (it's bogus styling in any case)
+ //
+ // Be careful with IMGs because they are inline or block depending on
+ // browser and browser mode.
+
+ // Although it would be easier to read, there are not separate versions of
+ // _getMarginBox for each browser because:
+ // 1. the branching is not expensive
+ // 2. factoring the shared code wastes cycles (function call overhead)
+ // 3. duplicating the shared code wastes bytes
+
+ dojo._getMarginBox = function(/*DomNode*/node, /*Object*/computedStyle){
+ // summary:
+ // returns an object that encodes the width, height, left and top
+ // positions of the node's margin box.
+ var s = computedStyle||gcs(node), me = d._getMarginExtents(node, s);
+ var l = node.offsetLeft - me.l, t = node.offsetTop - me.t;
+ if(d.isMoz){
+ // Mozilla:
+ // If offsetParent has a computed overflow != visible, the offsetLeft is decreased
+ // by the parent's border.
+ // We don't want to compute the parent's style, so instead we examine node's
+ // computed left/top which is more stable.
+ var sl = parseFloat(s.left), st = parseFloat(s.top);
+ if(!isNaN(sl) && !isNaN(st)){
+ l = sl, t = st;
+ }else{
+ // If child's computed left/top are not parseable as a number (e.g. "auto"), we
+ // have no choice but to examine the parent's computed style.
+ var p = node.parentNode;
+ if(p && p.style){
+ var pcs = gcs(p);
+ if(pcs.overflow != "visible"){
+ var be = d._getBorderExtents(p, pcs);
+ l += be.l, t += be.t;
+ }
+ }
+ }
+ }else if(d.isOpera){
+ // On Opera, offsetLeft includes the parent's border
+ var p = node.parentNode;
+ if(p){
+ var be = d._getBorderExtents(p);
+ l -= be.l, t -= be.t;
+ }
+ }
+ return {
+ l: l,
+ t: t,
+ w: node.offsetWidth + me.w,
+ h: node.offsetHeight + me.h
+ };
+ }
+
+ dojo._getContentBox = function(node, computedStyle){
+ // summary:
+ // Returns an object that encodes the width, height, left and top
+ // positions of the node's content box, irrespective of the
+ // current box model.
+
+ // clientWidth/Height are important since the automatically account for scrollbars
+ // fallback to offsetWidth/Height for special cases (see #3378)
+ var s=computedStyle||gcs(node), pe=d._getPadExtents(node, s), be=d._getBorderExtents(node, s), w=node.clientWidth, h;
+ if(!w){
+ w=node.offsetWidth, h=node.offsetHeight;
+ }else{
+ h=node.clientHeight, be.w = be.h = 0;
+ }
+ // On Opera, offsetLeft includes the parent's border
+ if(d.isOpera){ pe.l += be.l; pe.t += be.t; };
+ return {
+ l: pe.l,
+ t: pe.t,
+ w: w - pe.w - be.w,
+ h: h - pe.h - be.h
+ };
+ }
+
+ dojo._getBorderBox = function(node, computedStyle){
+ var s=computedStyle||gcs(node), pe=d._getPadExtents(node, s), cb=d._getContentBox(node, s);
+ return {
+ l: cb.l - pe.l,
+ t: cb.t - pe.t,
+ w: cb.w + pe.w,
+ h: cb.h + pe.h
+ };
+ }
+
+ // Box setters depend on box context because interpretation of width/height styles
+ // vary wrt box context.
+ //
+ // The value of dojo.boxModel is used to determine box context.
+ // dojo.boxModel can be set directly to change behavior.
+ //
+ // Beware of display: inline objects that have padding styles
+ // because the user agent ignores padding (it's a bogus setup anyway)
+ //
+ // Be careful with IMGs because they are inline or block depending on
+ // browser and browser mode.
+ //
+ // Elements other than DIV may have special quirks, like built-in
+ // margins or padding, or values not detectable via computedStyle.
+ // In particular, margins on TABLE do not seems to appear
+ // at all in computedStyle on Mozilla.
+
+ dojo._setBox = function(/*DomNode*/node, /*Number?*/l, /*Number?*/t, /*Number?*/w, /*Number?*/h, /*String?*/u){
+ // summary:
+ // sets width/height/left/top in the current (native) box-model
+ // dimentions. Uses the unit passed in u.
+ // node: DOM Node reference. Id string not supported for performance reasons.
+ // l: optional. left offset from parent.
+ // t: optional. top offset from parent.
+ // w: optional. width in current box model.
+ // h: optional. width in current box model.
+ // u: optional. unit measure to use for other measures. Defaults to "px".
+ u = u || "px";
+ var s = node.style;
+ if(!isNaN(l)){ s.left = l+u; }
+ if(!isNaN(t)){ s.top = t+u; }
+ if(w>=0){ s.width = w+u; }
+ if(h>=0){ s.height = h+u; }
+ }
+
+ dojo._usesBorderBox = function(/*DomNode*/node){
+ // summary:
+ // True if the node uses border-box layout.
+
+ // We could test the computed style of node to see if a particular box
+ // has been specified, but there are details and we choose not to bother.
+ var n = node.tagName;
+ // For whatever reason, TABLE and BUTTON are always border-box by default.
+ // If you have assigned a different box to either one via CSS then
+ // box functions will break.
+ return d.boxModel=="border-box" || n=="TABLE" || n=="BUTTON"; // boolean
+ }
+
+ dojo._setContentSize = function(/*DomNode*/node, /*Number*/widthPx, /*Number*/heightPx, /*Object*/computedStyle){
+ // summary:
+ // Sets the size of the node's contents, irrespective of margins,
+ // padding, or borders.
+ if(d._usesBorderBox(node)){
+ var pb = d._getPadBorderExtents(node, computedStyle);
+ if(widthPx >= 0){ widthPx += pb.w; }
+ if(heightPx >= 0){ heightPx += pb.h; }
+ }
+ d._setBox(node, NaN, NaN, widthPx, heightPx);
+ }
+
+ dojo._setMarginBox = function(/*DomNode*/node, /*Number?*/leftPx, /*Number?*/topPx,
+ /*Number?*/widthPx, /*Number?*/heightPx,
+ /*Object*/computedStyle){
+ // summary:
+ // sets the size of the node's margin box and placement
+ // (left/top), irrespective of box model. Think of it as a
+ // passthrough to dojo._setBox that handles box-model vagaries for
+ // you.
+
+ var s = computedStyle||gcs(node);
+ // Some elements have special padding, margin, and box-model settings.
+ // To use box functions you may need to set padding, margin explicitly.
+ // Controlling box-model is harder, in a pinch you might set dojo.boxModel.
+ var bb=d._usesBorderBox(node),
+ pb=bb ? _nilExtents : d._getPadBorderExtents(node, s),
+ mb=d._getMarginExtents(node, s);
+ if(widthPx>=0){ widthPx = Math.max(widthPx - pb.w - mb.w, 0); }
+ if(heightPx>=0){ heightPx = Math.max(heightPx - pb.h - mb.h, 0); }
+ d._setBox(node, leftPx, topPx, widthPx, heightPx);
+ }
+
+ var _nilExtents = { l:0, t:0, w:0, h:0 };
+
+ // public API
+
+ dojo.marginBox = function(/*DomNode|String*/node, /*Object?*/box){
+ // summary:
+ // Getter/setter for the margin-box of node.
+ // description:
+ // Returns an object in the expected format of box (regardless
+ // if box is passed). The object might look like:
+ // `{ l: 50, t: 200, w: 300: h: 150 }`
+ // for a node offset from its parent 50px to the left, 200px from
+ // the top with a margin width of 300px and a margin-height of
+ // 150px.
+ // node:
+ // id or reference to DOM Node to get/set box for
+ // box:
+ // If passed, denotes that dojo.marginBox() should
+ // update/set the margin box for node. Box is an object in the
+ // above format. All properties are optional if passed.
+ var n=d.byId(node), s=gcs(n), b=box;
+ return !b ? d._getMarginBox(n, s) : d._setMarginBox(n, b.l, b.t, b.w, b.h, s); // Object
+ }
+
+ dojo.contentBox = function(/*DomNode|String*/node, /*Object?*/box){
+ // summary:
+ // Getter/setter for the content-box of node.
+ // description:
+ // Returns an object in the expected format of box (regardless if box is passed).
+ // The object might look like:
+ // `{ l: 50, t: 200, w: 300: h: 150 }`
+ // for a node offset from its parent 50px to the left, 200px from
+ // the top with a content width of 300px and a content-height of
+ // 150px. Note that the content box may have a much larger border
+ // or margin box, depending on the box model currently in use and
+ // CSS values set/inherited for node.
+ // node:
+ // id or reference to DOM Node to get/set box for
+ // box:
+ // If passed, denotes that dojo.contentBox() should
+ // update/set the content box for node. Box is an object in the
+ // above format. All properties are optional if passed.
+ var n=dojo.byId(node), s=gcs(n), b=box;
+ return !b ? d._getContentBox(n, s) : d._setContentSize(n, b.w, b.h, s); // Object
+ }
+
+ // =============================
+ // Positioning
+ // =============================
+
+ var _sumAncestorProperties = function(node, prop){
+ if(!(node = (node||0).parentNode)){return 0};
+ var val, retVal = 0, _b = d.body();
+ while(node && node.style){
+ if(gcs(node).position == "fixed"){
+ return 0;
+ }
+ val = node[prop];
+ if(val){
+ retVal += val - 0;
+ // opera and khtml #body & #html has the same values, we only
+ // need one value
+ if(node == _b){ break; }
+ }
+ node = node.parentNode;
+ }
+ return retVal; // integer
+ }
+
+ dojo._docScroll = function(){
+ var
+ _b = d.body(),
+ _w = d.global,
+ de = d.doc.documentElement;
+ return {
+ y: (_w.pageYOffset || de.scrollTop || _b.scrollTop || 0),
+ x: (_w.pageXOffset || d._fixIeBiDiScrollLeft(de.scrollLeft) || _b.scrollLeft || 0)
+ };
+ };
+
+ dojo._isBodyLtr = function(){
+ //FIXME: could check html and body tags directly instead of computed style? need to ignore case, accept empty values
+ return !("_bodyLtr" in d) ?
+ d._bodyLtr = gcs(d.body()).direction == "ltr" :
+ d._bodyLtr; // Boolean
+ }
+
+ dojo._getIeDocumentElementOffset = function(){
+ // summary
+ // The following values in IE contain an offset:
+ // event.clientX
+ // event.clientY
+ // node.getBoundingClientRect().left
+ // node.getBoundingClientRect().top
+ // But other position related values do not contain this offset, such as
+ // node.offsetLeft, node.offsetTop, node.style.left and node.style.top.
+ // The offset is always (2, 2) in LTR direction. When the body is in RTL
+ // direction, the offset counts the width of left scroll bar's width.
+ // This function computes the actual offset.
+
+ //NOTE: assumes we're being called in an IE browser
+
+ var de = d.doc.documentElement;
+ //FIXME: use this instead? var de = d.compatMode == "BackCompat" ? d.body : d.documentElement;
+
+ return (d.isIE >= 7) ?
+ {x: de.getBoundingClientRect().left, y: de.getBoundingClientRect().top}
+ :
+ // IE 6.0
+ {x: d._isBodyLtr() || window.parent == window ?
+ de.clientLeft : de.offsetWidth - de.clientWidth - de.clientLeft,
+ y: de.clientTop}; // Object
+ };
+
+ dojo._fixIeBiDiScrollLeft = function(/*Integer*/ scrollLeft){
+ // In RTL direction, scrollLeft should be a negative value, but IE
+ // returns a positive one. All codes using documentElement.scrollLeft
+ // must call this function to fix this error, otherwise the position
+ // will offset to right when there is a horizontal scrollbar.
+ var dd = d.doc;
+ if(d.isIE && !dojo._isBodyLtr()){
+ var de = dd.compatMode == "BackCompat" ? dd.body : dd.documentElement;
+ return scrollLeft + de.clientWidth - de.scrollWidth; // Integer
+ }
+ return scrollLeft; // Integer
+ }
+
+ dojo._abs = function(/*DomNode*/node, /*Boolean?*/includeScroll){
+ // summary:
+ // Gets the position of the passed element relative to
+ // the viewport (if includeScroll==false), or relative to the
+ // document root (if includeScroll==true).
+ //
+ // Returns an object of the form:
+ // { x: 100, y: 300 }
+ // if includeScroll is passed, the x and y values will include any
+ // document offsets that may affect the position relative to the
+ // viewport.
+
+ // FIXME: need to decide in the brave-new-world if we're going to be
+ // margin-box or border-box.
+ var ownerDocument = node.ownerDocument;
+ var ret = {
+ x: 0,
+ y: 0
+ };
+
+ // targetBoxType == "border-box"
+ var db = d.body();
+ if(d.isIE || (d.isFF >= 3)){
+ var client = node.getBoundingClientRect();
+ var offset = (d.isIE) ? d._getIeDocumentElementOffset() : { x: 0, y: 0};
+ ret.x = client.left - offset.x;
+ ret.y = client.top - offset.y;
+ }else if(ownerDocument["getBoxObjectFor"]){
+ // mozilla
+ var bo = ownerDocument.getBoxObjectFor(node),
+ b = d._getBorderExtents(node);
+ ret.x = bo.x - b.l - _sumAncestorProperties(node, "scrollLeft");
+ ret.y = bo.y - b.t - _sumAncestorProperties(node, "scrollTop");
+ }else{
+ if(node["offsetParent"]){
+ var endNode;
+ // in Safari, if the node is an absolutely positioned child of
+ // the body and the body has a margin the offset of the child
+ // and the body contain the body's margins, so we need to end
+ // at the body
+ // FIXME: getting contrary results to the above in latest WebKit.
+ if(d.isSafari &&
+ //(node.style.getPropertyValue("position") == "absolute") &&
+ (gcs(node).position == "absolute") &&
+ (node.parentNode == db)){
+ endNode = db;
+ }else{
+ endNode = db.parentNode;
+ }
+ if(node.parentNode != db){
+ var nd = node;
+ if(d.isOpera){ nd = db; }
+ ret.x -= _sumAncestorProperties(nd, "scrollLeft");
+ ret.y -= _sumAncestorProperties(nd, "scrollTop");
+ }
+ var curnode = node;
+ do{
+ var n = curnode.offsetLeft;
+ //FIXME: ugly hack to workaround the submenu in
+ //popupmenu2 does not shown up correctly in opera.
+ //Someone have a better workaround?
+ if(!d.isOpera || n > 0){
+ ret.x += isNaN(n) ? 0 : n;
+ }
+ var t = curnode.offsetTop;
+ ret.y += isNaN(t) ? 0 : t;
+ if(d.isSafari && curnode != node){
+ var cs = gcs(curnode);
+ ret.x += px(curnode, cs.borderLeftWidth);
+ ret.y += px(curnode, cs.borderTopWidth);
+ }
+ curnode = curnode.offsetParent;
+ }while((curnode != endNode) && curnode);
+ }else if(node.x && node.y){
+ ret.x += isNaN(node.x) ? 0 : node.x;
+ ret.y += isNaN(node.y) ? 0 : node.y;
+ }
+ }
+ // account for document scrolling
+ // if offsetParent is used, ret value already includes scroll position
+ // so we may have to actually remove that value if !includeScroll
+ if(includeScroll){
+ var scroll = d._docScroll();
+ ret.y += scroll.y;
+ ret.x += scroll.x;
+ }
+
+ return ret; // object
+ }
+
+ // FIXME: need a setter for coords or a moveTo!!
+ dojo.coords = function(/*DomNode|String*/node, /*Boolean?*/includeScroll){
+ // summary:
+ // Returns an object that measures margin box width/height and
+ // absolute positioning data from dojo._abs().
+ //
+ // description:
+ // Returns an object that measures margin box width/height and
+ // absolute positioning data from dojo._abs().
+ // Return value will be in the form:
+ // `{ l: 50, t: 200, w: 300: h: 150, x: 100, y: 300 }`
+ // Does not act as a setter. If includeScroll is passed, the x and
+ // y params are affected as one would expect in dojo._abs().
+ var n=d.byId(node), s=gcs(n), mb=d._getMarginBox(n, s);
+ var abs = d._abs(n, includeScroll);
+ mb.x = abs.x;
+ mb.y = abs.y;
+ return mb;
+ }
+
+ // =============================
+ // Element attribute Functions
+ // =============================
+
+ var _fixAttrName = function(/*String*/name){
+ switch(name.toLowerCase()){
+ case "tabindex":
+ // Internet Explorer will only set or remove tabindex
+ // if it is spelled "tabIndex"
+ // console.debug((dojo.isIE && dojo.isIE < 8)? "tabIndex" : "tabindex");
+ return (d.isIE && d.isIE < 8) ? "tabIndex" : "tabindex";
+ default:
+ return name;
+ }
+ }
+
+ // non-deprecated HTML4 attributes with default values
+ // http://www.w3.org/TR/html401/index/attributes.html
+ // FF and Safari will return the default values if you
+ // access the attributes via a property but not
+ // via getAttribute()
+ var _attrProps = {
+ colspan: "colSpan",
+ enctype: "enctype",
+ frameborder: "frameborder",
+ method: "method",
+ rowspan: "rowSpan",
+ scrolling: "scrolling",
+ shape: "shape",
+ span: "span",
+ type: "type",
+ valuetype: "valueType"
+ }
+
+ dojo.hasAttr = function(/*DomNode|String*/node, /*String*/name){
+ // summary:
+ // Returns true if the requested attribute is specified on the
+ // given element, and false otherwise.
+ // node:
+ // id or reference to the element to check
+ // name:
+ // the name of the attribute
+ // returns:
+ // true if the requested attribute is specified on the
+ // given element, and false otherwise
+ var attr = d.byId(node).getAttributeNode(_fixAttrName(name));
+ return attr ? attr.specified : false; // Boolean
+ }
+
+ var _evtHdlrMap = {
+
+ }
+
+ var _ctr = 0;
+ var _attrId = dojo._scopeName + "attrid";
+
+ dojo.attr = function(/*DomNode|String*/node, /*String|Object*/name, /*String?*/value){
+ // summary:
+ // Gets or sets an attribute on an HTML element.
+ // description:
+ // Handles normalized getting and setting of attributes on DOM
+ // Nodes. If 2 arguments are passed, and a the second argumnt is a
+ // string, acts as a getter.
+ //
+ // If a third argument is passed, or if the second argumnt is a
+ // map of attributes, acts as a setter.
+ //
+ // When passing functions as values, note that they will not be
+ // directly assigned to slots on the node, but rather the default
+ // behavior will be removed and the new behavior will be added
+ // using `dojo.connect()`, meaning that event handler properties
+ // will be normalized and that some caveats with regards to
+ // non-standard behaviors for onsubmit apply. Namely that you
+ // should cancel form submission using `dojo.stopEvent()` on the
+ // passed event object instead of returning a boolean value from
+ // the handler itself.
+ // node:
+ // id or reference to the element to get or set the attribute on
+ // name:
+ // the name of the attribute to get or set.
+ // value:
+ // The value to set for the attribute
+ // returns:
+ // when used as a getter, the value of the requested attribute
+ // or null if that attribute does not have a specified or
+ // default value;
+ //
+ // when user as a setter, undefined
+ // example:
+ // | // get the current value of the "foo" attribute on a node
+ // | dojo.attr(dojo.byId("nodeId"), "foo");
+ // |
+ // | // we can just pass the id:
+ // | dojo.attr("nodeId", "foo");
+ // |
+ // | // use attr() to set the tab index
+ // | dojo.attr("nodeId", "tabindex", 3);
+ // |
+ // | // set multiple values at once, including event handlers:
+ // | dojo.attr("formId", {
+ // | "foo": "bar",
+ // | "tabindex": -1,
+ // | "method": "POST",
+ // | "onsubmit": function(e){
+ // | // stop submitting the form. Note that the IE behavior
+ // | // of returning true or false will have no effect here
+ // | // since our handler is connect()ed to the built-in
+ // | // onsubmit behavior and so we need to use
+ // | // dojo.stopEvent() to ensure that the submission
+ // | // doesn't proceed.
+ // | dojo.stopEvent(e);
+ // |
+ // | // submit the form with Ajax
+ // | dojo.xhrPost({ form: "formId" });
+ // | }
+ // | });
+
+ var args = arguments.length;
+ if(args == 2 && !d.isString(name)){
+ for(var x in name){ d.attr(node, x, name[x]); }
+ return;
+ }
+ node = d.byId(node);
+ name = _fixAttrName(name);
+ if(args == 3){
+ if(d.isFunction(value)){
+ // clobber if we can
+ var attrId = d.attr(node, _attrId);
+ if(!attrId){
+ attrId = _ctr++;
+ d.attr(node, _attrId, attrId);
+ }
+ if(!_evtHdlrMap[attrId]){
+ _evtHdlrMap[attrId] = {};
+ }
+ var h = _evtHdlrMap[attrId][name];
+ if(h){
+ d.disconnect(h);
+ }else{
+ try{
+ delete node[name];
+ }catch(e){}
+ }
+
+ // ensure that event objects are normalized, etc.
+ _evtHdlrMap[attrId][name] = d.connect(node, name, value);
+
+ }else if(typeof value == "boolean"){ // e.g. onsubmit, disabled
+ // if a function, we should normalize the event object here!!!
+ node[name] = value;
+ }else{
+ node.setAttribute(name, value);
+ }
+ return;
+ }else{
+ // should we access this attribute via a property or
+ // via getAttribute()?
+ var prop = _attrProps[name.toLowerCase()];
+ if(prop){
+ return node[prop];
+ }else{
+ var value = node[name];
+ return (typeof value == 'boolean' || typeof value == 'function') ? value : (d.hasAttr(node, name) ? node.getAttribute(name) : null);
+ }
+ }
+ }
+
+ dojo.removeAttr = function(/*DomNode|String*/node, /*String*/name){
+ // summary:
+ // Removes an attribute from an HTML element.
+ // node:
+ // id or reference to the element to remove the attribute from
+ // name:
+ // the name of the attribute to remove
+ d.byId(node).removeAttribute(_fixAttrName(name));
+ }
+})();
+
+// =============================
+// (CSS) Class Functions
+// =============================
+
+dojo.hasClass = function(/*DomNode|String*/node, /*String*/classStr){
+ // summary:
+ // Returns whether or not the specified classes are a portion of the
+ // class list currently applied to the node.
+ return ((" "+dojo.byId(node).className+" ").indexOf(" "+classStr+" ") >= 0); // Boolean
+};
+
+dojo.addClass = function(/*DomNode|String*/node, /*String*/classStr){
+ // summary:
+ // Adds the specified classes to the end of the class list on the
+ // passed node.
+ node = dojo.byId(node);
+ var cls = node.className;
+ if((" "+cls+" ").indexOf(" "+classStr+" ") < 0){
+ node.className = cls + (cls ? ' ' : '') + classStr;
+ }
+};
+
+dojo.removeClass = function(/*DomNode|String*/node, /*String*/classStr){
+ // summary: Removes the specified classes from node.
+ node = dojo.byId(node);
+ var t = dojo.trim((" " + node.className + " ").replace(" " + classStr + " ", " "));
+ if(node.className != t){ node.className = t; }
+};
+
+dojo.toggleClass = function(/*DomNode|String*/node, /*String*/classStr, /*Boolean?*/condition){
+ // summary:
+ // Adds a class to node if not present, or removes if present.
+ // Pass a boolean condition if you want to explicitly add or remove.
+ // condition:
+ // If passed, true means to add the class, false means to remove.
+ if(condition === undefined){
+ condition = !dojo.hasClass(node, classStr);
+ }
+ dojo[condition ? "addClass" : "removeClass"](node, classStr);
+};
+
+}
+
+if(!dojo._hasResource["dojo._base.NodeList"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.NodeList"] = true;
+dojo.provide("dojo._base.NodeList");
+
+(function(){
+
+ var d = dojo;
+
+ var tnl = function(arr){
+ // decorate an array to make it look like a NodeList
+ arr.constructor = dojo.NodeList;
+ dojo._mixin(arr, dojo.NodeList.prototype);
+ return arr;
+ }
+
+ var _mapIntoDojo = function(func, alwaysThis){
+ // returns a function which, when executed in the scope of its caller,
+ // applies the passed arguments to a particular dojo.* function (named
+ // in func) and aggregates the returns. if alwaysThis is true, it
+ // always returns the scope object and not the collected returns from
+ // the Dojo method
+ return function(){
+ var _a = arguments;
+ var aa = d._toArray(_a, 0, [null]);
+ var s = this.map(function(i){
+ aa[0] = i;
+ return d[func].apply(d, aa);
+ });
+ return (alwaysThis || ( (_a.length > 1) || !d.isString(_a[0]) )) ? this : s; // String||dojo.NodeList
+ }
+ };
+
+ dojo.NodeList = function(){
+ // summary:
+ // dojo.NodeList is as subclass of Array which adds syntactic
+ // sugar for chaining, common iteration operations, animation,
+ // and node manipulation. NodeLists are most often returned as
+ // the result of dojo.query() calls.
+ // example:
+ // create a node list from a node
+ // | new dojo.NodeList(dojo.byId("foo"));
+
+ return tnl(Array.apply(null, arguments));
+ }
+
+ dojo.NodeList._wrap = tnl;
+
+ dojo.extend(dojo.NodeList, {
+ // http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array#Methods
+
+ // FIXME: handle return values for #3244
+ // http://trac.dojotoolkit.org/ticket/3244
+
+ // FIXME:
+ // need to wrap or implement:
+ // join (perhaps w/ innerHTML/outerHTML overload for toString() of items?)
+ // reduce
+ // reduceRight
+
+ slice: function(/*===== begin, end =====*/){
+ // summary:
+ // Returns a new NodeList, maintaining this one in place
+ // description:
+ // This method behaves exactly like the Array.slice method
+ // with the caveat that it returns a dojo.NodeList and not a
+ // raw Array. For more details, see:
+ // http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:slice
+ // begin: Integer
+ // Can be a positive or negative integer, with positive
+ // integers noting the offset to begin at, and negative
+ // integers denoting an offset from the end (i.e., to the left
+ // of the end)
+ // end: Integer?
+ // Optional parameter to describe what position relative to
+ // the NodeList's zero index to end the slice at. Like begin,
+ // can be positive or negative.
+ var a = dojo._toArray(arguments);
+ return tnl(a.slice.apply(this, a));
+ },
+
+ splice: function(/*===== index, howmany, item =====*/){
+ // summary:
+ // Returns a new NodeList, manipulating this NodeList based on
+ // the arguments passed, potentially splicing in new elements
+ // at an offset, optionally deleting elements
+ // description:
+ // This method behaves exactly like the Array.splice method
+ // with the caveat that it returns a dojo.NodeList and not a
+ // raw Array. For more details, see:
+ //
+ // index: Integer
+ // begin can be a positive or negative integer, with positive
+ // integers noting the offset to begin at, and negative
+ // integers denoting an offset from the end (i.e., to the left
+ // of the end)
+ // howmany: Integer?
+ // Optional parameter to describe what position relative to
+ // the NodeList's zero index to end the slice at. Like begin,
+ // can be positive or negative.
+ // item: Object...?
+ // Any number of optional parameters may be passed in to be
+ // spliced into the NodeList
+ // returns:
+ // dojo.NodeList
+ var a = dojo._toArray(arguments);
+ return tnl(a.splice.apply(this, a));
+ },
+
+ concat: function(/*===== item =====*/){
+ // summary:
+ // Returns a new NodeList comprised of items in this NodeList
+ // as well as items passed in as parameters
+ // description:
+ // This method behaves exactly like the Array.concat method
+ // with the caveat that it returns a dojo.NodeList and not a
+ // raw Array. For more details, see:
+ //
+ // item: Object...?
+ // Any number of optional parameters may be passed in to be
+ // spliced into the NodeList
+ // returns:
+ // dojo.NodeList
+ var a = dojo._toArray(arguments, 0, [this]);
+ return tnl(a.concat.apply([], a));
+ },
+
+ indexOf: function(/*Object*/ value, /*Integer?*/ fromIndex){
+ // summary:
+ // see dojo.indexOf(). The primary difference is that the acted-on
+ // array is implicitly this NodeList
+ // value:
+ // The value to search for.
+ // fromIndex:
+ // The loction to start searching from. Optional. Defaults to 0.
+ // description:
+ // For more details on the behavior of indexOf, see:
+ //
+ // returns:
+ // Positive Integer or 0 for a match, -1 of not found.
+ return d.indexOf(this, value, fromIndex); // Integer
+ },
+
+ lastIndexOf: function(/*===== value, fromIndex =====*/){
+ // summary:
+ // see dojo.lastIndexOf(). The primary difference is that the
+ // acted-on array is implicitly this NodeList
+ // description:
+ // For more details on the behavior of lastIndexOf, see:
+ //
+ // value: Object
+ // The value to search for.
+ // fromIndex: Integer?
+ // The loction to start searching from. Optional. Defaults to 0.
+ // returns:
+ // Positive Integer or 0 for a match, -1 of not found.
+ return d.lastIndexOf.apply(d, d._toArray(arguments, 0, [this])); // Integer
+ },
+
+ every: function(/*Function*/callback, /*Object?*/thisObject){
+ // summary:
+ // see `dojo.every()` and:
+ //
+ // Takes the same structure of arguments and returns as
+ // dojo.every() with the caveat that the passed array is
+ // implicitly this NodeList
+ return d.every(this, callback, thisObject); // Boolean
+ },
+
+ some: function(/*Function*/callback, /*Object?*/thisObject){
+ // summary:
+ // see dojo.some() and:
+ // http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:some
+ // Takes the same structure of arguments and returns as
+ // dojo.some() with the caveat that the passed array is
+ // implicitly this NodeList
+ return d.some(this, callback, thisObject); // Boolean
+ },
+
+ map: function(/*Function*/ func, /*Function?*/ obj){
+ // summary:
+ // see dojo.map(). The primary difference is that the acted-on
+ // array is implicitly this NodeList and the return is a
+ // dojo.NodeList (a subclass of Array)
+
+ return d.map(this, func, obj, d.NodeList); // dojo.NodeList
+ },
+
+ forEach: function(callback, thisObj){
+ // summary:
+ // see dojo.forEach(). The primary difference is that the acted-on
+ // array is implicitly this NodeList
+
+ d.forEach(this, callback, thisObj);
+ // non-standard return to allow easier chaining
+ return this; // dojo.NodeList
+ },
+
+ // custom methods
+
+ coords: function(){
+ // summary:
+ // Returns the box objects all elements in a node list as
+ // an Array (*not* a NodeList)
+
+ return d.map(this, d.coords); // Array
+ },
+
+ /*=====
+ attr: function(property, value){
+ // summary:
+ // gets or sets the DOM attribute for every element in the
+ // NodeList
+ // property: String
+ // the attribute to get/set
+ // value: String?
+ // optional. The value to set the property to
+ // return:
+ // if no value is passed, the result is an array of attribute values
+ // If a value is passed, the return is this NodeList
+ },
+
+ style: function(property, value){
+ // summary:
+ // gets or sets the CSS property for every element in the NodeList
+ // property: String
+ // the CSS property to get/set, in JavaScript notation
+ // ("lineHieght" instead of "line-height")
+ // value: String?
+ // optional. The value to set the property to
+ // return:
+ // if no value is passed, the result is an array of strings.
+ // If a value is passed, the return is this NodeList
+ },
+
+ addClass: function(className){
+ // summary:
+ // adds the specified class to every node in the list
+ // className: String
+ // the CSS class to add
+ // return:
+ // dojo.NodeList, this list
+ },
+
+ removeClass: function(className){
+ // summary:
+ // removes the specified class from every node in the list
+ // className: String
+ // the CSS class to add
+ // return:
+ // dojo.NodeList, this list
+ },
+
+ toggleClass: function(className, condition){
+ // summary:
+ // Adds a class to node if not present, or removes if present.
+ // Pass a boolean condition if you want to explicitly add or remove.
+ // condition: Boolean?
+ // If passed, true means to add the class, false means to remove.
+ // className: String
+ // the CSS class to add
+ // return: dojo.NodeList
+ // this list
+ },
+
+ connect: function(methodName, objOrFunc, funcName){
+ // summary:
+ // attach event handlers to every item of the NodeList. Uses dojo.connect()
+ // so event properties are normalized
+ // methodName: String
+ // the name of the method to attach to. For DOM events, this should be
+ // the lower-case name of the event
+ // objOrFunc: Object|Function|String
+ // if 2 arguments are passed (methodName, objOrFunc), objOrFunc should
+ // reference a function or be the name of the function in the global
+ // namespace to attach. If 3 arguments are provided
+ // (methodName, objOrFunc, funcName), objOrFunc must be the scope to
+ // locate the bound function in
+ // funcName: String?
+ // optional. A string naming the function in objOrFunc to bind to the
+ // event. May also be a function reference.
+ // example:
+ // add an onclick handler to every button on the page
+ // | dojo.query("div:nth-child(odd)").connect("onclick", function(e){
+ // | console.debug("clicked!");
+ // | });
+ // example:
+ // attach foo.bar() to every odd div's onmouseover
+ // | dojo.query("div:nth-child(odd)").connect("onmouseover", foo, "bar");
+ },
+ =====*/
+ attr: _mapIntoDojo("attr"),
+ style: _mapIntoDojo("style"),
+ addClass: _mapIntoDojo("addClass", true),
+ removeClass: _mapIntoDojo("removeClass", true),
+ toggleClass: _mapIntoDojo("toggleClass", true),
+ connect: _mapIntoDojo("connect", true),
+
+ // FIXME: connectPublisher()? connectRunOnce()?
+
+ place: function(/*String||Node*/ queryOrNode, /*String*/ position){
+ // summary:
+ // places elements of this node list relative to the first element matched
+ // by queryOrNode. Returns the original NodeList.
+ // queryOrNode:
+ // may be a string representing any valid CSS3 selector or a DOM node.
+ // In the selector case, only the first matching element will be used
+ // for relative positioning.
+ // position:
+ // can be one of:
+ // * "last"||"end" (default)
+ // * "first||"start"
+ // * "before"
+ // * "after"
+ // or an offset in the childNodes property
+ var item = d.query(queryOrNode)[0];
+ return this.forEach(function(i){ d.place(i, item, (position||"last")); }); // dojo.NodeList
+ },
+
+ orphan: function(/*String?*/ simpleFilter){
+ // summary:
+ // removes elements in this list that match the simple
+ // filter from their parents and returns them as a new
+ // NodeList.
+ // simpleFilter:
+ // single-expression CSS filter
+ // return:
+ // `dojo.NodeList` the orpahned elements
+ var orphans = simpleFilter ? d._filterQueryResult(this, simpleFilter) : this;
+ orphans.forEach(function(item){
+ if(item.parentNode){
+ item.parentNode.removeChild(item);
+ }
+ });
+ return orphans; // dojo.NodeList
+ },
+
+ adopt: function(/*String||Array||DomNode*/ queryOrListOrNode, /*String?*/ position){
+ // summary:
+ // places any/all elements in queryOrListOrNode at a
+ // position relative to the first element in this list.
+ // Returns a dojo.NodeList of the adopted elements.
+ // queryOrListOrNode:
+ // a DOM node or a query string or a query result.
+ // Represents the nodes to be adopted relative to the
+ // first element of this NodeList.
+ // position:
+ // can be one of:
+ // * "last"||"end" (default)
+ // * "first||"start"
+ // * "before"
+ // * "after"
+ // or an offset in the childNodes property
+ var item = this[0];
+ return d.query(queryOrListOrNode).forEach(function(ai){ d.place(ai, item, position || "last"); }); // dojo.NodeList
+ },
+
+ // FIXME: do we need this?
+ query: function(/*String*/ queryStr){
+ // summary:
+ // Returns a new, flattened NodeList. Elements of the new list
+ // satisfy the passed query but use elements of the
+ // current NodeList as query roots.
+
+ if(!queryStr){ return this; }
+
+ // FIXME: probably slow
+ // FIXME: use map?
+ var ret = d.NodeList();
+ this.forEach(function(item){
+ d.query(queryStr, item).forEach(function(subItem){
+ if(subItem !== undefined){
+ ret.push(subItem);
+ }
+ });
+ });
+ return ret; // dojo.NodeList
+ },
+
+ filter: function(/*String*/ simpleQuery){
+ // summary:
+ // "masks" the built-in javascript filter() method to support
+ // passing a simple string filter in addition to supporting
+ // filtering function objects.
+ // example:
+ // "regular" JS filter syntax as exposed in dojo.filter:
+ // | dojo.query("*").filter(function(item){
+ // | // highlight every paragraph
+ // | return (item.nodeName == "p");
+ // | }).styles("backgroundColor", "yellow");
+ // example:
+ // the same filtering using a CSS selector
+ // | dojo.query("*").filter("p").styles("backgroundColor", "yellow");
+
+ var items = this;
+ var _a = arguments;
+ var r = d.NodeList();
+ var rp = function(t){
+ if(t !== undefined){
+ r.push(t);
+ }
+ }
+ if(d.isString(simpleQuery)){
+ items = d._filterQueryResult(this, _a[0]);
+ if(_a.length == 1){
+ // if we only got a string query, pass back the filtered results
+ return items; // dojo.NodeList
+ }
+ // if we got a callback, run it over the filtered items
+ _a.shift();
+ }
+ // handle the (callback, [thisObject]) case
+ d.forEach(d.filter(items, _a[0], _a[1]), rp);
+ return r; // dojo.NodeList
+ },
+
+ /*
+ // FIXME: should this be "copyTo" and include parenting info?
+ clone: function(){
+ // summary:
+ // creates node clones of each element of this list
+ // and returns a new list containing the clones
+ },
+ */
+
+ addContent: function(/*String*/ content, /*String||Integer?*/ position){
+ // summary:
+ // add a node or some HTML as a string to every item in the list.
+ // Returns the original list.
+ // description:
+ // a copy of the HTML content is added to each item in the
+ // list, with an optional position argument. If no position
+ // argument is provided, the content is appended to the end of
+ // each item.
+ // content:
+ // the HTML in string format to add at position to every item
+ // position:
+ // can be one of:
+ // * "last"||"end" (default)
+ // * "first||"start"
+ // * "before"
+ // * "after"
+ // or an offset in the childNodes property
+ // example:
+ // appends content to the end if the position is ommitted
+ // | dojo.query("h3 > p").addContent("hey there!");
+ // example:
+ // add something to the front of each element that has a "thinger" property:
+ // | dojo.query("[thinger]").addContent("...", "first");
+ // example:
+ // adds a header before each element of the list
+ // | dojo.query(".note").addContent("
NOTE:
", "before");
+ var ta = d.doc.createElement("span");
+ if(d.isString(content)){
+ ta.innerHTML = content;
+ }else{
+ ta.appendChild(content);
+ }
+ if(position === undefined){
+ position = "last";
+ }
+ var ct = (position == "first" || position == "after") ? "lastChild" : "firstChild";
+ this.forEach(function(item){
+ var tn = ta.cloneNode(true);
+ while(tn[ct]){
+ d.place(tn[ct], item, position);
+ }
+ });
+ return this; // dojo.NodeList
+ },
+
+ empty: function(){
+ // summary:
+ // clears all content from each node in the list
+ return this.forEach("item.innerHTML='';"); // dojo.NodeList
+
+ // FIXME: should we be checking for and/or disposing of widgets below these nodes?
+ },
+
+ instantiate: function(/*String|Object*/ declaredClass, /*Object?*/ properties){
+ // summary:
+ // Create a new instance of a specified class, using the
+ // specified properties and each node in the nodeList as a
+ // srcNodeRef
+ //
+ var c = d.isFunction(declaredClass) ? declaredClass : d.getObject(declaredClass);
+ return this.forEach(function(i){
+ new c(properties||{},i);
+ }) // dojo.NodeList
+ }
+
+ });
+
+ // syntactic sugar for DOM events
+ d.forEach([
+ "blur", "focus", "click", "keydown", "keypress", "keyup", "mousedown",
+ "mouseenter", "mouseleave", "mousemove", "mouseout", "mouseover",
+ "mouseup"
+ ], function(evt){
+ var _oe = "on"+evt;
+ dojo.NodeList.prototype[_oe] = function(a, b){
+ return this.connect(_oe, a, b);
+ }
+ // FIXME: should these events trigger publishes?
+ /*
+ return (a ? this.connect(_oe, a, b) :
+ this.forEach(function(n){
+ // FIXME:
+ // listeners get buried by
+ // addEventListener and can't be dug back
+ // out to be triggered externally.
+ // see:
+ // http://developer.mozilla.org/en/docs/DOM:element
+
+ console.debug(n, evt, _oe);
+
+ // FIXME: need synthetic event support!
+ var _e = { target: n, faux: true, type: evt };
+ // dojo._event_listener._synthesizeEvent({}, { target: n, faux: true, type: evt });
+ try{ n[evt](_e); }catch(e){ console.debug(e); }
+ try{ n[_oe](_e); }catch(e){ console.debug(e); }
+ })
+ );
+ }
+ */
+ }
+ );
+
+})();
+
+}
+
+if(!dojo._hasResource["dojo._base.query"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.query"] = true;
+dojo.provide("dojo._base.query");
+
+/*
+ dojo.query() architectural overview:
+
+ dojo.query is a relatively full-featured CSS3 query library. It is
+ designed to take any valid CSS3 selector and return the nodes matching
+ the selector. To do this quickly, it processes queries in several
+ steps, applying caching where profitable.
+
+ The steps (roughly in reverse order of the way they appear in the code):
+ 1.) check to see if we already have a "query dispatcher"
+ - if so, use that with the given parameterization. Skip to step 4.
+ 2.) attempt to determine which branch to dispatch the query to:
+ - JS (optimized DOM iteration)
+ - xpath (for browsers that support it and where it's fast)
+ - native (not available in any browser yet)
+ 3.) tokenize and convert to executable "query dispatcher"
+ - this is where the lion's share of the complexity in the
+ system lies. In the DOM version, the query dispatcher is
+ assembled as a chain of "yes/no" test functions pertaining to
+ a section of a simple query statement (".blah:nth-child(odd)"
+ but not "div div", which is 2 simple statements). Individual
+ statement dispatchers are cached (to prevent re-definition)
+ as are entire dispatch chains (to make re-execution of the
+ same query fast)
+ - in the xpath path, tokenization yeilds a concatenation of
+ parameterized xpath selectors. As with the DOM version, both
+ simple selector blocks and overall evaluators are cached to
+ prevent re-defintion
+ 4.) the resulting query dispatcher is called in the passed scope (by default the top-level document)
+ - for DOM queries, this results in a recursive, top-down
+ evaluation of nodes based on each simple query section
+ - xpath queries can, thankfully, be executed in one shot
+ 5.) matched nodes are pruned to ensure they are unique
+*/
+
+;(function(){
+ // define everything in a closure for compressability reasons. "d" is an
+ // alias to "dojo" since it's so frequently used. This seems a
+ // transformation that the build system could perform on a per-file basis.
+
+ ////////////////////////////////////////////////////////////////////////
+ // Utility code
+ ////////////////////////////////////////////////////////////////////////
+
+ var d = dojo;
+ var childNodesName = dojo.isIE ? "children" : "childNodes";
+ var caseSensitive = false;
+
+ var getQueryParts = function(query){
+ // summary: state machine for query tokenization
+ if(">~+".indexOf(query.charAt(query.length-1)) >= 0){
+ query += " *"
+ }
+ query += " "; // ensure that we terminate the state machine
+
+ var ts = function(s, e){
+ return d.trim(query.slice(s, e));
+ }
+
+ // the overall data graph of the full query, as represented by queryPart objects
+ var qparts = [];
+ // state keeping vars
+ var inBrackets = -1;
+ var inParens = -1;
+ var inMatchFor = -1;
+ var inPseudo = -1;
+ var inClass = -1;
+ var inId = -1;
+ var inTag = -1;
+ var lc = ""; // the last character
+ var cc = ""; // the current character
+ var pStart;
+ // iteration vars
+ var x = 0; // index in the query
+ var ql = query.length;
+ var currentPart = null; // data structure representing the entire clause
+ var _cp = null; // the current pseudo or attr matcher
+
+ var endTag = function(){
+ if(inTag >= 0){
+ var tv = (inTag == x) ? null : ts(inTag, x).toLowerCase();
+ currentPart[ (">~+".indexOf(tv) < 0) ? "tag" : "oper" ] = tv;
+ inTag = -1;
+ }
+ }
+
+ var endId = function(){
+ if(inId >= 0){
+ currentPart.id = ts(inId, x).replace(/\\/g, "");
+ inId = -1;
+ }
+ }
+
+ var endClass = function(){
+ if(inClass >= 0){
+ currentPart.classes.push(ts(inClass+1, x).replace(/\\/g, ""));
+ inClass = -1;
+ }
+ }
+
+ var endAll = function(){
+ endId(); endTag(); endClass();
+ }
+
+ for(; lc=cc, cc=query.charAt(x),x= 0){
+ // look for a the close first
+ if(cc == "]"){
+ if(!_cp.attr){
+ _cp.attr = ts(inBrackets+1, x);
+ }else{
+ _cp.matchFor = ts((inMatchFor||inBrackets+1), x);
+ }
+ var cmf = _cp.matchFor;
+ if(cmf){
+ if( (cmf.charAt(0) == '"') || (cmf.charAt(0) == "'") ){
+ _cp.matchFor = cmf.substring(1, cmf.length-1);
+ }
+ }
+ currentPart.attrs.push(_cp);
+ _cp = null; // necessaray?
+ inBrackets = inMatchFor = -1;
+ }else if(cc == "="){
+ var addToCc = ("|~^$*".indexOf(lc) >=0 ) ? lc : "";
+ _cp.type = addToCc+cc;
+ _cp.attr = ts(inBrackets+1, x-addToCc.length);
+ inMatchFor = x+1;
+ }
+ // now look for other clause parts
+ }else if(inParens >= 0){
+ if(cc == ")"){
+ if(inPseudo >= 0){
+ _cp.value = ts(inParens+1, x);
+ }
+ inPseudo = inParens = -1;
+ }
+ }else if(cc == "#"){
+ endAll();
+ inId = x+1;
+ }else if(cc == "."){
+ endAll();
+ inClass = x;
+ }else if(cc == ":"){
+ endAll();
+ inPseudo = x;
+ }else if(cc == "["){
+ endAll();
+ inBrackets = x;
+ _cp = {
+ /*=====
+ attr: null, type: null, matchFor: null
+ =====*/
+ };
+ }else if(cc == "("){
+ if(inPseudo >= 0){
+ _cp = {
+ name: ts(inPseudo+1, x),
+ value: null
+ }
+ currentPart.pseudos.push(_cp);
+ }
+ inParens = x;
+ }else if(cc == " " && lc != cc){
+ // note that we expect the string to be " " terminated
+ endAll();
+ if(inPseudo >= 0){
+ currentPart.pseudos.push({ name: ts(inPseudo+1, x) });
+ }
+ currentPart.hasLoops = (
+ currentPart.pseudos.length ||
+ currentPart.attrs.length ||
+ currentPart.classes.length );
+ currentPart.query = ts(pStart, x);
+ currentPart.tag = (currentPart["oper"]) ? null : (currentPart.tag || "*");
+ qparts.push(currentPart);
+ currentPart = null;
+ }
+ }
+ return qparts;
+ };
+
+ ////////////////////////////////////////////////////////////////////////
+ // XPath query code
+ ////////////////////////////////////////////////////////////////////////
+
+ // this array is a lookup used to generate an attribute matching function.
+ // There is a similar lookup/generator list for the DOM branch with similar
+ // calling semantics.
+ var xPathAttrs = {
+ "*=": function(attr, value){
+ return "[contains(@"+attr+", '"+ value +"')]";
+ },
+ "^=": function(attr, value){
+ return "[starts-with(@"+attr+", '"+ value +"')]";
+ },
+ "$=": function(attr, value){
+ return "[substring(@"+attr+", string-length(@"+attr+")-"+(value.length-1)+")='"+value+"']";
+ },
+ "~=": function(attr, value){
+ return "[contains(concat(' ',@"+attr+",' '), ' "+ value +" ')]";
+ },
+ "|=": function(attr, value){
+ return "[contains(concat(' ',@"+attr+",' '), ' "+ value +"-')]";
+ },
+ "=": function(attr, value){
+ return "[@"+attr+"='"+ value +"']";
+ }
+ };
+
+ // takes a list of attribute searches, the overall query, a function to
+ // generate a default matcher, and a closure-bound method for providing a
+ // matching function that generates whatever type of yes/no distinguisher
+ // the query method needs. The method is a bit tortured and hard to read
+ // because it needs to be used in both the XPath and DOM branches.
+ var handleAttrs = function( attrList,
+ query,
+ getDefault,
+ handleMatch){
+ d.forEach(query.attrs, function(attr){
+ var matcher;
+ // type, attr, matchFor
+ if(attr.type && attrList[attr.type]){
+ matcher = attrList[attr.type](attr.attr, attr.matchFor);
+ }else if(attr.attr.length){
+ matcher = getDefault(attr.attr);
+ }
+ if(matcher){ handleMatch(matcher); }
+ });
+ }
+
+ var buildPath = function(query){
+ var xpath = ".";
+ var qparts = getQueryParts(d.trim(query));
+ while(qparts.length){
+ var tqp = qparts.shift();
+ var prefix;
+ var postfix = "";
+ if(tqp.oper == ">"){
+ prefix = "/";
+ // prefix = "/child::*";
+ tqp = qparts.shift();
+ }else if(tqp.oper == "~"){
+ prefix = "/following-sibling::"; // get element following siblings
+ tqp = qparts.shift();
+ }else if(tqp.oper == "+"){
+ // FIXME:
+ // fails when selecting subsequent siblings by node type
+ // because the position() checks the position in the list
+ // of matching elements and not the localized siblings
+ prefix = "/following-sibling::";
+ postfix = "[position()=1]";
+ tqp = qparts.shift();
+ }else{
+ prefix = "//";
+ // prefix = "/descendant::*"
+ }
+
+ // get the tag name (if any)
+
+ xpath += prefix + tqp.tag + postfix;
+
+ // check to see if it's got an id. Needs to come first in xpath.
+ if(tqp.id){
+ xpath += "[@id='"+tqp.id+"'][1]";
+ }
+
+ d.forEach(tqp.classes, function(cn){
+ var cnl = cn.length;
+ var padding = " ";
+ if(cn.charAt(cnl-1) == "*"){
+ padding = ""; cn = cn.substr(0, cnl-1);
+ }
+ xpath +=
+ "[contains(concat(' ',@class,' '), ' "+
+ cn + padding + "')]";
+ });
+
+ handleAttrs(xPathAttrs, tqp,
+ function(condition){
+ return "[@"+condition+"]";
+ },
+ function(matcher){
+ xpath += matcher;
+ }
+ );
+
+ // FIXME: need to implement pseudo-class checks!!
+ };
+ return xpath;
+ };
+
+ var _xpathFuncCache = {};
+ var getXPathFunc = function(path){
+ if(_xpathFuncCache[path]){
+ return _xpathFuncCache[path];
+ }
+
+ var doc = d.doc;
+ // don't need to memoize. The closure scope handles it for us.
+ var xpath = buildPath(path);
+
+ var tf = function(parent){
+ // XPath query strings are memoized.
+ var ret = [];
+ var xpathResult;
+ try{
+ xpathResult = doc.evaluate(xpath, parent, null,
+ // XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
+ XPathResult.ANY_TYPE, null);
+ }catch(e){
+ console.debug("failure in exprssion:", xpath, "under:", parent);
+ console.debug(e);
+ }
+ var result = xpathResult.iterateNext();
+ while(result){
+ ret.push(result);
+ result = xpathResult.iterateNext();
+ }
+ return ret;
+ }
+ return _xpathFuncCache[path] = tf;
+ };
+
+ /*
+ d.xPathMatch = function(query){
+ // XPath based DOM query system. Handles a small subset of CSS
+ // selectors, subset is identical to the non-XPath version of this
+ // function.
+
+ return getXPathFunc(query)();
+ }
+ */
+
+ ////////////////////////////////////////////////////////////////////////
+ // DOM query code
+ ////////////////////////////////////////////////////////////////////////
+
+ var _filtersCache = {};
+ var _simpleFiltersCache = {};
+
+ // the basic building block of the yes/no chaining system. agree(f1, f2)
+ // generates a new function which returns the boolean results of both of
+ // the passed functions to a single logical-anded result.
+ var agree = function(first, second){
+ if(!first){ return second; }
+ if(!second){ return first; }
+
+ return function(){
+ return first.apply(window, arguments) && second.apply(window, arguments);
+ }
+ }
+
+ var _childElements = function(root){
+ var ret = [];
+ var te, x=0, tret = root[childNodesName];
+ while(te=tret[x++]){
+ if(te.nodeType == 1){ ret.push(te); }
+ }
+ return ret;
+ }
+
+ var _nextSiblings = function(root, single){
+ var ret = [];
+ var te = root;
+ while(te = te.nextSibling){
+ if(te.nodeType == 1){
+ ret.push(te);
+ if(single){ break; }
+ }
+ }
+ return ret;
+ }
+
+ var _filterDown = function(element, queryParts, matchArr, idx){
+ // NOTE:
+ // in the fast path! this function is called recursively and for
+ // every run of a query.
+ var nidx = idx+1;
+ var isFinal = (queryParts.length == nidx);
+ var tqp = queryParts[idx];
+
+ // see if we can constrain our next level to direct children
+ if(tqp.oper){
+ var ecn = (tqp.oper == ">") ?
+ _childElements(element) :
+ _nextSiblings(element, (tqp.oper == "+"));
+
+ if(!ecn || !ecn.length){
+ return;
+ }
+ nidx++;
+ isFinal = (queryParts.length == nidx);
+ // kinda janky, too much array alloc
+ var tf = getFilterFunc(queryParts[idx+1]);
+ // for(var x=ecn.length-1, te; x>=0, te=ecn[x]; x--){
+ for(var x=0, ecnl=ecn.length, te; x=0, te=elements[x]; x--){
+ var x = elements.length - 1, te;
+ while(te = elements[x--]){
+ _filterDown(te, queryParts, ret, 0);
+ }
+ return ret;
+ }
+
+ var getFilterFunc = function(q){
+ // note: query can't have spaces!
+ if(_filtersCache[q.query]){
+ return _filtersCache[q.query];
+ }
+ var ff = null;
+
+ // does it have a tagName component?
+ if(q.tag){
+ if(q.tag == "*"){
+ ff = agree(ff,
+ function(elem){
+ return (elem.nodeType == 1);
+ }
+ );
+ }else{
+ // tag name match
+ ff = agree(ff,
+ function(elem){
+ return (
+ (elem.nodeType == 1) &&
+ (q.tag == elem.tagName.toLowerCase())
+ );
+ // return isTn;
+ }
+ );
+ }
+ }
+
+ // does the node have an ID?
+ if(q.id){
+ ff = agree(ff,
+ function(elem){
+ return (
+ (elem.nodeType == 1) &&
+ (elem.id == q.id)
+ );
+ }
+ );
+ }
+
+ if(q.hasLoops){
+ // if we have other query param parts, make sure we add them to the
+ // filter chain
+ ff = agree(ff, getSimpleFilterFunc(q));
+ }
+
+ return _filtersCache[q.query] = ff;
+ }
+
+ var getNodeIndex = function(node){
+ // NOTE:
+ // we could have a more accurate caching mechanism by invalidating
+ // caches after the query has finished, but I think that'd lead to
+ // significantly more cache churn than the cache would provide
+ // value for in the common case. Generally, we're more
+ // conservative (and therefore, more accurate) than jQuery and
+ // DomQuery WRT node node indexes, but there may be corner cases
+ // in which we fall down. How much we care about them is TBD.
+
+ var pn = node.parentNode;
+ var pnc = pn.childNodes;
+
+ // check to see if we can trust the cache. If not, re-key the whole
+ // thing and return our node match from that.
+
+ var nidx = -1;
+ var child = pn.firstChild;
+ if(!child){
+ return nidx;
+ }
+
+ var ci = node["__cachedIndex"];
+ var cl = pn["__cachedLength"];
+
+ // only handle cache building if we've gone out of sync
+ if(((typeof cl == "number")&&(cl != pnc.length))||(typeof ci != "number")){
+ // rip though the whole set, building cache indexes as we go
+ pn["__cachedLength"] = pnc.length;
+ var idx = 1;
+ do{
+ // we only assign indexes for nodes with nodeType == 1, as per:
+ // http://www.w3.org/TR/css3-selectors/#nth-child-pseudo
+ // only elements are counted in the search order, and they
+ // begin at 1 for the first child's index
+
+ if(child === node){
+ nidx = idx;
+ }
+ if(child.nodeType == 1){
+ child["__cachedIndex"] = idx;
+ idx++;
+ }
+ child = child.nextSibling;
+ }while(child);
+ }else{
+ // NOTE:
+ // could be incorrect in some cases (node swaps involving the
+ // passed node, etc.), but we ignore those due to the relative
+ // unlikelihood of that occuring
+ nidx = ci;
+ }
+ return nidx;
+ }
+
+ var firedCount = 0;
+
+ var blank = "";
+ var _getAttr = function(elem, attr){
+ if(attr == "class"){
+ return elem.className || blank;
+ }
+ if(attr == "for"){
+ return elem.htmlFor || blank;
+ }
+ return elem.getAttribute(attr, 2) || blank;
+ }
+
+ var attrs = {
+ "*=": function(attr, value){
+ return function(elem){
+ // E[foo*="bar"]
+ // an E element whose "foo" attribute value contains
+ // the substring "bar"
+ return (_getAttr(elem, attr).indexOf(value)>=0);
+ }
+ },
+ "^=": function(attr, value){
+ // E[foo^="bar"]
+ // an E element whose "foo" attribute value begins exactly
+ // with the string "bar"
+ return function(elem){
+ return (_getAttr(elem, attr).indexOf(value)==0);
+ }
+ },
+ "$=": function(attr, value){
+ // E[foo$="bar"]
+ // an E element whose "foo" attribute value ends exactly
+ // with the string "bar"
+ var tval = " "+value;
+ return function(elem){
+ var ea = " "+_getAttr(elem, attr);
+ return (ea.lastIndexOf(value)==(ea.length-value.length));
+ }
+ },
+ "~=": function(attr, value){
+ // E[foo~="bar"]
+ // an E element whose "foo" attribute value is a list of
+ // space-separated values, one of which is exactly equal
+ // to "bar"
+
+ // return "[contains(concat(' ',@"+attr+",' '), ' "+ value +" ')]";
+ var tval = " "+value+" ";
+ return function(elem){
+ var ea = " "+_getAttr(elem, attr)+" ";
+ return (ea.indexOf(tval)>=0);
+ }
+ },
+ "|=": function(attr, value){
+ // E[hreflang|="en"]
+ // an E element whose "hreflang" attribute has a
+ // hyphen-separated list of values beginning (from the
+ // left) with "en"
+ var valueDash = " "+value+"-";
+ return function(elem){
+ var ea = " "+(elem.getAttribute(attr, 2) || "");
+ return (
+ (ea == value) ||
+ (ea.indexOf(valueDash)==0)
+ );
+ }
+ },
+ "=": function(attr, value){
+ return function(elem){
+ return (_getAttr(elem, attr) == value);
+ }
+ }
+ };
+
+ var pseudos = {
+ "first-child": function(name, condition){
+ return function(elem){
+ if(elem.nodeType != 1){ return false; }
+ // check to see if any of the previous siblings are elements
+ var fc = elem.previousSibling;
+ while(fc && (fc.nodeType != 1)){
+ fc = fc.previousSibling;
+ }
+ return (!fc);
+ }
+ },
+ "last-child": function(name, condition){
+ return function(elem){
+ if(elem.nodeType != 1){ return false; }
+ // check to see if any of the next siblings are elements
+ var nc = elem.nextSibling;
+ while(nc && (nc.nodeType != 1)){
+ nc = nc.nextSibling;
+ }
+ return (!nc);
+ }
+ },
+ "empty": function(name, condition){
+ return function(elem){
+ // DomQuery and jQuery get this wrong, oddly enough.
+ // The CSS 3 selectors spec is pretty explicit about
+ // it, too.
+ var cn = elem.childNodes;
+ var cnl = elem.childNodes.length;
+ // if(!cnl){ return true; }
+ for(var x=cnl-1; x >= 0; x--){
+ var nt = cn[x].nodeType;
+ if((nt == 1)||(nt == 3)){ return false; }
+ }
+ return true;
+ }
+ },
+ "contains": function(name, condition){
+ return function(elem){
+ // FIXME: I dislike this version of "contains", as
+ // whimsical attribute could set it off. An inner-text
+ // based version might be more accurate, but since
+ // jQuery and DomQuery also potentially get this wrong,
+ // I'm leaving it for now.
+ return (elem.innerHTML.indexOf(condition) >= 0);
+ }
+ },
+ "not": function(name, condition){
+ var ntf = getFilterFunc(getQueryParts(condition)[0]);
+ return function(elem){
+ return (!ntf(elem));
+ }
+ },
+ "nth-child": function(name, condition){
+ var pi = parseInt;
+ if(condition == "odd"){
+ return function(elem){
+ return (
+ ((getNodeIndex(elem)) % 2) == 1
+ );
+ }
+ }else if((condition == "2n")||
+ (condition == "even")){
+ return function(elem){
+ return ((getNodeIndex(elem) % 2) == 0);
+ }
+ }else if(condition.indexOf("0n+") == 0){
+ var ncount = pi(condition.substr(3));
+ return function(elem){
+ return (elem.parentNode[childNodesName][ncount-1] === elem);
+ }
+ }else if( (condition.indexOf("n+") > 0) &&
+ (condition.length > 3) ){
+ var tparts = condition.split("n+", 2);
+ var pred = pi(tparts[0]);
+ var idx = pi(tparts[1]);
+ return function(elem){
+ return ((getNodeIndex(elem) % pred) == idx);
+ }
+ }else if(condition.indexOf("n") == -1){
+ var ncount = pi(condition);
+ return function(elem){
+ return (getNodeIndex(elem) == ncount);
+ }
+ }
+ }
+ };
+
+ var defaultGetter = (d.isIE) ? function(cond){
+ var clc = cond.toLowerCase();
+ return function(elem){
+ return elem[cond]||elem[clc];
+ }
+ } : function(cond){
+ return function(elem){
+ return (elem && elem.getAttribute && elem.hasAttribute(cond));
+ }
+ };
+
+ var getSimpleFilterFunc = function(query){
+
+ var fcHit = (_simpleFiltersCache[query.query]||_filtersCache[query.query]);
+ if(fcHit){ return fcHit; }
+
+ var ff = null;
+
+ // the only case where we'll need the tag name is if we came from an ID query
+ if(query.id){ // do we have an ID component?
+ if(query.tag != "*"){
+ ff = agree(ff, function(elem){
+ return (elem.tagName.toLowerCase() == query.tag);
+ });
+ }
+ }
+
+ // if there's a class in our query, generate a match function for it
+ d.forEach(query.classes, function(cname, idx, arr){
+ // get the class name
+ var isWildcard = cname.charAt(cname.length-1) == "*";
+ if(isWildcard){
+ cname = cname.substr(0, cname.length-1);
+ }
+ // I dislike the regex thing, even if memozied in a cache, but it's VERY short
+ var re = new RegExp("(?:^|\\s)" + cname + (isWildcard ? ".*" : "") + "(?:\\s|$)");
+ ff = agree(ff, function(elem){
+ return re.test(elem.className);
+ });
+ ff.count = idx;
+ });
+
+ d.forEach(query.pseudos, function(pseudo){
+ if(pseudos[pseudo.name]){
+ ff = agree(ff, pseudos[pseudo.name](pseudo.name, pseudo.value));
+ }
+ });
+
+ handleAttrs(attrs, query, defaultGetter,
+ function(tmatcher){ ff = agree(ff, tmatcher); }
+ );
+ if(!ff){
+ ff = function(){ return true; };
+ }
+ return _simpleFiltersCache[query.query] = ff;
+ }
+
+ var _getElementsFuncCache = { };
+
+ var getElementsFunc = function(query, root){
+ var fHit = _getElementsFuncCache[query.query];
+ if(fHit){ return fHit; }
+
+ // NOTE: this function is in the fast path! not memoized!!!
+
+ // the query doesn't contain any spaces, so there's only so many
+ // things it could be
+
+ if(query.id && !query.hasLoops && !query.tag){
+ // ID-only query. Easy.
+ return _getElementsFuncCache[query.query] = function(root){
+ // FIXME: if root != document, check for parenting!
+ return [ d.byId(query.id) ];
+ }
+ }
+
+ var filterFunc = getSimpleFilterFunc(query);
+
+ var retFunc;
+ if(query.tag && query.id && !query.hasLoops){
+ // we got a filtered ID search (e.g., "h4#thinger")
+ retFunc = function(root){
+ var te = d.byId(query.id);
+ if(filterFunc(te)){
+ return [ te ];
+ }
+ }
+ }else{
+ var tret;
+
+ if(!query.hasLoops){
+ // it's just a plain-ol elements-by-tag-name query from the root
+ retFunc = function(root){
+ var ret = [];
+ var te, x=0, tret = root.getElementsByTagName(query.tag);
+ while(te=tret[x++]){
+ ret.push(te);
+ }
+ return ret;
+ }
+ }else{
+ retFunc = function(root){
+ var ret = [];
+ var te, x=0, tret = root.getElementsByTagName(query.tag);
+ while(te=tret[x++]){
+ if(filterFunc(te)){
+ ret.push(te);
+ }
+ }
+ return ret;
+ }
+ }
+ }
+ return _getElementsFuncCache[query.query] = retFunc;
+ }
+
+ var _partsCache = {};
+
+ ////////////////////////////////////////////////////////////////////////
+ // the query runner
+ ////////////////////////////////////////////////////////////////////////
+
+ // this is the second level of spliting, from full-length queries (e.g.,
+ // "div.foo .bar") into simple query expressions (e.g., ["div.foo",
+ // ".bar"])
+ var _queryFuncCache = {
+ "*": d.isIE ?
+ function(root){
+ return root.all;
+ } :
+ function(root){
+ return root.getElementsByTagName("*");
+ },
+ "~": _nextSiblings,
+ "+": function(root){ return _nextSiblings(root, true); },
+ ">": _childElements
+ };
+
+ var getStepQueryFunc = function(query){
+ // if it's trivial, get a fast-path dispatcher
+ var qparts = getQueryParts(d.trim(query));
+ // if(query[query.length-1] == ">"){ query += " *"; }
+ if(qparts.length == 1){
+ var tt = getElementsFunc(qparts[0]);
+ tt.nozip = true;
+ return tt;
+ }
+
+ // otherwise, break it up and return a runner that iterates over the parts recursively
+ var sqf = function(root){
+ var localQueryParts = qparts.slice(0); // clone the src arr
+ var candidates;
+ if(localQueryParts[0].oper == ">"){ // FIXME: what if it's + or ~?
+ candidates = [ root ];
+ // root = document;
+ }else{
+ candidates = getElementsFunc(localQueryParts.shift())(root);
+ }
+ return filterDown(candidates, localQueryParts);
+ }
+ return sqf;
+ }
+
+ // a specialized method that implements our primoridal "query optimizer".
+ // This allows us to dispatch queries to the fastest subsystem we can get.
+ var _getQueryFunc = (
+ // NOTE:
+ // XPath on the Webkit nighlies is slower than it's DOM iteration
+ // for most test cases
+ // FIXME:
+ // we should try to capture some runtime speed data for each query
+ // function to determine on the fly if we should stick w/ the
+ // potentially optimized variant or if we should try something
+ // new.
+ (document["evaluate"] && !d.isSafari) ?
+ function(query){
+ // has xpath support that's faster than DOM
+ var qparts = query.split(" ");
+ // can we handle it?
+ if( (document["evaluate"])&&
+ (query.indexOf(":") == -1)&&
+ (query.indexOf("+") == -1) // skip direct sibling matches. See line ~344
+ ){
+ // dojo.debug(query);
+ // should we handle it?
+
+ // kind of a lame heuristic, but it works
+ if(
+ // a "div div div" style query
+ ((qparts.length > 2)&&(query.indexOf(">") == -1))||
+ // or something else with moderate complexity. kinda janky
+ (qparts.length > 3)||
+ (query.indexOf("[")>=0)||
+ // or if it's a ".thinger" query
+ ((1 == qparts.length)&&(0 <= query.indexOf(".")))
+
+ ){
+ // use get and cache a xpath runner for this selector
+ return getXPathFunc(query);
+ }
+ }
+
+ // fallthrough
+ return getStepQueryFunc(query);
+ } : getStepQueryFunc
+ );
+ // uncomment to disable XPath for testing and tuning the DOM path
+ // _getQueryFunc = getStepQueryFunc;
+
+ // FIXME: we've got problems w/ the NodeList query()/filter() functions if we go XPath for everything
+
+ // uncomment to disable DOM queries for testing and tuning XPath
+ // _getQueryFunc = getXPathFunc;
+
+ // this is the primary caching for full-query results. The query dispatcher
+ // functions are generated here and then pickled for hash lookup in the
+ // future
+ var getQueryFunc = function(query){
+ // return a cached version if one is available
+ if(_queryFuncCache[query]){ return _queryFuncCache[query]; }
+ if(0 > query.indexOf(",")){
+ // if it's not a compound query (e.g., ".foo, .bar"), cache and return a dispatcher
+ return _queryFuncCache[query] = _getQueryFunc(query);
+ }else{
+ // if it's a complex query, break it up into it's constituent parts
+ // and return a dispatcher that will merge the parts when run
+
+ // var parts = query.split(", ");
+ var parts = query.split(/\s*,\s*/);
+ var tf = function(root){
+ var pindex = 0; // avoid array alloc for every invocation
+ var ret = [];
+ var tp;
+ while(tp = parts[pindex++]){
+ ret = ret.concat(_getQueryFunc(tp, tp.indexOf(" "))(root));
+ }
+ return ret;
+ }
+ // ...cache and return
+ return _queryFuncCache[query] = tf;
+ }
+ }
+
+ // FIXME:
+ // Dean's Base2 uses a system whereby queries themselves note if
+ // they'll need duplicate filtering. We need to get on that plan!!
+
+ // attempt to efficiently determine if an item in a list is a dupe,
+ // returning a list of "uniques", hopefully in doucment order
+ var _zipIdx = 0;
+ var _zip = function(arr){
+ if(arr && arr.nozip){ return d.NodeList._wrap(arr); }
+ var ret = new d.NodeList();
+ if(!arr){ return ret; }
+ if(arr[0]){
+ ret.push(arr[0]);
+ }
+ if(arr.length < 2){ return ret; }
+ _zipIdx++;
+ arr[0]["_zipIdx"] = _zipIdx;
+ for(var x=1, te; te = arr[x]; x++){
+ if(arr[x]["_zipIdx"] != _zipIdx){
+ ret.push(te);
+ }
+ te["_zipIdx"] = _zipIdx;
+ }
+ // FIXME: should we consider stripping these properties?
+ return ret;
+ }
+
+ // the main executor
+ d.query = function(/*String*/ query, /*String|DOMNode?*/ root){
+ // summary:
+ // Returns nodes which match the given CSS3 selector, searching the
+ // entire document by default but optionally taking a node to scope
+ // the search by. Returns an instance of dojo.NodeList.
+ // description:
+ // dojo.query() is the swiss army knife of DOM node manipulation in
+ // Dojo. Much like Prototype's "$$" (bling-bling) function or JQuery's
+ // "$" function, dojo.query provides robust, high-performance
+ // CSS-based node selector support with the option of scoping searches
+ // to a particular sub-tree of a document.
+ //
+ // Supported Selectors:
+ // --------------------
+ //
+ // dojo.query() supports a rich set of CSS3 selectors, including:
+ //
+ // * class selectors (e.g., `.foo`)
+ // * node type selectors like `span`
+ // * ` ` descendant selectors
+ // * `>` child element selectors
+ // * `#foo` style ID selectors
+ // * `*` universal selector
+ // * `~`, the immediately preceeded-by sibling selector
+ // * `+`, the preceeded-by sibling selector
+ // * attribute queries:
+ // | * `[foo]` attribute presence selector
+ // | * `[foo='bar']` attribute value exact match
+ // | * `[foo~='bar']` attribute value list item match
+ // | * `[foo^='bar']` attribute start match
+ // | * `[foo$='bar']` attribute end match
+ // | * `[foo*='bar']` attribute substring match
+ // * `:first-child`, `:last-child` positional selectors
+ // * `:empty` content emtpy selector
+ // * `:empty` content emtpy selector
+ // * `:nth-child(n)`, `:nth-child(2n+1)` style positional calculations
+ // * `:nth-child(even)`, `:nth-child(odd)` positional selectors
+ // * `:not(...)` negation pseudo selectors
+ //
+ // Any legal combination of these selectors will work with
+ // `dojo.query()`, including compound selectors ("," delimited).
+ // Very complex and useful searches can be constructed with this
+ // palette of selectors and when combined with functions for
+ // maniplation presented by dojo.NodeList, many types of DOM
+ // manipulation operations become very straightforward.
+ //
+ // Unsupported Selectors:
+ // ----------------------
+ //
+ // While dojo.query handles many CSS3 selectors, some fall outside of
+ // what's resaonable for a programmatic node querying engine to
+ // handle. Currently unsupported selectors include:
+ //
+ // * namespace-differentiated selectors of any form
+ // * all `::` pseduo-element selectors
+ // * certain pseduo-selectors which don't get a lot of day-to-day use:
+ // | * `:root`, `:lang()`, `:target`, `:focus`
+ // * all visual and state selectors:
+ // | * `:root`, `:active`, `:hover`, `:visisted`, `:link`,
+ // `:enabled`, `:disabled`, `:checked`
+ // * `:*-of-type` pseudo selectors
+ //
+ // dojo.query and XML Documents:
+ // -----------------------------
+ //
+ // `dojo.query` currently only supports searching XML documents
+ // whose tags and attributes are 100% lower-case. This is a known
+ // limitation and will [be addressed soon](http://trac.dojotoolkit.org/ticket/3866)
+ // Non-selector Queries:
+ // ---------------------
+ //
+ // If something other than a String is passed for the query,
+ // `dojo.query` will return a new `dojo.NodeList` constructed from
+ // that parameter alone and all further processing will stop. This
+ // means that if you have a reference to a node or NodeList, you
+ // can quickly construct a new NodeList from the original by
+ // calling `dojo.query(node)` or `dojo.query(list)`.
+ //
+ // query:
+ // The CSS3 expression to match against. For details on the syntax of
+ // CSS3 selectors, see
+ // root:
+ // A DOMNode (or node id) to scope the search from. Optional.
+ // returns: dojo.NodeList
+ // An instance of `dojo.NodeList`. Many methods are available on
+ // NodeLists for searching, iterating, manipulating, and handling
+ // events on the matched nodes in the returned list.
+ // example:
+ // search the entire document for elements with the class "foo":
+ // | dojo.query(".foo");
+ // these elements will match:
+ // |
+ // |
+ // |
+ // example:
+ // search the entire document for elements with the classes "foo" *and* "bar":
+ // | dojo.query(".foo.bar");
+ // these elements will match:
+ // |
+ // while these will not:
+ // |
+ // |
+ // example:
+ // find `` elements which are descendants of paragraphs and
+ // which have a "highlighted" class:
+ // | dojo.query("p span.highlighted");
+ // the innermost span in this fragment matches:
+ // |
+ // | ...
+ // | ...
+ // |
+ // |
+ // example:
+ // set an "odd" class on all odd table rows inside of the table
+ // `#tabular_data`, using the `>` (direct child) selector to avoid
+ // affecting any nested tables:
+ // | dojo.query("#tabular_data > tbody > tr:nth-child(odd)").addClass("odd");
+ // example:
+ // remove all elements with the class "error" from the document
+ // and store them in a list:
+ // | var errors = dojo.query(".error").orphan();
+ // example:
+ // add an onclick handler to every submit button in the document
+ // which causes the form to be sent via Ajax instead:
+ // | dojo.query("input[type='submit']").onclick(function(e){
+ // | dojo.stopEvent(e); // prevent sending the form
+ // | var btn = e.target;
+ // | dojo.xhrPost({
+ // | form: btn.form,
+ // | load: function(data){
+ // | // replace the form with the response
+ // | var div = dojo.doc.createElement("div");
+ // | dojo.place(div, btn.form, "after");
+ // | div.innerHTML = data;
+ // | dojo.style(btn.form, "display", "none");
+ // | }
+ // | });
+ // | });
+
+ // NOTE: elementsById is not currently supported
+ // NOTE: ignores xpath-ish queries for now
+
+ if(query.constructor == d.NodeList){
+ return query;
+ }
+ if(!d.isString(query)){
+ return new d.NodeList(query); // dojo.NodeList
+ }
+ if(d.isString(root)){
+ root = d.byId(root);
+ }
+
+ return _zip(getQueryFunc(query)(root||d.doc)); // dojo.NodeList
+ }
+
+ /*
+ // exposing this was a mistake
+ d.query.attrs = attrs;
+ */
+ // exposing this because new pseudo matches are only executed through the
+ // DOM query path (never through the xpath optimizing branch)
+ d.query.pseudos = pseudos;
+
+ // one-off function for filtering a NodeList based on a simple selector
+ d._filterQueryResult = function(nodeList, simpleFilter){
+ var tnl = new d.NodeList();
+ var ff = (simpleFilter) ? getFilterFunc(getQueryParts(simpleFilter)[0]) : function(){ return true; };
+ for(var x=0, te; te = nodeList[x]; x++){
+ if(ff(te)){ tnl.push(te); }
+ }
+ return tnl;
+ }
+})();
+
+}
+
+if(!dojo._hasResource["dojo._base.xhr"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.xhr"] = true;
+dojo.provide("dojo._base.xhr");
+
+(function(){
+ var _d = dojo;
+ function setValue(/*Object*/obj, /*String*/name, /*String*/value){
+ //summary:
+ // For the nameed property in object, set the value. If a value
+ // already exists and it is a string, convert the value to be an
+ // array of values.
+ var val = obj[name];
+ if(_d.isString(val)){
+ obj[name] = [val, value];
+ }else if(_d.isArray(val)){
+ val.push(value);
+ }else{
+ obj[name] = value;
+ }
+ }
+
+ dojo.formToObject = function(/*DOMNode||String*/ formNode){
+ // summary:
+ // dojo.formToObject returns the values encoded in an HTML form as
+ // string properties in an object which it then returns. Disabled form
+ // elements, buttons, and other non-value form elements are skipped.
+ // Multi-select elements are returned as an array of string values.
+ // description:
+ // This form:
+ //
+ // |
+ //
+ // yields this object structure as the result of a call to
+ // formToObject():
+ //
+ // | {
+ // | blah: "blah",
+ // | multi: [
+ // | "thud",
+ // | "thonk"
+ // | ]
+ // | };
+
+ var ret = {};
+ var iq = "input:not([type=file]):not([type=submit]):not([type=image]):not([type=reset]):not([type=button]), select, textarea";
+ _d.query(iq, formNode).filter(function(node){
+ return !node.disabled && node.name;
+ }).forEach(function(item){
+ var _in = item.name;
+ var type = (item.type||"").toLowerCase();
+ if(type == "radio" || type == "checkbox"){
+ if(item.checked){ setValue(ret, _in, item.value); }
+ }else if(item.multiple){
+ ret[_in] = [];
+ _d.query("option", item).forEach(function(opt){
+ if(opt.selected){
+ setValue(ret, _in, opt.value);
+ }
+ });
+ }else{
+ setValue(ret, _in, item.value);
+ if(type == "image"){
+ ret[_in+".x"] = ret[_in+".y"] = ret[_in].x = ret[_in].y = 0;
+ }
+ }
+ });
+ return ret; // Object
+ }
+
+ dojo.objectToQuery = function(/*Object*/ map){
+ // summary:
+ // takes a name/value mapping object and returns a string representing
+ // a URL-encoded version of that object.
+ // example:
+ // this object:
+ //
+ // | {
+ // | blah: "blah",
+ // | multi: [
+ // | "thud",
+ // | "thonk"
+ // | ]
+ // | };
+ //
+ // yields the following query string:
+ //
+ // | "blah=blah&multi=thud&multi=thonk"
+
+ // FIXME: need to implement encodeAscii!!
+ var enc = encodeURIComponent;
+ var pairs = [];
+ var backstop = {};
+ for(var name in map){
+ var value = map[name];
+ if(value != backstop[name]){
+ var assign = enc(name) + "=";
+ if(_d.isArray(value)){
+ for(var i=0; i < value.length; i++){
+ pairs.push(assign + enc(value[i]));
+ }
+ }else{
+ pairs.push(assign + enc(value));
+ }
+ }
+ }
+ return pairs.join("&"); // String
+ }
+
+ dojo.formToQuery = function(/*DOMNode||String*/ formNode){
+ // summary:
+ // Returns a URL-encoded string representing the form passed as either a
+ // node or string ID identifying the form to serialize
+ return _d.objectToQuery(_d.formToObject(formNode)); // String
+ }
+
+ dojo.formToJson = function(/*DOMNode||String*/ formNode, /*Boolean?*/prettyPrint){
+ // summary:
+ // return a serialized JSON string from a form node or string
+ // ID identifying the form to serialize
+ return _d.toJson(_d.formToObject(formNode), prettyPrint); // String
+ }
+
+ dojo.queryToObject = function(/*String*/ str){
+ // summary:
+ // returns an object representing a de-serialized query section of a
+ // URL. Query keys with multiple values are returned in an array.
+ // description:
+ // This string:
+ //
+ // | "foo=bar&foo=baz&thinger=%20spaces%20=blah&zonk=blarg&"
+ //
+ // results in this object structure:
+ //
+ // | {
+ // | foo: [ "bar", "baz" ],
+ // | thinger: " spaces =blah",
+ // | zonk: "blarg"
+ // | }
+ //
+ // Note that spaces and other urlencoded entities are correctly
+ // handled.
+
+ // FIXME: should we grab the URL string if we're not passed one?
+ var ret = {};
+ var qp = str.split("&");
+ var dec = decodeURIComponent;
+ _d.forEach(qp, function(item){
+ if(item.length){
+ var parts = item.split("=");
+ var name = dec(parts.shift());
+ var val = dec(parts.join("="));
+ if(_d.isString(ret[name])){
+ ret[name] = [ret[name]];
+ }
+ if(_d.isArray(ret[name])){
+ ret[name].push(val);
+ }else{
+ ret[name] = val;
+ }
+ }
+ });
+ return ret; // Object
+ }
+
+ /*
+ from refactor.txt:
+
+ all bind() replacement APIs take the following argument structure:
+
+ {
+ url: "blah.html",
+
+ // all below are optional, but must be supported in some form by
+ // every IO API
+ timeout: 1000, // milliseconds
+ handleAs: "text", // replaces the always-wrong "mimetype"
+ content: {
+ key: "value"
+ },
+
+ // browser-specific, MAY be unsupported
+ sync: true, // defaults to false
+ form: dojo.byId("someForm")
+ }
+ */
+
+ // need to block async callbacks from snatching this thread as the result
+ // of an async callback might call another sync XHR, this hangs khtml forever
+ // must checked by watchInFlight()
+
+ dojo._blockAsync = false;
+
+ dojo._contentHandlers = {
+ "text": function(xhr){ return xhr.responseText; },
+ "json": function(xhr){
+ if(!dojo.config.usePlainJson){
+ console.warn("Consider using mimetype:text/json-comment-filtered"
+ + " to avoid potential security issues with JSON endpoints"
+ + " (use djConfig.usePlainJson=true to turn off this message)");
+ }
+ return (xhr.status == 204) ? undefined : _d.fromJson(xhr.responseText);
+ },
+ "json-comment-filtered": function(xhr){
+ // NOTE: we provide the json-comment-filtered option as one solution to
+ // the "JavaScript Hijacking" issue noted by Fortify and others. It is
+ // not appropriate for all circumstances.
+
+ var value = xhr.responseText;
+ var cStartIdx = value.indexOf("\/*");
+ var cEndIdx = value.lastIndexOf("*\/");
+ if(cStartIdx == -1 || cEndIdx == -1){
+ throw new Error("JSON was not comment filtered");
+ }
+ return (xhr.status == 204) ? undefined :
+ _d.fromJson(value.substring(cStartIdx+2, cEndIdx));
+ },
+ "javascript": function(xhr){
+ // FIXME: try Moz and IE specific eval variants?
+ return _d.eval(xhr.responseText);
+ },
+ "xml": function(xhr){
+ var result = xhr.responseXML;
+ if(_d.isIE && (!result || window.location.protocol == "file:")){
+ _d.forEach(["MSXML2", "Microsoft", "MSXML", "MSXML3"], function(prefix){
+ try{
+ var dom = new ActiveXObject(prefix + ".XMLDOM");
+ dom.async = false;
+ dom.loadXML(xhr.responseText);
+ result = dom;
+ }catch(e){ /* Not available. Squelch and try next one. */ }
+ });
+ }
+ return result; // DOMDocument
+ }
+ };
+
+ dojo._contentHandlers["json-comment-optional"] = function(xhr){
+ var handlers = _d._contentHandlers;
+ try{
+ return handlers["json-comment-filtered"](xhr);
+ }catch(e){
+ return handlers["json"](xhr);
+ }
+ };
+
+ /*=====
+ dojo.__IoArgs = function(){
+ // url: String
+ // URL to server endpoint.
+ // content: Object?
+ // Contains properties with string values. These
+ // properties will be serialized as name1=value2 and
+ // passed in the request.
+ // timeout: Integer?
+ // Milliseconds to wait for the response. If this time
+ // passes, the then error callbacks are called.
+ // form: DOMNode?
+ // DOM node for a form. Used to extract the form values
+ // and send to the server.
+ // preventCache: Boolean?
+ // Default is false. If true, then a
+ // "dojo.preventCache" parameter is sent in the request
+ // with a value that changes with each request
+ // (timestamp). Useful only with GET-type requests.
+ // handleAs: String?
+ // Acceptable values depend on the type of IO
+ // transport (see specific IO calls for more information).
+ // load: Function?
+ // function(response, ioArgs){}. response is an Object, ioArgs
+ // is of type dojo.__IoCallbackArgs. The load function will be
+ // called on a successful response.
+ // error: Function?
+ // function(response, ioArgs){}. response is an Object, ioArgs
+ // is of type dojo.__IoCallbackArgs. The error function will
+ // be called in an error case.
+ // handle: Function?
+ // function(response, ioArgs){}. response is an Object, ioArgs
+ // is of type dojo.__IoCallbackArgs. The handle function will
+ // be called in either the successful or error case. For
+ // the load, error and handle functions, the ioArgs object
+ // will contain the following properties:
+ this.url = url;
+ this.content = content;
+ this.timeout = timeout;
+ this.form = form;
+ this.preventCache = preventCache;
+ this.handleAs = handleAs;
+ this.load = load;
+ this.error = error;
+ this.handle = handle;
+ }
+ =====*/
+
+ /*=====
+ dojo.__IoCallbackArgs = function(args, xhr, url, query, handleAs, id, canDelete, json){
+ // args: Object
+ // the original object argument to the IO call.
+ // xhr: XMLHttpRequest
+ // For XMLHttpRequest calls only, the
+ // XMLHttpRequest object that was used for the
+ // request.
+ // url: String
+ // The final URL used for the call. Many times it
+ // will be different than the original args.url
+ // value.
+ // query: String
+ // For non-GET requests, the
+ // name1=value1&name2=value2 parameters sent up in
+ // the request.
+ // handleAs: String
+ // The final indicator on how the response will be
+ // handled.
+ // id: String
+ // For dojo.io.script calls only, the internal
+ // script ID used for the request.
+ // canDelete: Boolean
+ // For dojo.io.script calls only, indicates
+ // whether the script tag that represents the
+ // request can be deleted after callbacks have
+ // been called. Used internally to know when
+ // cleanup can happen on JSONP-type requests.
+ // json: Object
+ // For dojo.io.script calls only: holds the JSON
+ // response for JSONP-type requests. Used
+ // internally to hold on to the JSON responses.
+ // You should not need to access it directly --
+ // the same object should be passed to the success
+ // callbacks directly.
+ this.args = args;
+ this.xhr = xhr;
+ this.url = url;
+ this.query = query;
+ this.handleAs = handleAs;
+ this.id = id;
+ this.canDelete = canDelete;
+ this.json = json;
+ }
+ =====*/
+
+ dojo._ioSetArgs = function(/*dojo.__IoArgs*/args,
+ /*Function*/canceller,
+ /*Function*/okHandler,
+ /*Function*/errHandler){
+ // summary:
+ // sets up the Deferred and ioArgs property on the Deferred so it
+ // can be used in an io call.
+ // args:
+ // The args object passed into the public io call. Recognized properties on
+ // the args object are:
+ // canceller:
+ // The canceller function used for the Deferred object. The function
+ // will receive one argument, the Deferred object that is related to the
+ // canceller.
+ // okHandler:
+ // The first OK callback to be registered with Deferred. It has the opportunity
+ // to transform the OK response. It will receive one argument -- the Deferred
+ // object returned from this function.
+ // errHandler:
+ // The first error callback to be registered with Deferred. It has the opportunity
+ // to do cleanup on an error. It will receive two arguments: error (the
+ // Error object) and dfd, the Deferred object returned from this function.
+
+ var ioArgs = {args: args, url: args.url};
+
+ //Get values from form if requestd.
+ var formObject = null;
+ if(args.form){
+ var form = _d.byId(args.form);
+ //IE requires going through getAttributeNode instead of just getAttribute in some form cases,
+ //so use it for all. See #2844
+ var actnNode = form.getAttributeNode("action");
+ ioArgs.url = ioArgs.url || (actnNode ? actnNode.value : null);
+ formObject = _d.formToObject(form);
+ }
+
+ // set up the query params
+ var miArgs = [{}];
+
+ if(formObject){
+ // potentially over-ride url-provided params w/ form values
+ miArgs.push(formObject);
+ }
+ if(args.content){
+ // stuff in content over-rides what's set by form
+ miArgs.push(args.content);
+ }
+ if(args.preventCache){
+ miArgs.push({"dojo.preventCache": new Date().valueOf()});
+ }
+ ioArgs.query = _d.objectToQuery(_d.mixin.apply(null, miArgs));
+
+ // .. and the real work of getting the deferred in order, etc.
+ ioArgs.handleAs = args.handleAs || "text";
+ var d = new _d.Deferred(canceller);
+ d.addCallbacks(okHandler, function(error){
+ return errHandler(error, d);
+ });
+
+ //Support specifying load, error and handle callback functions from the args.
+ //For those callbacks, the "this" object will be the args object.
+ //The callbacks will get the deferred result value as the
+ //first argument and the ioArgs object as the second argument.
+ var ld = args.load;
+ if(ld && _d.isFunction(ld)){
+ d.addCallback(function(value){
+ return ld.call(args, value, ioArgs);
+ });
+ }
+ var err = args.error;
+ if(err && _d.isFunction(err)){
+ d.addErrback(function(value){
+ return err.call(args, value, ioArgs);
+ });
+ }
+ var handle = args.handle;
+ if(handle && _d.isFunction(handle)){
+ d.addBoth(function(value){
+ return handle.call(args, value, ioArgs);
+ });
+ }
+
+ d.ioArgs = ioArgs;
+
+ // FIXME: need to wire up the xhr object's abort method to something
+ // analagous in the Deferred
+ return d;
+ }
+
+ var _deferredCancel = function(/*Deferred*/dfd){
+ //summary: canceller function for dojo._ioSetArgs call.
+
+ dfd.canceled = true;
+ var xhr = dfd.ioArgs.xhr;
+ var _at = typeof xhr.abort;
+ if(_at == "function" || _at == "unknown"){
+ xhr.abort();
+ }
+ var err = new Error("xhr cancelled");
+ err.dojoType = "cancel";
+ return err;
+ }
+ var _deferredOk = function(/*Deferred*/dfd){
+ //summary: okHandler function for dojo._ioSetArgs call.
+
+ return _d._contentHandlers[dfd.ioArgs.handleAs](dfd.ioArgs.xhr);
+ }
+ var _deferError = function(/*Error*/error, /*Deferred*/dfd){
+ //summary: errHandler function for dojo._ioSetArgs call.
+
+ // console.debug("xhr error in:", dfd.ioArgs.xhr);
+ console.debug(error);
+ return error;
+ }
+
+ var _makeXhrDeferred = function(/*dojo.__XhrArgs*/args){
+ //summary: makes the Deferred object for this xhr request.
+ var dfd = _d._ioSetArgs(args, _deferredCancel, _deferredOk, _deferError);
+ //Pass the args to _xhrObj, to allow xhr iframe proxy interceptions.
+ dfd.ioArgs.xhr = _d._xhrObj(dfd.ioArgs.args);
+ return dfd;
+ }
+
+ // avoid setting a timer per request. It degrades performance on IE
+ // something fierece if we don't use unified loops.
+ var _inFlightIntvl = null;
+ var _inFlight = [];
+ var _watchInFlight = function(){
+ //summary:
+ // internal method that checks each inflight XMLHttpRequest to see
+ // if it has completed or if the timeout situation applies.
+
+ var now = (new Date()).getTime();
+ // make sure sync calls stay thread safe, if this callback is called
+ // during a sync call and this results in another sync call before the
+ // first sync call ends the browser hangs
+ if(!_d._blockAsync){
+ // we need manual loop because we often modify _inFlight (and therefore 'i') while iterating
+ // note: the second clause is an assigment on purpose, lint may complain
+ for(var i = 0, tif; i < _inFlight.length && (tif = _inFlight[i]); i++){
+ var dfd = tif.dfd;
+ try{
+ if(!dfd || dfd.canceled || !tif.validCheck(dfd)){
+ _inFlight.splice(i--, 1);
+ }else if(tif.ioCheck(dfd)){
+ _inFlight.splice(i--, 1);
+ tif.resHandle(dfd);
+ }else if(dfd.startTime){
+ //did we timeout?
+ if(dfd.startTime + (dfd.ioArgs.args.timeout || 0) < now){
+ _inFlight.splice(i--, 1);
+ var err = new Error("timeout exceeded");
+ err.dojoType = "timeout";
+ dfd.errback(err);
+ //Cancel the request so the io module can do appropriate cleanup.
+ dfd.cancel();
+ }
+ }
+ }catch(e){
+ // FIXME: make sure we errback! (fixed? remove console.debug?)
+ console.debug(e);
+ dfd.errback(new Error("_watchInFlightError!"));
+ }
+ }
+ }
+
+ if(!_inFlight.length){
+ clearInterval(_inFlightIntvl);
+ _inFlightIntvl = null;
+ return;
+ }
+
+ }
+
+ dojo._ioCancelAll = function(){
+ //summary: Cancels all pending IO requests, regardless of IO type
+ //(xhr, script, iframe).
+ try{
+ _d.forEach(_inFlight, function(i){
+ i.dfd.cancel();
+ });
+ }catch(e){/*squelch*/}
+ }
+
+ //Automatically call cancel all io calls on unload
+ //in IE for trac issue #2357.
+ if(_d.isIE){
+ _d.addOnUnload(_d._ioCancelAll);
+ }
+
+ _d._ioWatch = function(/*Deferred*/dfd,
+ /*Function*/validCheck,
+ /*Function*/ioCheck,
+ /*Function*/resHandle){
+ //summary: watches the io request represented by dfd to see if it completes.
+ //dfd:
+ // The Deferred object to watch.
+ //validCheck:
+ // Function used to check if the IO request is still valid. Gets the dfd
+ // object as its only argument.
+ //ioCheck:
+ // Function used to check if basic IO call worked. Gets the dfd
+ // object as its only argument.
+ //resHandle:
+ // Function used to process response. Gets the dfd
+ // object as its only argument.
+ if(dfd.ioArgs.args.timeout){
+ dfd.startTime = (new Date()).getTime();
+ }
+ _inFlight.push({dfd: dfd, validCheck: validCheck, ioCheck: ioCheck, resHandle: resHandle});
+ if(!_inFlightIntvl){
+ _inFlightIntvl = setInterval(_watchInFlight, 50);
+ }
+ _watchInFlight(); // handle sync requests
+ }
+
+ var _defaultContentType = "application/x-www-form-urlencoded";
+
+ var _validCheck = function(/*Deferred*/dfd){
+ return dfd.ioArgs.xhr.readyState; //boolean
+ }
+ var _ioCheck = function(/*Deferred*/dfd){
+ return 4 == dfd.ioArgs.xhr.readyState; //boolean
+ }
+ var _resHandle = function(/*Deferred*/dfd){
+ var xhr = dfd.ioArgs.xhr;
+ if(_d._isDocumentOk(xhr)){
+ dfd.callback(dfd);
+ }else{
+ var err = new Error("Unable to load " + dfd.ioArgs.url + " status:" + xhr.status);
+ err.status = xhr.status;
+ err.responseText = xhr.responseText;
+ dfd.errback(err);
+ }
+ }
+
+ var _doIt = function(/*String*/type, /*Deferred*/dfd){
+ // IE 6 is a steaming pile. It won't let you call apply() on the native function (xhr.open).
+ // workaround for IE6's apply() "issues"
+ var ioArgs = dfd.ioArgs;
+ var args = ioArgs.args;
+ var xhr = ioArgs.xhr;
+ xhr.open(type, ioArgs.url, args.sync !== true, args.user || undefined, args.password || undefined);
+ if(args.headers){
+ for(var hdr in args.headers){
+ if(hdr.toLowerCase() === "content-type" && !args.contentType){
+ args.contentType = args.headers[hdr];
+ }else{
+ xhr.setRequestHeader(hdr, args.headers[hdr]);
+ }
+ }
+ }
+ // FIXME: is this appropriate for all content types?
+ xhr.setRequestHeader("Content-Type", args.contentType || _defaultContentType);
+ if(!args.headers || !args.headers["X-Requested-With"]){
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ }
+ // FIXME: set other headers here!
+ try{
+ xhr.send(ioArgs.query);
+ }catch(e){
+ dfd.cancel();
+ }
+ _d._ioWatch(dfd, _validCheck, _ioCheck, _resHandle);
+ xhr = null;
+ return dfd; //Deferred
+ }
+
+ dojo._ioAddQueryToUrl = function(/*dojo.__IoCallbackArgs*/ioArgs){
+ //summary: Adds query params discovered by the io deferred construction to the URL.
+ //Only use this for operations which are fundamentally GET-type operations.
+ if(ioArgs.query.length){
+ ioArgs.url += (ioArgs.url.indexOf("?") == -1 ? "?" : "&") + ioArgs.query;
+ ioArgs.query = null;
+ }
+ }
+
+ /*=====
+ dojo.declare("dojo.__XhrArgs", dojo.__IoArgs, {
+ constructor: function(){
+ // summary:
+ // In addition to the properties listed for the dojo._IoArgs type,
+ // the following properties are allowed for dojo.xhr* methods.
+ // handleAs: String?
+ // Acceptable values are: text (default), json, json-comment-optional,
+ // json-comment-filtered, javascript, xml
+ // sync: Boolean?
+ // false is default. Indicates whether the request should
+ // be a synchronous (blocking) request.
+ // headers: Object?
+ // Additional HTTP headers to send in the request.
+ this.handleAs = handleAs;
+ this.sync = sync;
+ this.headers = headers;
+ }
+ });
+ =====*/
+
+ dojo.xhr = function(/*String*/ method, /*dojo.__XhrArgs*/ args, /*Boolean?*/ hasBody){
+ // summary:
+ // Sends an HTTP request with the given method. If the request has an
+ // HTTP body, then pass true for hasBody. The method argument should be uppercase.
+ // Also look at dojo.xhrGet(), xhrPost(), xhrPut() and dojo.xhrDelete() for shortcuts
+ // for those HTTP methods. There are also methods for "raw" PUT and POST methods
+ // via dojo.rawXhrPut() and dojo.rawXhrPost() respectively.
+ var dfd = _makeXhrDeferred(args);
+ if(!hasBody){
+ _d._ioAddQueryToUrl(dfd.ioArgs);
+ }
+ return _doIt(method, dfd); // dojo.Deferred
+ }
+
+ dojo.xhrGet = function(/*dojo.__XhrArgs*/ args){
+ // summary:
+ // Sends an HTTP GET request to the server.
+ return _d.xhr("GET", args); //dojo.Deferred
+ }
+
+ dojo.xhrPost = function(/*dojo.__XhrArgs*/ args){
+ //summary:
+ // Sends an HTTP POST request to the server.
+ return _d.xhr("POST", args, true); // dojo.Deferred
+ }
+
+ dojo.rawXhrPost = function(/*dojo.__XhrArgs*/ args){
+ // summary:
+ // Sends an HTTP POST request to the server. In addtion to the properties
+ // listed for the dojo.__XhrArgs type, the following property is allowed:
+ // postData:
+ // String. The raw data to send in the body of the POST request.
+ var dfd = _makeXhrDeferred(args);
+ dfd.ioArgs.query = args.postData;
+ return _doIt("POST", dfd); // dojo.Deferred
+ }
+
+ dojo.xhrPut = function(/*dojo.__XhrArgs*/ args){
+ // summary:
+ // Sends an HTTP PUT request to the server.
+ return _d.xhr("PUT", args, true); // dojo.Deferred
+ }
+
+ dojo.rawXhrPut = function(/*dojo.__XhrArgs*/ args){
+ // summary:
+ // Sends an HTTP PUT request to the server. In addtion to the properties
+ // listed for the dojo.__XhrArgs type, the following property is allowed:
+ // putData:
+ // String. The raw data to send in the body of the PUT request.
+ var dfd = _makeXhrDeferred(args);
+ var ioArgs = dfd.ioArgs;
+ if(args.putData){
+ ioArgs.query = args.putData;
+ args.putData = null;
+ }
+ return _doIt("PUT", dfd); // dojo.Deferred
+ }
+
+ dojo.xhrDelete = function(/*dojo.__XhrArgs*/ args){
+ // summary:
+ // Sends an HTTP DELETE request to the server.
+ return _d.xhr("DELETE", args); //dojo.Deferred
+ }
+
+ /*
+ dojo.wrapForm = function(formNode){
+ //summary:
+ // A replacement for FormBind, but not implemented yet.
+
+ // FIXME: need to think harder about what extensions to this we might
+ // want. What should we allow folks to do w/ this? What events to
+ // set/send?
+ throw new Error("dojo.wrapForm not yet implemented");
+ }
+ */
+})();
+
+}
+
+if(!dojo._hasResource["dojo._base.fx"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.fx"] = true;
+dojo.provide("dojo._base.fx");
+
+/*
+ Animation losely package based on Dan Pupius' work, contributed under CLA:
+ http://pupius.co.uk/js/Toolkit.Drawing.js
+*/
+(function(){
+
+ var d = dojo;
+
+ dojo._Line = function(/*int*/ start, /*int*/ end){
+ // summary:
+ // dojo._Line is the object used to generate values from a start value
+ // to an end value
+ // start: int
+ // Beginning value for range
+ // end: int
+ // Ending value for range
+ this.start = start;
+ this.end = end;
+ this.getValue = function(/*float*/ n){
+ // summary: returns the point on the line
+ // n: a floating point number greater than 0 and less than 1
+ return ((this.end - this.start) * n) + this.start; // Decimal
+ }
+ }
+
+ d.declare("dojo._Animation", null, {
+ // summary
+ // A generic animation class that fires callbacks into its handlers
+ // object at various states. Nearly all dojo animation functions
+ // return an instance of this method, usually without calling the
+ // .play() method beforehand. Therefore, you will likely need to
+ // call .play() on instances of dojo._Animation when one is
+ // returned.
+ constructor: function(/*Object*/ args){
+ d.mixin(this, args);
+ if(d.isArray(this.curve)){
+ /* curve: Array
+ pId: a */
+ this.curve = new d._Line(this.curve[0], this.curve[1]);
+ }
+ },
+
+ // duration: Integer
+ // The time in milliseonds the animation will take to run
+ duration: 350,
+
+ /*=====
+ // curve: dojo._Line||Array
+ // A two element array of start and end values, or a dojo._Line instance to be
+ // used in the Animation.
+ curve: null,
+
+ // easing: Function
+ // A Function to adjust the acceleration (or deceleration) of the progress
+ // across a dojo._Line
+ easing: null,
+ =====*/
+
+ // repeat: Integer
+ // The number of times to loop the animation
+ repeat: 0,
+
+ // rate: Integer
+ // the time in milliseconds to wait before advancing to next frame
+ // (used as a fps timer: rate/1000 = fps)
+ rate: 10 /* 100 fps */,
+
+ /*=====
+ // delay: Integer
+ // The time in milliseconds to wait before starting animation after it has been .play()'ed
+ delay: null,
+
+ // events
+ //
+ // beforeBegin: Event
+ // Synthetic event fired before a dojo._Animation begins playing (synchronous)
+ beforeBegin: null,
+
+ // onBegin: Event
+ // Synthetic event fired as a dojo._Animation begins playing (useful?)
+ onBegin: null,
+
+ // onAnimate: Event
+ // Synthetic event fired at each interval of a dojo._Animation
+ onAnimate: null,
+
+ // onEnd: Event
+ // Synthetic event fired after the final frame of a dojo._Animation
+ onEnd: null,
+
+ // onPlay: Event
+ // Synthetic event fired any time a dojo._Animation is play()'ed
+ onPlay: null,
+
+ // onPause: Event
+ // Synthetic event fired when a dojo._Animation is paused
+ onPause: null,
+
+ // onStop: Event
+ // Synthetic event fires when a dojo._Animation is stopped
+ onStop: null,
+
+ =====*/
+
+ _percent: 0,
+ _startRepeatCount: 0,
+
+ _fire: function(/*Event*/ evt, /*Array?*/ args){
+ // summary:
+ // Convenience function. Fire event "evt" and pass it the
+ // arguments specified in "args".
+ // evt:
+ // The event to fire.
+ // args:
+ // The arguments to pass to the event.
+ try{
+ if(this[evt]){
+ this[evt].apply(this, args||[]);
+ }
+ }catch(e){
+ // squelch and log because we shouldn't allow exceptions in
+ // synthetic event handlers to cause the internal timer to run
+ // amuck, potentially pegging the CPU. I'm not a fan of this
+ // squelch, but hopefully logging will make it clear what's
+ // going on
+ console.error("exception in animation handler for:", evt);
+ console.error(e);
+ }
+ return this; // dojo._Animation
+ },
+
+ play: function(/*int?*/ delay, /*Boolean?*/ gotoStart){
+ // summary:
+ // Start the animation.
+ // delay:
+ // How many milliseconds to delay before starting.
+ // gotoStart:
+ // If true, starts the animation from the beginning; otherwise,
+ // starts it from its current position.
+ var _t = this;
+ if(gotoStart){
+ _t._stopTimer();
+ _t._active = _t._paused = false;
+ _t._percent = 0;
+ }else if(_t._active && !_t._paused){
+ return _t; // dojo._Animation
+ }
+
+ _t._fire("beforeBegin");
+
+ var de = delay||_t.delay;
+ var _p = dojo.hitch(_t, "_play", gotoStart);
+ if(de > 0){
+ setTimeout(_p, de);
+ return _t; // dojo._Animation
+ }
+ _p();
+ return _t;
+ },
+
+ _play: function(gotoStart){
+ var _t = this;
+ _t._startTime = new Date().valueOf();
+ if(_t._paused){
+ _t._startTime -= _t.duration * _t._percent;
+ }
+ _t._endTime = _t._startTime + _t.duration;
+
+ _t._active = true;
+ _t._paused = false;
+
+ var value = _t.curve.getValue(_t._percent);
+ if(!_t._percent){
+ if(!_t._startRepeatCount){
+ _t._startRepeatCount = _t.repeat;
+ }
+ _t._fire("onBegin", [value]);
+ }
+
+ _t._fire("onPlay", [value]);
+
+ _t._cycle();
+ return _t; // dojo._Animation
+ },
+
+ pause: function(){
+ // summary: Pauses a running animation.
+ this._stopTimer();
+ if(!this._active){ return this; /*dojo._Animation*/ }
+ this._paused = true;
+ this._fire("onPause", [this.curve.getValue(this._percent)]);
+ return this; // dojo._Animation
+ },
+
+ gotoPercent: function(/*Decimal*/ percent, /*Boolean?*/ andPlay){
+ // summary:
+ // Sets the progress of the animation.
+ // percent:
+ // A percentage in decimal notation (between and including 0.0 and 1.0).
+ // andPlay:
+ // If true, play the animation after setting the progress.
+ this._stopTimer();
+ this._active = this._paused = true;
+ this._percent = percent;
+ if(andPlay){ this.play(); }
+ return this; // dojo._Animation
+ },
+
+ stop: function(/*boolean?*/ gotoEnd){
+ // summary: Stops a running animation.
+ // gotoEnd: If true, the animation will end.
+ if(!this._timer){ return this; /* dojo._Animation */ }
+ this._stopTimer();
+ if(gotoEnd){
+ this._percent = 1;
+ }
+ this._fire("onStop", [this.curve.getValue(this._percent)]);
+ this._active = this._paused = false;
+ return this; // dojo._Animation
+ },
+
+ status: function(){
+ // summary: Returns a string token representation of the status of
+ // the animation, one of: "paused", "playing", "stopped"
+ if(this._active){
+ return this._paused ? "paused" : "playing"; // String
+ }
+ return "stopped"; // String
+ },
+
+ _cycle: function(){
+ var _t = this;
+ if(_t._active){
+ var curr = new Date().valueOf();
+ var step = (curr - _t._startTime) / (_t._endTime - _t._startTime);
+
+ if(step >= 1){
+ step = 1;
+ }
+ _t._percent = step;
+
+ // Perform easing
+ if(_t.easing){
+ step = _t.easing(step);
+ }
+
+ _t._fire("onAnimate", [_t.curve.getValue(step)]);
+
+ if(_t._percent < 1){
+ _t._startTimer();
+ }else{
+ _t._active = false;
+
+ if(_t.repeat > 0){
+ _t.repeat--;
+ _t.play(null, true);
+ }else if(_t.repeat == -1){
+ _t.play(null, true);
+ }else{
+ if(_t._startRepeatCount){
+ _t.repeat = _t._startRepeatCount;
+ _t._startRepeatCount = 0;
+ }
+ }
+ _t._percent = 0;
+ _t._fire("onEnd");
+ _t._stopTimer();
+ }
+ }
+ return _t; // dojo._Animation
+ }
+ });
+
+ var ctr = 0;
+ var _globalTimerList = [];
+ var runner = {
+ run: function(){ }
+ };
+ var timer = null;
+ dojo._Animation.prototype._startTimer = function(){
+ // this._timer = setTimeout(dojo.hitch(this, "_cycle"), this.rate);
+ if(!this._timer){
+ this._timer = d.connect(runner, "run", this, "_cycle");
+ ctr++;
+ }
+ if(!timer){
+ timer = setInterval(d.hitch(runner, "run"), this.rate);
+ }
+ };
+
+ dojo._Animation.prototype._stopTimer = function(){
+ if(this._timer){
+ d.disconnect(this._timer);
+ this._timer = null;
+ ctr--;
+ }
+ if(ctr <= 0){
+ clearInterval(timer);
+ timer = null;
+ ctr = 0;
+ }
+ };
+
+ var _makeFadeable = (d.isIE) ? function(node){
+ // only set the zoom if the "tickle" value would be the same as the
+ // default
+ var ns = node.style;
+ if(!ns.zoom.length && d.style(node, "zoom") == "normal"){
+ // make sure the node "hasLayout"
+ // NOTE: this has been tested with larger and smaller user-set text
+ // sizes and works fine
+ ns.zoom = "1";
+ // node.style.zoom = "normal";
+ }
+ // don't set the width to auto if it didn't already cascade that way.
+ // We don't want to f anyones designs
+ if(!ns.width.length && d.style(node, "width") == "auto"){
+ ns.width = "auto";
+ }
+ } : function(){};
+
+ dojo._fade = function(/*Object*/ args){
+ // summary:
+ // Returns an animation that will fade the node defined by
+ // args.node from the start to end values passed (args.start
+ // args.end) (end is mandatory, start is optional)
+
+ args.node = d.byId(args.node);
+ var fArgs = d.mixin({ properties: {} }, args);
+ var props = (fArgs.properties.opacity = {});
+ props.start = !("start" in fArgs) ?
+ function(){
+ return Number(d.style(fArgs.node, "opacity"));
+ } : fArgs.start;
+ props.end = fArgs.end;
+
+ var anim = d.animateProperty(fArgs);
+ d.connect(anim, "beforeBegin", d.partial(_makeFadeable, fArgs.node));
+
+ return anim; // dojo._Animation
+ }
+
+ /*=====
+ dojo.__FadeArgs = function(node, duration, easing){
+ // node: DOMNode|String
+ // The node referenced in the animation
+ // duration: Integer?
+ // Duration of the animation in milliseconds.
+ // easing: Function?
+ // An easing function.
+ this.node = node;
+ this.duration = duration;
+ this.easing = easing;
+ }
+ =====*/
+
+ dojo.fadeIn = function(/*dojo.__FadeArgs*/ args){
+ // summary:
+ // Returns an animation that will fade node defined in 'args' from
+ // its current opacity to fully opaque.
+ return d._fade(d.mixin({ end: 1 }, args)); // dojo._Animation
+ }
+
+ dojo.fadeOut = function(/*dojo.__FadeArgs*/ args){
+ // summary:
+ // Returns an animation that will fade node defined in 'args'
+ // from its current opacity to fully transparent.
+ return d._fade(d.mixin({ end: 0 }, args)); // dojo._Animation
+ }
+
+ dojo._defaultEasing = function(/*Decimal?*/ n){
+ // summary: The default easing function for dojo._Animation(s)
+ return 0.5 + ((Math.sin((n + 1.5) * Math.PI))/2);
+ }
+
+ var PropLine = function(properties){
+ // PropLine is an internal class which is used to model the values of
+ // an a group of CSS properties across an animation lifecycle. In
+ // particular, the "getValue" function handles getting interpolated
+ // values between start and end for a particular CSS value.
+ this._properties = properties;
+ for(var p in properties){
+ var prop = properties[p];
+ if(prop.start instanceof d.Color){
+ // create a reusable temp color object to keep intermediate results
+ prop.tempColor = new d.Color();
+ }
+ }
+ this.getValue = function(r){
+ var ret = {};
+ for(var p in this._properties){
+ var prop = this._properties[p];
+ var start = prop.start;
+ if(start instanceof d.Color){
+ ret[p] = d.blendColors(start, prop.end, r, prop.tempColor).toCss();
+ }else if(!d.isArray(start)){
+ ret[p] = ((prop.end - start) * r) + start + (p != "opacity" ? prop.units||"px" : "");
+ }
+ }
+ return ret;
+ }
+ }
+
+ /*=====
+ dojo.declare("dojo.__AnimArgs", [dojo.__FadeArgs], {
+ // Properties: Object?
+ // A hash map of style properties to Objects describing the transition,
+ // such as the properties of dojo._Line with an additional 'unit' property
+ properties: {}
+
+ //TODOC: add event callbacks
+ });
+ =====*/
+
+ dojo.animateProperty = function(/*dojo.__AnimArgs*/ args){
+ // summary:
+ // Returns an animation that will transition the properties of
+ // node defined in 'args' depending how they are defined in
+ // 'args.properties'
+ //
+ // description:
+ // dojo.animateProperty is the foundation of most dojo.fx
+ // animations. It takes an object of "properties" corresponding to
+ // style properties, and animates them in parallel over a set
+ // duration.
+ //
+ // example:
+ // A simple animation that changes the width of the specified node.
+ // | dojo.animateProperty({
+ // | node: "nodeId",
+ // | properties: { width: 400 },
+ // | }).play();
+ // Dojo figures out the start value for the width and converts the
+ // integer specified for the width to the more expressive but
+ // verbose form `{ width: { end: '400', units: 'px' } }` which you
+ // can also specify directly
+ // example:
+ // animate width, height, and padding over 2 seconds...the
+ // pedantic way:
+ // | dojo.animateProperty({ node: node, duration:2000,
+ // | properties: {
+ // | width: { start: '200', end: '400', unit:"px" },
+ // | height: { start:'200', end: '400', unit:"px" },
+ // | paddingTop: { start:'5', end:'50', unit:"px" }
+ // | }
+ // | }).play();
+ //
+ // example:
+ // plug in a different easing function and register a callback for
+ // when the animation ends. Easing functions accept values between
+ // zero and one and return a value on that basis. In this case, an
+ // exponential-in curve.
+ // | dojo.animateProperty({
+ // | node: "nodeId",
+ // | // dojo figures out the start value
+ // | properties: { width: { end: 400 } },
+ // | easing: function(n){
+ // | return (n==0) ? 0 : Math.pow(2, 10 * (n - 1));
+ // | },
+ // | onEnd: function(){
+ // | // called when the animation finishes
+ // | }
+ // | }).play(500); // delay playing half a second
+
+ args.node = d.byId(args.node);
+ if(!args.easing){ args.easing = d._defaultEasing; }
+
+ var anim = new d._Animation(args);
+ d.connect(anim, "beforeBegin", anim, function(){
+ var pm = {};
+ for(var p in this.properties){
+ // Make shallow copy of properties into pm because we overwrite
+ // some values below. In particular if start/end are functions
+ // we don't want to overwrite them or the functions won't be
+ // called if the animation is reused.
+ if(p == "width" || p == "height"){
+ this.node.display = "block";
+ }
+ var prop = this.properties[p];
+ prop = pm[p] = d.mixin({}, (d.isObject(prop) ? prop: { end: prop }));
+
+ if(d.isFunction(prop.start)){
+ prop.start = prop.start();
+ }
+ if(d.isFunction(prop.end)){
+ prop.end = prop.end();
+ }
+ var isColor = (p.toLowerCase().indexOf("color") >= 0);
+ function getStyle(node, p){
+ // dojo.style(node, "height") can return "auto" or "" on IE; this is more reliable:
+ var v = ({height: node.offsetHeight, width: node.offsetWidth})[p];
+ if(v !== undefined){ return v; }
+ v = d.style(node, p);
+ return (p=="opacity") ? Number(v) : (isColor ? v : parseFloat(v));
+ }
+ if(!("end" in prop)){
+ prop.end = getStyle(this.node, p);
+ }else if(!("start" in prop)){
+ prop.start = getStyle(this.node, p);
+ }
+
+ if(isColor){
+ prop.start = new d.Color(prop.start);
+ prop.end = new d.Color(prop.end);
+ }else{
+ prop.start = (p == "opacity") ? Number(prop.start) : parseFloat(prop.start);
+ }
+ }
+ this.curve = new PropLine(pm);
+ });
+ d.connect(anim, "onAnimate", anim, function(propValues){
+ // try{
+ for(var s in propValues){
+ d.style(this.node, s, propValues[s]);
+ // this.node.style[s] = propValues[s];
+ }
+ });
+ return anim; // dojo._Animation
+ }
+
+ dojo.anim = function( /*DOMNode|String*/ node,
+ /*Object*/ properties,
+ /*Integer?*/ duration,
+ /*Function?*/ easing,
+ /*Function?*/ onEnd,
+ /*Integer?*/ delay){
+ // summary:
+ // A simpler interface to `dojo.animateProperty()`, also returns
+ // an instance of `dojo._Animation` but begins the animation
+ // immediately, unlike nearly every other Dojo animation API.
+ // description:
+ // `dojo.anim` is a simpler (but somewhat less powerful) version
+ // of `dojo.animateProperty`. It uses defaults for many basic properties
+ // and allows for positional parameters to be used in place of the
+ // packed "property bag" which is used for other Dojo animation
+ // methods.
+ //
+ // The `dojo._Animation` object returned from `dojo.anim` will be
+ // already playing when it is returned from this function, so
+ // calling play() on it again is (usually) a no-op.
+ // node:
+ // a DOM node or the id of a node to animate CSS properties on
+ // duration:
+ // The number of milliseconds over which the animation
+ // should run. Defaults to the global animation default duration
+ // (350ms).
+ // easing:
+ // An easing function over which to calculate acceleration
+ // and deceleration of the animation through its duration.
+ // A default easing algorithm is provided, but you may
+ // plug in any you wish. A large selection of easing algorithms
+ // are available in `dojox.fx.easing`.
+ // onEnd:
+ // A function to be called when the animation finishes
+ // running.
+ // delay:
+ // The number of milliseconds to delay beginning the
+ // animation by. The default is 0.
+ // example:
+ // Fade out a node
+ // | dojo.anim("id", { opacity: 0 });
+ // example:
+ // Fade out a node over a full second
+ // | dojo.anim("id", { opacity: 0 }, 1000);
+ return d.animateProperty({
+ node: node,
+ duration: duration||d._Animation.prototype.duration,
+ properties: properties,
+ easing: easing,
+ onEnd: onEnd
+ }).play(delay||0);
+ }
+})();
+
+}
+
+if(!dojo._hasResource["dojo._base.browser"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.browser"] = true;
+dojo.provide("dojo._base.browser");
+
+//Need this to be the last code segment in base, so do not place any
+//dojo.requireIf calls in this file. Otherwise, due to how the build system
+//puts all requireIf dependencies after the current file, the require calls
+//could be called before all of base is defined.
+if(dojo.config.require){
+ dojo.forEach(dojo.config.require, "dojo['require'](item);");
+}
+
+}
+
+ if(dojo.config.afterOnLoad && dojo.isBrowser){
+ //Dojo is being added to the page after page load, so just trigger
+ //the init sequence after a timeout. Using a timeout so the rest of this
+ //script gets evaluated properly. This work needs to happen after the
+ //dojo.config.require work done in dojo._base.
+ window.setTimeout(dojo._fakeLoadInit, 1000);
+ }
+
+})();
+
diff --git a/tests/javascript/frameworks/dojo/dojo-1.2.4.js b/tests/javascript/frameworks/dojo/dojo-1.2.4.js
new file mode 100644
index 0000000000000000000000000000000000000000..6a2824679e976298a84cc98ec0ce4163db6b7191
--- /dev/null
+++ b/tests/javascript/frameworks/dojo/dojo-1.2.4.js
@@ -0,0 +1,16 @@
+/*
+ Copyright (c) 2004-2008, The Dojo Foundation All Rights Reserved.
+ Available via Academic Free License >= 2.1 OR the modified BSD license.
+ see: http://dojotoolkit.org/license for details
+*/
+
+/*
+ This is a compiled version of Dojo, built for deployment and not for
+ development. To get an editable version, please visit:
+
+ http://dojotoolkit.org
+
+ for documentation and information on getting the source.
+*/
+
+(function(){var _1=null;if((_1||(typeof djConfig!="undefined"&&djConfig.scopeMap))&&(typeof window!="undefined")){var _2="",_3="",_4="",_5={},_6={};_1=_1||djConfig.scopeMap;for(var i=0;i<_1.length;i++){var _8=_1[i];_2+="var "+_8[0]+" = {}; "+_8[1]+" = "+_8[0]+";"+_8[1]+"._scopeName = '"+_8[1]+"';";_3+=(i==0?"":",")+_8[0];_4+=(i==0?"":",")+_8[1];_5[_8[0]]=_8[1];_6[_8[1]]=_8[0];}eval(_2+"dojo._scopeArgs = ["+_4+"];");dojo._scopePrefixArgs=_3;dojo._scopePrefix="(function("+_3+"){";dojo._scopeSuffix="})("+_4+")";dojo._scopeMap=_5;dojo._scopeMapRev=_6;}(function(){if(!this["console"]){this.console={};}var cn=["assert","count","debug","dir","dirxml","error","group","groupEnd","info","profile","profileEnd","time","timeEnd","trace","warn","log"];var i=0,tn;while((tn=cn[i++])){if(!console[tn]){(function(){var _c=tn+"";console[_c]=("log" in console)?function(){var a=Array.apply({},arguments);a.unshift(_c+":");console["log"](a.join(" "));}:function(){};})();}}if(typeof dojo=="undefined"){this.dojo={_scopeName:"dojo",_scopePrefix:"",_scopePrefixArgs:"",_scopeSuffix:"",_scopeMap:{},_scopeMapRev:{}};}var d=dojo;if(typeof dijit=="undefined"){this.dijit={_scopeName:"dijit"};}if(typeof dojox=="undefined"){this.dojox={_scopeName:"dojox"};}if(!d._scopeArgs){d._scopeArgs=[dojo,dijit,dojox];}d.global=this;d.config={isDebug:false,debugAtAllCosts:false};if(typeof djConfig!="undefined"){for(var _f in djConfig){d.config[_f]=djConfig[_f];}}var _10=["Browser","Rhino","Spidermonkey","Mobile"];var t;while((t=_10.shift())){d["is"+t]=false;}dojo.locale=d.config.locale;var rev="$Rev: 21540 $".match(/\d+/);dojo.version={major:1,minor:2,patch:4,flag:"",revision:rev?+rev[0]:999999,toString:function(){with(d.version){return major+"."+minor+"."+patch+flag+" ("+revision+")";}}};if(typeof OpenAjax!="undefined"){OpenAjax.hub.registerLibrary(dojo._scopeName,"http://dojotoolkit.org",d.version.toString());}dojo._mixin=function(obj,_14){var _15={};for(var x in _14){if(_15[x]===undefined||_15[x]!=_14[x]){obj[x]=_14[x];}}if(d["isIE"]&&_14){var p=_14.toString;if(typeof p=="function"&&p!=obj.toString&&p!=_15.toString&&p!="\nfunction toString() {\n [native code]\n}\n"){obj.toString=_14.toString;}}return obj;};dojo.mixin=function(obj,_19){for(var i=1,l=arguments.length;i0){console.warn("files still in flight!");return;}d._callLoaded();};dojo._callLoaded=function(){if(typeof setTimeout=="object"||(dojo.config.useXDomain&&d.isOpera)){if(dojo.isAIR){setTimeout(function(){dojo.loaded();},0);}else{setTimeout(dojo._scopeName+".loaded();",0);}}else{d.loaded();}};dojo._getModuleSymbols=function(_4b){var _4c=_4b.split(".");for(var i=_4c.length;i>0;i--){var _4e=_4c.slice(0,i).join(".");if((i==1)&&!this._moduleHasPrefix(_4e)){_4c[0]="../"+_4c[0];}else{var _4f=this._getModulePrefix(_4e);if(_4f!=_4e){_4c.splice(0,i,_4f);break;}}}return _4c;};dojo._global_omit_module_check=false;dojo.loadInit=function(_50){_50();};dojo._loadModule=dojo.require=function(_51,_52){_52=this._global_omit_module_check||_52;var _53=this._loadedModules[_51];if(_53){return _53;}var _54=this._getModuleSymbols(_51).join("/")+".js";var _55=(!_52)?_51:null;var ok=this._loadPath(_54,_55);if(!ok&&!_52){throw new Error("Could not load '"+_51+"'; last tried '"+_54+"'");}if(!_52&&!this._isXDomain){_53=this._loadedModules[_51];if(!_53){throw new Error("symbol '"+_51+"' is not defined after loading '"+_54+"'");}}return _53;};dojo.provide=function(_57){_57=_57+"";return (d._loadedModules[_57]=d.getObject(_57,true));};dojo.platformRequire=function(_58){var _59=_58.common||[];var _5a=_59.concat(_58[d._name]||_58["default"]||[]);for(var x=0;x<_5a.length;x++){var _5c=_5a[x];if(_5c.constructor==Array){d._loadModule.apply(d,_5c);}else{d._loadModule(_5c);}}};dojo.requireIf=function(_5d,_5e){if(_5d===true){var _5f=[];for(var i=1;i0&&!(j==1&&_70[0]=="")&&_70[j]==".."&&_70[j-1]!=".."){if(j==(_70.length-1)){_70.splice(j,1);_70[j-1]="";}else{_70.splice(j-1,2);j-=2;}}}}_6d.path=_70.join("/");}}}}uri=[];if(_6d.scheme){uri.push(_6d.scheme,":");}if(_6d.authority){uri.push("//",_6d.authority);}uri.push(_6d.path);if(_6d.query){uri.push("?",_6d.query);}if(_6d.fragment){uri.push("#",_6d.fragment);}}this.uri=uri.join("");var r=this.uri.match(ore);this.scheme=r[2]||(r[1]?"":n);this.authority=r[4]||(r[3]?"":n);this.path=r[5];this.query=r[7]||(r[6]?"":n);this.fragment=r[9]||(r[8]?"":n);if(this.authority!=n){r=this.authority.match(ire);this.user=r[3]||n;this.password=r[4]||n;this.host=r[6]||r[7];this.port=r[9]||n;}};dojo._Url.prototype.toString=function(){return this.uri;};dojo.moduleUrl=function(_73,url){var loc=d._getModuleSymbols(_73).join("/");if(!loc){return null;}if(loc.lastIndexOf("/")!=loc.length-1){loc+="/";}var _76=loc.indexOf(":");if(loc.charAt(0)!="/"&&(_76==-1||_76>loc.indexOf("/"))){loc=d.baseUrl+loc;}return new d._Url(loc,url);};})();if(typeof window!="undefined"){dojo.isBrowser=true;dojo._name="browser";(function(){var d=dojo;if(document&&document.getElementsByTagName){var _78=document.getElementsByTagName("script");var _79=/dojo(\.xd)?\.js(\W|$)/i;for(var i=0;i<_78.length;i++){var src=_78[i].getAttribute("src");if(!src){continue;}var m=src.match(_79);if(m){if(!d.config.baseUrl){d.config.baseUrl=src.substring(0,m.index);}var cfg=_78[i].getAttribute("djConfig");if(cfg){var _7e=eval("({ "+cfg+" })");for(var x in _7e){dojo.config[x]=_7e[x];}}break;}}}d.baseUrl=d.config.baseUrl;var n=navigator;var dua=n.userAgent;var dav=n.appVersion;var tv=parseFloat(dav);if(dua.indexOf("Opera")>=0){d.isOpera=tv;}var _84=Math.max(dav.indexOf("WebKit"),dav.indexOf("Safari"),0);if(_84){d.isSafari=parseFloat(dav.split("Version/")[1])||(parseFloat(dav.substr(_84+7))>419.3)?3:2;}if(dua.indexOf("AdobeAIR")>=0){d.isAIR=1;}if(dav.indexOf("Konqueror")>=0||d.isSafari){d.isKhtml=tv;}if(dua.indexOf("Gecko")>=0&&!d.isKhtml){d.isMozilla=d.isMoz=tv;}if(d.isMoz){d.isFF=parseFloat(dua.split("Firefox/")[1])||undefined;}if(document.all&&!d.isOpera){d.isIE=parseFloat(dav.split("MSIE ")[1])||undefined;}if(dojo.isIE&&window.location.protocol==="file:"){dojo.config.ieForceActiveXXhr=true;}var cm=document.compatMode;d.isQuirks=cm=="BackCompat"||cm=="QuirksMode"||d.isIE<6;d.locale=dojo.config.locale||(d.isIE?n.userLanguage:n.language).toLowerCase();d._XMLHTTP_PROGIDS=["Msxml2.XMLHTTP","Microsoft.XMLHTTP","Msxml2.XMLHTTP.4.0"];d._xhrObj=function(){var _86=null;var _87=null;if(!dojo.isIE||!dojo.config.ieForceActiveXXhr){try{_86=new XMLHttpRequest();}catch(e){}}if(!_86){for(var i=0;i<3;++i){var _89=d._XMLHTTP_PROGIDS[i];try{_86=new ActiveXObject(_89);}catch(e){_87=e;}if(_86){d._XMLHTTP_PROGIDS=[_89];break;}}}if(!_86){throw new Error("XMLHTTP not available: "+_87);}return _86;};d._isDocumentOk=function(_8a){var _8b=_8a.status||0;return (_8b>=200&&_8b<300)||_8b==304||_8b==1223||(!_8b&&(location.protocol=="file:"||location.protocol=="chrome:"));};var _8c=window.location+"";var _8d=document.getElementsByTagName("base");var _8e=(_8d&&_8d.length>0);d._getText=function(uri,_90){var _91=this._xhrObj();if(!_8e&&dojo._Url){uri=(new dojo._Url(_8c,uri)).toString();}if(d.config.cacheBust){uri+="";uri+=(uri.indexOf("?")==-1?"?":"&")+String(d.config.cacheBust).replace(/\W+/g,"");}_91.open("GET",uri,false);try{_91.send(null);if(!d._isDocumentOk(_91)){var err=Error("Unable to load "+uri+" status:"+_91.status);err.status=_91.status;err.responseText=_91.responseText;throw err;}}catch(e){if(_90){return null;}throw e;}return _91.responseText;};d._windowUnloaders=[];d.windowUnloaded=function(){var mll=this._windowUnloaders;while(mll.length){(mll.pop())();}};d.addOnWindowUnload=function(obj,_95){d._onto(d._windowUnloaders,obj,_95);};})();dojo._initFired=false;dojo._loadInit=function(e){dojo._initFired=true;var _97=(e&&e.type)?e.type.toLowerCase():"load";if(arguments.callee.initialized||(_97!="domcontentloaded"&&_97!="load")){return;}arguments.callee.initialized=true;if("_khtmlTimer" in dojo){clearInterval(dojo._khtmlTimer);delete dojo._khtmlTimer;}if(dojo._inFlightCount==0){dojo._modulesLoaded();}};dojo._fakeLoadInit=function(){dojo._loadInit({type:"load"});};if(!dojo.config.afterOnLoad){if(document.addEventListener){if(dojo.isOpera||dojo.isFF>=3||(dojo.isMoz&&dojo.config.enableMozDomContentLoaded===true)){document.addEventListener("DOMContentLoaded",dojo._loadInit,null);}window.addEventListener("load",dojo._loadInit,null);}if(dojo.isAIR){window.addEventListener("load",dojo._loadInit,null);}else{if(/(WebKit|khtml)/i.test(navigator.userAgent)){dojo._khtmlTimer=setInterval(function(){if(/loaded|complete/.test(document.readyState)){dojo._loadInit();}},10);}}}(function(){var _w=window;var _99=function(_9a,fp){var _9c=_w[_9a]||function(){};_w[_9a]=function(){fp.apply(_w,arguments);_9c.apply(_w,arguments);};};if(dojo.isIE){if(!dojo.config.afterOnLoad){document.write(""+"");}try{document.namespaces.add("v","urn:schemas-microsoft-com:vml");document.createStyleSheet().addRule("v\\:*","behavior:url(#default#VML)");}catch(e){}}_99("onbeforeunload",function(){dojo.unloaded();});_99("onunload",function(){dojo.windowUnloaded();});})();}(function(){var mp=dojo.config["modulePaths"];if(mp){for(var _9e in mp){dojo.registerModulePath(_9e,mp[_9e]);}}})();if(dojo.config.isDebug){dojo.require("dojo._firebug.firebug");}if(dojo.config.debugAtAllCosts){dojo.config.useXDomain=true;dojo.require("dojo._base._loader.loader_xd");dojo.require("dojo._base._loader.loader_debug");dojo.require("dojo.i18n");}if(!dojo._hasResource["dojo._base.lang"]){dojo._hasResource["dojo._base.lang"]=true;dojo.provide("dojo._base.lang");dojo.isString=function(it){return !!arguments.length&&it!=null&&(typeof it=="string"||it instanceof String);};dojo.isArray=function(it){return it&&(it instanceof Array||typeof it=="array");};dojo.isFunction=(function(){var _a1=function(it){return it&&(typeof it=="function"||it instanceof Function);};return dojo.isSafari?function(it){if(typeof it=="function"&&it=="[object NodeList]"){return false;}return _a1(it);}:_a1;})();dojo.isObject=function(it){return it!==undefined&&(it===null||typeof it=="object"||dojo.isArray(it)||dojo.isFunction(it));};dojo.isArrayLike=function(it){var d=dojo;return it&&it!==undefined&&!d.isString(it)&&!d.isFunction(it)&&!(it.tagName&&it.tagName.toLowerCase()=="form")&&(d.isArray(it)||isFinite(it.length));};dojo.isAlien=function(it){return it&&!dojo.isFunction(it)&&/\{\s*\[native code\]\s*\}/.test(String(it));};dojo.extend=function(_a8,_a9){for(var i=1,l=arguments.length;i2){return dojo._hitchArgs.apply(dojo,arguments);}if(!_b3){_b3=_b2;_b2=null;}if(dojo.isString(_b3)){_b2=_b2||dojo.global;if(!_b2[_b3]){throw (["dojo.hitch: scope[\"",_b3,"\"] is null (scope=\"",_b2,"\")"].join(""));}return function(){return _b2[_b3].apply(_b2,arguments||[]);};}return !_b2?_b3:function(){return _b3.apply(_b2,arguments||[]);};};dojo.delegate=dojo._delegate=(function(){function TMP(){};return function(obj,_b5){TMP.prototype=obj;var tmp=new TMP();if(_b5){dojo._mixin(tmp,_b5);}return tmp;};})();(function(){var _b7=function(obj,_b9,_ba){return (_ba||[]).concat(Array.prototype.slice.call(obj,_b9||0));};var _bb=function(obj,_bd,_be){var arr=_be||[];for(var x=_bd||0;x=0){this._fire();}return this;},_fire:function(){var _134=this.chain;var _135=this.fired;var res=this.results[_135];var self=this;var cb=null;while((_134.length>0)&&(this.paused==0)){var f=_134.shift()[_135];if(!f){continue;}var func=function(){var ret=f(res);if(typeof ret!="undefined"){res=ret;}_135=((res instanceof Error)?1:0);if(res instanceof dojo.Deferred){cb=function(res){self._resback(res);self.paused--;if((self.paused==0)&&(self.fired>=0)){self._fire();}};this.paused++;}};if(dojo.config.isDebug){func.call(this);}else{try{func.call(this);}catch(err){_135=1;res=err;}}}this.fired=_135;this.results[_135]=res;if((cb)&&(this.paused)){res.addBoth(cb);}}});}if(!dojo._hasResource["dojo._base.json"]){dojo._hasResource["dojo._base.json"]=true;dojo.provide("dojo._base.json");dojo.fromJson=function(json){return eval("("+json+")");};dojo._escapeString=function(str){return ("\""+str.replace(/(["\\])/g,"\\$1")+"\"").replace(/[\f]/g,"\\f").replace(/[\b]/g,"\\b").replace(/[\n]/g,"\\n").replace(/[\t]/g,"\\t").replace(/[\r]/g,"\\r");};dojo.toJsonIndentStr="\t";dojo.toJson=function(it,_140,_141){if(it===undefined){return "undefined";}var _142=typeof it;if(_142=="number"||_142=="boolean"){return it+"";}if(it===null){return "null";}if(dojo.isString(it)){return dojo._escapeString(it);}var _143=arguments.callee;var _144;_141=_141||"";var _145=_140?_141+dojo.toJsonIndentStr:"";var tf=it.__json__||it.json;if(dojo.isFunction(tf)){_144=tf.call(it);if(it!==_144){return _143(_144,_140,_145);}}if(it.nodeType&&it.cloneNode){throw new Error("Can't serialize DOM nodes");}var sep=_140?" ":"";var _148=_140?"\n":"";if(dojo.isArray(it)){var res=dojo.map(it,function(obj){var val=_143(obj,_140,_145);if(typeof val!="string"){val="undefined";}return _148+_145+val;});return "["+res.join(","+sep)+_148+_141+"]";}if(_142=="function"){return null;}var _14c=[],key;for(key in it){var _14e,val;if(typeof key=="number"){_14e="\""+key+"\"";}else{if(typeof key=="string"){_14e=dojo._escapeString(key);}else{continue;}}val=_143(it[key],_140,_145);if(typeof val!="string"){continue;}_14c.push(_148+_145+_14e+":"+sep+val);}return "{"+_14c.join(","+sep)+_148+_141+"}";};}if(!dojo._hasResource["dojo._base.array"]){dojo._hasResource["dojo._base.array"]=true;dojo.provide("dojo._base.array");(function(){var _150=function(arr,obj,cb){return [dojo.isString(arr)?arr.split(""):arr,obj||dojo.global,dojo.isString(cb)?new Function("item","index","array",cb):cb];};dojo.mixin(dojo,{indexOf:function(_154,_155,_156,_157){var step=1,end=_154.length||0,i=0;if(_157){i=end-1;step=end=-1;}if(_156!=undefined){i=_156;}if((_157&&i>end)||i>=bits;t[x]=bits==4?17*c:c;});t.a=1;return t;};dojo.colorFromArray=function(a,obj){var t=obj||new dojo.Color();t._set(Number(a[0]),Number(a[1]),Number(a[2]),Number(a[3]));if(isNaN(t.a)){t.a=1;}return t.sanitize();};dojo.colorFromString=function(str,obj){var a=dojo.Color.named[str];return a&&dojo.colorFromArray(a,obj)||dojo.colorFromRgb(str,obj)||dojo.colorFromHex(str,obj);};}if(!dojo._hasResource["dojo._base"]){dojo._hasResource["dojo._base"]=true;dojo.provide("dojo._base");}if(!dojo._hasResource["dojo._base.window"]){dojo._hasResource["dojo._base.window"]=true;dojo.provide("dojo._base.window");dojo.doc=window["document"]||null;dojo.body=function(){return dojo.doc.body||dojo.doc.getElementsByTagName("body")[0];};dojo.setContext=function(_1a7,_1a8){dojo.global=_1a7;dojo.doc=_1a8;};dojo._fireCallback=function(_1a9,_1aa,_1ab){if(_1aa&&dojo.isString(_1a9)){_1a9=_1aa[_1a9];}return _1a9.apply(_1aa,_1ab||[]);};dojo.withGlobal=function(_1ac,_1ad,_1ae,_1af){var rval;var _1b1=dojo.global;var _1b2=dojo.doc;try{dojo.setContext(_1ac,_1ac.document);rval=dojo._fireCallback(_1ad,_1ae,_1af);}finally{dojo.setContext(_1b1,_1b2);}return rval;};dojo.withDoc=function(_1b3,_1b4,_1b5,_1b6){var rval;var _1b8=dojo.doc;try{dojo.doc=_1b3;rval=dojo._fireCallback(_1b4,_1b5,_1b6);}finally{dojo.doc=_1b8;}return rval;};}if(!dojo._hasResource["dojo._base.event"]){dojo._hasResource["dojo._base.event"]=true;dojo.provide("dojo._base.event");(function(){var del=(dojo._event_listener={add:function(node,name,fp){if(!node){return;}name=del._normalizeEventName(name);fp=del._fixCallback(name,fp);var _1bd=name;if(!dojo.isIE&&(name=="mouseenter"||name=="mouseleave")){var ofp=fp;name=(name=="mouseenter")?"mouseover":"mouseout";fp=function(e){try{e.relatedTarget.tagName;}catch(e2){return;}if(!dojo.isDescendant(e.relatedTarget,node)){return ofp.call(this,e);}};}node.addEventListener(name,fp,false);return fp;},remove:function(node,_1c1,_1c2){if(node){_1c1=del._normalizeEventName(_1c1);if(!dojo.isIE&&(_1c1=="mouseenter"||_1c1=="mouseleave")){_1c1=(_1c1=="mouseenter")?"mouseover":"mouseout";}node.removeEventListener(_1c1,_1c2,false);}},_normalizeEventName:function(name){return name.slice(0,2)=="on"?name.slice(2):name;},_fixCallback:function(name,fp){return name!="keypress"?fp:function(e){return fp.call(this,del._fixEvent(e,this));};},_fixEvent:function(evt,_1c8){switch(evt.type){case "keypress":del._setKeyChar(evt);break;}return evt;},_setKeyChar:function(evt){evt.keyChar=evt.charCode?String.fromCharCode(evt.charCode):"";evt.charOrCode=evt.keyChar||evt.keyCode;},_punctMap:{106:42,111:47,186:59,187:43,188:44,189:45,190:46,191:47,192:96,219:91,220:92,221:93,222:39}});dojo.fixEvent=function(evt,_1cb){return del._fixEvent(evt,_1cb);};dojo.stopEvent=function(evt){evt.preventDefault();evt.stopPropagation();};var _1cd=dojo._listener;dojo._connect=function(obj,_1cf,_1d0,_1d1,_1d2){var _1d3=obj&&(obj.nodeType||obj.attachEvent||obj.addEventListener);var lid=!_1d3?0:(!_1d2?1:2),l=[dojo._listener,del,_1cd][lid];var h=l.add(obj,_1cf,dojo.hitch(_1d0,_1d1));return [obj,_1cf,h,lid];};dojo._disconnect=function(obj,_1d8,_1d9,_1da){([dojo._listener,del,_1cd][_1da]).remove(obj,_1d8,_1d9);};dojo.keys={BACKSPACE:8,TAB:9,CLEAR:12,ENTER:13,SHIFT:16,CTRL:17,ALT:18,PAUSE:19,CAPS_LOCK:20,ESCAPE:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT_ARROW:37,UP_ARROW:38,RIGHT_ARROW:39,DOWN_ARROW:40,INSERT:45,DELETE:46,HELP:47,LEFT_WINDOW:91,RIGHT_WINDOW:92,SELECT:93,NUMPAD_0:96,NUMPAD_1:97,NUMPAD_2:98,NUMPAD_3:99,NUMPAD_4:100,NUMPAD_5:101,NUMPAD_6:102,NUMPAD_7:103,NUMPAD_8:104,NUMPAD_9:105,NUMPAD_MULTIPLY:106,NUMPAD_PLUS:107,NUMPAD_ENTER:108,NUMPAD_MINUS:109,NUMPAD_PERIOD:110,NUMPAD_DIVIDE:111,F1:112,F2:113,F3:114,F4:115,F5:116,F6:117,F7:118,F8:119,F9:120,F10:121,F11:122,F12:123,F13:124,F14:125,F15:126,NUM_LOCK:144,SCROLL_LOCK:145};if(dojo.isIE){var _1db=function(e,code){try{return (e.keyCode=code);}catch(e){return 0;}};var iel=dojo._listener;var _1df=dojo._ieListenersName="_"+dojo._scopeName+"_listeners";if(!dojo.config._allow_leaks){_1cd=iel=dojo._ie_listener={handlers:[],add:function(_1e0,_1e1,_1e2){_1e0=_1e0||dojo.global;var f=_1e0[_1e1];if(!f||!f[_1df]){var d=dojo._getIeDispatcher();d.target=f&&(ieh.push(f)-1);d[_1df]=[];f=_1e0[_1e1]=d;}return f[_1df].push(ieh.push(_1e2)-1);},remove:function(_1e6,_1e7,_1e8){var f=(_1e6||dojo.global)[_1e7],l=f&&f[_1df];if(f&&l&&_1e8--){delete ieh[l[_1e8]];delete l[_1e8];}}};var ieh=iel.handlers;}dojo.mixin(del,{add:function(node,_1ec,fp){if(!node){return;}_1ec=del._normalizeEventName(_1ec);if(_1ec=="onkeypress"){var kd=node.onkeydown;if(!kd||!kd[_1df]||!kd._stealthKeydownHandle){var h=del.add(node,"onkeydown",del._stealthKeyDown);kd=node.onkeydown;kd._stealthKeydownHandle=h;kd._stealthKeydownRefs=1;}else{kd._stealthKeydownRefs++;}}return iel.add(node,_1ec,del._fixCallback(fp));},remove:function(node,_1f1,_1f2){_1f1=del._normalizeEventName(_1f1);iel.remove(node,_1f1,_1f2);if(_1f1=="onkeypress"){var kd=node.onkeydown;if(--kd._stealthKeydownRefs<=0){iel.remove(node,"onkeydown",kd._stealthKeydownHandle);delete kd._stealthKeydownHandle;}}},_normalizeEventName:function(_1f4){return _1f4.slice(0,2)!="on"?"on"+_1f4:_1f4;},_nop:function(){},_fixEvent:function(evt,_1f6){if(!evt){var w=_1f6&&(_1f6.ownerDocument||_1f6.document||_1f6).parentWindow||window;evt=w.event;}if(!evt){return (evt);}evt.target=evt.srcElement;evt.currentTarget=(_1f6||evt.srcElement);evt.layerX=evt.offsetX;evt.layerY=evt.offsetY;var se=evt.srcElement,doc=(se&&se.ownerDocument)||document;var _1fa=((dojo.isIE<6)||(doc["compatMode"]=="BackCompat"))?doc.body:doc.documentElement;var _1fb=dojo._getIeDocumentElementOffset();evt.pageX=evt.clientX+dojo._fixIeBiDiScrollLeft(_1fa.scrollLeft||0)-_1fb.x;evt.pageY=evt.clientY+(_1fa.scrollTop||0)-_1fb.y;if(evt.type=="mouseover"){evt.relatedTarget=evt.fromElement;}if(evt.type=="mouseout"){evt.relatedTarget=evt.toElement;}evt.stopPropagation=del._stopPropagation;evt.preventDefault=del._preventDefault;return del._fixKeys(evt);},_fixKeys:function(evt){switch(evt.type){case "keypress":var c=("charCode" in evt?evt.charCode:evt.keyCode);if(c==10){c=0;evt.keyCode=13;}else{if(c==13||c==27){c=0;}else{if(c==3){c=99;}}}evt.charCode=c;del._setKeyChar(evt);break;}return evt;},_stealthKeyDown:function(evt){var kp=evt.currentTarget.onkeypress;if(!kp||!kp[_1df]){return;}var k=evt.keyCode;var _201=(k!=13)&&(k!=32)&&(k!=27)&&(k<48||k>90)&&(k<96||k>111)&&(k<186||k>192)&&(k<219||k>222);if(_201||evt.ctrlKey){var c=_201?0:k;if(evt.ctrlKey){if(k==3||k==13){return;}else{if(c>95&&c<106){c-=48;}else{if((!evt.shiftKey)&&(c>=65&&c<=90)){c+=32;}else{c=del._punctMap[c]||c;}}}}var faux=del._synthesizeEvent(evt,{type:"keypress",faux:true,charCode:c});kp.call(evt.currentTarget,faux);evt.cancelBubble=faux.cancelBubble;evt.returnValue=faux.returnValue;_1db(evt,faux.keyCode);}},_stopPropagation:function(){this.cancelBubble=true;},_preventDefault:function(){this.bubbledKeyCode=this.keyCode;if(this.ctrlKey){_1db(this,0);}this.returnValue=false;}});dojo.stopEvent=function(evt){evt=evt||window.event;del._stopPropagation.call(evt);del._preventDefault.call(evt);};}del._synthesizeEvent=function(evt,_206){var faux=dojo.mixin({},evt,_206);del._setKeyChar(faux);faux.preventDefault=function(){evt.preventDefault();};faux.stopPropagation=function(){evt.stopPropagation();};return faux;};if(dojo.isOpera){dojo.mixin(del,{_fixEvent:function(evt,_209){switch(evt.type){case "keypress":var c=evt.which;if(c==3){c=99;}c=((c<41)&&(!evt.shiftKey)?0:c);if((evt.ctrlKey)&&(!evt.shiftKey)&&(c>=65)&&(c<=90)){c+=32;}return del._synthesizeEvent(evt,{charCode:c});}return evt;}});}if(dojo.isSafari){del._add=del.add;del._remove=del.remove;dojo.mixin(del,{add:function(node,_20c,fp){if(!node){return;}var _20e=del._add(node,_20c,fp);if(del._normalizeEventName(_20c)=="keypress"){_20e._stealthKeyDownHandle=del._add(node,"keydown",function(evt){var k=evt.keyCode;var _211=(k!=13)&&(k!=32)&&(k!=27)&&(k<48||k>90)&&(k<96||k>111)&&(k<186||k>192)&&(k<219||k>222);if(_211||evt.ctrlKey){var c=_211?0:k;if(evt.ctrlKey){if(k==3||k==13){return;}else{if(c>95&&c<106){c-=48;}else{if((!evt.shiftKey)&&(c>=65&&c<=90)){c+=32;}else{c=del._punctMap[c]||c;}}}}var faux=del._synthesizeEvent(evt,{type:"keypress",faux:true,charCode:c});fp.call(evt.currentTarget,faux);}});}return _20e;},remove:function(node,_215,_216){if(node){if(_216._stealthKeyDownHandle){del._remove(node,"keydown",_216._stealthKeyDownHandle);}del._remove(node,_215,_216);}},_fixEvent:function(evt,_218){switch(evt.type){case "keypress":if(evt.faux){return evt;}var c=evt.charCode;c=c>=32?c:0;return del._synthesizeEvent(evt,{charCode:c,faux:true});}return evt;}});}})();if(dojo.isIE){dojo._ieDispatcher=function(args,_21b){var ap=Array.prototype,h=dojo._ie_listener.handlers,c=args.callee,ls=c[dojo._ieListenersName],t=h[c.target];var r=t&&t.apply(_21b,args);var lls=[].concat(ls);for(var i in lls){if(!(i in ap)){h[lls[i]].apply(_21b,args);}}return r;};dojo._getIeDispatcher=function(){return new Function(dojo._scopeName+"._ieDispatcher(arguments, this)");};dojo._event_listener._fixCallback=function(fp){var f=dojo._event_listener._fixEvent;return function(e){return fp.call(this,f(e,this));};};}}if(!dojo._hasResource["dojo._base.html"]){dojo._hasResource["dojo._base.html"]=true;dojo.provide("dojo._base.html");try{document.execCommand("BackgroundImageCache",false,true);}catch(e){}if(dojo.isIE||dojo.isOpera){dojo.byId=function(id,doc){if(dojo.isString(id)){var _d=doc||dojo.doc;var te=_d.getElementById(id);if(te&&te.attributes.id.value==id){return te;}else{var eles=_d.all[id];if(!eles||!eles.length){return eles;}var i=0;while((te=eles[i++])){if(te.attributes.id.value==id){return te;}}}}else{return id;}};}else{dojo.byId=function(id,doc){return dojo.isString(id)?(doc||dojo.doc).getElementById(id):id;};}(function(){var d=dojo;var _230=null;dojo.addOnWindowUnload(function(){_230=null;});dojo._destroyElement=function(node){node=d.byId(node);try{if(!_230||_230.ownerDocument!=node.ownerDocument){_230=node.ownerDocument.createElement("div");}_230.appendChild(node.parentNode?node.parentNode.removeChild(node):node);_230.innerHTML="";}catch(e){}};dojo.isDescendant=function(node,_233){try{node=d.byId(node);_233=d.byId(_233);while(node){if(node===_233){return true;}node=node.parentNode;}}catch(e){}return false;};dojo.setSelectable=function(node,_235){node=d.byId(node);if(d.isMozilla){node.style.MozUserSelect=_235?"":"none";}else{if(d.isKhtml){node.style.KhtmlUserSelect=_235?"auto":"none";}else{if(d.isIE){var v=(node.unselectable=_235?"":"on");d.query("*",node).forEach("item.unselectable = '"+v+"'");}}}};var _237=function(node,ref){ref.parentNode.insertBefore(node,ref);return true;};var _23a=function(node,ref){var pn=ref.parentNode;if(ref==pn.lastChild){pn.appendChild(node);}else{return _237(node,ref.nextSibling);}return true;};dojo.place=function(node,_23f,_240){if(!node||!_23f){return false;}node=d.byId(node);_23f=d.byId(_23f);if(typeof _240=="number"){var cn=_23f.childNodes;if(!cn.length||cn.length<=_240){_23f.appendChild(node);return true;}return _237(node,_240<=0?_23f.firstChild:cn[_240]);}switch(_240){case "before":return _237(node,_23f);case "after":return _23a(node,_23f);case "first":if(_23f.firstChild){return _237(node,_23f.firstChild);}default:_23f.appendChild(node);return true;}};dojo.boxModel="content-box";if(d.isIE){var _dcm=document.compatMode;d.boxModel=_dcm=="BackCompat"||_dcm=="QuirksMode"||d.isIE<6?"border-box":"content-box";}var gcs;if(d.isSafari){gcs=function(node){var s;if(node instanceof HTMLElement){var dv=node.ownerDocument.defaultView;s=dv.getComputedStyle(node,null);if(!s&&node.style){node.style.display="";s=dv.getComputedStyle(node,null);}}return s||{};};}else{if(d.isIE){gcs=function(node){return node.nodeType==1?node.currentStyle:{};};}else{gcs=function(node){return node instanceof HTMLElement?node.ownerDocument.defaultView.getComputedStyle(node,null):{};};}}dojo.getComputedStyle=gcs;if(!d.isIE){dojo._toPixelValue=function(_249,_24a){return parseFloat(_24a)||0;};}else{dojo._toPixelValue=function(_24b,_24c){if(!_24c){return 0;}if(_24c=="medium"){return 4;}if(_24c.slice&&(_24c.slice(-2)=="px")){return parseFloat(_24c);}with(_24b){var _24d=style.left;var _24e=runtimeStyle.left;runtimeStyle.left=currentStyle.left;try{style.left=_24c;_24c=style.pixelLeft;}catch(e){_24c=0;}style.left=_24d;runtimeStyle.left=_24e;}return _24c;};}var px=d._toPixelValue;var astr="DXImageTransform.Microsoft.Alpha";var af=function(n,f){try{return n.filters.item(astr);}catch(e){return f?{}:null;}};dojo._getOpacity=d.isIE?function(node){try{return af(node).Opacity/100;}catch(e){return 1;}}:function(node){return gcs(node).opacity;};dojo._setOpacity=d.isIE?function(node,_257){var ov=_257*100;node.style.zoom=1;af(node,1).Enabled=(_257==1?false:true);if(!af(node)){node.style.filter+=" progid:"+astr+"(Opacity="+ov+")";}else{af(node,1).Opacity=ov;}if(node.nodeName.toLowerCase()=="tr"){d.query("> td",node).forEach(function(i){d._setOpacity(i,_257);});}return _257;}:function(node,_25b){return node.style.opacity=_25b;};var _25c={left:true,top:true};var _25d=/margin|padding|width|height|max|min|offset/;var _25e=function(node,type,_261){type=type.toLowerCase();if(d.isIE){if(_261=="auto"){if(type=="height"){return node.offsetHeight;}if(type=="width"){return node.offsetWidth;}}if(type=="fontweight"){switch(_261){case 700:return "bold";case 400:default:return "normal";}}}if(!(type in _25c)){_25c[type]=_25d.test(type);}return _25c[type]?px(node,_261):_261;};var _262=d.isIE?"styleFloat":"cssFloat";var _263={"cssFloat":_262,"styleFloat":_262,"float":_262};dojo.style=function(node,_265,_266){var n=d.byId(node),args=arguments.length,op=(_265=="opacity");_265=_263[_265]||_265;if(args==3){return op?d._setOpacity(n,_266):n.style[_265]=_266;}if(args==2&&op){return d._getOpacity(n);}var s=gcs(n);if(args==2&&!d.isString(_265)){for(var x in _265){d.style(node,x,_265[x]);}return s;}return (args==1)?s:_25e(n,_265,s[_265]||n.style[_265]);};dojo._getPadExtents=function(n,_26d){var s=_26d||gcs(n),l=px(n,s.paddingLeft),t=px(n,s.paddingTop);return {l:l,t:t,w:l+px(n,s.paddingRight),h:t+px(n,s.paddingBottom)};};dojo._getBorderExtents=function(n,_272){var ne="none",s=_272||gcs(n),bl=(s.borderLeftStyle!=ne?px(n,s.borderLeftWidth):0),bt=(s.borderTopStyle!=ne?px(n,s.borderTopWidth):0);return {l:bl,t:bt,w:bl+(s.borderRightStyle!=ne?px(n,s.borderRightWidth):0),h:bt+(s.borderBottomStyle!=ne?px(n,s.borderBottomWidth):0)};};dojo._getPadBorderExtents=function(n,_278){var s=_278||gcs(n),p=d._getPadExtents(n,s),b=d._getBorderExtents(n,s);return {l:p.l+b.l,t:p.t+b.t,w:p.w+b.w,h:p.h+b.h};};dojo._getMarginExtents=function(n,_27d){var s=_27d||gcs(n),l=px(n,s.marginLeft),t=px(n,s.marginTop),r=px(n,s.marginRight),b=px(n,s.marginBottom);if(d.isSafari&&(s.position!="absolute")){r=l;}return {l:l,t:t,w:l+r,h:t+b};};dojo._getMarginBox=function(node,_284){var s=_284||gcs(node),me=d._getMarginExtents(node,s);var l=node.offsetLeft-me.l,t=node.offsetTop-me.t,p=node.parentNode;if(d.isMoz){var sl=parseFloat(s.left),st=parseFloat(s.top);if(!isNaN(sl)&&!isNaN(st)){l=sl,t=st;}else{if(p&&p.style){var pcs=gcs(p);if(pcs.overflow!="visible"){var be=d._getBorderExtents(p,pcs);l+=be.l,t+=be.t;}}}}else{if(d.isOpera){if(p){var be=d._getBorderExtents(p);l-=be.l;t-=be.t;}}}return {l:l,t:t,w:node.offsetWidth+me.w,h:node.offsetHeight+me.h};};dojo._getContentBox=function(node,_28f){var s=_28f||gcs(node),pe=d._getPadExtents(node,s),be=d._getBorderExtents(node,s),w=node.clientWidth,h;if(!w){w=node.offsetWidth,h=node.offsetHeight;}else{h=node.clientHeight,be.w=be.h=0;}if(d.isOpera){pe.l+=be.l;pe.t+=be.t;}return {l:pe.l,t:pe.t,w:w-pe.w-be.w,h:h-pe.h-be.h};};dojo._getBorderBox=function(node,_296){var s=_296||gcs(node),pe=d._getPadExtents(node,s),cb=d._getContentBox(node,s);return {l:cb.l-pe.l,t:cb.t-pe.t,w:cb.w+pe.w,h:cb.h+pe.h};};dojo._setBox=function(node,l,t,w,h,u){u=u||"px";var s=node.style;if(!isNaN(l)){s.left=l+u;}if(!isNaN(t)){s.top=t+u;}if(w>=0){s.width=w+u;}if(h>=0){s.height=h+u;}};dojo._isButtonTag=function(node){return node.tagName=="BUTTON"||node.tagName=="INPUT"&&node.getAttribute("type").toUpperCase()=="BUTTON";};dojo._usesBorderBox=function(node){var n=node.tagName;return d.boxModel=="border-box"||n=="TABLE"||dojo._isButtonTag(node);};dojo._setContentSize=function(node,_2a5,_2a6,_2a7){if(d._usesBorderBox(node)){var pb=d._getPadBorderExtents(node,_2a7);if(_2a5>=0){_2a5+=pb.w;}if(_2a6>=0){_2a6+=pb.h;}}d._setBox(node,NaN,NaN,_2a5,_2a6);};dojo._setMarginBox=function(node,_2aa,_2ab,_2ac,_2ad,_2ae){var s=_2ae||gcs(node);var bb=d._usesBorderBox(node),pb=bb?_2b2:d._getPadBorderExtents(node,s);if(dojo.isSafari){if(dojo._isButtonTag(node)){var ns=node.style;if(_2ac>=0&&!ns.width){ns.width="4px";}if(_2ad>=0&&!ns.height){ns.height="4px";}}}var mb=d._getMarginExtents(node,s);if(_2ac>=0){_2ac=Math.max(_2ac-pb.w-mb.w,0);}if(_2ad>=0){_2ad=Math.max(_2ad-pb.h-mb.h,0);}d._setBox(node,_2aa,_2ab,_2ac,_2ad);};var _2b2={l:0,t:0,w:0,h:0};dojo.marginBox=function(node,box){var n=d.byId(node),s=gcs(n),b=box;return !b?d._getMarginBox(n,s):d._setMarginBox(n,b.l,b.t,b.w,b.h,s);};dojo.contentBox=function(node,box){var n=d.byId(node),s=gcs(n),b=box;return !b?d._getContentBox(n,s):d._setContentSize(n,b.w,b.h,s);};var _2bf=function(node,prop){if(!(node=(node||0).parentNode)){return 0;}var val,_2c3=0,_b=d.body();while(node&&node.style){if(gcs(node).position=="fixed"){return 0;}val=node[prop];if(val){_2c3+=val-0;if(node==_b){break;}}node=node.parentNode;}return _2c3;};dojo._docScroll=function(){var _b=d.body(),_w=d.global,de=d.doc.documentElement;return {y:(_w.pageYOffset||de.scrollTop||_b.scrollTop||0),x:(_w.pageXOffset||d._fixIeBiDiScrollLeft(de.scrollLeft)||_b.scrollLeft||0)};};dojo._isBodyLtr=function(){return !("_bodyLtr" in d)?d._bodyLtr=gcs(d.body()).direction=="ltr":d._bodyLtr;};dojo._getIeDocumentElementOffset=function(){var de=d.doc.documentElement;return (d.isIE>=7)?{x:de.getBoundingClientRect().left,y:de.getBoundingClientRect().top}:{x:d._isBodyLtr()||window.parent==window?de.clientLeft:de.offsetWidth-de.clientWidth-de.clientLeft,y:de.clientTop};};dojo._fixIeBiDiScrollLeft=function(_2c9){var dd=d.doc;if(d.isIE&&!dojo._isBodyLtr()){var de=dd.compatMode=="BackCompat"?dd.body:dd.documentElement;return _2c9+de.clientWidth-de.scrollWidth;}return _2c9;};dojo._abs=function(node,_2cd){var _2ce=node.ownerDocument;var ret={x:0,y:0};var db=d.body();if(d.isIE||(d.isFF>=3)){var _2d1=node.getBoundingClientRect();var cs;if(d.isFF){var dv=node.ownerDocument.defaultView;cs=dv.getComputedStyle(db.parentNode,null);}var _2d4=(d.isIE)?d._getIeDocumentElementOffset():{x:px(db.parentNode,cs.marginLeft),y:px(db.parentNode,cs.marginTop)};ret.x=_2d1.left-_2d4.x;ret.y=_2d1.top-_2d4.y;}else{if(node["offsetParent"]){var _2d5;if(d.isSafari&&(gcs(node).position=="absolute")&&(node.parentNode==db)){_2d5=db;}else{_2d5=db.parentNode;}var cs=gcs(node);var n=node;if(d.isOpera&&cs.position!="absolute"){n=n.offsetParent;}ret.x-=_2bf(n,"scrollLeft");ret.y-=_2bf(n,"scrollTop");var _2d7=node;do{var n=_2d7.offsetLeft;if(!d.isOpera||n>0){ret.x+=isNaN(n)?0:n;}var t=_2d7.offsetTop;ret.y+=isNaN(t)?0:t;var cs=gcs(_2d7);if(_2d7!=node){if(d.isSafari){ret.x+=px(_2d7,cs.borderLeftWidth);ret.y+=px(_2d7,cs.borderTopWidth);}else{if(d.isFF){ret.x+=2*px(_2d7,cs.borderLeftWidth);ret.y+=2*px(_2d7,cs.borderTopWidth);}}}if(d.isFF&&cs.position=="static"){var _2d9=_2d7.parentNode;while(_2d9!=_2d7.offsetParent){var pcs=gcs(_2d9);if(pcs.position=="static"){ret.x+=px(_2d7,pcs.borderLeftWidth);ret.y+=px(_2d7,pcs.borderTopWidth);}_2d9=_2d9.parentNode;}}_2d7=_2d7.offsetParent;}while((_2d7!=_2d5)&&_2d7);}else{if(node.x&&node.y){ret.x+=isNaN(node.x)?0:node.x;ret.y+=isNaN(node.y)?0:node.y;}}}if(_2cd){var _2db=d._docScroll();ret.y+=_2db.y;ret.x+=_2db.x;}return ret;};dojo.coords=function(node,_2dd){var n=d.byId(node),s=gcs(n),mb=d._getMarginBox(n,s);var abs=d._abs(n,_2dd);mb.x=abs.x;mb.y=abs.y;return mb;};var _2e2=d.isIE<8;var _2e3=function(name){switch(name.toLowerCase()){case "tabindex":return _2e2?"tabIndex":"tabindex";case "for":case "htmlfor":return _2e2?"htmlFor":"for";case "class":return d.isIE?"className":"class";default:return name;}};var _2e5={colspan:"colSpan",enctype:"enctype",frameborder:"frameborder",method:"method",rowspan:"rowSpan",scrolling:"scrolling",shape:"shape",span:"span",type:"type",valuetype:"valueType"};dojo.hasAttr=function(node,name){node=d.byId(node);var _2e8=_2e3(name);_2e8=_2e8=="htmlFor"?"for":_2e8;var attr=node.getAttributeNode&&node.getAttributeNode(_2e8);return attr?attr.specified:false;};var _2ea={};var _ctr=0;var _2ec=dojo._scopeName+"attrid";dojo.attr=function(node,name,_2ef){var args=arguments.length;if(args==2&&!d.isString(name)){for(var x in name){d.attr(node,x,name[x]);}return;}node=d.byId(node);name=_2e3(name);if(args==3){if(d.isFunction(_2ef)){var _2f2=d.attr(node,_2ec);if(!_2f2){_2f2=_ctr++;d.attr(node,_2ec,_2f2);}if(!_2ea[_2f2]){_2ea[_2f2]={};}var h=_2ea[_2f2][name];if(h){d.disconnect(h);}else{try{delete node[name];}catch(e){}}_2ea[_2f2][name]=d.connect(node,name,_2ef);}else{if((typeof _2ef=="boolean")||(name=="innerHTML")){node[name]=_2ef;}else{if((name=="style")&&(!d.isString(_2ef))){d.style(node,_2ef);}else{node.setAttribute(name,_2ef);}}}return;}else{var prop=_2e5[name.toLowerCase()];if(prop){return node[prop];}else{var _2f5=node[name];return (typeof _2f5=="boolean"||typeof _2f5=="function")?_2f5:(d.hasAttr(node,name)?node.getAttribute(name):null);}}};dojo.removeAttr=function(node,name){d.byId(node).removeAttribute(_2e3(name));};var _2f8="className";dojo.hasClass=function(node,_2fa){return ((" "+d.byId(node)[_2f8]+" ").indexOf(" "+_2fa+" ")>=0);};dojo.addClass=function(node,_2fc){node=d.byId(node);var cls=node[_2f8];if((" "+cls+" ").indexOf(" "+_2fc+" ")<0){node[_2f8]=cls+(cls?" ":"")+_2fc;}};dojo.removeClass=function(node,_2ff){node=d.byId(node);var t=d.trim((" "+node[_2f8]+" ").replace(" "+_2ff+" "," "));if(node[_2f8]!=t){node[_2f8]=t;}};dojo.toggleClass=function(node,_302,_303){if(_303===undefined){_303=!d.hasClass(node,_302);}d[_303?"addClass":"removeClass"](node,_302);};})();}if(!dojo._hasResource["dojo._base.NodeList"]){dojo._hasResource["dojo._base.NodeList"]=true;dojo.provide("dojo._base.NodeList");(function(){var d=dojo;var tnl=function(arr){arr.constructor=dojo.NodeList;dojo._mixin(arr,dojo.NodeList.prototype);return arr;};var _307=function(func,_309){return function(){var _a=arguments;var aa=d._toArray(_a,0,[null]);var s=this.map(function(i){aa[0]=i;return d[func].apply(d,aa);});return (_309||((_a.length>1)||!d.isString(_a[0])))?this:s;};};dojo.NodeList=function(){return tnl(Array.apply(null,arguments));};dojo.NodeList._wrap=tnl;dojo.extend(dojo.NodeList,{slice:function(){var a=d._toArray(arguments);return tnl(a.slice.apply(this,a));},splice:function(){var a=d._toArray(arguments);return tnl(a.splice.apply(this,a));},concat:function(){var a=d._toArray(arguments,0,[this]);return tnl(a.concat.apply([],a));},indexOf:function(_311,_312){return d.indexOf(this,_311,_312);},lastIndexOf:function(){return d.lastIndexOf.apply(d,d._toArray(arguments,0,[this]));},every:function(_313,_314){return d.every(this,_313,_314);},some:function(_315,_316){return d.some(this,_315,_316);},map:function(func,obj){return d.map(this,func,obj,d.NodeList);},forEach:function(_319,_31a){d.forEach(this,_319,_31a);return this;},coords:function(){return d.map(this,d.coords);},attr:_307("attr"),style:_307("style"),addClass:_307("addClass",true),removeClass:_307("removeClass",true),toggleClass:_307("toggleClass",true),connect:_307("connect",true),place:function(_31b,_31c){var item=d.query(_31b)[0];return this.forEach(function(i){d.place(i,item,_31c);});},orphan:function(_31f){return (_31f?d._filterQueryResult(this,_31f):this).forEach("if(item.parentNode){ item.parentNode.removeChild(item); }");},adopt:function(_320,_321){var item=this[0];return d.query(_320).forEach(function(ai){d.place(ai,item,_321||"last");});},query:function(_324){if(!_324){return this;}var ret=d.NodeList();this.forEach(function(item){ret=ret.concat(d.query(_324,item).filter(function(_327){return (_327!==undefined);}));});return ret;},filter:function(_328){var _329=this;var _a=arguments;var r=d.NodeList();var rp=function(t){if(t!==undefined){r.push(t);}};if(d.isString(_328)){_329=d._filterQueryResult(this,_a[0]);if(_a.length==1){return _329;}_a.shift();}d.forEach(d.filter(_329,_a[0],_a[1]),rp);return r;},addContent:function(_32e,_32f){var ta=d.doc.createElement("span");if(d.isString(_32e)){ta.innerHTML=_32e;}else{ta.appendChild(_32e);}if(_32f===undefined){_32f="last";}var ct=(_32f=="first"||_32f=="after")?"lastChild":"firstChild";this.forEach(function(item){var tn=ta.cloneNode(true);while(tn[ct]){d.place(tn[ct],item,_32f);}});return this;},empty:function(){return this.forEach("item.innerHTML='';");},instantiate:function(_334,_335){var c=d.isFunction(_334)?_334:d.getObject(_334);return this.forEach(function(i){new c(_335||{},i);});},at:function(){var nl=new dojo.NodeList();dojo.forEach(arguments,function(i){if(this[i]){nl.push(this[i]);}},this);return nl;}});d.forEach(["blur","focus","click","keydown","keypress","keyup","mousedown","mouseenter","mouseleave","mousemove","mouseout","mouseover","mouseup","submit","load","error"],function(evt){var _oe="on"+evt;d.NodeList.prototype[_oe]=function(a,b){return this.connect(_oe,a,b);};});})();}if(!dojo._hasResource["dojo._base.query"]){dojo._hasResource["dojo._base.query"]=true;dojo.provide("dojo._base.query");(function(){var d=dojo;var _33f=dojo.isIE?"children":"childNodes";var _340=false;var _341=function(_342){if(">~+".indexOf(_342.charAt(_342.length-1))>=0){_342+=" *";}_342+=" ";var ts=function(s,e){return d.trim(_342.slice(s,e));};var _346=[];var _347=-1;var _348=-1;var _349=-1;var _34a=-1;var _34b=-1;var inId=-1;var _34d=-1;var lc="";var cc="";var _350;var x=0;var ql=_342.length;var _353=null;var _cp=null;var _355=function(){if(_34d>=0){var tv=(_34d==x)?null:ts(_34d,x);_353[(">~+".indexOf(tv)<0)?"tag":"oper"]=tv;_34d=-1;}};var _357=function(){if(inId>=0){_353.id=ts(inId,x).replace(/\\/g,"");inId=-1;}};var _358=function(){if(_34b>=0){_353.classes.push(ts(_34b+1,x).replace(/\\/g,""));_34b=-1;}};var _359=function(){_357();_355();_358();};for(;lc=cc,cc=_342.charAt(x),x=0){if(cc=="]"){if(!_cp.attr){_cp.attr=ts(_347+1,x);}else{_cp.matchFor=ts((_349||_347+1),x);}var cmf=_cp.matchFor;if(cmf){if((cmf.charAt(0)=="\"")||(cmf.charAt(0)=="'")){_cp.matchFor=cmf.substring(1,cmf.length-1);}}_353.attrs.push(_cp);_cp=null;_347=_349=-1;}else{if(cc=="="){var _35b=("|~^$*".indexOf(lc)>=0)?lc:"";_cp.type=_35b+cc;_cp.attr=ts(_347+1,x-_35b.length);_349=x+1;}}}else{if(_348>=0){if(cc==")"){if(_34a>=0){_cp.value=ts(_348+1,x);}_34a=_348=-1;}}else{if(cc=="#"){_359();inId=x+1;}else{if(cc=="."){_359();_34b=x;}else{if(cc==":"){_359();_34a=x;}else{if(cc=="["){_359();_347=x;_cp={};}else{if(cc=="("){if(_34a>=0){_cp={name:ts(_34a+1,x),value:null};_353.pseudos.push(_cp);}_348=x;}else{if(cc==" "&&lc!=cc){_359();if(_34a>=0){_353.pseudos.push({name:ts(_34a+1,x)});}_353.hasLoops=(_353.pseudos.length||_353.attrs.length||_353.classes.length);_353.query=ts(_350,x);_353.otag=_353.tag=(_353["oper"])?null:(_353.tag||"*");if(_353.tag){_353.tag=_353.tag.toUpperCase();}_346.push(_353);_353=null;}}}}}}}}}return _346;};var _35c={"*=":function(attr,_35e){return "[contains(@"+attr+", '"+_35e+"')]";},"^=":function(attr,_360){return "[starts-with(@"+attr+", '"+_360+"')]";},"$=":function(attr,_362){return "[substring(@"+attr+", string-length(@"+attr+")-"+(_362.length-1)+")='"+_362+"']";},"~=":function(attr,_364){return "[contains(concat(' ',@"+attr+",' '), ' "+_364+" ')]";},"|=":function(attr,_366){return "[contains(concat(' ',@"+attr+",' '), ' "+_366+"-')]";},"=":function(attr,_368){return "[@"+attr+"='"+_368+"']";}};var _369=function(_36a,_36b,_36c,_36d){d.forEach(_36b.attrs,function(attr){var _36f;if(attr.type&&_36a[attr.type]){_36f=_36a[attr.type](attr.attr,attr.matchFor);}else{if(attr.attr.length){_36f=_36c(attr.attr);}}if(_36f){_36d(_36f);}});};var _370=function(_371){var _372=".";var _373=_341(d.trim(_371));while(_373.length){var tqp=_373.shift();var _375;var _376="";if(tqp.oper==">"){_375="/";tqp=_373.shift();}else{if(tqp.oper=="~"){_375="/following-sibling::";tqp=_373.shift();}else{if(tqp.oper=="+"){_375="/following-sibling::";_376="[position()=1]";tqp=_373.shift();}else{_375="//";}}}_372+=_375+tqp.tag+_376;if(tqp.id){_372+="[@id='"+tqp.id+"'][1]";}d.forEach(tqp.classes,function(cn){var cnl=cn.length;var _379=" ";if(cn.charAt(cnl-1)=="*"){_379="";cn=cn.substr(0,cnl-1);}_372+="[contains(concat(' ',@class,' '), ' "+cn+_379+"')]";});_369(_35c,tqp,function(_37a){return "[@"+_37a+"]";},function(_37b){_372+=_37b;});}return _372;};var _37c={};var _37d=function(path){if(_37c[path]){return _37c[path];}var doc=d.doc;var _380=_370(path);var tf=function(_382){var ret=[];var _384;var tdoc=doc;if(_382){tdoc=(_382.nodeType==9)?_382:_382.ownerDocument;}try{_384=tdoc.evaluate(_380,_382,null,XPathResult.ANY_TYPE,null);}catch(e){}var _386=_384.iterateNext();while(_386){ret.push(_386);_386=_384.iterateNext();}return ret;};return _37c[path]=tf;};var _387={};var _388={};var _389=function(_38a,_38b){if(!_38a){return _38b;}if(!_38b){return _38a;}return function(){return _38a.apply(window,arguments)&&_38b.apply(window,arguments);};};var _38c=function(root){var ret=[];var te,x=0,tret=root[_33f];while((te=tret[x++])){if(te.nodeType==1){ret.push(te);}}return ret;};var _392=function(root,_394){var ret=[];var te=root;while(te=te.nextSibling){if(te.nodeType==1){ret.push(te);if(_394){break;}}}return ret;};var _397=function(_398,_399,_39a,idx){var nidx=idx+1;var _39d=(_399.length==nidx);var tqp=_399[idx];if(tqp.oper){var ecn=(tqp.oper==">")?_38c(_398):_392(_398,(tqp.oper=="+"));if(!ecn||!ecn.length){return;}nidx++;_39d=(_399.length==nidx);var tf=_3a1(_399[idx+1]);for(var x=0,ecnl=ecn.length,te;x=0);};},"^=":function(attr,_3c6){return function(elem){return (_3be(elem,attr).indexOf(_3c6)==0);};},"$=":function(attr,_3c9){var tval=" "+_3c9;return function(elem){var ea=" "+_3be(elem,attr);return (ea.lastIndexOf(_3c9)==(ea.length-_3c9.length));};},"~=":function(attr,_3ce){var tval=" "+_3ce+" ";return function(elem){var ea=" "+_3be(elem,attr)+" ";return (ea.indexOf(tval)>=0);};},"|=":function(attr,_3d3){var _3d4=" "+_3d3+"-";return function(elem){var ea=" "+(elem.getAttribute(attr,2)||"");return ((ea==_3d3)||(ea.indexOf(_3d4)==0));};},"=":function(attr,_3d8){return function(elem){return (_3be(elem,attr)==_3d8);};}};var _3da={"checked":function(name,_3dc){return function(elem){return !!d.attr(elem,"checked");};},"first-child":function(name,_3df){return function(elem){if(elem.nodeType!=1){return false;}var fc=elem.previousSibling;while(fc&&(fc.nodeType!=1)){fc=fc.previousSibling;}return (!fc);};},"last-child":function(name,_3e3){return function(elem){if(elem.nodeType!=1){return false;}var nc=elem.nextSibling;while(nc&&(nc.nodeType!=1)){nc=nc.nextSibling;}return (!nc);};},"empty":function(name,_3e7){return function(elem){var cn=elem.childNodes;var cnl=elem.childNodes.length;for(var x=cnl-1;x>=0;x--){var nt=cn[x].nodeType;if((nt==1)||(nt==3)){return false;}}return true;};},"contains":function(name,_3ee){return function(elem){if(_3ee.charAt(0)=="\""||_3ee.charAt(0)=="'"){_3ee=_3ee.substr(1,_3ee.length-2);}return (elem.innerHTML.indexOf(_3ee)>=0);};},"not":function(name,_3f1){var ntf=_3a1(_341(_3f1)[0]);return function(elem){return (!ntf(elem));};},"nth-child":function(name,_3f5){var pi=parseInt;if(_3f5=="odd"){_3f5="2n+1";}else{if(_3f5=="even"){_3f5="2n";}}if(_3f5.indexOf("n")!=-1){var _3f7=_3f5.split("n",2);var pred=_3f7[0]?(_3f7[0]=="-"?-1:pi(_3f7[0])):1;var idx=_3f7[1]?pi(_3f7[1]):0;var lb=0,ub=-1;if(pred>0){if(idx<0){idx=(idx%pred)&&(pred+(idx%pred));}else{if(idx>0){if(idx>=pred){lb=idx-idx%pred;}idx=idx%pred;}}}else{if(pred<0){pred*=-1;if(idx>0){ub=idx;idx=idx%pred;}}}if(pred>0){return function(elem){var i=_3b3(elem);return (i>=lb)&&(ub<0||i<=ub)&&((i%pred)==idx);};}else{_3f5=idx;}}var _3fe=pi(_3f5);return function(elem){return (_3b3(elem)==_3fe);};}};var _400=(d.isIE)?function(cond){var clc=cond.toLowerCase();return function(elem){return (_340?elem.getAttribute(cond):elem[cond]||elem[clc]);};}:function(cond){return function(elem){return (elem&&elem.getAttribute&&elem.hasAttribute(cond));};};var _3b2=function(_406){var _407=(_388[_406.query]||_387[_406.query]);if(_407){return _407;}var ff=null;if(_406.id){if(_406.tag!="*"){ff=_389(ff,function(elem){return (elem.tagName==_406[_340?"otag":"tag"]);});}}d.forEach(_406.classes,function(_40a,idx,arr){var _40d=_40a.charAt(_40a.length-1)=="*";if(_40d){_40a=_40a.substr(0,_40a.length-1);}var re=new RegExp("(?:^|\\s)"+_40a+(_40d?".*":"")+"(?:\\s|$)");ff=_389(ff,function(elem){return re.test(elem.className);});ff.count=idx;});d.forEach(_406.pseudos,function(_410){if(_3da[_410.name]){ff=_389(ff,_3da[_410.name](_410.name,_410.value));}});_369(_3c1,_406,_400,function(_411){ff=_389(ff,_411);});if(!ff){ff=function(){return true;};}return _388[_406.query]=ff;};var _412={};var _3a6=function(_413,root){var fHit=_412[_413.query];if(fHit){return fHit;}if(_413.id&&!_413.hasLoops&&!_413.tag){return _412[_413.query]=function(root){return [d.byId(_413.id)];};}var _417=_3b2(_413);var _418;if(_413.tag&&_413.id&&!_413.hasLoops){_418=function(root){var te=d.byId(_413.id,(root.ownerDocument||root));if(_417(te)){return [te];}};}else{var tret;if(!_413.hasLoops){_418=function(root){var ret=[];var te,x=0,tret=root.getElementsByTagName(_413[_340?"otag":"tag"]);while((te=tret[x++])){ret.push(te);}return ret;};}else{_418=function(root){var ret=[];var te,x=0,tret=root.getElementsByTagName(_413[_340?"otag":"tag"]);while((te=tret[x++])){if(_417(te)){ret.push(te);}}return ret;};}}return _412[_413.query]=_418;};var _424={};var _425={"*":d.isIE?function(root){return root.all;}:function(root){return root.getElementsByTagName("*");},"~":_392,"+":function(root){return _392(root,true);},">":_38c};var _429=function(_42a){var _42b=_341(d.trim(_42a));if(_42b.length==1){var tt=_3a6(_42b[0]);tt.nozip=true;return tt;}var sqf=function(root){var _42f=_42b.slice(0);var _430;if(_42f[0].oper==">"){_430=[root];}else{_430=_3a6(_42f.shift())(root);}return _3a7(_430,_42f);};return sqf;};var _431=((document["evaluate"]&&!d.isSafari)?function(_432,root){var _434=_432.split(" ");if((!_340)&&(document["evaluate"])&&(_432.indexOf(":")==-1)&&(_432.indexOf("+")==-1)){if(((_434.length>2)&&(_432.indexOf(">")==-1))||(_434.length>3)||(_432.indexOf("[")>=0)||((1==_434.length)&&(0<=_432.indexOf(".")))){return _37d(_432);}}return _429(_432);}:_429);var _435=function(_436){if(_425[_436]){return _425[_436];}if(0>_436.indexOf(",")){return _425[_436]=_431(_436);}else{var _437=_436.split(/\s*,\s*/);var tf=function(root){var _43a=0;var ret=[];var tp;while((tp=_437[_43a++])){ret=ret.concat(_431(tp,tp.indexOf(" "))(root));}return ret;};return _425[_436]=tf;}};var _43d=0;var _zip=function(arr){if(arr&&arr.nozip){return d.NodeList._wrap(arr);}var ret=new d.NodeList();if(!arr){return ret;}if(arr[0]){ret.push(arr[0]);}if(arr.length<2){return ret;}_43d++;if(d.isIE&&_340){var _441=_43d+"";arr[0].setAttribute("_zipIdx",_441);for(var x=1,te;te=arr[x];x++){if(arr[x].getAttribute("_zipIdx")!=_441){ret.push(te);}te.setAttribute("_zipIdx",_441);}}else{arr[0]["_zipIdx"]=_43d;for(var x=1,te;te=arr[x];x++){if(arr[x]["_zipIdx"]!=_43d){ret.push(te);}te["_zipIdx"]=_43d;}}return ret;};d.query=function(_444,root){if(_444.constructor==d.NodeList){return _444;}if(!d.isString(_444)){return new d.NodeList(_444);}if(d.isString(root)){root=d.byId(root);}root=root||d.doc;var od=root.ownerDocument||root.documentElement;_340=(root.contentType&&root.contentType=="application/xml")||(!!od)&&(d.isIE?od.xml:(root.xmlVersion||od.xmlVersion));return _zip(_435(_444)(root));};d.query.pseudos=_3da;d._filterQueryResult=function(_447,_448){var tnl=new d.NodeList();var ff=(_448)?_3a1(_341(_448)[0]):function(){return true;};for(var x=0,te;te=_447[x];x++){if(ff(te)){tnl.push(te);}}return tnl;};})();}if(!dojo._hasResource["dojo._base.xhr"]){dojo._hasResource["dojo._base.xhr"]=true;dojo.provide("dojo._base.xhr");(function(){var _d=dojo;function setValue(obj,name,_450){var val=obj[name];if(_d.isString(val)){obj[name]=[val,_450];}else{if(_d.isArray(val)){val.push(_450);}else{obj[name]=_450;}}};dojo.formToObject=function(_452){var ret={};var _454="file|submit|image|reset|button|";_d.forEach(dojo.byId(_452).elements,function(item){var _in=item.name;var type=(item.type||"").toLowerCase();if(_in&&type&&_454.indexOf(type)==-1&&!item.disabled){if(type=="radio"||type=="checkbox"){if(item.checked){setValue(ret,_in,item.value);}}else{if(item.multiple){ret[_in]=[];_d.query("option",item).forEach(function(opt){if(opt.selected){setValue(ret,_in,opt.value);}});}else{setValue(ret,_in,item.value);if(type=="image"){ret[_in+".x"]=ret[_in+".y"]=ret[_in].x=ret[_in].y=0;}}}}});return ret;};dojo.objectToQuery=function(map){var enc=encodeURIComponent;var _45b=[];var _45c={};for(var name in map){var _45e=map[name];if(_45e!=_45c[name]){var _45f=enc(name)+"=";if(_d.isArray(_45e)){for(var i=0;i<_45e.length;i++){_45b.push(_45f+enc(_45e[i]));}}else{_45b.push(_45f+enc(_45e));}}}return _45b.join("&");};dojo.formToQuery=function(_461){return _d.objectToQuery(_d.formToObject(_461));};dojo.formToJson=function(_462,_463){return _d.toJson(_d.formToObject(_462),_463);};dojo.queryToObject=function(str){var ret={};var qp=str.split("&");var dec=decodeURIComponent;_d.forEach(qp,function(item){if(item.length){var _469=item.split("=");var name=dec(_469.shift());var val=dec(_469.join("="));if(_d.isString(ret[name])){ret[name]=[ret[name]];}if(_d.isArray(ret[name])){ret[name].push(val);}else{ret[name]=val;}}});return ret;};dojo._blockAsync=false;dojo._contentHandlers={"text":function(xhr){return xhr.responseText;},"json":function(xhr){return _d.fromJson(xhr.responseText||null);},"json-comment-filtered":function(xhr){if(!dojo.config.useCommentedJson){console.warn("Consider using the standard mimetype:application/json."+" json-commenting can introduce security issues. To"+" decrease the chances of hijacking, use the standard the 'json' handler and"+" prefix your json with: {}&&\n"+"Use djConfig.useCommentedJson=true to turn off this message.");}var _46f=xhr.responseText;var _470=_46f.indexOf("/*");var _471=_46f.lastIndexOf("*/");if(_470==-1||_471==-1){throw new Error("JSON was not comment filtered");}return _d.fromJson(_46f.substring(_470+2,_471));},"javascript":function(xhr){return _d.eval(xhr.responseText);},"xml":function(xhr){var _474=xhr.responseXML;if(_d.isIE&&(!_474||_474.documentElement==null)){_d.forEach(["MSXML2","Microsoft","MSXML","MSXML3"],function(_475){try{var dom=new ActiveXObject(_475+".XMLDOM");dom.async=false;dom.loadXML(xhr.responseText);_474=dom;}catch(e){}});}return _474;}};dojo._contentHandlers["json-comment-optional"]=function(xhr){var _478=_d._contentHandlers;if(xhr.responseText&&xhr.responseText.indexOf("/*")!=-1){return _478["json-comment-filtered"](xhr);}else{return _478["json"](xhr);}};dojo._ioSetArgs=function(args,_47a,_47b,_47c){var _47d={args:args,url:args.url};var _47e=null;if(args.form){var form=_d.byId(args.form);var _480=form.getAttributeNode("action");_47d.url=_47d.url||(_480?_480.value:null);_47e=_d.formToObject(form);}var _481=[{}];if(_47e){_481.push(_47e);}if(args.content){_481.push(args.content);}if(args.preventCache){_481.push({"dojo.preventCache":new Date().valueOf()});}_47d.query=_d.objectToQuery(_d.mixin.apply(null,_481));_47d.handleAs=args.handleAs||"text";var d=new _d.Deferred(_47a);d.addCallbacks(_47b,function(_483){return _47c(_483,d);});var ld=args.load;if(ld&&_d.isFunction(ld)){d.addCallback(function(_485){return ld.call(args,_485,_47d);});}var err=args.error;if(err&&_d.isFunction(err)){d.addErrback(function(_487){return err.call(args,_487,_47d);});}var _488=args.handle;if(_488&&_d.isFunction(_488)){d.addBoth(function(_489){return _488.call(args,_489,_47d);});}d.ioArgs=_47d;return d;};var _48a=function(dfd){dfd.canceled=true;var xhr=dfd.ioArgs.xhr;var _at=typeof xhr.abort;if(_at=="function"||_at=="object"||_at=="unknown"){xhr.abort();}var err=dfd.ioArgs.error;if(!err){err=new Error("xhr cancelled");err.dojoType="cancel";}return err;};var _48f=function(dfd){var ret=_d._contentHandlers[dfd.ioArgs.handleAs](dfd.ioArgs.xhr);return (typeof ret=="undefined")?null:ret;};var _492=function(_493,dfd){return _493;};var _495=null;var _496=[];var _497=function(){var now=(new Date()).getTime();if(!_d._blockAsync){for(var i=0,tif;i<_496.length&&(tif=_496[i]);i++){var dfd=tif.dfd;var func=function(){if(!dfd||dfd.canceled||!tif.validCheck(dfd)){_496.splice(i--,1);}else{if(tif.ioCheck(dfd)){_496.splice(i--,1);tif.resHandle(dfd);}else{if(dfd.startTime){if(dfd.startTime+(dfd.ioArgs.args.timeout||0)0){setTimeout(_p,de);return _t;}_p();return _t;},_play:function(_4c4){var _t=this;_t._startTime=new Date().valueOf();if(_t._paused){_t._startTime-=_t.duration*_t._percent;}_t._endTime=_t._startTime+_t.duration;_t._active=true;_t._paused=false;var _4c6=_t.curve.getValue(_t._percent);if(!_t._percent){if(!_t._startRepeatCount){_t._startRepeatCount=_t.repeat;}_t._fire("onBegin",[_4c6]);}_t._fire("onPlay",[_4c6]);_t._cycle();return _t;},pause:function(){this._stopTimer();if(!this._active){return this;}this._paused=true;this._fire("onPause",[this.curve.getValue(this._percent)]);return this;},gotoPercent:function(_4c7,_4c8){this._stopTimer();this._active=this._paused=true;this._percent=_4c7;if(_4c8){this.play();}return this;},stop:function(_4c9){if(!this._timer){return this;}this._stopTimer();if(_4c9){this._percent=1;}this._fire("onStop",[this.curve.getValue(this._percent)]);this._active=this._paused=false;return this;},status:function(){if(this._active){return this._paused?"paused":"playing";}return "stopped";},_cycle:function(){var _t=this;if(_t._active){var curr=new Date().valueOf();var step=(curr-_t._startTime)/(_t._endTime-_t._startTime);if(step>=1){step=1;}_t._percent=step;if(_t.easing){step=_t.easing(step);}_t._fire("onAnimate",[_t.curve.getValue(step)]);if(_t._percent<1){_t._startTimer();}else{_t._active=false;if(_t.repeat>0){_t.repeat--;_t.play(null,true);}else{if(_t.repeat==-1){_t.play(null,true);}else{if(_t._startRepeatCount){_t.repeat=_t._startRepeatCount;_t._startRepeatCount=0;}}}_t._percent=0;_t._fire("onEnd");_t._stopTimer();}}return _t;}});var ctr=0;var _4ce=[];var _4cf={run:function(){}};var _4d0=null;dojo._Animation.prototype._startTimer=function(){if(!this._timer){this._timer=d.connect(_4cf,"run",this,"_cycle");ctr++;}if(!_4d0){_4d0=setInterval(d.hitch(_4cf,"run"),this.rate);}};dojo._Animation.prototype._stopTimer=function(){if(this._timer){d.disconnect(this._timer);this._timer=null;ctr--;}if(ctr<=0){clearInterval(_4d0);_4d0=null;ctr=0;}};var _4d1=(d.isIE)?function(node){var ns=node.style;if(!ns.width.length&&d.style(node,"width")=="auto"){ns.width="auto";}}:function(){};dojo._fade=function(args){args.node=d.byId(args.node);var _4d5=d.mixin({properties:{}},args);var _4d6=(_4d5.properties.opacity={});_4d6.start=!("start" in _4d5)?function(){return Number(d.style(_4d5.node,"opacity"));}:_4d5.start;_4d6.end=_4d5.end;var anim=d.animateProperty(_4d5);d.connect(anim,"beforeBegin",d.partial(_4d1,_4d5.node));return anim;};dojo.fadeIn=function(args){return d._fade(d.mixin({end:1},args));};dojo.fadeOut=function(args){return d._fade(d.mixin({end:0},args));};dojo._defaultEasing=function(n){return 0.5+((Math.sin((n+1.5)*Math.PI))/2);};var _4db=function(_4dc){this._properties=_4dc;for(var p in _4dc){var prop=_4dc[p];if(prop.start instanceof d.Color){prop.tempColor=new d.Color();}}this.getValue=function(r){var ret={};for(var p in this._properties){var prop=this._properties[p];var _4e3=prop.start;if(_4e3 instanceof d.Color){ret[p]=d.blendColors(_4e3,prop.end,r,prop.tempColor).toCss();}else{if(!d.isArray(_4e3)){ret[p]=((prop.end-_4e3)*r)+_4e3+(p!="opacity"?prop.units||"px":"");}}}return ret;};};dojo.animateProperty=function(args){args.node=d.byId(args.node);if(!args.easing){args.easing=d._defaultEasing;}var anim=new d._Animation(args);d.connect(anim,"beforeBegin",anim,function(){var pm={};for(var p in this.properties){if(p=="width"||p=="height"){this.node.display="block";}var prop=this.properties[p];prop=pm[p]=d.mixin({},(d.isObject(prop)?prop:{end:prop}));if(d.isFunction(prop.start)){prop.start=prop.start();}if(d.isFunction(prop.end)){prop.end=prop.end();}var _4e9=(p.toLowerCase().indexOf("color")>=0);function getStyle(node,p){var v=({height:node.offsetHeight,width:node.offsetWidth})[p];if(v!==undefined){return v;}v=d.style(node,p);return (p=="opacity")?Number(v):(_4e9?v:parseFloat(v));};if(!("end" in prop)){prop.end=getStyle(this.node,p);}else{if(!("start" in prop)){prop.start=getStyle(this.node,p);}}if(_4e9){prop.start=new d.Color(prop.start);prop.end=new d.Color(prop.end);}else{prop.start=(p=="opacity")?Number(prop.start):parseFloat(prop.start);}}this.curve=new _4db(pm);});d.connect(anim,"onAnimate",d.hitch(d,"style",anim.node));return anim;};dojo.anim=function(node,_4ee,_4ef,_4f0,_4f1,_4f2){return d.animateProperty({node:node,duration:_4ef||d._Animation.prototype.duration,properties:_4ee,easing:_4f0,onEnd:_4f1}).play(_4f2||0);};})();}if(!dojo._hasResource["dojo._base.browser"]){dojo._hasResource["dojo._base.browser"]=true;dojo.provide("dojo._base.browser");if(dojo.config.require){dojo.forEach(dojo.config.require,"dojo['require'](item);");}}if(dojo.config.afterOnLoad&&dojo.isBrowser){window.setTimeout(dojo._fakeLoadInit,1000);}})();
diff --git a/tests/javascript/frameworks/dojo/dojo-1.3.3.js b/tests/javascript/frameworks/dojo/dojo-1.3.3.js
new file mode 100644
index 0000000000000000000000000000000000000000..a7b343fbedf2d7c8168fc0247b56b4aa05b38c1d
--- /dev/null
+++ b/tests/javascript/frameworks/dojo/dojo-1.3.3.js
@@ -0,0 +1,9382 @@
+/*
+ Copyright (c) 2004-2009, The Dojo Foundation All Rights Reserved.
+ Available via Academic Free License >= 2.1 OR the modified BSD license.
+ see: http://dojotoolkit.org/license for details
+*/
+
+/*
+ This is a compiled version of Dojo, built for deployment and not for
+ development. To get an editable version, please visit:
+
+ http://dojotoolkit.org
+
+ for documentation and information on getting the source.
+*/
+
+;(function(){
+
+ /*
+ dojo, dijit, and dojox must always be the first three, and in that order.
+ djConfig.scopeMap = [
+ ["dojo", "fojo"],
+ ["dijit", "fijit"],
+ ["dojox", "fojox"]
+
+ ]
+ */
+
+ /**Build will replace this comment with a scoped djConfig **/
+
+ //The null below can be relaced by a build-time value used instead of djConfig.scopeMap.
+ var sMap = null;
+
+ //See if new scopes need to be defined.
+ if((sMap || (typeof djConfig != "undefined" && djConfig.scopeMap)) && (typeof window != "undefined")){
+ var scopeDef = "", scopePrefix = "", scopeSuffix = "", scopeMap = {}, scopeMapRev = {};
+ sMap = sMap || djConfig.scopeMap;
+ for(var i = 0; i < sMap.length; i++){
+ //Make local variables, then global variables that use the locals.
+ var newScope = sMap[i];
+ scopeDef += "var " + newScope[0] + " = {}; " + newScope[1] + " = " + newScope[0] + ";" + newScope[1] + "._scopeName = '" + newScope[1] + "';";
+ scopePrefix += (i == 0 ? "" : ",") + newScope[0];
+ scopeSuffix += (i == 0 ? "" : ",") + newScope[1];
+ scopeMap[newScope[0]] = newScope[1];
+ scopeMapRev[newScope[1]] = newScope[0];
+ }
+
+ eval(scopeDef + "dojo._scopeArgs = [" + scopeSuffix + "];");
+
+ dojo._scopePrefixArgs = scopePrefix;
+ dojo._scopePrefix = "(function(" + scopePrefix + "){";
+ dojo._scopeSuffix = "})(" + scopeSuffix + ")";
+ dojo._scopeMap = scopeMap;
+ dojo._scopeMapRev = scopeMapRev;
+ }
+
+/*=====
+// note:
+// 'djConfig' does not exist under 'dojo.*' so that it can be set before the
+// 'dojo' variable exists.
+// note:
+// Setting any of these variables *after* the library has loaded does
+// nothing at all.
+
+djConfig = {
+ // summary:
+ // Application code can set the global 'djConfig' prior to loading
+ // the library to override certain global settings for how dojo works.
+ //
+ // isDebug: Boolean
+ // Defaults to `false`. If set to `true`, ensures that Dojo provides
+ // extended debugging feedback via Firebug. If Firebug is not available
+ // on your platform, setting `isDebug` to `true` will force Dojo to
+ // pull in (and display) the version of Firebug Lite which is
+ // integrated into the Dojo distribution, thereby always providing a
+ // debugging/logging console when `isDebug` is enabled. Note that
+ // Firebug's `console.*` methods are ALWAYS defined by Dojo. If
+ // `isDebug` is false and you are on a platform without Firebug, these
+ // methods will be defined as no-ops.
+ isDebug: false,
+ // debugAtAllCosts: Boolean
+ // Defaults to `false`. If set to `true`, this triggers an alternate
+ // mode of the package system in which dependencies are detected and
+ // only then are resources evaluated in dependency order via
+ // `
+ // |
+ d._modulePrefixes[module] = { name: module, value: prefix };
+ }
+
+ dojo.requireLocalization = function(/*String*/moduleName, /*String*/bundleName, /*String?*/locale, /*String?*/availableFlatLocales){
+ // summary:
+ // Declares translated resources and loads them if necessary, in the
+ // same style as dojo.require. Contents of the resource bundle are
+ // typically strings, but may be any name/value pair, represented in
+ // JSON format. See also `dojo.i18n.getLocalization`.
+ //
+ // description:
+ // Load translated resource bundles provided underneath the "nls"
+ // directory within a package. Translated resources may be located in
+ // different packages throughout the source tree.
+ //
+ // Each directory is named for a locale as specified by RFC 3066,
+ // (http://www.ietf.org/rfc/rfc3066.txt), normalized in lowercase.
+ // Note that the two bundles in the example do not define all the
+ // same variants. For a given locale, bundles will be loaded for
+ // that locale and all more general locales above it, including a
+ // fallback at the root directory. For example, a declaration for
+ // the "de-at" locale will first load `nls/de-at/bundleone.js`,
+ // then `nls/de/bundleone.js` and finally `nls/bundleone.js`. The
+ // data will be flattened into a single Object so that lookups
+ // will follow this cascading pattern. An optional build step can
+ // preload the bundles to avoid data redundancy and the multiple
+ // network hits normally required to load these resources.
+ //
+ // moduleName:
+ // name of the package containing the "nls" directory in which the
+ // bundle is found
+ //
+ // bundleName:
+ // bundle name, i.e. the filename without the '.js' suffix
+ //
+ // locale:
+ // the locale to load (optional) By default, the browser's user
+ // locale as defined by dojo.locale
+ //
+ // availableFlatLocales:
+ // A comma-separated list of the available, flattened locales for this
+ // bundle. This argument should only be set by the build process.
+ //
+ // example:
+ // A particular widget may define one or more resource bundles,
+ // structured in a program as follows, where moduleName is
+ // mycode.mywidget and bundleNames available include bundleone and
+ // bundletwo:
+ // | ...
+ // | mycode/
+ // | mywidget/
+ // | nls/
+ // | bundleone.js (the fallback translation, English in this example)
+ // | bundletwo.js (also a fallback translation)
+ // | de/
+ // | bundleone.js
+ // | bundletwo.js
+ // | de-at/
+ // | bundleone.js
+ // | en/
+ // | (empty; use the fallback translation)
+ // | en-us/
+ // | bundleone.js
+ // | en-gb/
+ // | bundleone.js
+ // | es/
+ // | bundleone.js
+ // | bundletwo.js
+ // | ...etc
+ // | ...
+ //
+
+ d.require("dojo.i18n");
+ d.i18n._requireLocalization.apply(d.hostenv, arguments);
+ };
+
+ var ore = new RegExp("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?$");
+ var ire = new RegExp("^((([^\\[:]+):)?([^@]+)@)?(\\[([^\\]]+)\\]|([^\\[:]*))(:([0-9]+))?$");
+
+ dojo._Url = function(/*dojo._Url||String...*/){
+ // summary:
+ // Constructor to create an object representing a URL.
+ // It is marked as private, since we might consider removing
+ // or simplifying it.
+ // description:
+ // Each argument is evaluated in order relative to the next until
+ // a canonical uri is produced. To get an absolute Uri relative to
+ // the current document use:
+ // new dojo._Url(document.baseURI, url)
+
+ var n = null;
+
+ var _a = arguments;
+ var uri = [_a[0]];
+ // resolve uri components relative to each other
+ for(var i = 1; i<_a.length; i++){
+ if(!_a[i]){ continue; }
+
+ // Safari doesn't support this.constructor so we have to be explicit
+ // FIXME: Tracked (and fixed) in Webkit bug 3537.
+ // http://bugs.webkit.org/show_bug.cgi?id=3537
+ var relobj = new d._Url(_a[i]+"");
+ var uriobj = new d._Url(uri[0]+"");
+
+ if(
+ relobj.path == "" &&
+ !relobj.scheme &&
+ !relobj.authority &&
+ !relobj.query
+ ){
+ if(relobj.fragment != n){
+ uriobj.fragment = relobj.fragment;
+ }
+ relobj = uriobj;
+ }else if(!relobj.scheme){
+ relobj.scheme = uriobj.scheme;
+
+ if(!relobj.authority){
+ relobj.authority = uriobj.authority;
+
+ if(relobj.path.charAt(0) != "/"){
+ var path = uriobj.path.substring(0,
+ uriobj.path.lastIndexOf("/") + 1) + relobj.path;
+
+ var segs = path.split("/");
+ for(var j = 0; j < segs.length; j++){
+ if(segs[j] == "."){
+ // flatten "./" references
+ if(j == segs.length - 1){
+ segs[j] = "";
+ }else{
+ segs.splice(j, 1);
+ j--;
+ }
+ }else if(j > 0 && !(j == 1 && segs[0] == "") &&
+ segs[j] == ".." && segs[j-1] != ".."){
+ // flatten "../" references
+ if(j == (segs.length - 1)){
+ segs.splice(j, 1);
+ segs[j - 1] = "";
+ }else{
+ segs.splice(j - 1, 2);
+ j -= 2;
+ }
+ }
+ }
+ relobj.path = segs.join("/");
+ }
+ }
+ }
+
+ uri = [];
+ if(relobj.scheme){
+ uri.push(relobj.scheme, ":");
+ }
+ if(relobj.authority){
+ uri.push("//", relobj.authority);
+ }
+ uri.push(relobj.path);
+ if(relobj.query){
+ uri.push("?", relobj.query);
+ }
+ if(relobj.fragment){
+ uri.push("#", relobj.fragment);
+ }
+ }
+
+ this.uri = uri.join("");
+
+ // break the uri into its main components
+ var r = this.uri.match(ore);
+
+ this.scheme = r[2] || (r[1] ? "" : n);
+ this.authority = r[4] || (r[3] ? "" : n);
+ this.path = r[5]; // can never be undefined
+ this.query = r[7] || (r[6] ? "" : n);
+ this.fragment = r[9] || (r[8] ? "" : n);
+
+ if(this.authority != n){
+ // server based naming authority
+ r = this.authority.match(ire);
+
+ this.user = r[3] || n;
+ this.password = r[4] || n;
+ this.host = r[6] || r[7]; // ipv6 || ipv4
+ this.port = r[9] || n;
+ }
+ }
+
+ dojo._Url.prototype.toString = function(){ return this.uri; };
+
+ dojo.moduleUrl = function(/*String*/module, /*dojo._Url||String*/url){
+ // summary:
+ // Returns a `dojo._Url` object relative to a module.
+ // example:
+ // | var pngPath = dojo.moduleUrl("acme","images/small.png");
+ // | // list the object properties
+ // | // create an image and set it's source to pngPath's value:
+ // | var img = document.createElement("img");
+ // | // NOTE: we assign the string representation of the url object
+ // | img.src = pngPath.toString();
+ // | // add our image to the document
+ // | dojo.body().appendChild(img);
+ // example:
+ // you may de-reference as far as you like down the package
+ // hierarchy. This is sometimes handy to avoid lenghty relative
+ // urls or for building portable sub-packages. In this example,
+ // the `acme.widget` and `acme.util` directories may be located
+ // under different roots (see `dojo.registerModulePath`) but the
+ // the modules which reference them can be unaware of their
+ // relative locations on the filesystem:
+ // | // somewhere in a configuration block
+ // | dojo.registerModulePath("acme.widget", "../../acme/widget");
+ // | dojo.registerModulePath("acme.util", "../../util");
+ // |
+ // | // ...
+ // |
+ // | // code in a module using acme resources
+ // | var tmpltPath = dojo.moduleUrl("acme.widget","templates/template.html");
+ // | var dataPath = dojo.moduleUrl("acme.util","resources/data.json");
+
+ var loc = d._getModuleSymbols(module).join('/');
+ if(!loc){ return null; }
+ if(loc.lastIndexOf("/") != loc.length-1){
+ loc += "/";
+ }
+
+ //If the path is an absolute path (starts with a / or is on another
+ //domain/xdomain) then don't add the baseUrl.
+ var colonIndex = loc.indexOf(":");
+ if(loc.charAt(0) != "/" && (colonIndex == -1 || colonIndex > loc.indexOf("/"))){
+ loc = d.baseUrl + loc;
+ }
+
+ return new d._Url(loc, url); // String
+ }
+})();
+
+/*=====
+dojo.isBrowser = {
+ // example:
+ // | if(dojo.isBrowser){ ... }
+};
+
+dojo.isFF = {
+ // example:
+ // | if(dojo.isFF > 1){ ... }
+};
+
+dojo.isIE = {
+ // example:
+ // | if(dojo.isIE > 6){
+ // | // we are IE7
+ // | }
+};
+
+dojo.isSafari = {
+ // example:
+ // | if(dojo.isSafari){ ... }
+ // example:
+ // Detect iPhone:
+ // | if(dojo.isSafari && navigator.userAgent.indexOf("iPhone") != -1){
+ // | // we are iPhone. Note, iPod touch reports "iPod" above and fails this test.
+ // | }
+};
+
+dojo = {
+ // isBrowser: Boolean
+ // True if the client is a web-browser
+ isBrowser: true,
+ // isFF: Number | undefined
+ // Version as a Number if client is FireFox. undefined otherwise. Corresponds to
+ // major detected FireFox version (1.5, 2, 3, etc.)
+ isFF: 2,
+ // isIE: Number | undefined
+ // Version as a Number if client is MSIE(PC). undefined otherwise. Corresponds to
+ // major detected IE version (6, 7, 8, etc.)
+ isIE: 6,
+ // isKhtml: Number | undefined
+ // Version as a Number if client is a KHTML browser. undefined otherwise. Corresponds to major
+ // detected version.
+ isKhtml: 0,
+ // isWebKit: Number | undefined
+ // Version as a Number if client is a WebKit-derived browser (Konqueror,
+ // Safari, Chrome, etc.). undefined otherwise.
+ isWebKit: 0,
+ // isMozilla: Number | undefined
+ // Version as a Number if client is a Mozilla-based browser (Firefox,
+ // SeaMonkey). undefined otherwise. Corresponds to major detected version.
+ isMozilla: 0,
+ // isOpera: Number | undefined
+ // Version as a Number if client is Opera. undefined otherwise. Corresponds to
+ // major detected version.
+ isOpera: 0,
+ // isSafari: Number | undefined
+ // Version as a Number if client is Safari or iPhone. undefined otherwise.
+ isSafari: 0
+ // isChrome: Number | undefined
+ // Version as a Number if client is Chrome browser. undefined otherwise.
+ isChrome: 0
+}
+=====*/
+
+if(typeof window != 'undefined'){
+ dojo.isBrowser = true;
+ dojo._name = "browser";
+
+ // attempt to figure out the path to dojo if it isn't set in the config
+ (function(){
+ var d = dojo;
+
+ // this is a scope protection closure. We set browser versions and grab
+ // the URL we were loaded from here.
+
+ // grab the node we were loaded from
+ if(document && document.getElementsByTagName){
+ var scripts = document.getElementsByTagName("script");
+ var rePkg = /dojo(\.xd)?\.js(\W|$)/i;
+ for(var i = 0; i < scripts.length; i++){
+ var src = scripts[i].getAttribute("src");
+ if(!src){ continue; }
+ var m = src.match(rePkg);
+ if(m){
+ // find out where we came from
+ if(!d.config.baseUrl){
+ d.config.baseUrl = src.substring(0, m.index);
+ }
+ // and find out if we need to modify our behavior
+ var cfg = scripts[i].getAttribute("djConfig");
+ if(cfg){
+ var cfgo = eval("({ "+cfg+" })");
+ for(var x in cfgo){
+ dojo.config[x] = cfgo[x];
+ }
+ }
+ break; // "first Dojo wins"
+ }
+ }
+ }
+ d.baseUrl = d.config.baseUrl;
+
+ // fill in the rendering support information in dojo.render.*
+ var n = navigator;
+ var dua = n.userAgent,
+ dav = n.appVersion,
+ tv = parseFloat(dav);
+
+ if(dua.indexOf("Opera") >= 0){ d.isOpera = tv; }
+ if(dua.indexOf("AdobeAIR") >= 0){ d.isAIR = 1; }
+ d.isKhtml = (dav.indexOf("Konqueror") >= 0) ? tv : 0;
+ d.isWebKit = parseFloat(dua.split("WebKit/")[1]) || undefined;
+ d.isChrome = parseFloat(dua.split("Chrome/")[1]) || undefined;
+
+ // safari detection derived from:
+ // http://developer.apple.com/internet/safari/faq.html#anchor2
+ // http://developer.apple.com/internet/safari/uamatrix.html
+ var index = Math.max(dav.indexOf("WebKit"), dav.indexOf("Safari"), 0);
+ if(index && !dojo.isChrome){
+ // try to grab the explicit Safari version first. If we don't get
+ // one, look for less than 419.3 as the indication that we're on something
+ // "Safari 2-ish".
+ d.isSafari = parseFloat(dav.split("Version/")[1]);
+ if(!d.isSafari || parseFloat(dav.substr(index + 7)) <= 419.3){
+ d.isSafari = 2;
+ }
+ }
+
+ if(dua.indexOf("Gecko") >= 0 && !d.isKhtml && !d.isWebKit){ d.isMozilla = d.isMoz = tv; }
+ if(d.isMoz){
+ //We really need to get away from this. Consider a sane isGecko approach for the future.
+ d.isFF = parseFloat(dua.split("Firefox/")[1] || dua.split("Minefield/")[1] || dua.split("Shiretoko/")[1]) || undefined;
+ }
+ if(document.all && !d.isOpera){
+ d.isIE = parseFloat(dav.split("MSIE ")[1]) || undefined;
+ //In cases where the page has an HTTP header or META tag with
+ //X-UA-Compatible, then it is in emulation mode, for a previous
+ //version. Make sure isIE reflects the desired version.
+ //document.documentMode of 5 means quirks mode.
+ if(d.isIE >= 8 && document.documentMode != 5){
+ d.isIE = document.documentMode;
+ }
+ }
+
+ //Workaround to get local file loads of dojo to work on IE 7
+ //by forcing to not use native xhr.
+ if(dojo.isIE && window.location.protocol === "file:"){
+ dojo.config.ieForceActiveXXhr=true;
+ }
+
+ var cm = document.compatMode;
+ d.isQuirks = cm == "BackCompat" || cm == "QuirksMode" || d.isIE < 6;
+
+ // TODO: is the HTML LANG attribute relevant?
+ d.locale = dojo.config.locale || (d.isIE ? n.userLanguage : n.language).toLowerCase();
+
+ // These are in order of decreasing likelihood; this will change in time.
+ d._XMLHTTP_PROGIDS = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'];
+
+ d._xhrObj = function(){
+ // summary:
+ // does the work of portably generating a new XMLHTTPRequest object.
+ var http, last_e;
+ if(!dojo.isIE || !dojo.config.ieForceActiveXXhr){
+ try{ http = new XMLHttpRequest(); }catch(e){}
+ }
+ if(!http){
+ for(var i=0; i<3; ++i){
+ var progid = d._XMLHTTP_PROGIDS[i];
+ try{
+ http = new ActiveXObject(progid);
+ }catch(e){
+ last_e = e;
+ }
+
+ if(http){
+ d._XMLHTTP_PROGIDS = [progid]; // so faster next time
+ break;
+ }
+ }
+ }
+
+ if(!http){
+ throw new Error("XMLHTTP not available: "+last_e);
+ }
+
+ return http; // XMLHTTPRequest instance
+ }
+
+ d._isDocumentOk = function(http){
+ var stat = http.status || 0;
+ return (stat >= 200 && stat < 300) || // Boolean
+ stat == 304 || // allow any 2XX response code
+ stat == 1223 || // get it out of the cache
+ (!stat && (location.protocol=="file:" || location.protocol=="chrome:") ); // Internet Explorer mangled the status code
+ }
+
+ //See if base tag is in use.
+ //This is to fix http://trac.dojotoolkit.org/ticket/3973,
+ //but really, we need to find out how to get rid of the dojo._Url reference
+ //below and still have DOH work with the dojo.i18n test following some other
+ //test that uses the test frame to load a document (trac #2757).
+ //Opera still has problems, but perhaps a larger issue of base tag support
+ //with XHR requests (hasBase is true, but the request is still made to document
+ //path, not base path).
+ var owloc = window.location+"";
+ var base = document.getElementsByTagName("base");
+ var hasBase = (base && base.length > 0);
+
+ d._getText = function(/*URI*/ uri, /*Boolean*/ fail_ok){
+ // summary: Read the contents of the specified uri and return those contents.
+ // uri:
+ // A relative or absolute uri. If absolute, it still must be in
+ // the same "domain" as we are.
+ // fail_ok:
+ // Default false. If fail_ok and loading fails, return null
+ // instead of throwing.
+ // returns: The response text. null is returned when there is a
+ // failure and failure is okay (an exception otherwise)
+
+ // NOTE: must be declared before scope switches ie. this._xhrObj()
+ var http = this._xhrObj();
+
+ if(!hasBase && dojo._Url){
+ uri = (new dojo._Url(owloc, uri)).toString();
+ }
+
+ if(d.config.cacheBust){
+ //Make sure we have a string before string methods are used on uri
+ uri += "";
+ uri += (uri.indexOf("?") == -1 ? "?" : "&") + String(d.config.cacheBust).replace(/\W+/g,"");
+ }
+
+ http.open('GET', uri, false);
+ try{
+ http.send(null);
+ if(!d._isDocumentOk(http)){
+ var err = Error("Unable to load "+uri+" status:"+ http.status);
+ err.status = http.status;
+ err.responseText = http.responseText;
+ throw err;
+ }
+ }catch(e){
+ if(fail_ok){ return null; } // null
+ // rethrow the exception
+ throw e;
+ }
+ return http.responseText; // String
+ }
+
+ var _w = window;
+ var _handleNodeEvent = function(/*String*/evtName, /*Function*/fp){
+ // summary:
+ // non-destructively adds the specified function to the node's
+ // evtName handler.
+ // evtName: should be in the form "onclick" for "onclick" handlers.
+ // Make sure you pass in the "on" part.
+ var oldHandler = _w[evtName] || function(){};
+ _w[evtName] = function(){
+ fp.apply(_w, arguments);
+ oldHandler.apply(_w, arguments);
+ };
+ };
+
+ d._windowUnloaders = [];
+
+ d.windowUnloaded = function(){
+ // summary:
+ // signal fired by impending window destruction. You may use
+ // dojo.addOnWindowUnload() to register a listener for this
+ // event. NOTE: if you wish to dojo.connect() to this method
+ // to perform page/application cleanup, be aware that this
+ // event WILL NOT fire if no handler has been registered with
+ // dojo.addOnWindowUnload. This behavior started in Dojo 1.3.
+ // Previous versions always triggered dojo.windowUnloaded. See
+ // dojo.addOnWindowUnload for more info.
+ var mll = d._windowUnloaders;
+ while(mll.length){
+ (mll.pop())();
+ }
+ };
+
+ var _onWindowUnloadAttached = 0;
+ d.addOnWindowUnload = function(/*Object?|Function?*/obj, /*String|Function?*/functionName){
+ // summary:
+ // registers a function to be triggered when window.onunload
+ // fires.
+ // description:
+ // The first time that addOnWindowUnload is called Dojo
+ // will register a page listener to trigger your unload
+ // handler with. Note that registering these handlers may
+ // destory "fastback" page caching in browsers that support
+ // it. Be careful trying to modify the DOM or access
+ // JavaScript properties during this phase of page unloading:
+ // they may not always be available. Consider
+ // dojo.addOnUnload() if you need to modify the DOM or do
+ // heavy JavaScript work since it fires at the eqivalent of
+ // the page's "onbeforeunload" event.
+ // example:
+ // | dojo.addOnWindowUnload(functionPointer)
+ // | dojo.addOnWindowUnload(object, "functionName");
+ // | dojo.addOnWindowUnload(object, function(){ /* ... */});
+
+ d._onto(d._windowUnloaders, obj, functionName);
+ if(!_onWindowUnloadAttached){
+ _onWindowUnloadAttached = 1;
+ _handleNodeEvent("onunload", d.windowUnloaded);
+ }
+ };
+
+ var _onUnloadAttached = 0;
+ d.addOnUnload = function(/*Object?|Function?*/obj, /*String|Function?*/functionName){
+ // summary:
+ // registers a function to be triggered when the page unloads.
+ // description:
+ // The first time that addOnUnload is called Dojo will
+ // register a page listener to trigger your unload handler
+ // with.
+ //
+ // In a browser enviroment, the functions will be triggered
+ // during the window.onbeforeunload event. Be careful of doing
+ // too much work in an unload handler. onbeforeunload can be
+ // triggered if a link to download a file is clicked, or if
+ // the link is a javascript: link. In these cases, the
+ // onbeforeunload event fires, but the document is not
+ // actually destroyed. So be careful about doing destructive
+ // operations in a dojo.addOnUnload callback.
+ //
+ // Further note that calling dojo.addOnUnload will prevent
+ // browsers from using a "fast back" cache to make page
+ // loading via back button instantaneous.
+ // example:
+ // | dojo.addOnUnload(functionPointer)
+ // | dojo.addOnUnload(object, "functionName")
+ // | dojo.addOnUnload(object, function(){ /* ... */});
+
+ d._onto(d._unloaders, obj, functionName);
+ if(!_onUnloadAttached){
+ _onUnloadAttached = 1;
+ _handleNodeEvent("onbeforeunload", dojo.unloaded);
+ }
+ };
+
+ })();
+
+ dojo._initFired = false;
+ // BEGIN DOMContentLoaded, from Dean Edwards (http://dean.edwards.name/weblog/2006/06/again/)
+ dojo._loadInit = function(e){
+ dojo._initFired = true;
+ // allow multiple calls, only first one will take effect
+ // A bug in khtml calls events callbacks for document for event which isnt supported
+ // for example a created contextmenu event calls DOMContentLoaded, workaround
+ var type = e && e.type ? e.type.toLowerCase() : "load";
+ if(arguments.callee.initialized || (type != "domcontentloaded" && type != "load")){ return; }
+ arguments.callee.initialized = true;
+ if("_khtmlTimer" in dojo){
+ clearInterval(dojo._khtmlTimer);
+ delete dojo._khtmlTimer;
+ }
+
+ if(dojo._inFlightCount == 0){
+ dojo._modulesLoaded();
+ }
+ }
+
+ if(!dojo.config.afterOnLoad){
+ // START DOMContentLoaded
+ // Mozilla and Opera 9 expose the event we could use
+ if(document.addEventListener){
+ // NOTE:
+ // due to a threading issue in Firefox 2.0, we can't enable
+ // DOMContentLoaded on that platform. For more information, see:
+ // http://trac.dojotoolkit.org/ticket/1704
+ if(dojo.isWebKit > 525 || dojo.isOpera || dojo.isFF >= 3 || (dojo.isMoz && dojo.config.enableMozDomContentLoaded === true)){
+ document.addEventListener("DOMContentLoaded", dojo._loadInit, null);
+ }
+
+ // mainly for Opera 8.5, won't be fired if DOMContentLoaded fired already.
+ // also used for Mozilla because of trac #1640
+ window.addEventListener("load", dojo._loadInit, null);
+ }
+
+ if(dojo.isAIR){
+ window.addEventListener("load", dojo._loadInit, null);
+ }else if((dojo.isWebKit < 525) || dojo.isKhtml){
+ dojo._khtmlTimer = setInterval(function(){
+ if(/loaded|complete/.test(document.readyState)){
+ dojo._loadInit(); // call the onload handler
+ }
+ }, 10);
+ }
+ // END DOMContentLoaded
+ }
+
+ if(dojo.isIE){
+ // for Internet Explorer. readyState will not be achieved on init
+ // call, but dojo doesn't need it however, we'll include it
+ // because we don't know if there are other functions added that
+ // might. Note that this has changed because the build process
+ // strips all comments -- including conditional ones.
+ if(!dojo.config.afterOnLoad){
+ document.write(''
+ + ''
+ );
+ }
+
+ try{
+ document.namespaces.add("v","urn:schemas-microsoft-com:vml");
+ document.createStyleSheet().addRule("v\\:*", "behavior:url(#default#VML); display:inline-block");
+ }catch(e){}
+ }
+
+ /*
+ OpenAjax.subscribe("OpenAjax", "onload", function(){
+ if(dojo._inFlightCount == 0){
+ dojo._modulesLoaded();
+ }
+ });
+
+ OpenAjax.subscribe("OpenAjax", "onunload", function(){
+ dojo.unloaded();
+ });
+ */
+} //if (typeof window != 'undefined')
+
+//Register any module paths set up in djConfig. Need to do this
+//in the hostenvs since hostenv_browser can read djConfig from a
+//script tag's attribute.
+(function(){
+ var mp = dojo.config["modulePaths"];
+ if(mp){
+ for(var param in mp){
+ dojo.registerModulePath(param, mp[param]);
+ }
+ }
+})();
+
+//Load debug code if necessary.
+if(dojo.config.isDebug){
+ dojo.require("dojo._firebug.firebug");
+}
+
+if(dojo.config.debugAtAllCosts){
+ dojo.config.useXDomain = true;
+ dojo.require("dojo._base._loader.loader_xd");
+ dojo.require("dojo._base._loader.loader_debug");
+ dojo.require("dojo.i18n");
+}
+
+if(!dojo._hasResource["dojo._base.lang"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.lang"] = true;
+dojo.provide("dojo._base.lang");
+
+// Crockford (ish) functions
+
+dojo.isString = function(/*anything*/ it){
+ // summary:
+ // Return true if it is a String
+ return !!arguments.length && it != null && (typeof it == "string" || it instanceof String); // Boolean
+}
+
+dojo.isArray = function(/*anything*/ it){
+ // summary:
+ // Return true if it is an Array
+ return it && (it instanceof Array || typeof it == "array"); // Boolean
+}
+
+/*=====
+dojo.isFunction = function(it){
+ // summary: Return true if it is a Function
+ // it: anything
+ return; // Boolean
+}
+=====*/
+
+dojo.isFunction = (function(){
+ var _isFunction = function(/*anything*/ it){
+ var t = typeof it; // must evaluate separately due to bizarre Opera bug. See #8937
+ return it && (t == "function" || it instanceof Function); // Boolean
+ };
+
+ return dojo.isSafari ?
+ // only slow this down w/ gratuitious casting in Safari (not WebKit)
+ function(/*anything*/ it){
+ if(typeof it == "function" && it == "[object NodeList]"){ return false; }
+ return _isFunction(it); // Boolean
+ } : _isFunction;
+})();
+
+dojo.isObject = function(/*anything*/ it){
+ // summary:
+ // Returns true if it is a JavaScript object (or an Array, a Function
+ // or null)
+ return it !== undefined &&
+ (it === null || typeof it == "object" || dojo.isArray(it) || dojo.isFunction(it)); // Boolean
+}
+
+dojo.isArrayLike = function(/*anything*/ it){
+ // summary:
+ // similar to dojo.isArray() but more permissive
+ // description:
+ // Doesn't strongly test for "arrayness". Instead, settles for "isn't
+ // a string or number and has a length property". Arguments objects
+ // and DOM collections will return true when passed to
+ // dojo.isArrayLike(), but will return false when passed to
+ // dojo.isArray().
+ // returns:
+ // If it walks like a duck and quacks like a duck, return `true`
+ var d = dojo;
+ return it && it !== undefined && // Boolean
+ // keep out built-in constructors (Number, String, ...) which have length
+ // properties
+ !d.isString(it) && !d.isFunction(it) &&
+ !(it.tagName && it.tagName.toLowerCase() == 'form') &&
+ (d.isArray(it) || isFinite(it.length));
+}
+
+dojo.isAlien = function(/*anything*/ it){
+ // summary:
+ // Returns true if it is a built-in function or some other kind of
+ // oddball that *should* report as a function but doesn't
+ return it && !dojo.isFunction(it) && /\{\s*\[native code\]\s*\}/.test(String(it)); // Boolean
+}
+
+dojo.extend = function(/*Object*/ constructor, /*Object...*/ props){
+ // summary:
+ // Adds all properties and methods of props to constructor's
+ // prototype, making them available to all instances created with
+ // constructor.
+ for(var i=1, l=arguments.length; i 2){
+ return dojo._hitchArgs.apply(dojo, arguments); // Function
+ }
+ if(!method){
+ method = scope;
+ scope = null;
+ }
+ if(dojo.isString(method)){
+ scope = scope || dojo.global;
+ if(!scope[method]){ throw(['dojo.hitch: scope["', method, '"] is null (scope="', scope, '")'].join('')); }
+ return function(){ return scope[method].apply(scope, arguments || []); }; // Function
+ }
+ return !scope ? method : function(){ return method.apply(scope, arguments || []); }; // Function
+}
+
+/*=====
+dojo.delegate = function(obj, props){
+ // summary:
+ // Returns a new object which "looks" to obj for properties which it
+ // does not have a value for. Optionally takes a bag of properties to
+ // seed the returned object with initially.
+ // description:
+ // This is a small implementaton of the Boodman/Crockford delegation
+ // pattern in JavaScript. An intermediate object constructor mediates
+ // the prototype chain for the returned object, using it to delegate
+ // down to obj for property lookup when object-local lookup fails.
+ // This can be thought of similarly to ES4's "wrap", save that it does
+ // not act on types but rather on pure objects.
+ // obj:
+ // The object to delegate to for properties not found directly on the
+ // return object or in props.
+ // props:
+ // an object containing properties to assign to the returned object
+ // returns:
+ // an Object of anonymous type
+ // example:
+ // | var foo = { bar: "baz" };
+ // | var thinger = dojo.delegate(foo, { thud: "xyzzy"});
+ // | thinger.bar == "baz"; // delegated to foo
+ // | foo.thud == undefined; // by definition
+ // | thinger.thud == "xyzzy"; // mixed in from props
+ // | foo.bar = "thonk";
+ // | thinger.bar == "thonk"; // still delegated to foo's bar
+}
+=====*/
+
+dojo.delegate = dojo._delegate = (function(){
+ // boodman/crockford delegation w/ cornford optimization
+ function TMP(){}
+ return function(obj, props){
+ TMP.prototype = obj;
+ var tmp = new TMP();
+ if(props){
+ dojo._mixin(tmp, props);
+ }
+ return tmp; // Object
+ }
+})();
+
+/*=====
+dojo._toArray = function(obj, offset, startWith){
+ // summary:
+ // Converts an array-like object (i.e. arguments, DOMCollection) to an
+ // array. Returns a new Array with the elements of obj.
+ // obj: Object
+ // the object to "arrayify". We expect the object to have, at a
+ // minimum, a length property which corresponds to integer-indexed
+ // properties.
+ // offset: Number?
+ // the location in obj to start iterating from. Defaults to 0.
+ // Optional.
+ // startWith: Array?
+ // An array to pack with the properties of obj. If provided,
+ // properties in obj are appended at the end of startWith and
+ // startWith is the returned array.
+}
+=====*/
+
+(function(){
+ var efficient = function(obj, offset, startWith){
+ return (startWith||[]).concat(Array.prototype.slice.call(obj, offset||0));
+ };
+
+ var slow = function(obj, offset, startWith){
+ var arr = startWith||[];
+ for(var x = offset || 0; x < obj.length; x++){
+ arr.push(obj[x]);
+ }
+ return arr;
+ };
+
+ dojo._toArray =
+ dojo.isIE ? function(obj){
+ return ((obj.item) ? slow : efficient).apply(this, arguments);
+ } :
+ efficient;
+
+})();
+
+dojo.partial = function(/*Function|String*/method /*, ...*/){
+ // summary:
+ // similar to hitch() except that the scope object is left to be
+ // whatever the execution context eventually becomes.
+ // description:
+ // Calling dojo.partial is the functional equivalent of calling:
+ // | dojo.hitch(null, funcName, ...);
+ var arr = [ null ];
+ return dojo.hitch.apply(dojo, arr.concat(dojo._toArray(arguments))); // Function
+}
+
+dojo.clone = function(/*anything*/ o){
+ // summary:
+ // Clones objects (including DOM nodes) and all children.
+ // Warning: do not clone cyclic structures.
+ if(!o){ return o; }
+ if(dojo.isArray(o)){
+ var r = [];
+ for(var i = 0; i < o.length; ++i){
+ r.push(dojo.clone(o[i]));
+ }
+ return r; // Array
+ }
+ if(!dojo.isObject(o)){
+ return o; /*anything*/
+ }
+ if(o.nodeType && o.cloneNode){ // isNode
+ return o.cloneNode(true); // Node
+ }
+ if(o instanceof Date){
+ return new Date(o.getTime()); // Date
+ }
+ // Generic objects
+ r = new o.constructor(); // specific to dojo.declare()'d classes!
+ for(i in o){
+ if(!(i in r) || r[i] != o[i]){
+ r[i] = dojo.clone(o[i]);
+ }
+ }
+ return r; // Object
+}
+
+/*=====
+dojo.trim = function(str){
+ // summary:
+ // Trims whitespace from both sides of the string
+ // str: String
+ // String to be trimmed
+ // returns: String
+ // Returns the trimmed string
+ // description:
+ // This version of trim() was selected for inclusion into the base due
+ // to its compact size and relatively good performance
+ // (see [Steven Levithan's blog](http://blog.stevenlevithan.com/archives/faster-trim-javascript)
+ // Uses String.prototype.trim instead, if available.
+ // The fastest but longest version of this function is located at
+ // dojo.string.trim()
+ return ""; // String
+}
+=====*/
+
+dojo.trim = String.prototype.trim ?
+ function(str){ return str.trim(); } :
+ function(str){ return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); };
+
+}
+
+if(!dojo._hasResource["dojo._base.declare"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.declare"] = true;
+dojo.provide("dojo._base.declare");
+
+// this file courtesy of the TurboAjax Group, licensed under a Dojo CLA
+
+dojo.declare = function(/*String*/ className, /*Function|Function[]*/ superclass, /*Object*/ props){
+ // summary:
+ // Create a feature-rich constructor from compact notation
+ // className:
+ // The name of the constructor (loosely, a "class")
+ // stored in the "declaredClass" property in the created prototype
+ // superclass:
+ // May be null, a Function, or an Array of Functions. If an array,
+ // the first element is used as the prototypical ancestor and
+ // any following Functions become mixin ancestors.
+ // props:
+ // An object whose properties are copied to the
+ // created prototype.
+ // Add an instance-initialization function by making it a property
+ // named "constructor".
+ // description:
+ // Create a constructor using a compact notation for inheritance and
+ // prototype extension.
+ //
+ // All superclasses (including mixins) must be Functions (not simple Objects).
+ //
+ // Mixin ancestors provide a type of multiple inheritance. Prototypes of mixin
+ // ancestors are copied to the new class: changes to mixin prototypes will
+ // not affect classes to which they have been mixed in.
+ //
+ // "className" is cached in "declaredClass" property of the new class.
+ //
+ // example:
+ // | dojo.declare("my.classes.bar", my.classes.foo, {
+ // | // properties to be added to the class prototype
+ // | someValue: 2,
+ // | // initialization function
+ // | constructor: function(){
+ // | this.myComplicatedObject = new ReallyComplicatedObject();
+ // | },
+ // | // other functions
+ // | someMethod: function(){
+ // | doStuff();
+ // | }
+ // | );
+
+ // process superclass argument
+ var dd = arguments.callee, mixins;
+ if(dojo.isArray(superclass)){
+ mixins = superclass;
+ superclass = mixins.shift();
+ }
+ // construct intermediate classes for mixins
+ if(mixins){
+ dojo.forEach(mixins, function(m, i){
+ if(!m){ throw(className + ": mixin #" + i + " is null"); } // It's likely a required module is not loaded
+ superclass = dd._delegate(superclass, m);
+ });
+ }
+ // create constructor
+ var ctor = dd._delegate(superclass);
+ // extend with "props"
+ props = props || {};
+ ctor.extend(props);
+ // more prototype decoration
+ dojo.extend(ctor, {declaredClass: className, _constructor: props.constructor/*, preamble: null*/});
+ // special help for IE
+ ctor.prototype.constructor = ctor;
+ // create named reference
+ return dojo.setObject(className, ctor); // Function
+};
+
+dojo.mixin(dojo.declare, {
+ _delegate: function(base, mixin){
+ var bp = (base||0).prototype, mp = (mixin||0).prototype, dd=dojo.declare;
+ // fresh constructor, fresh prototype
+ var ctor = dd._makeCtor();
+ // cache ancestry
+ dojo.mixin(ctor, {superclass: bp, mixin: mp, extend: dd._extend});
+ // chain prototypes
+ if(base){ctor.prototype = dojo._delegate(bp);}
+ // add mixin and core
+ dojo.extend(ctor, dd._core, mp||0, {_constructor: null, preamble: null});
+ // special help for IE
+ ctor.prototype.constructor = ctor;
+ // name this class for debugging
+ ctor.prototype.declaredClass = (bp||0).declaredClass + '_' + (mp||0).declaredClass;
+ return ctor;
+ },
+ _extend: function(props){
+ var i, fn;
+ for(i in props){ if(dojo.isFunction(fn=props[i]) && !0[i]){fn.nom=i;fn.ctor=this;} }
+ dojo.extend(this, props);
+ },
+ _makeCtor: function(){
+ // we have to make a function, but don't want to close over anything
+ return function(){ this._construct(arguments); };
+ },
+ _core: {
+ _construct: function(args){
+ var c=args.callee, s=c.superclass, ct=s&&s.constructor, m=c.mixin, mct=m&&m.constructor, a=args, ii, fn;
+ // side-effect of = used on purpose here, lint may complain, don't try this at home
+ if(a[0]){
+ // FIXME: preambles for each mixin should be allowed
+ // FIXME:
+ // should we allow the preamble here NOT to modify the
+ // default args, but instead to act on each mixin
+ // independently of the class instance being constructed
+ // (for impedence matching)?
+
+ // allow any first argument w/ a "preamble" property to act as a
+ // class preamble (not exclusive of the prototype preamble)
+ if(/*dojo.isFunction*/((fn = a[0].preamble))){
+ a = fn.apply(this, a) || a;
+ }
+ }
+ // prototype preamble
+ if((fn = c.prototype.preamble)){a = fn.apply(this, a) || a;}
+ // FIXME:
+ // need to provide an optional prototype-settable
+ // "_explicitSuper" property which disables this
+ // initialize superclass
+ if(ct&&ct.apply){ct.apply(this, a);}
+ // initialize mixin
+ if(mct&&mct.apply){mct.apply(this, a);}
+ // initialize self
+ if((ii=c.prototype._constructor)){ii.apply(this, args);}
+ // post construction
+ if(this.constructor.prototype==c.prototype && (ct=this.postscript)){ ct.apply(this, args); }
+ },
+ _findMixin: function(mixin){
+ var c = this.constructor, p, m;
+ while(c){
+ p = c.superclass;
+ m = c.mixin;
+ if(m==mixin || (m instanceof mixin.constructor)){return p;}
+ if(m && m._findMixin && (m=m._findMixin(mixin))){return m;}
+ c = p && p.constructor;
+ }
+ },
+ _findMethod: function(name, method, ptype, has){
+ // consciously trading readability for bytes and speed in this low-level method
+ var p=ptype, c, m, f;
+ do{
+ c = p.constructor;
+ m = c.mixin;
+ // find method by name in our mixin ancestor
+ if(m && (m=this._findMethod(name, method, m, has))){return m;}
+ // if we found a named method that either exactly-is or exactly-is-not 'method'
+ if((f=p[name])&&(has==(f==method))){return p;}
+ // ascend chain
+ p = c.superclass;
+ }while(p);
+ // if we couldn't find an ancestor in our primary chain, try a mixin chain
+ return !has && (p=this._findMixin(ptype)) && this._findMethod(name, method, p, has);
+ },
+ inherited: function(name, args, newArgs){
+ // optionalize name argument
+ var a = arguments;
+ if(!dojo.isString(a[0])){newArgs=args; args=name; name=args.callee.nom;}
+ a = newArgs||args;
+ var c = args.callee, p = this.constructor.prototype, fn, mp;
+ // if not an instance override
+ if(this[name] != c || p[name] == c){
+ // start from memoized prototype, or
+ // find a prototype that has property 'name' == 'c'
+ mp = (c.ctor||0).superclass || this._findMethod(name, c, p, true);
+ if(!mp){throw(this.declaredClass + ': inherited method "' + name + '" mismatch');}
+ // find a prototype that has property 'name' != 'c'
+ p = this._findMethod(name, c, mp, false);
+ }
+ // we expect 'name' to be in prototype 'p'
+ fn = p && p[name];
+ if(!fn){throw(mp.declaredClass + ': inherited method "' + name + '" not found');}
+ // if the function exists, invoke it in our scope
+ return fn.apply(this, a);
+ }
+ }
+});
+
+}
+
+if(!dojo._hasResource["dojo._base.connect"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.connect"] = true;
+dojo.provide("dojo._base.connect");
+
+// this file courtesy of the TurboAjax Group, licensed under a Dojo CLA
+
+// low-level delegation machinery
+dojo._listener = {
+ // create a dispatcher function
+ getDispatcher: function(){
+ // following comments pulled out-of-line to prevent cloning them
+ // in the returned function.
+ // - indices (i) that are really in the array of listeners (ls) will
+ // not be in Array.prototype. This is the 'sparse array' trick
+ // that keeps us safe from libs that take liberties with built-in
+ // objects
+ // - listener is invoked with current scope (this)
+ return function(){
+ var ap=Array.prototype, c=arguments.callee, ls=c._listeners, t=c.target;
+ // return value comes from original target function
+ var r = t && t.apply(this, arguments);
+ // make local copy of listener array so it is immutable during processing
+ var lls;
+ lls = [].concat(ls);
+
+ // invoke listeners after target function
+ for(var i in lls){
+ if(!(i in ap)){
+ lls[i].apply(this, arguments);
+ }
+ }
+ // return value comes from original target function
+ return r;
+ }
+ },
+ // add a listener to an object
+ add: function(/*Object*/ source, /*String*/ method, /*Function*/ listener){
+ // Whenever 'method' is invoked, 'listener' will have the same scope.
+ // Trying to supporting a context object for the listener led to
+ // complexity.
+ // Non trivial to provide 'once' functionality here
+ // because listener could be the result of a dojo.hitch call,
+ // in which case two references to the same hitch target would not
+ // be equivalent.
+ source = source || dojo.global;
+ // The source method is either null, a dispatcher, or some other function
+ var f = source[method];
+ // Ensure a dispatcher
+ if(!f||!f._listeners){
+ var d = dojo._listener.getDispatcher();
+ // original target function is special
+ d.target = f;
+ // dispatcher holds a list of listeners
+ d._listeners = [];
+ // redirect source to dispatcher
+ f = source[method] = d;
+ }
+ // The contract is that a handle is returned that can
+ // identify this listener for disconnect.
+ //
+ // The type of the handle is private. Here is it implemented as Integer.
+ // DOM event code has this same contract but handle is Function
+ // in non-IE browsers.
+ //
+ // We could have separate lists of before and after listeners.
+ return f._listeners.push(listener) ; /*Handle*/
+ },
+ // remove a listener from an object
+ remove: function(/*Object*/ source, /*String*/ method, /*Handle*/ handle){
+ var f = (source||dojo.global)[method];
+ // remember that handle is the index+1 (0 is not a valid handle)
+ if(f && f._listeners && handle--){
+ delete f._listeners[handle];
+ }
+ }
+};
+
+// Multiple delegation for arbitrary methods.
+
+// This unit knows nothing about DOM,
+// but we include DOM aware
+// documentation and dontFix
+// argument here to help the autodocs.
+// Actual DOM aware code is in event.js.
+
+dojo.connect = function(/*Object|null*/ obj,
+ /*String*/ event,
+ /*Object|null*/ context,
+ /*String|Function*/ method,
+ /*Boolean*/ dontFix){
+ // summary:
+ // Create a link that calls one function when another executes.
+ //
+ // description:
+ // Connects method to event, so that after event fires, method
+ // does too. All connected functions are passed the same arguments as
+ // the event function was initially called with. You may connect as
+ // many methods to event as needed.
+ //
+ // event must be a string. If obj is null, dojo.global is used.
+ //
+ // null arguments may simply be omitted.
+ //
+ // obj[event] can resolve to a function or undefined (null).
+ // If obj[event] is null, it is assigned a function.
+ //
+ // The return value is a handle that is needed to
+ // remove this connection with dojo.disconnect.
+ //
+ // obj:
+ // The source object for the event function.
+ // Defaults to dojo.global if null.
+ // If obj is a DOM node, the connection is delegated
+ // to the DOM event manager (unless dontFix is true).
+ //
+ // event:
+ // String name of the event function in obj.
+ // I.e. identifies a property obj[event].
+ //
+ // context:
+ // The object that method will receive as "this".
+ //
+ // If context is null and method is a function, then method
+ // inherits the context of event.
+ //
+ // If method is a string then context must be the source
+ // object object for method (context[method]). If context is null,
+ // dojo.global is used.
+ //
+ // method:
+ // A function reference, or name of a function in context.
+ // The function identified by method fires after event does.
+ // method receives the same arguments as the event.
+ // See context argument comments for information on method's scope.
+ //
+ // dontFix:
+ // If obj is a DOM node, set dontFix to true to prevent delegation
+ // of this connection to the DOM event manager.
+ //
+ // example:
+ // When obj.onchange(), do ui.update():
+ // | dojo.connect(obj, "onchange", ui, "update");
+ // | dojo.connect(obj, "onchange", ui, ui.update); // same
+ //
+ // example:
+ // Using return value for disconnect:
+ // | var link = dojo.connect(obj, "onchange", ui, "update");
+ // | ...
+ // | dojo.disconnect(link);
+ //
+ // example:
+ // When onglobalevent executes, watcher.handler is invoked:
+ // | dojo.connect(null, "onglobalevent", watcher, "handler");
+ //
+ // example:
+ // When ob.onCustomEvent executes, customEventHandler is invoked:
+ // | dojo.connect(ob, "onCustomEvent", null, "customEventHandler");
+ // | dojo.connect(ob, "onCustomEvent", "customEventHandler"); // same
+ //
+ // example:
+ // When ob.onCustomEvent executes, customEventHandler is invoked
+ // with the same scope (this):
+ // | dojo.connect(ob, "onCustomEvent", null, customEventHandler);
+ // | dojo.connect(ob, "onCustomEvent", customEventHandler); // same
+ //
+ // example:
+ // When globalEvent executes, globalHandler is invoked
+ // with the same scope (this):
+ // | dojo.connect(null, "globalEvent", null, globalHandler);
+ // | dojo.connect("globalEvent", globalHandler); // same
+
+ // normalize arguments
+ var a=arguments, args=[], i=0;
+ // if a[0] is a String, obj was ommited
+ args.push(dojo.isString(a[0]) ? null : a[i++], a[i++]);
+ // if the arg-after-next is a String or Function, context was NOT omitted
+ var a1 = a[i+1];
+ args.push(dojo.isString(a1)||dojo.isFunction(a1) ? a[i++] : null, a[i++]);
+ // absorb any additional arguments
+ for(var l=a.length; i.
+ // description:
+ // JavaScript has no threads, and even if it did, threads are hard.
+ // Deferreds are a way of abstracting non-blocking events, such as the
+ // final response to an XMLHttpRequest. Deferreds create a promise to
+ // return a response a some point in the future and an easy way to
+ // register your interest in receiving that response.
+ //
+ // The most important methods for Deffered users are:
+ //
+ // * addCallback(handler)
+ // * addErrback(handler)
+ // * callback(result)
+ // * errback(result)
+ //
+ // In general, when a function returns a Deferred, users then "fill
+ // in" the second half of the contract by registering callbacks and
+ // error handlers. You may register as many callback and errback
+ // handlers as you like and they will be executed in the order
+ // registered when a result is provided. Usually this result is
+ // provided as the result of an asynchronous operation. The code
+ // "managing" the Deferred (the code that made the promise to provide
+ // an answer later) will use the callback() and errback() methods to
+ // communicate with registered listeners about the result of the
+ // operation. At this time, all registered result handlers are called
+ // *with the most recent result value*.
+ //
+ // Deferred callback handlers are treated as a chain, and each item in
+ // the chain is required to return a value that will be fed into
+ // successive handlers. The most minimal callback may be registered
+ // like this:
+ //
+ // | var d = new dojo.Deferred();
+ // | d.addCallback(function(result){ return result; });
+ //
+ // Perhaps the most common mistake when first using Deferreds is to
+ // forget to return a value (in most cases, the value you were
+ // passed).
+ //
+ // The sequence of callbacks is internally represented as a list of
+ // 2-tuples containing the callback/errback pair. For example, the
+ // following call sequence:
+ //
+ // | var d = new dojo.Deferred();
+ // | d.addCallback(myCallback);
+ // | d.addErrback(myErrback);
+ // | d.addBoth(myBoth);
+ // | d.addCallbacks(myCallback, myErrback);
+ //
+ // is translated into a Deferred with the following internal
+ // representation:
+ //
+ // | [
+ // | [myCallback, null],
+ // | [null, myErrback],
+ // | [myBoth, myBoth],
+ // | [myCallback, myErrback]
+ // | ]
+ //
+ // The Deferred also keeps track of its current status (fired). Its
+ // status may be one of three things:
+ //
+ // * -1: no value yet (initial condition)
+ // * 0: success
+ // * 1: error
+ //
+ // A Deferred will be in the error state if one of the following three
+ // conditions are met:
+ //
+ // 1. The result given to callback or errback is "instanceof" Error
+ // 2. The previous callback or errback raised an exception while
+ // executing
+ // 3. The previous callback or errback returned a value
+ // "instanceof" Error
+ //
+ // Otherwise, the Deferred will be in the success state. The state of
+ // the Deferred determines the next element in the callback sequence
+ // to run.
+ //
+ // When a callback or errback occurs with the example deferred chain,
+ // something equivalent to the following will happen (imagine
+ // that exceptions are caught and returned):
+ //
+ // | // d.callback(result) or d.errback(result)
+ // | if(!(result instanceof Error)){
+ // | result = myCallback(result);
+ // | }
+ // | if(result instanceof Error){
+ // | result = myErrback(result);
+ // | }
+ // | result = myBoth(result);
+ // | if(result instanceof Error){
+ // | result = myErrback(result);
+ // | }else{
+ // | result = myCallback(result);
+ // | }
+ //
+ // The result is then stored away in case another step is added to the
+ // callback sequence. Since the Deferred already has a value
+ // available, any new callbacks added will be called immediately.
+ //
+ // There are two other "advanced" details about this implementation
+ // that are useful:
+ //
+ // Callbacks are allowed to return Deferred instances themselves, so
+ // you can build complicated sequences of events with ease.
+ //
+ // The creator of the Deferred may specify a canceller. The canceller
+ // is a function that will be called if Deferred.cancel is called
+ // before the Deferred fires. You can use this to implement clean
+ // aborting of an XMLHttpRequest, etc. Note that cancel will fire the
+ // deferred with a CancelledError (unless your canceller returns
+ // another kind of error), so the errbacks should be prepared to
+ // handle that error for cancellable Deferreds.
+ // example:
+ // | var deferred = new dojo.Deferred();
+ // | setTimeout(function(){ deferred.callback({success: true}); }, 1000);
+ // | return deferred;
+ // example:
+ // Deferred objects are often used when making code asynchronous. It
+ // may be easiest to write functions in a synchronous manner and then
+ // split code using a deferred to trigger a response to a long-lived
+ // operation. For example, instead of register a callback function to
+ // denote when a rendering operation completes, the function can
+ // simply return a deferred:
+ //
+ // | // callback style:
+ // | function renderLotsOfData(data, callback){
+ // | var success = false
+ // | try{
+ // | for(var x in data){
+ // | renderDataitem(data[x]);
+ // | }
+ // | success = true;
+ // | }catch(e){ }
+ // | if(callback){
+ // | callback(success);
+ // | }
+ // | }
+ //
+ // | // using callback style
+ // | renderLotsOfData(someDataObj, function(success){
+ // | // handles success or failure
+ // | if(!success){
+ // | promptUserToRecover();
+ // | }
+ // | });
+ // | // NOTE: no way to add another callback here!!
+ // example:
+ // Using a Deferred doesn't simplify the sending code any, but it
+ // provides a standard interface for callers and senders alike,
+ // providing both with a simple way to service multiple callbacks for
+ // an operation and freeing both sides from worrying about details
+ // such as "did this get called already?". With Deferreds, new
+ // callbacks can be added at any time.
+ //
+ // | // Deferred style:
+ // | function renderLotsOfData(data){
+ // | var d = new dojo.Deferred();
+ // | try{
+ // | for(var x in data){
+ // | renderDataitem(data[x]);
+ // | }
+ // | d.callback(true);
+ // | }catch(e){
+ // | d.errback(new Error("rendering failed"));
+ // | }
+ // | return d;
+ // | }
+ //
+ // | // using Deferred style
+ // | renderLotsOfData(someDataObj).addErrback(function(){
+ // | promptUserToRecover();
+ // | });
+ // | // NOTE: addErrback and addCallback both return the Deferred
+ // | // again, so we could chain adding callbacks or save the
+ // | // deferred for later should we need to be notified again.
+ // example:
+ // In this example, renderLotsOfData is syncrhonous and so both
+ // versions are pretty artificial. Putting the data display on a
+ // timeout helps show why Deferreds rock:
+ //
+ // | // Deferred style and async func
+ // | function renderLotsOfData(data){
+ // | var d = new dojo.Deferred();
+ // | setTimeout(function(){
+ // | try{
+ // | for(var x in data){
+ // | renderDataitem(data[x]);
+ // | }
+ // | d.callback(true);
+ // | }catch(e){
+ // | d.errback(new Error("rendering failed"));
+ // | }
+ // | }, 100);
+ // | return d;
+ // | }
+ //
+ // | // using Deferred style
+ // | renderLotsOfData(someDataObj).addErrback(function(){
+ // | promptUserToRecover();
+ // | });
+ //
+ // Note that the caller doesn't have to change his code at all to
+ // handle the asynchronous case.
+
+ this.chain = [];
+ this.id = this._nextId();
+ this.fired = -1;
+ this.paused = 0;
+ this.results = [null, null];
+ this.canceller = canceller;
+ this.silentlyCancelled = false;
+};
+
+dojo.extend(dojo.Deferred, {
+ /*
+ makeCalled: function(){
+ // summary:
+ // returns a new, empty deferred, which is already in the called
+ // state. Calling callback() or errback() on this deferred will
+ // yeild an error and adding new handlers to it will result in
+ // them being called immediately.
+ var deferred = new dojo.Deferred();
+ deferred.callback();
+ return deferred;
+ },
+
+ toString: function(){
+ var state;
+ if(this.fired == -1){
+ state = 'unfired';
+ }else{
+ state = this.fired ? 'success' : 'error';
+ }
+ return 'Deferred(' + this.id + ', ' + state + ')';
+ },
+ */
+
+ _nextId: (function(){
+ var n = 1;
+ return function(){ return n++; };
+ })(),
+
+ cancel: function(){
+ // summary:
+ // Cancels a Deferred that has not yet received a value, or is
+ // waiting on another Deferred as its value.
+ // description:
+ // If a canceller is defined, the canceller is called. If the
+ // canceller did not return an error, or there was no canceller,
+ // then the errback chain is started.
+ var err;
+ if(this.fired == -1){
+ if(this.canceller){
+ err = this.canceller(this);
+ }else{
+ this.silentlyCancelled = true;
+ }
+ if(this.fired == -1){
+ if(!(err instanceof Error)){
+ var res = err;
+ var msg = "Deferred Cancelled";
+ if(err && err.toString){
+ msg += ": " + err.toString();
+ }
+ err = new Error(msg);
+ err.dojoType = "cancel";
+ err.cancelResult = res;
+ }
+ this.errback(err);
+ }
+ }else if( (this.fired == 0) &&
+ (this.results[0] instanceof dojo.Deferred)
+ ){
+ this.results[0].cancel();
+ }
+ },
+
+ _resback: function(res){
+ // summary:
+ // The private primitive that means either callback or errback
+ this.fired = ((res instanceof Error) ? 1 : 0);
+ this.results[this.fired] = res;
+ this._fire();
+ },
+
+ _check: function(){
+ if(this.fired != -1){
+ if(!this.silentlyCancelled){
+ throw new Error("already called!");
+ }
+ this.silentlyCancelled = false;
+ return;
+ }
+ },
+
+ callback: function(res){
+ // summary:
+ // Begin the callback sequence with a non-error value.
+
+ /*
+ callback or errback should only be called once on a given
+ Deferred.
+ */
+ this._check();
+ this._resback(res);
+ },
+
+ errback: function(/*Error*/res){
+ // summary:
+ // Begin the callback sequence with an error result.
+ this._check();
+ if(!(res instanceof Error)){
+ res = new Error(res);
+ }
+ this._resback(res);
+ },
+
+ addBoth: function(/*Function|Object*/cb, /*String?*/cbfn){
+ // summary:
+ // Add the same function as both a callback and an errback as the
+ // next element on the callback sequence.This is useful for code
+ // that you want to guarantee to run, e.g. a finalizer.
+ var enclosed = dojo.hitch.apply(dojo, arguments);
+ return this.addCallbacks(enclosed, enclosed); // dojo.Deferred
+ },
+
+ addCallback: function(/*Function|Object*/cb, /*String?*/cbfn /*...*/){
+ // summary:
+ // Add a single callback to the end of the callback sequence.
+ return this.addCallbacks(dojo.hitch.apply(dojo, arguments)); // dojo.Deferred
+ },
+
+ addErrback: function(cb, cbfn){
+ // summary:
+ // Add a single callback to the end of the callback sequence.
+ return this.addCallbacks(null, dojo.hitch.apply(dojo, arguments)); // dojo.Deferred
+ },
+
+ addCallbacks: function(cb, eb){
+ // summary:
+ // Add separate callback and errback to the end of the callback
+ // sequence.
+ this.chain.push([cb, eb])
+ if(this.fired >= 0){
+ this._fire();
+ }
+ return this; // dojo.Deferred
+ },
+
+ _fire: function(){
+ // summary:
+ // Used internally to exhaust the callback sequence when a result
+ // is available.
+ var chain = this.chain;
+ var fired = this.fired;
+ var res = this.results[fired];
+ var self = this;
+ var cb = null;
+ while(
+ (chain.length > 0) &&
+ (this.paused == 0)
+ ){
+ // Array
+ var f = chain.shift()[fired];
+ if(!f){ continue; }
+ var func = function(){
+ var ret = f(res);
+ //If no response, then use previous response.
+ if(typeof ret != "undefined"){
+ res = ret;
+ }
+ fired = ((res instanceof Error) ? 1 : 0);
+ if(res instanceof dojo.Deferred){
+ cb = function(res){
+ self._resback(res);
+ // inlined from _pause()
+ self.paused--;
+ if(
+ (self.paused == 0) &&
+ (self.fired >= 0)
+ ){
+ self._fire();
+ }
+ }
+ // inlined from _unpause
+ this.paused++;
+ }
+ };
+ if(dojo.config.debugAtAllCosts){
+ func.call(this);
+ }else{
+ try{
+ func.call(this);
+ }catch(err){
+ fired = 1;
+ res = err;
+ }
+ }
+ }
+ this.fired = fired;
+ this.results[fired] = res;
+ if((cb)&&(this.paused)){
+ // this is for "tail recursion" in case the dependent
+ // deferred is already fired
+ res.addBoth(cb);
+ }
+ }
+});
+
+}
+
+if(!dojo._hasResource["dojo._base.json"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.json"] = true;
+dojo.provide("dojo._base.json");
+
+dojo.fromJson = function(/*String*/ json){
+ // summary:
+ // Parses a [JSON](http://json.org) string to return a JavaScript object. Throws for invalid JSON strings.
+ // json:
+ // a string literal of a JSON item, for instance:
+ // `'{ "foo": [ "bar", 1, { "baz": "thud" } ] }'`
+
+ return eval("(" + json + ")"); // Object
+}
+
+dojo._escapeString = function(/*String*/str){
+ //summary:
+ // Adds escape sequences for non-visual characters, double quote and
+ // backslash and surrounds with double quotes to form a valid string
+ // literal.
+ return ('"' + str.replace(/(["\\])/g, '\\$1') + '"').
+ replace(/[\f]/g, "\\f").replace(/[\b]/g, "\\b").replace(/[\n]/g, "\\n").
+ replace(/[\t]/g, "\\t").replace(/[\r]/g, "\\r"); // string
+}
+
+dojo.toJsonIndentStr = "\t";
+dojo.toJson = function(/*Object*/ it, /*Boolean?*/ prettyPrint, /*String?*/ _indentStr){
+ // summary:
+ // Returns a [JSON](http://json.org) serialization of an object.
+ //
+ // description:
+ // Returns a [JSON](http://json.org) serialization of an object.
+ // Note that this doesn't check for infinite recursion, so don't do that!
+ //
+ // it:
+ // an object to be serialized. Objects may define their own
+ // serialization via a special "__json__" or "json" function
+ // property. If a specialized serializer has been defined, it will
+ // be used as a fallback.
+ //
+ // prettyPrint:
+ // if true, we indent objects and arrays to make the output prettier.
+ // The variable dojo.toJsonIndentStr is used as the indent string
+ // -- to use something other than the default (tab),
+ // change that variable before calling dojo.toJson().
+ //
+ // _indentStr:
+ // private variable for recursive calls when pretty printing, do not use.
+
+ if(it === undefined){
+ return "undefined";
+ }
+ var objtype = typeof it;
+ if(objtype == "number" || objtype == "boolean"){
+ return it + "";
+ }
+ if(it === null){
+ return "null";
+ }
+ if(dojo.isString(it)){
+ return dojo._escapeString(it);
+ }
+ // recurse
+ var recurse = arguments.callee;
+ // short-circuit for objects that support "json" serialization
+ // if they return "self" then just pass-through...
+ var newObj;
+ _indentStr = _indentStr || "";
+ var nextIndent = prettyPrint ? _indentStr + dojo.toJsonIndentStr : "";
+ var tf = it.__json__||it.json;
+ if(dojo.isFunction(tf)){
+ newObj = tf.call(it);
+ if(it !== newObj){
+ return recurse(newObj, prettyPrint, nextIndent);
+ }
+ }
+ if(it.nodeType && it.cloneNode){ // isNode
+ // we can't seriailize DOM nodes as regular objects because they have cycles
+ // DOM nodes could be serialized with something like outerHTML, but
+ // that can be provided by users in the form of .json or .__json__ function.
+ throw new Error("Can't serialize DOM nodes");
+ }
+
+ var sep = prettyPrint ? " " : "";
+ var newLine = prettyPrint ? "\n" : "";
+
+ // array
+ if(dojo.isArray(it)){
+ var res = dojo.map(it, function(obj){
+ var val = recurse(obj, prettyPrint, nextIndent);
+ if(typeof val != "string"){
+ val = "undefined";
+ }
+ return newLine + nextIndent + val;
+ });
+ return "[" + res.join("," + sep) + newLine + _indentStr + "]";
+ }
+ /*
+ // look in the registry
+ try {
+ window.o = it;
+ newObj = dojo.json.jsonRegistry.match(it);
+ return recurse(newObj, prettyPrint, nextIndent);
+ }catch(e){
+ //
+ }
+ // it's a function with no adapter, skip it
+ */
+ if(objtype == "function"){
+ return null; // null
+ }
+ // generic object code path
+ var output = [], key;
+ for(key in it){
+ var keyStr, val;
+ if(typeof key == "number"){
+ keyStr = '"' + key + '"';
+ }else if(typeof key == "string"){
+ keyStr = dojo._escapeString(key);
+ }else{
+ // skip non-string or number keys
+ continue;
+ }
+ val = recurse(it[key], prettyPrint, nextIndent);
+ if(typeof val != "string"){
+ // skip non-serializable values
+ continue;
+ }
+ // FIXME: use += on Moz!!
+ // MOW NOTE: using += is a pain because you have to account for the dangling comma...
+ output.push(newLine + nextIndent + keyStr + ":" + sep + val);
+ }
+ return "{" + output.join("," + sep) + newLine + _indentStr + "}"; // String
+}
+
+}
+
+if(!dojo._hasResource["dojo._base.array"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.array"] = true;
+
+dojo.provide("dojo._base.array");
+
+(function(){
+ var _getParts = function(arr, obj, cb){
+ return [
+ dojo.isString(arr) ? arr.split("") : arr,
+ obj || dojo.global,
+ // FIXME: cache the anonymous functions we create here?
+ dojo.isString(cb) ? new Function("item", "index", "array", cb) : cb
+ ];
+ };
+
+ dojo.mixin(dojo, {
+ indexOf: function( /*Array*/ array,
+ /*Object*/ value,
+ /*Integer?*/ fromIndex,
+ /*Boolean?*/ findLast){
+ // summary:
+ // locates the first index of the provided value in the
+ // passed array. If the value is not found, -1 is returned.
+ // description:
+ // For details on this method, see:
+ // http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:indexOf
+
+ var step = 1, end = array.length || 0, i = 0;
+ if(findLast){
+ i = end - 1;
+ step = end = -1;
+ }
+ if(fromIndex != undefined){ i = fromIndex; }
+ if((findLast && i > end) || i < end){
+ for(; i != end; i += step){
+ if(array[i] == value){ return i; }
+ }
+ }
+ return -1; // Number
+ },
+
+ lastIndexOf: function(/*Array*/array, /*Object*/value, /*Integer?*/fromIndex){
+ // summary:
+ // locates the last index of the provided value in the passed
+ // array. If the value is not found, -1 is returned.
+ // description:
+ // For details on this method, see:
+ // http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:lastIndexOf
+ return dojo.indexOf(array, value, fromIndex, true); // Number
+ },
+
+ forEach: function(/*Array|String*/arr, /*Function|String*/callback, /*Object?*/thisObject){
+ // summary:
+ // for every item in arr, callback is invoked. Return values are ignored.
+ // arr:
+ // the array to iterate over. If a string, operates on individual characters.
+ // callback:
+ // a function is invoked with three arguments: item, index, and array
+ // thisObject:
+ // may be used to scope the call to callback
+ // description:
+ // This function corresponds to the JavaScript 1.6
+ // Array.forEach() method. For more details, see:
+ // http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:forEach
+ // example:
+ // | // log out all members of the array:
+ // | dojo.forEach(
+ // | [ "thinger", "blah", "howdy", 10 ],
+ // | function(item){
+ // |
+ // | }
+ // | );
+ // example:
+ // | // log out the members and their indexes
+ // | dojo.forEach(
+ // | [ "thinger", "blah", "howdy", 10 ],
+ // | function(item, idx, arr){
+ // |
+ // | }
+ // | );
+ // example:
+ // | // use a scoped object member as the callback
+ // |
+ // | var obj = {
+ // | prefix: "logged via obj.callback:",
+ // | callback: function(item){
+ // |
+ // | }
+ // | };
+ // |
+ // | // specifying the scope function executes the callback in that scope
+ // | dojo.forEach(
+ // | [ "thinger", "blah", "howdy", 10 ],
+ // | obj.callback,
+ // | obj
+ // | );
+ // |
+ // | // alternately, we can accomplish the same thing with dojo.hitch()
+ // | dojo.forEach(
+ // | [ "thinger", "blah", "howdy", 10 ],
+ // | dojo.hitch(obj, "callback")
+ // | );
+
+ // match the behavior of the built-in forEach WRT empty arrs
+ if(!arr || !arr.length){ return; }
+
+ // FIXME: there are several ways of handilng thisObject. Is
+ // dojo.global always the default context?
+ var _p = _getParts(arr, thisObject, callback); arr = _p[0];
+ for(var i=0,l=arr.length; i1; });
+ // example:
+ // | // returns true
+ // | dojo.every([1, 2, 3, 4], function(item){ return item>0; });
+ return dojo._everyOrSome(true, arr, callback, thisObject); // Boolean
+ },
+
+ some: function(/*Array|String*/arr, /*Function|String*/callback, /*Object?*/thisObject){
+ // summary:
+ // Determines whether or not any item in arr satisfies the
+ // condition implemented by callback.
+ // arr:
+ // the array to iterate over. If a string, operates on individual characters.
+ // callback:
+ // a function is invoked with three arguments: item, index,
+ // and array and returns true if the condition is met.
+ // thisObject:
+ // may be used to scope the call to callback
+ // description:
+ // This function corresponds to the JavaScript 1.6
+ // Array.some() method. For more details, see:
+ // http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:some
+ // example:
+ // | // is true
+ // | dojo.some([1, 2, 3, 4], function(item){ return item>1; });
+ // example:
+ // | // is false
+ // | dojo.some([1, 2, 3, 4], function(item){ return item<1; });
+ return dojo._everyOrSome(false, arr, callback, thisObject); // Boolean
+ },
+
+ map: function(/*Array|String*/arr, /*Function|String*/callback, /*Function?*/thisObject){
+ // summary:
+ // applies callback to each element of arr and returns
+ // an Array with the results
+ // arr:
+ // the array to iterate on. If a string, operates on
+ // individual characters.
+ // callback:
+ // a function is invoked with three arguments, (item, index,
+ // array), and returns a value
+ // thisObject:
+ // may be used to scope the call to callback
+ // description:
+ // This function corresponds to the JavaScript 1.6 Array.map()
+ // method. For more details, see:
+ // http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:map
+ // example:
+ // | // returns [2, 3, 4, 5]
+ // | dojo.map([1, 2, 3, 4], function(item){ return item+1 });
+
+ var _p = _getParts(arr, thisObject, callback); arr = _p[0];
+ var outArr = (arguments[3] ? (new arguments[3]()) : []);
+ for(var i=0,l=arr.length; i1; });
+
+ var _p = _getParts(arr, thisObject, callback); arr = _p[0];
+ var outArr = [];
+ for(var i=0,l=arr.length; i>= bits;
+ t[x] = bits == 4 ? 17 * c : c;
+ });
+ t.a = 1;
+ return t; // dojo.Color
+ };
+
+ dojo.colorFromArray = function(/*Array*/ a, /*dojo.Color?*/ obj){
+ // summary: builds a color from 1, 2, 3, or 4 element array
+ var t = obj || new d.Color();
+ t._set(Number(a[0]), Number(a[1]), Number(a[2]), Number(a[3]));
+ if(isNaN(t.a)){ t.a = 1; }
+ return t.sanitize(); // dojo.Color
+ };
+
+ dojo.colorFromString = function(/*String*/ str, /*dojo.Color?*/ obj){
+ // summary:
+ // parses str for a color value.
+ // description:
+ // Acceptable input values for str may include arrays of any form
+ // accepted by dojo.colorFromArray, hex strings such as "#aaaaaa", or
+ // rgb or rgba strings such as "rgb(133, 200, 16)" or "rgba(10, 10,
+ // 10, 50)"
+ // returns:
+ // a dojo.Color object. If obj is passed, it will be the return value.
+ var a = d.Color.named[str];
+ return a && d.colorFromArray(a, obj) || d.colorFromRgb(str, obj) || d.colorFromHex(str, obj);
+ };
+
+})();
+
+}
+
+if(!dojo._hasResource["dojo._base"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base"] = true;
+dojo.provide("dojo._base");
+
+}
+
+if(!dojo._hasResource["dojo._base.window"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.window"] = true;
+dojo.provide("dojo._base.window");
+
+/*=====
+dojo.doc = {
+ // summary:
+ // Alias for the current document. 'dojo.doc' can be modified
+ // for temporary context shifting. Also see dojo.withDoc().
+ // description:
+ // Refer to dojo.doc rather
+ // than referring to 'window.document' to ensure your code runs
+ // correctly in managed contexts.
+ // example:
+ // | n.appendChild(dojo.doc.createElement('div'));
+}
+=====*/
+dojo.doc = window["document"] || null;
+
+dojo.body = function(){
+ // summary:
+ // Return the body element of the document
+ // return the body object associated with dojo.doc
+ // example:
+ // | dojo.body().appendChild(dojo.doc.createElement('div'));
+
+ // Note: document.body is not defined for a strict xhtml document
+ // Would like to memoize this, but dojo.doc can change vi dojo.withDoc().
+ return dojo.doc.body || dojo.doc.getElementsByTagName("body")[0]; // Node
+}
+
+dojo.setContext = function(/*Object*/globalObject, /*DocumentElement*/globalDocument){
+ // summary:
+ // changes the behavior of many core Dojo functions that deal with
+ // namespace and DOM lookup, changing them to work in a new global
+ // context (e.g., an iframe). The varibles dojo.global and dojo.doc
+ // are modified as a result of calling this function and the result of
+ // `dojo.body()` likewise differs.
+ dojo.global = globalObject;
+ dojo.doc = globalDocument;
+};
+
+dojo.withGlobal = function( /*Object*/globalObject,
+ /*Function*/callback,
+ /*Object?*/thisObject,
+ /*Array?*/cbArguments){
+ // summary:
+ // Invoke callback with globalObject as dojo.global and
+ // globalObject.document as dojo.doc.
+ // description:
+ // Invoke callback with globalObject as dojo.global and
+ // globalObject.document as dojo.doc. If provided, globalObject
+ // will be executed in the context of object thisObject
+ // When callback() returns or throws an error, the dojo.global
+ // and dojo.doc will be restored to its previous state.
+
+ var oldGlob = dojo.global;
+ try{
+ dojo.global = globalObject;
+ return dojo.withDoc.call(null, globalObject.document, callback, thisObject, cbArguments);
+ }finally{
+ dojo.global = oldGlob;
+ }
+}
+
+dojo.withDoc = function( /*DocumentElement*/documentObject,
+ /*Function*/callback,
+ /*Object?*/thisObject,
+ /*Array?*/cbArguments){
+ // summary:
+ // Invoke callback with documentObject as dojo.doc.
+ // description:
+ // Invoke callback with documentObject as dojo.doc. If provided,
+ // callback will be executed in the context of object thisObject
+ // When callback() returns or throws an error, the dojo.doc will
+ // be restored to its previous state.
+
+ var oldDoc = dojo.doc,
+ oldLtr = dojo._bodyLtr;
+
+ try{
+ dojo.doc = documentObject;
+ delete dojo._bodyLtr; // uncache
+
+ if(thisObject && dojo.isString(callback)){
+ callback = thisObject[callback];
+ }
+
+ return callback.apply(thisObject, cbArguments || []);
+ }finally{
+ dojo.doc = oldDoc;
+ if(oldLtr !== undefined){ dojo._bodyLtr = oldLtr; }
+ }
+};
+
+}
+
+if(!dojo._hasResource["dojo._base.event"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.event"] = true;
+dojo.provide("dojo._base.event");
+
+// this file courtesy of the TurboAjax Group, licensed under a Dojo CLA
+
+(function(){
+ // DOM event listener machinery
+ var del = (dojo._event_listener = {
+ add: function(/*DOMNode*/node, /*String*/name, /*Function*/fp){
+ if(!node){return;}
+ name = del._normalizeEventName(name);
+ fp = del._fixCallback(name, fp);
+ var oname = name;
+ if(
+ !dojo.isIE &&
+ (name == "mouseenter" || name == "mouseleave")
+ ){
+ var ofp = fp;
+ //oname = name;
+ name = (name == "mouseenter") ? "mouseover" : "mouseout";
+ fp = function(e){
+ if(dojo.isFF <= 2) {
+ // check tagName to fix a FF2 bug with invalid nodes (hidden child DIV of INPUT)
+ // which causes isDescendant to return false which causes
+ // spurious, and more importantly, incorrect mouse events to fire.
+ // TODO: remove tagName check when Firefox 2 is no longer supported
+ try{ e.relatedTarget.tagName; }catch(e2){ return; }
+ }
+ if(!dojo.isDescendant(e.relatedTarget, node)){
+ // e.type = oname; // FIXME: doesn't take? SJM: event.type is generally immutable.
+ return ofp.call(this, e);
+ }
+ }
+ }
+ node.addEventListener(name, fp, false);
+ return fp; /*Handle*/
+ },
+ remove: function(/*DOMNode*/node, /*String*/event, /*Handle*/handle){
+ // summary:
+ // clobbers the listener from the node
+ // node:
+ // DOM node to attach the event to
+ // event:
+ // the name of the handler to remove the function from
+ // handle:
+ // the handle returned from add
+ if(node){
+ event = del._normalizeEventName(event);
+ if(!dojo.isIE && (event == "mouseenter" || event == "mouseleave")){
+ event = (event == "mouseenter") ? "mouseover" : "mouseout";
+ }
+
+ node.removeEventListener(event, handle, false);
+ }
+ },
+ _normalizeEventName: function(/*String*/name){
+ // Generally, name should be lower case, unless it is special
+ // somehow (e.g. a Mozilla DOM event).
+ // Remove 'on'.
+ return name.slice(0,2) =="on" ? name.slice(2) : name;
+ },
+ _fixCallback: function(/*String*/name, fp){
+ // By default, we only invoke _fixEvent for 'keypress'
+ // If code is added to _fixEvent for other events, we have
+ // to revisit this optimization.
+ // This also applies to _fixEvent overrides for Safari and Opera
+ // below.
+ return name != "keypress" ? fp : function(e){ return fp.call(this, del._fixEvent(e, this)); };
+ },
+ _fixEvent: function(evt, sender){
+ // _fixCallback only attaches us to keypress.
+ // Switch on evt.type anyway because we might
+ // be called directly from dojo.fixEvent.
+ switch(evt.type){
+ case "keypress":
+ del._setKeyChar(evt);
+ break;
+ }
+ return evt;
+ },
+ _setKeyChar: function(evt){
+ evt.keyChar = evt.charCode ? String.fromCharCode(evt.charCode) : '';
+ evt.charOrCode = evt.keyChar || evt.keyCode;
+ },
+ // For IE and Safari: some ctrl-key combinations (mostly w/punctuation) do not emit a char code in IE
+ // we map those virtual key codes to ascii here
+ // not valid for all (non-US) keyboards, so maybe we shouldn't bother
+ _punctMap: {
+ 106:42,
+ 111:47,
+ 186:59,
+ 187:43,
+ 188:44,
+ 189:45,
+ 190:46,
+ 191:47,
+ 192:96,
+ 219:91,
+ 220:92,
+ 221:93,
+ 222:39
+ }
+ });
+
+ // DOM events
+
+ dojo.fixEvent = function(/*Event*/evt, /*DOMNode*/sender){
+ // summary:
+ // normalizes properties on the event object including event
+ // bubbling methods, keystroke normalization, and x/y positions
+ // evt: Event
+ // native event object
+ // sender: DOMNode
+ // node to treat as "currentTarget"
+ return del._fixEvent(evt, sender);
+ }
+
+ dojo.stopEvent = function(/*Event*/evt){
+ // summary:
+ // prevents propagation and clobbers the default action of the
+ // passed event
+ // evt: Event
+ // The event object. If omitted, window.event is used on IE.
+ evt.preventDefault();
+ evt.stopPropagation();
+ // NOTE: below, this method is overridden for IE
+ }
+
+ // the default listener to use on dontFix nodes, overriden for IE
+ var node_listener = dojo._listener;
+
+ // Unify connect and event listeners
+ dojo._connect = function(obj, event, context, method, dontFix){
+ // FIXME: need a more strict test
+ var isNode = obj && (obj.nodeType||obj.attachEvent||obj.addEventListener);
+ // choose one of three listener options: raw (connect.js), DOM event on a Node, custom event on a Node
+ // we need the third option to provide leak prevention on broken browsers (IE)
+ var lid = isNode ? (dontFix ? 2 : 1) : 0, l = [dojo._listener, del, node_listener][lid];
+ // create a listener
+ var h = l.add(obj, event, dojo.hitch(context, method));
+ // formerly, the disconnect package contained "l" directly, but if client code
+ // leaks the disconnect package (by connecting it to a node), referencing "l"
+ // compounds the problem.
+ // instead we return a listener id, which requires custom _disconnect below.
+ // return disconnect package
+ return [ obj, event, h, lid ];
+ }
+
+ dojo._disconnect = function(obj, event, handle, listener){
+ ([dojo._listener, del, node_listener][listener]).remove(obj, event, handle);
+ }
+
+ // Constants
+
+ // Public: client code should test
+ // keyCode against these named constants, as the
+ // actual codes can vary by browser.
+ dojo.keys = {
+ // summary: definitions for common key values
+ BACKSPACE: 8,
+ TAB: 9,
+ CLEAR: 12,
+ ENTER: 13,
+ SHIFT: 16,
+ CTRL: 17,
+ ALT: 18,
+ PAUSE: 19,
+ CAPS_LOCK: 20,
+ ESCAPE: 27,
+ SPACE: 32,
+ PAGE_UP: 33,
+ PAGE_DOWN: 34,
+ END: 35,
+ HOME: 36,
+ LEFT_ARROW: 37,
+ UP_ARROW: 38,
+ RIGHT_ARROW: 39,
+ DOWN_ARROW: 40,
+ INSERT: 45,
+ DELETE: 46,
+ HELP: 47,
+ LEFT_WINDOW: 91,
+ RIGHT_WINDOW: 92,
+ SELECT: 93,
+ NUMPAD_0: 96,
+ NUMPAD_1: 97,
+ NUMPAD_2: 98,
+ NUMPAD_3: 99,
+ NUMPAD_4: 100,
+ NUMPAD_5: 101,
+ NUMPAD_6: 102,
+ NUMPAD_7: 103,
+ NUMPAD_8: 104,
+ NUMPAD_9: 105,
+ NUMPAD_MULTIPLY: 106,
+ NUMPAD_PLUS: 107,
+ NUMPAD_ENTER: 108,
+ NUMPAD_MINUS: 109,
+ NUMPAD_PERIOD: 110,
+ NUMPAD_DIVIDE: 111,
+ F1: 112,
+ F2: 113,
+ F3: 114,
+ F4: 115,
+ F5: 116,
+ F6: 117,
+ F7: 118,
+ F8: 119,
+ F9: 120,
+ F10: 121,
+ F11: 122,
+ F12: 123,
+ F13: 124,
+ F14: 125,
+ F15: 126,
+ NUM_LOCK: 144,
+ SCROLL_LOCK: 145
+ };
+
+ // IE event normalization
+ if(dojo.isIE){
+ var _trySetKeyCode = function(e, code){
+ try{
+ // squelch errors when keyCode is read-only
+ // (e.g. if keyCode is ctrl or shift)
+ return (e.keyCode = code);
+ }catch(e){
+ return 0;
+ }
+ }
+
+ // by default, use the standard listener
+ var iel = dojo._listener;
+ var listenersName = (dojo._ieListenersName = "_" + dojo._scopeName + "_listeners");
+ // dispatcher tracking property
+ if(!dojo.config._allow_leaks){
+ // custom listener that handles leak protection for DOM events
+ node_listener = iel = dojo._ie_listener = {
+ // support handler indirection: event handler functions are
+ // referenced here. Event dispatchers hold only indices.
+ handlers: [],
+ // add a listener to an object
+ add: function(/*Object*/ source, /*String*/ method, /*Function*/ listener){
+ source = source || dojo.global;
+ var f = source[method];
+ if(!f||!f[listenersName]){
+ var d = dojo._getIeDispatcher();
+ // original target function is special
+ d.target = f && (ieh.push(f) - 1);
+ // dispatcher holds a list of indices into handlers table
+ d[listenersName] = [];
+ // redirect source to dispatcher
+ f = source[method] = d;
+ }
+ return f[listenersName].push(ieh.push(listener) - 1) ; /*Handle*/
+ },
+ // remove a listener from an object
+ remove: function(/*Object*/ source, /*String*/ method, /*Handle*/ handle){
+ var f = (source||dojo.global)[method], l = f && f[listenersName];
+ if(f && l && handle--){
+ delete ieh[l[handle]];
+ delete l[handle];
+ }
+ }
+ };
+ // alias used above
+ var ieh = iel.handlers;
+ }
+
+ dojo.mixin(del, {
+ add: function(/*DOMNode*/node, /*String*/event, /*Function*/fp){
+ if(!node){return;} // undefined
+ event = del._normalizeEventName(event);
+ if(event=="onkeypress"){
+ // we need to listen to onkeydown to synthesize
+ // keypress events that otherwise won't fire
+ // on IE
+ var kd = node.onkeydown;
+ if(!kd || !kd[listenersName] || !kd._stealthKeydownHandle){
+ var h = del.add(node, "onkeydown", del._stealthKeyDown);
+ kd = node.onkeydown;
+ kd._stealthKeydownHandle = h;
+ kd._stealthKeydownRefs = 1;
+ }else{
+ kd._stealthKeydownRefs++;
+ }
+ }
+ return iel.add(node, event, del._fixCallback(fp));
+ },
+ remove: function(/*DOMNode*/node, /*String*/event, /*Handle*/handle){
+ event = del._normalizeEventName(event);
+ iel.remove(node, event, handle);
+ if(event=="onkeypress"){
+ var kd = node.onkeydown;
+ if(--kd._stealthKeydownRefs <= 0){
+ iel.remove(node, "onkeydown", kd._stealthKeydownHandle);
+ delete kd._stealthKeydownHandle;
+ }
+ }
+ },
+ _normalizeEventName: function(/*String*/eventName){
+ // Generally, eventName should be lower case, unless it is
+ // special somehow (e.g. a Mozilla event)
+ // ensure 'on'
+ return eventName.slice(0,2) != "on" ? "on" + eventName : eventName;
+ },
+ _nop: function(){},
+ _fixEvent: function(/*Event*/evt, /*DOMNode*/sender){
+ // summary:
+ // normalizes properties on the event object including event
+ // bubbling methods, keystroke normalization, and x/y positions
+ // evt: native event object
+ // sender: node to treat as "currentTarget"
+ if(!evt){
+ var w = sender && (sender.ownerDocument || sender.document || sender).parentWindow || window;
+ evt = w.event;
+ }
+ if(!evt){return(evt);}
+ evt.target = evt.srcElement;
+ evt.currentTarget = (sender || evt.srcElement);
+ evt.layerX = evt.offsetX;
+ evt.layerY = evt.offsetY;
+ // FIXME: scroll position query is duped from dojo.html to
+ // avoid dependency on that entire module. Now that HTML is in
+ // Base, we should convert back to something similar there.
+ var se = evt.srcElement, doc = (se && se.ownerDocument) || document;
+ // DO NOT replace the following to use dojo.body(), in IE, document.documentElement should be used
+ // here rather than document.body
+ var docBody = ((dojo.isIE < 6) || (doc["compatMode"] == "BackCompat")) ? doc.body : doc.documentElement;
+ var offset = dojo._getIeDocumentElementOffset();
+ evt.pageX = evt.clientX + dojo._fixIeBiDiScrollLeft(docBody.scrollLeft || 0) - offset.x;
+ evt.pageY = evt.clientY + (docBody.scrollTop || 0) - offset.y;
+ if(evt.type == "mouseover"){
+ evt.relatedTarget = evt.fromElement;
+ }
+ if(evt.type == "mouseout"){
+ evt.relatedTarget = evt.toElement;
+ }
+ evt.stopPropagation = del._stopPropagation;
+ evt.preventDefault = del._preventDefault;
+ return del._fixKeys(evt);
+ },
+ _fixKeys: function(evt){
+ switch(evt.type){
+ case "keypress":
+ var c = ("charCode" in evt ? evt.charCode : evt.keyCode);
+ if (c==10){
+ // CTRL-ENTER is CTRL-ASCII(10) on IE, but CTRL-ENTER on Mozilla
+ c=0;
+ evt.keyCode = 13;
+ }else if(c==13||c==27){
+ c=0; // Mozilla considers ENTER and ESC non-printable
+ }else if(c==3){
+ c=99; // Mozilla maps CTRL-BREAK to CTRL-c
+ }
+ // Mozilla sets keyCode to 0 when there is a charCode
+ // but that stops the event on IE.
+ evt.charCode = c;
+ del._setKeyChar(evt);
+ break;
+ }
+ return evt;
+ },
+ _stealthKeyDown: function(evt){
+ // IE doesn't fire keypress for most non-printable characters.
+ // other browsers do, we simulate it here.
+ var kp = evt.currentTarget.onkeypress;
+ // only works if kp exists and is a dispatcher
+ if(!kp || !kp[listenersName]){ return; }
+ // munge key/charCode
+ var k=evt.keyCode;
+ // These are Windows Virtual Key Codes
+ // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/WinUI/WindowsUserInterface/UserInput/VirtualKeyCodes.asp
+ var unprintable = k!=13 && k!=32 && k!=27 && (k<48||k>90) && (k<96||k>111) && (k<186||k>192) && (k<219||k>222);
+ // synthesize keypress for most unprintables and CTRL-keys
+ if(unprintable||evt.ctrlKey){
+ var c = unprintable ? 0 : k;
+ if(evt.ctrlKey){
+ if(k==3 || k==13){
+ return; // IE will post CTRL-BREAK, CTRL-ENTER as keypress natively
+ }else if(c>95 && c<106){
+ c -= 48; // map CTRL-[numpad 0-9] to ASCII
+ }else if((!evt.shiftKey)&&(c>=65&&c<=90)){
+ c += 32; // map CTRL-[A-Z] to lowercase
+ }else{
+ c = del._punctMap[c] || c; // map other problematic CTRL combinations to ASCII
+ }
+ }
+ // simulate a keypress event
+ var faux = del._synthesizeEvent(evt, {type: 'keypress', faux: true, charCode: c});
+ kp.call(evt.currentTarget, faux);
+ evt.cancelBubble = faux.cancelBubble;
+ evt.returnValue = faux.returnValue;
+ _trySetKeyCode(evt, faux.keyCode);
+ }
+ },
+ // Called in Event scope
+ _stopPropagation: function(){
+ this.cancelBubble = true;
+ },
+ _preventDefault: function(){
+ // Setting keyCode to 0 is the only way to prevent certain keypresses (namely
+ // ctrl-combinations that correspond to menu accelerator keys).
+ // Otoh, it prevents upstream listeners from getting this information
+ // Try to split the difference here by clobbering keyCode only for ctrl
+ // combinations. If you still need to access the key upstream, bubbledKeyCode is
+ // provided as a workaround.
+ this.bubbledKeyCode = this.keyCode;
+ if(this.ctrlKey){_trySetKeyCode(this, 0);}
+ this.returnValue = false;
+ }
+ });
+
+ // override stopEvent for IE
+ dojo.stopEvent = function(evt){
+ evt = evt || window.event;
+ del._stopPropagation.call(evt);
+ del._preventDefault.call(evt);
+ }
+ }
+
+ del._synthesizeEvent = function(evt, props){
+ var faux = dojo.mixin({}, evt, props);
+ del._setKeyChar(faux);
+ // FIXME: would prefer to use dojo.hitch: dojo.hitch(evt, evt.preventDefault);
+ // but it throws an error when preventDefault is invoked on Safari
+ // does Event.preventDefault not support "apply" on Safari?
+ faux.preventDefault = function(){ evt.preventDefault(); };
+ faux.stopPropagation = function(){ evt.stopPropagation(); };
+ return faux;
+ }
+
+ // Opera event normalization
+ if(dojo.isOpera){
+ dojo.mixin(del, {
+ _fixEvent: function(evt, sender){
+ switch(evt.type){
+ case "keypress":
+ var c = evt.which;
+ if(c==3){
+ c=99; // Mozilla maps CTRL-BREAK to CTRL-c
+ }
+ // can't trap some keys at all, like INSERT and DELETE
+ // there is no differentiating info between DELETE and ".", or INSERT and "-"
+ c = c<41 && !evt.shiftKey ? 0 : c;
+ if(evt.ctrlKey && !evt.shiftKey && c>=65 && c<=90){
+ // lowercase CTRL-[A-Z] keys
+ c += 32;
+ }
+ return del._synthesizeEvent(evt, { charCode: c });
+ }
+ return evt;
+ }
+ });
+ }
+
+ // Webkit event normalization
+ if(dojo.isWebKit){
+ del._add = del.add;
+ del._remove = del.remove;
+
+ dojo.mixin(del, {
+ add: function(/*DOMNode*/node, /*String*/event, /*Function*/fp){
+ if(!node){return;} // undefined
+ var handle = del._add(node, event, fp);
+ if(del._normalizeEventName(event) == "keypress"){
+ // we need to listen to onkeydown to synthesize
+ // keypress events that otherwise won't fire
+ // in Safari 3.1+: https://lists.webkit.org/pipermail/webkit-dev/2007-December/002992.html
+ handle._stealthKeyDownHandle = del._add(node, "keydown", function(evt){
+ //A variation on the IE _stealthKeydown function
+ //Synthesize an onkeypress event, but only for unprintable characters.
+ var k=evt.keyCode;
+ // These are Windows Virtual Key Codes
+ // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/WinUI/WindowsUserInterface/UserInput/VirtualKeyCodes.asp
+ var unprintable = k!=13 && k!=32 && k!=27 && (k<48 || k>90) && (k<96 || k>111) && (k<186 || k>192) && (k<219 || k>222);
+ // synthesize keypress for most unprintables and CTRL-keys
+ if(unprintable || evt.ctrlKey){
+ var c = unprintable ? 0 : k;
+ if(evt.ctrlKey){
+ if(k==3 || k==13){
+ return; // IE will post CTRL-BREAK, CTRL-ENTER as keypress natively
+ }else if(c>95 && c<106){
+ c -= 48; // map CTRL-[numpad 0-9] to ASCII
+ }else if(!evt.shiftKey && c>=65 && c<=90){
+ c += 32; // map CTRL-[A-Z] to lowercase
+ }else{
+ c = del._punctMap[c] || c; // map other problematic CTRL combinations to ASCII
+ }
+ }
+ // simulate a keypress event
+ var faux = del._synthesizeEvent(evt, {type: 'keypress', faux: true, charCode: c});
+ fp.call(evt.currentTarget, faux);
+ }
+ });
+ }
+ return handle; /*Handle*/
+ },
+
+ remove: function(/*DOMNode*/node, /*String*/event, /*Handle*/handle){
+ if(node){
+ if(handle._stealthKeyDownHandle){
+ del._remove(node, "keydown", handle._stealthKeyDownHandle);
+ }
+ del._remove(node, event, handle);
+ }
+ },
+ _fixEvent: function(evt, sender){
+ switch(evt.type){
+ case "keypress":
+ if(evt.faux){ return evt; }
+ var c = evt.charCode;
+ c = c>=32 ? c : 0;
+ return del._synthesizeEvent(evt, {charCode: c, faux: true});
+ }
+ return evt;
+ }
+ });
+ }
+ })();
+
+if(dojo.isIE){
+ // keep this out of the closure
+ // closing over 'iel' or 'ieh' b0rks leak prevention
+ // ls[i] is an index into the master handler array
+ dojo._ieDispatcher = function(args, sender){
+ var ap=Array.prototype, h=dojo._ie_listener.handlers, c=args.callee, ls=c[dojo._ieListenersName], t=h[c.target];
+ // return value comes from original target function
+ var r = t && t.apply(sender, args);
+ // make local copy of listener array so it's immutable during processing
+ var lls = [].concat(ls);
+ // invoke listeners after target function
+ for(var i in lls){
+ var f = h[lls[i]];
+ if(!(i in ap) && f){
+ f.apply(sender, args);
+ }
+ }
+ return r;
+ }
+ dojo._getIeDispatcher = function(){
+ // ensure the returned function closes over nothing ("new Function" apparently doesn't close)
+ return new Function(dojo._scopeName + "._ieDispatcher(arguments, this)"); // function
+ }
+ // keep this out of the closure to reduce RAM allocation
+ dojo._event_listener._fixCallback = function(fp){
+ var f = dojo._event_listener._fixEvent;
+ return function(e){ return fp.call(this, f(e, this)); };
+ }
+}
+
+}
+
+if(!dojo._hasResource["dojo._base.html"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
+dojo._hasResource["dojo._base.html"] = true;
+
+dojo.provide("dojo._base.html");
+
+// FIXME: need to add unit tests for all the semi-public methods
+
+try{
+ document.execCommand("BackgroundImageCache", false, true);
+}catch(e){
+ // sane browsers don't have cache "issues"
+}
+
+// =============================
+// DOM Functions
+// =============================
+
+/*=====
+dojo.byId = function(id, doc){
+ // summary:
+ // Returns DOM node with matching `id` attribute or `null`
+ // if not found, similar to "$" function in another library.
+ // If `id` is a DomNode, this function is a no-op.
+ //
+ // id: String|DOMNode
+ // A string to match an HTML id attribute or a reference to a DOM Node
+ //
+ // doc: Document?
+ // Document to work in. Defaults to the current value of
+ // dojo.doc. Can be used to retrieve
+ // node references from other documents.
+ //
+ // example:
+ // Look up a node by ID:
+ // | var n = dojo.byId("foo");
+ //
+ // example:
+ // Check if a node exists.
+ // | if(dojo.byId("bar")){ ... }
+ //
+ // example:
+ // Allow string or DomNode references to be passed to a custom function:
+ // | var foo = function(nodeOrId){
+ // | nodeOrId = dojo.byId(nodeOrId);
+ // | // ... more stuff
+ // | }
+=====*/
+
+if(dojo.isIE || dojo.isOpera){
+ dojo.byId = function(id, doc){
+ if(dojo.isString(id)){
+ var _d = doc || dojo.doc;
+ var te = _d.getElementById(id);
+ // attributes.id.value is better than just id in case the
+ // user has a name=id inside a form
+ if(te && (te.attributes.id.value == id || te.id == id)){
+ return te;
+ }else{
+ var eles = _d.all[id];
+ if(!eles || eles.nodeName){
+ eles = [eles];
+ }
+ // if more than 1, choose first with the correct id
+ var i=0;
+ while((te=eles[i++])){
+ if((te.attributes && te.attributes.id && te.attributes.id.value == id)
+ || te.id == id){
+ return te;
+ }
+ }
+ }
+ }else{
+ return id; // DomNode
+ }
+ };
+}else{
+ dojo.byId = function(id, doc){
+ return dojo.isString(id) ? (doc || dojo.doc).getElementById(id) : id; // DomNode
+ };
+}
+/*=====
+}
+=====*/
+
+(function(){
+ var d = dojo;
+
+ var _destroyContainer = null;
+ d.addOnWindowUnload(function(){
+ _destroyContainer = null; //prevent IE leak
+ });
+
+/*=====
+ dojo._destroyElement = function(node){
+ // summary: Existing alias for `dojo.destroy`. Deprecated, will be removed in 2.0
+ }
+=====*/
+ dojo._destroyElement = dojo.destroy = function(/*String|DomNode*/node){
+ // summary:
+ // Removes a node from its parent, clobbering it and all of its
+ // children.
+ //
+ // description:
+ // Removes a node from its parent, clobbering it and all of its
+ // children. Function only works with DomNodes, and returns nothing.
+ //
+ // node:
+ // A String ID or DomNode reference of the element to be destroyed
+ //
+ // example:
+ // Destroy a node byId:
+ // | dojo.destroy("someId");
+ //
+ // example:
+ // Destroy all nodes in a list by reference:
+ // | dojo.query(".someNode").forEach(dojo.destroy);
+
+ node = d.byId(node);
+ try{
+ if(!_destroyContainer || _destroyContainer.ownerDocument != node.ownerDocument){
+ _destroyContainer = node.ownerDocument.createElement("div");
+ }
+ _destroyContainer.appendChild(node.parentNode ? node.parentNode.removeChild(node) : node);
+ // NOTE: see http://trac.dojotoolkit.org/ticket/2931. This may be a bug and not a feature
+ _destroyContainer.innerHTML = "";
+ }catch(e){
+ /* squelch */
+ }
+ };
+
+ dojo.isDescendant = function(/*DomNode|String*/node, /*DomNode|String*/ancestor){
+ // summary:
+ // Returns true if node is a descendant of ancestor
+ // node: string id or node reference to test
+ // ancestor: string id or node reference of potential parent to test against
+ try{
+ node = d.byId(node);
+ ancestor = d.byId(ancestor);
+ while(node){
+ if(node === ancestor){
+ return true; // Boolean
+ }
+ node = node.parentNode;
+ }
+ }catch(e){ /* squelch, return false */ }
+ return false; // Boolean
+ };
+
+ dojo.setSelectable = function(/*DomNode|String*/node, /*Boolean*/selectable){
+ // summary: enable or disable selection on a node
+ // node:
+ // id or reference to node
+ // selectable:
+ // state to put the node in. false indicates unselectable, true
+ // allows selection.
+ node = d.byId(node);
+ if(d.isMozilla){
+ node.style.MozUserSelect = selectable ? "" : "none";
+ }else if(d.isKhtml || d.isWebKit){
+ node.style.KhtmlUserSelect = selectable ? "auto" : "none";
+ }else if(d.isIE){
+ var v = (node.unselectable = selectable ? "" : "on");
+ d.query("*", node).forEach("item.unselectable = '"+v+"'");
+ }
+ //FIXME: else? Opera?
+ };
+
+ var _insertBefore = function(/*DomNode*/node, /*DomNode*/ref){
+ var parent = ref.parentNode;
+ if(parent){
+ parent.insertBefore(node, ref);
+ }
+ }
+
+ var _insertAfter = function(/*DomNode*/node, /*DomNode*/ref){
+ // summary:
+ // Try to insert node after ref
+ var parent = ref.parentNode;
+ if(parent){
+ if(parent.lastChild == ref){
+ parent.appendChild(node);
+ }else{
+ parent.insertBefore(node, ref.nextSibling);
+ }
+ }
+ }
+
+ dojo.place = function(node, refNode, position){
+ // summary:
+ // Attempt to insert node into the DOM, choosing from various positioning options.
+ // Returns true if successful, false otherwise.
+ //
+ // node: String|DomNode
+ // id or node reference, or HTML fragment starting with "<" to place relative to refNode
+ //
+ // refNode: String|DomNode
+ // id or node reference to use as basis for placement
+ //
+ // position: String|Number?
+ // string noting the position of node relative to refNode or a
+ // number indicating the location in the childNodes collection of refNode.
+ // Accepted string values are:
+ // | * before
+ // | * after
+ // | * replace
+ // | * only
+ // | * first
+ // | * last
+ // "first" and "last" indicate positions as children of refNode, "replace" replaces refNode,
+ // "only" replaces all children. position defaults to "last" if not specified
+ //
+ // returns: DomNode
+ // Returned values is the first argument resolved to a DOM node.
+ //
+ // .place() is also a method of `dojo.NodeList`, allowing `dojo.query` node lookups.
+ //
+ // example:
+ // Place a node by string id as the last child of another node by string id:
+ // | dojo.place("someNode", "anotherNode");
+ //
+ // example:
+ // Place a node by string id before another node by string id
+ // | dojo.place("someNode", "anotherNode", "before");
+ //
+ // example:
+ // Create a Node, and place it in the body element (last child):
+ // | dojo.place(dojo.create('div'), dojo.body());
+ //
+ // example:
+ // Put a new LI as the first child of a list by id:
+ // | dojo.place(dojo.create('li'), "someUl", "first");
+
+ refNode = d.byId(refNode);
+ if(d.isString(node)){
+ node = node.charAt(0) == "<" ? d._toDom(node, refNode.ownerDocument) : d.byId(node);
+ }
+ if(typeof position == "number"){
+ var cn = refNode.childNodes;
+ if(!cn.length || cn.length <= position){
+ refNode.appendChild(node);
+ }else{
+ _insertBefore(node, cn[position < 0 ? 0 : position]);
+ }
+ }else{
+ switch(position){
+ case "before":
+ _insertBefore(node, refNode);
+ break;
+ case "after":
+ _insertAfter(node, refNode);
+ break;
+ case "replace":
+ refNode.parentNode.replaceChild(node, refNode);
+ break;
+ case "only":
+ d.empty(refNode);
+ refNode.appendChild(node);
+ break;
+ case "first":
+ if(refNode.firstChild){
+ _insertBefore(node, refNode.firstChild);
+ break;
+ }
+ // else fallthrough...
+ default: // aka: last
+ refNode.appendChild(node);
+ }
+ }
+ return node; // DomNode
+ }
+
+ // Box functions will assume this model.
+ // On IE/Opera, BORDER_BOX will be set if the primary document is in quirks mode.
+ // Can be set to change behavior of box setters.
+
+ // can be either:
+ // "border-box"
+ // "content-box" (default)
+ dojo.boxModel = "content-box";
+
+ // We punt per-node box mode testing completely.
+ // If anybody cares, we can provide an additional (optional) unit
+ // that overrides existing code to include per-node box sensitivity.
+
+ // Opera documentation claims that Opera 9 uses border-box in BackCompat mode.
+ // but experiments (Opera 9.10.8679 on Windows Vista) indicate that it actually continues to use content-box.
+ // IIRC, earlier versions of Opera did in fact use border-box.
+ // Opera guys, this is really confusing. Opera being broken in quirks mode is not our fault.
+
+ if(d.isIE /*|| dojo.isOpera*/){
+ var _dcm = document.compatMode;
+ // client code may have to adjust if compatMode varies across iframes
+ d.boxModel = _dcm == "BackCompat" || _dcm == "QuirksMode" || d.isIE < 6 ? "border-box" : "content-box"; // FIXME: remove IE < 6 support?
+ }
+
+ // =============================
+ // Style Functions
+ // =============================
+
+ // getComputedStyle drives most of the style code.
+ // Wherever possible, reuse the returned object.
+ //
+ // API functions below that need to access computed styles accept an
+ // optional computedStyle parameter.
+ // If this parameter is omitted, the functions will call getComputedStyle themselves.
+ // This way, calling code can access computedStyle once, and then pass the reference to
+ // multiple API functions.
+
+/*=====
+ dojo.getComputedStyle = function(node){
+ // summary:
+ // Returns a "computed style" object.
+ //
+ // description:
+ // Gets a "computed style" object which can be used to gather
+ // information about the current state of the rendered node.
+ //
+ // Note that this may behave differently on different browsers.
+ // Values may have different formats and value encodings across
+ // browsers.
+ //
+ // Note also that this method is expensive. Wherever possible,
+ // reuse the returned object.
+ //
+ // Use the dojo.style() method for more consistent (pixelized)
+ // return values.
+ //
+ // node: DOMNode
+ // A reference to a DOM node. Does NOT support taking an
+ // ID string for speed reasons.
+ // example:
+ // | dojo.getComputedStyle(dojo.byId('foo')).borderWidth;
+ //
+ // example:
+ // Reusing the returned object, avoiding multiple lookups:
+ // | var cs = dojo.getComputedStyle(dojo.byId("someNode"));
+ // | var w = cs.width, h = cs.height;
+ return; // CSS2Properties
+ }
+=====*/
+
+ // Although we normally eschew argument validation at this
+ // level, here we test argument 'node' for (duck)type,
+ // by testing nodeType, ecause 'document' is the 'parentNode' of 'body'
+ // it is frequently sent to this function even
+ // though it is not Element.
+ var gcs;
+ if(d.isWebKit){
+ gcs = function(/*DomNode*/node){
+ var s;
+ if(node.nodeType == 1){
+ var dv = node.ownerDocument.defaultView;
+ s = dv.getComputedStyle(node, null);
+ if(!s && node.style){
+ node.style.display = "";
+ s = dv.getComputedStyle(node, null);
+ }
+ }
+ return s || {};
+ };
+ }else if(d.isIE){
+ gcs = function(node){
+ // IE (as of 7) doesn't expose Element like sane browsers
+ return node.nodeType == 1 /* ELEMENT_NODE*/ ? node.currentStyle : {};
+ };
+ }else{
+ gcs = function(node){
+ return node.nodeType == 1 ?
+ node.ownerDocument.defaultView.getComputedStyle(node, null) : {};
+ };
+ }
+ dojo.getComputedStyle = gcs;
+
+ if(!d.isIE){
+ d._toPixelValue = function(element, value){
+ // style values can be floats, client code may want
+ // to round for integer pixels.
+ return parseFloat(value) || 0;
+ };
+ }else{
+ d._toPixelValue = function(element, avalue){
+ if(!avalue){ return 0; }
+ // on IE7, medium is usually 4 pixels
+ if(avalue == "medium"){ return 4; }
+ // style values can be floats, client code may
+ // want to round this value for integer pixels.
+ if(avalue.slice && avalue.slice(-2) == 'px'){ return parseFloat(avalue); }
+ with(element){
+ var sLeft = style.left;
+ var rsLeft = runtimeStyle.left;
+ runtimeStyle.left = currentStyle.left;
+ try{
+ // 'avalue' may be incompatible with style.left, which can cause IE to throw
+ // this has been observed for border widths using "thin", "medium", "thick" constants
+ // those particular constants could be trapped by a lookup
+ // but perhaps there are more
+ style.left = avalue;
+ avalue = style.pixelLeft;
+ }catch(e){
+ avalue = 0;
+ }
+ style.left = sLeft;
+ runtimeStyle.left = rsLeft;
+ }
+ return avalue;
+ }
+ }
+ var px = d._toPixelValue;
+
+ // FIXME: there opacity quirks on FF that we haven't ported over. Hrm.
+ /*=====
+ dojo._getOpacity = function(node){
+ // summary:
+ // Returns the current opacity of the passed node as a
+ // floating-point value between 0 and 1.
+ // node: DomNode
+ // a reference to a DOM node. Does NOT support taking an
+ // ID string for speed reasons.
+ // returns: Number between 0 and 1
+ return; // Number
+ }
+ =====*/
+
+ var astr = "DXImageTransform.Microsoft.Alpha";
+ var af = function(n, f){
+ try{
+ return n.filters.item(astr);
+ }catch(e){
+ return f ? {} : null;
+ }
+ }
+
+ dojo._getOpacity =
+ d.isIE ? function(node){
+ try{
+ return af(node).Opacity / 100; // Number
+ }catch(e){
+ return 1; // Number
+ }
+ } :
+ function(node){
+ return gcs(node).opacity;
+ };
+
+ /*=====
+ dojo._setOpacity = function(node, opacity){
+ // summary:
+ // set the opacity of the passed node portably. Returns the
+ // new opacity of the node.
+ // node: DOMNode
+ // a reference to a DOM node. Does NOT support taking an
+ // ID string for performance reasons.
+ // opacity: Number
+ // A Number between 0 and 1. 0 specifies transparent.
+ // returns: Number between 0 and 1
+ return; // Number
+ }
+ =====*/
+
+ dojo._setOpacity =
+ d.isIE ? function(/*DomNode*/node, /*Number*/opacity){
+ var ov = opacity * 100;
+ node.style.zoom = 1.0;
+
+ // on IE7 Alpha(Filter opacity=100) makes text look fuzzy so disable it altogether (bug #2661),
+ //but still update the opacity value so we can get a correct reading if it is read later.
+ af(node, 1).Enabled = !(opacity == 1);
+
+ if(!af(node)){
+ node.style.filter += " progid:" + astr + "(Opacity=" + ov + ")";
+ }else{
+ af(node, 1).Opacity = ov;
+ }
+
+ if(node.nodeName.toLowerCase() == "tr"){
+ d.query("> td", node).forEach(function(i){
+ d._setOpacity(i, opacity);
+ });
+ }
+ return opacity;
+ } :
+ function(node, opacity){
+ return node.style.opacity = opacity;
+ };
+
+ var _pixelNamesCache = {
+ left: true, top: true
+ };
+ var _pixelRegExp = /margin|padding|width|height|max|min|offset/; // |border
+ var _toStyleValue = function(node, type, value){
+ type = type.toLowerCase(); // FIXME: should we really be doing string case conversion here? Should we cache it? Need to profile!
+ if(d.isIE){
+ if(value == "auto"){
+ if(type == "height"){ return node.offsetHeight; }
+ if(type == "width"){ return node.offsetWidth; }
+ }
+ if(type == "fontweight"){
+ switch(value){
+ case 700: return "bold";
+ case 400:
+ default: return "normal";
+ }
+ }
+ }
+ if(!(type in _pixelNamesCache)){
+ _pixelNamesCache[type] = _pixelRegExp.test(type);
+ }
+ return _pixelNamesCache[type] ? px(node, value) : value;
+ }
+
+ var _floatStyle = d.isIE ? "styleFloat" : "cssFloat",
+ _floatAliases = { "cssFloat": _floatStyle, "styleFloat": _floatStyle, "float": _floatStyle }
+ ;
+
+ // public API
+
+ dojo.style = function( /*DomNode|String*/ node,
+ /*String?|Object?*/ style,
+ /*String?*/ value){
+ // summary:
+ // Accesses styles on a node. If 2 arguments are
+ // passed, acts as a getter. If 3 arguments are passed, acts
+ // as a setter.
+ // description:
+ // Getting the style value uses the computed style for the node, so the value
+ // will be a calculated value, not just the immediate node.style value.
+ // Also when getting values, use specific style names,
+ // like "borderBottomWidth" instead of "border" since compound values like
+ // "border" are not necessarily reflected as expected.
+ // If you want to get node dimensions, use dojo.marginBox() or
+ // dojo.contentBox().
+ // node:
+ // id or reference to node to get/set style for
+ // style:
+ // the style property to set in DOM-accessor format
+ // ("borderWidth", not "border-width") or an object with key/value
+ // pairs suitable for setting each property.
+ // value:
+ // If passed, sets value on the node for style, handling
+ // cross-browser concerns. When setting a pixel value,
+ // be sure to include "px" in the value. For instance, top: "200px".
+ // Otherwise, in some cases, some browsers will not apply the style.
+ // example:
+ // Passing only an ID or node returns the computed style object of
+ // the node:
+ // | dojo.style("thinger");
+ // example:
+ // Passing a node and a style property returns the current
+ // normalized, computed value for that property:
+ // | dojo.style("thinger", "opacity"); // 1 by default
+ //
+ // example:
+ // Passing a node, a style property, and a value changes the
+ // current display of the node and returns the new computed value
+ // | dojo.style("thinger", "opacity", 0.5); // == 0.5
+ //
+ // example:
+ // Passing a node, an object-style style property sets each of the values in turn and returns the computed style object of the node:
+ // | dojo.style("thinger", {
+ // | "opacity": 0.5,
+ // | "border": "3px solid black",
+ // | "height": "300px"
+ // | });
+ //
+ // example:
+ // When the CSS style property is hyphenated, the JavaScript property is camelCased.
+ // font-size becomes fontSize, and so on.
+ // | dojo.style("thinger",{
+ // | fontSize:"14pt",
+ // | letterSpacing:"1.2em"
+ // | });
+ //
+ // example:
+ // dojo.NodeList implements .style() using the same syntax, omitting the "node" parameter, calling
+ // dojo.style() on every element of the list. See: dojo.query and dojo.NodeList
+ // | dojo.query(".someClassName").style("visibility","hidden");
+ // | // or
+ // | dojo.query("#baz > div").style({
+ // | opacity:0.75,
+ // | fontSize:"13pt"
+ // | });
+
+ var n = d.byId(node), args = arguments.length, op = (style == "opacity");
+ style = _floatAliases[style] || style;
+ if(args == 3){
+ return op ? d._setOpacity(n, value) : n.style[style] = value; /*Number*/
+ }
+ if(args == 2 && op){
+ return d._getOpacity(n);
+ }
+ var s = gcs(n);
+ if(args == 2 && !d.isString(style)){
+ for(var x in style){
+ d.style(node, x, style[x]);
+ }
+ return s;
+ }
+ return (args == 1) ? s : _toStyleValue(n, style, s[style] || n.style[style]); /* CSS2Properties||String||Number */
+ }
+
+ // =============================
+ // Box Functions
+ // =============================
+
+ dojo._getPadExtents = function(/*DomNode*/n, /*Object*/computedStyle){
+ // summary:
+ // Returns object with special values specifically useful for node
+ // fitting.
+ //
+ // * l/t = left/top padding (respectively)
+ // * w = the total of the left and right padding
+ // * h = the total of the top and bottom padding
+ //
+ // If 'node' has position, l/t forms the origin for child nodes.
+ // The w/h are used for calculating boxes.
+ // Normally application code will not need to invoke this
+ // directly, and will use the ...box... functions instead.
+ var
+ s = computedStyle||gcs(n),
+ l = px(n, s.paddingLeft),
+ t = px(n, s.paddingTop);
+ return {
+ l: l,
+ t: t,
+ w: l+px(n, s.paddingRight),
+ h: t+px(n, s.paddingBottom)
+ };
+ }
+
+ dojo._getBorderExtents = function(/*DomNode*/n, /*Object*/computedStyle){
+ // summary:
+ // returns an object with properties useful for noting the border
+ // dimensions.
+ //
+ // * l/t = the sum of left/top border (respectively)
+ // * w = the sum of the left and right border
+ // * h = the sum of the top and bottom border
+ //
+ // The w/h are used for calculating boxes.
+ // Normally application code will not need to invoke this
+ // directly, and will use the ...box... functions instead.
+ var
+ ne = "none",
+ s = computedStyle||gcs(n),
+ bl = (s.borderLeftStyle != ne ? px(n, s.borderLeftWidth) : 0),
+ bt = (s.borderTopStyle != ne ? px(n, s.borderTopWidth) : 0);
+ return {
+ l: bl,
+ t: bt,
+ w: bl + (s.borderRightStyle!=ne ? px(n, s.borderRightWidth) : 0),
+ h: bt + (s.borderBottomStyle!=ne ? px(n, s.borderBottomWidth) : 0)
+ };
+ }
+
+ dojo._getPadBorderExtents = function(/*DomNode*/n, /*Object*/computedStyle){
+ // summary:
+ // Returns object with properties useful for box fitting with
+ // regards to padding.
+ //
+ // * l/t = the sum of left/top padding and left/top border (respectively)
+ // * w = the sum of the left and right padding and border
+ // * h = the sum of the top and bottom padding and border
+ //
+ // The w/h are used for calculating boxes.
+ // Normally application code will not need to invoke this
+ // directly, and will use the ...box... functions instead.
+ var
+ s = computedStyle||gcs(n),
+ p = d._getPadExtents(n, s),
+ b = d._getBorderExtents(n, s);
+ return {
+ l: p.l + b.l,
+ t: p.t + b.t,
+ w: p.w + b.w,
+ h: p.h + b.h
+ };
+ }
+
+ dojo._getMarginExtents = function(n, computedStyle){
+ // summary:
+ // returns object with properties useful for box fitting with
+ // regards to box margins (i.e., the outer-box).
+ //
+ // * l/t = marginLeft, marginTop, respectively
+ // * w = total width, margin inclusive
+ // * h = total height, margin inclusive
+ //
+ // The w/h are used for calculating boxes.
+ // Normally application code will not need to invoke this
+ // directly, and will use the ...box... functions instead.
+ var
+ s = computedStyle||gcs(n),
+ l = px(n, s.marginLeft),
+ t = px(n, s.marginTop),
+ r = px(n, s.marginRight),
+ b = px(n, s.marginBottom);
+ if(d.isWebKit && (s.position != "absolute")){
+ // FIXME: Safari's version of the computed right margin
+ // is the space between our right edge and the right edge
+ // of our offsetParent.
+ // What we are looking for is the actual margin value as
+ // determined by CSS.
+ // Hack solution is to assume left/right margins are the same.
+ r = l;
+ }
+ return {
+ l: l,
+ t: t,
+ w: l+r,
+ h: t+b
+ };
+ }
+
+ // Box getters work in any box context because offsetWidth/clientWidth
+ // are invariant wrt box context
+ //
+ // They do *not* work for display: inline objects that have padding styles
+ // because the user agent ignores padding (it's bogus styling in any case)
+ //
+ // Be careful with IMGs because they are inline or block depending on
+ // browser and browser mode.
+
+ // Although it would be easier to read, there are not separate versions of
+ // _getMarginBox for each browser because:
+ // 1. the branching is not expensive
+ // 2. factoring the shared code wastes cycles (function call overhead)
+ // 3. duplicating the shared code wastes bytes
+
+ dojo._getMarginBox = function(/*DomNode*/node, /*Object*/computedStyle){
+ // summary:
+ // returns an object that encodes the width, height, left and top
+ // positions of the node's margin box.
+ var s = computedStyle || gcs(node), me = d._getMarginExtents(node, s);
+ var l = node.offsetLeft - me.l, t = node.offsetTop - me.t, p = node.parentNode;
+ if(d.isMoz){
+ // Mozilla:
+ // If offsetParent has a computed overflow != visible, the offsetLeft is decreased
+ // by the parent's border.
+ // We don't want to compute the parent's style, so instead we examine node's
+ // computed left/top which is more stable.
+ var sl = parseFloat(s.left), st = parseFloat(s.top);
+ if(!isNaN(sl) && !isNaN(st)){
+ l = sl, t = st;
+ }else{
+ // If child's computed left/top are not parseable as a number (e.g. "auto"), we
+ // have no choice but to examine the parent's computed style.
+ if(p && p.style){
+ var pcs = gcs(p);
+ if(pcs.overflow != "visible"){
+ var be = d._getBorderExtents(p, pcs);
+ l += be.l, t += be.t;
+ }
+ }
+ }
+ }else if(d.isOpera || (d.isIE > 7 && !d.isQuirks)){
+ // On Opera and IE 8, offsetLeft/Top includes the parent's border
+ if(p){
+ be = d._getBorderExtents(p);
+ l -= be.l;
+ t -= be.t;
+ }
+ }
+ return {
+ l: l,
+ t: t,
+ w: node.offsetWidth + me.w,
+ h: node.offsetHeight + me.h
+ };
+ }
+
+ dojo._getContentBox = function(node, computedStyle){
+ // summary:
+ // Returns an object that encodes the width, height, left and top
+ // positions of the node's content box, irrespective of the
+ // current box model.
+
+ // clientWidth/Height are important since the automatically account for scrollbars
+ // fallback to offsetWidth/Height for special cases (see #3378)
+ var s = computedStyle || gcs(node),
+ pe = d._getPadExtents(node, s),
+ be = d._getBorderExtents(node, s),
+ w = node.clientWidth,
+ h
+ ;
+ if(!w){
+ w = node.offsetWidth, h = node.offsetHeight;
+ }else{
+ h = node.clientHeight, be.w = be.h = 0;
+ }
+ // On Opera, offsetLeft includes the parent's border
+ if(d.isOpera){ pe.l += be.l; pe.t += be.t; };
+ return {
+ l: pe.l,
+ t: pe.t,
+ w: w - pe.w - be.w,
+ h: h - pe.h - be.h
+ };
+ }
+
+ dojo._getBorderBox = function(node, computedStyle){
+ var s = computedStyle || gcs(node),
+ pe = d._getPadExtents(node, s),
+ cb = d._getContentBox(node, s)
+ ;
+ return {
+ l: cb.l - pe.l,
+ t: cb.t - pe.t,
+ w: cb.w + pe.w,
+ h: cb.h + pe.h
+ };
+ }
+
+ // Box setters depend on box context because interpretation of width/height styles
+ // vary wrt box context.
+ //
+ // The value of dojo.boxModel is used to determine box context.
+ // dojo.boxModel can be set directly to change behavior.
+ //
+ // Beware of display: inline objects that have padding styles
+ // because the user agent ignores padding (it's a bogus setup anyway)
+ //
+ // Be careful with IMGs because they are inline or block depending on
+ // browser and browser mode.
+ //
+ // Elements other than DIV may have special quirks, like built-in
+ // margins or padding, or values not detectable via computedStyle.
+ // In particular, margins on TABLE do not seems to appear
+ // at all in computedStyle on Mozilla.
+
+ dojo._setBox = function(/*DomNode*/node, /*Number?*/l, /*Number?*/t, /*Number?*/w, /*Number?*/h, /*String?*/u){
+ // summary:
+ // sets width/height/left/top in the current (native) box-model
+ // dimentions. Uses the unit passed in u.
+ // node: DOM Node reference. Id string not supported for performance reasons.
+ // l: optional. left offset from parent.
+ // t: optional. top offset from parent.
+ // w: optional. width in current box model.
+ // h: optional. width in current box model.
+ // u: optional. unit measure to use for other measures. Defaults to "px".
+ u = u || "px";
+ var s = node.style;
+ if(!isNaN(l)){ s.left = l + u; }
+ if(!isNaN(t)){ s.top = t + u; }
+ if(w >= 0){ s.width = w + u; }
+ if(h >= 0){ s.height = h + u; }
+ }
+
+ dojo._isButtonTag = function(/*DomNode*/node) {
+ // summary:
+ // True if the node is BUTTON or INPUT.type="button".
+ return node.tagName == "BUTTON"
+ || node.tagName=="INPUT" && node.getAttribute("type").toUpperCase() == "BUTTON"; // boolean
+ }
+
+ dojo._usesBorderBox = function(/*DomNode*/node){
+ // summary:
+ // True if the node uses border-box layout.
+
+ // We could test the computed style of node to see if a particular box
+ // has been specified, but there are details and we choose not to bother.
+
+ // TABLE and BUTTON (and INPUT type=button) are always border-box by default.
+ // If you have assigned a different box to either one via CSS then
+ // box functions will break.
+
+ var n = node.tagName;
+ return d.boxModel=="border-box" || n=="TABLE" || d._isButtonTag(node); // boolean
+ }
+
+ dojo._setContentSize = function(/*DomNode*/node, /*Number*/widthPx, /*Number*/heightPx, /*Object*/computedStyle){
+ // summary:
+ // Sets the size of the node's contents, irrespective of margins,
+ // padding, or borders.
+ if(d._usesBorderBox(node)){
+ var pb = d._getPadBorderExtents(node, computedStyle);
+ if(widthPx >= 0){ widthPx += pb.w; }
+ if(heightPx >= 0){ heightPx += pb.h; }
+ }
+ d._setBox(node, NaN, NaN, widthPx, heightPx);
+ }
+
+ dojo._setMarginBox = function(/*DomNode*/node, /*Number?*/leftPx, /*Number?*/topPx,
+ /*Number?*/widthPx, /*Number?*/heightPx,
+ /*Object*/computedStyle){
+ // summary:
+ // sets the size of the node's margin box and placement
+ // (left/top), irrespective of box model. Think of it as a
+ // passthrough to dojo._setBox that handles box-model vagaries for
+ // you.
+
+ var s = computedStyle || gcs(node),
+ // Some elements have special padding, margin, and box-model settings.
+ // To use box functions you may need to set padding, margin explicitly.
+ // Controlling box-model is harder, in a pinch you might set dojo.boxModel.
+ bb = d._usesBorderBox(node),
+ pb = bb ? _nilExtents : d._getPadBorderExtents(node, s)
+ ;
+ if(d.isWebKit){
+ // on Safari (3.1.2), button nodes with no explicit size have a default margin
+ // setting an explicit size eliminates the margin.
+ // We have to swizzle the width to get correct margin reading.
+ if(d._isButtonTag(node)){
+ var ns = node.style;
+ if(widthPx >= 0 && !ns.width) { ns.width = "4px"; }
+ if(heightPx >= 0 && !ns.height) { ns.height = "4px"; }
+ }
+ }
+ var mb = d._getMarginExtents(node, s);
+ if(widthPx >= 0){ widthPx = Math.max(widthPx - pb.w - mb.w, 0); }
+ if(heightPx >= 0){ heightPx = Math.max(heightPx - pb.h - mb.h, 0); }
+ d._setBox(node, leftPx, topPx, widthPx, heightPx);
+ }
+
+ var _nilExtents = { l:0, t:0, w:0, h:0 };
+
+ // public API
+
+ dojo.marginBox = function(/*DomNode|String*/node, /*Object?*/box){
+ // summary:
+ // Getter/setter for the margin-box of node.
+ // description:
+ // Returns an object in the expected format of box (regardless
+ // if box is passed). The object might look like:
+ // `{ l: 50, t: 200, w: 300: h: 150 }`
+ // for a node offset from its parent 50px to the left, 200px from
+ // the top with a margin width of 300px and a margin-height of
+ // 150px.
+ // node:
+ // id or reference to DOM Node to get/set box for
+ // box:
+ // If passed, denotes that dojo.marginBox() should
+ // update/set the margin box for node. Box is an object in the
+ // above format. All properties are optional if passed.
+ var n = d.byId(node), s = gcs(n), b = box;
+ return !b ? d._getMarginBox(n, s) : d._setMarginBox(n, b.l, b.t, b.w, b.h, s); // Object
+ }
+
+ dojo.contentBox = function(/*DomNode|String*/node, /*Object?*/box){
+ // summary:
+ // Getter/setter for the content-box of node.
+ // description:
+ // Returns an object in the expected format of box (regardless if box is passed).
+ // The object might look like:
+ // `{ l: 50, t: 200, w: 300: h: 150 }`
+ // for a node offset from its parent 50px to the left, 200px from
+ // the top with a content width of 300px and a content-height of
+ // 150px. Note that the content box may have a much larger border
+ // or margin box, depending on the box model currently in use and
+ // CSS values set/inherited for node.
+ // node:
+ // id or reference to DOM Node to get/set box for
+ // box:
+ // If passed, denotes that dojo.contentBox() should
+ // update/set the content box for node. Box is an object in the
+ // above format. All properties are optional if passed.
+ var n = d.byId(node), s = gcs(n), b = box;
+ return !b ? d._getContentBox(n, s) : d._setContentSize(n, b.w, b.h, s); // Object
+ }
+
+ // =============================
+ // Positioning
+ // =============================
+
+ var _sumAncestorProperties = function(node, prop){
+ if(!(node = (node||0).parentNode)){return 0}
+ var val, retVal = 0, _b = d.body();
+ while(node && node.style){
+ if(gcs(node).position == "fixed"){
+ return 0;
+ }
+ val = node[prop];
+ if(val){
+ retVal += val - 0;
+ // opera and khtml #body & #html has the same values, we only
+ // need one value
+ if(node == _b){ break; }
+ }
+ node = node.parentNode;
+ }
+ return retVal; // integer
+ }
+
+ dojo._docScroll = function(){
+ var
+ _b = d.body(),
+ _w = d.global,
+ de = d.doc.documentElement;
+ return {
+ y: (_w.pageYOffset || de.scrollTop || _b.scrollTop || 0),
+ x: (_w.pageXOffset || d._fixIeBiDiScrollLeft(de.scrollLeft) || _b.scrollLeft || 0)
+ };
+ };
+
+ dojo._isBodyLtr = function(){
+ //FIXME: could check html and body tags directly instead of computed style? need to ignore case, accept empty values
+ return ("_bodyLtr" in d) ? d._bodyLtr :
+ d._bodyLtr = gcs(d.body()).direction == "ltr"; // Boolean
+ }
+
+ dojo._getIeDocumentElementOffset = function(){
+ // summary
+ // The following values in IE contain an offset:
+ // event.clientX
+ // event.clientY
+ // node.getBoundingClientRect().left
+ // node.getBoundingClientRect().top
+ // But other position related values do not contain this offset, such as
+ // node.offsetLeft, node.offsetTop, node.style.left and node.style.top.
+ // The offset is always (2, 2) in LTR direction. When the body is in RTL
+ // direction, the offset counts the width of left scroll bar's width.
+ // This function computes the actual offset.
+
+ //NOTE: assumes we're being called in an IE browser
+
+ var de = d.doc.documentElement;
+ //FIXME: use this instead? var de = d.compatMode == "BackCompat" ? d.body : d.documentElement;
+
+ if(d.isIE < 7){
+ return { x: d._isBodyLtr() || window.parent == window ?
+ de.clientLeft : de.offsetWidth - de.clientWidth - de.clientLeft,
+ y: de.clientTop }; // Object
+ }else if(d.isIE < 8){
+ return {x: de.getBoundingClientRect().left, y: de.getBoundingClientRect().top};
+ }else{
+ return {
+ x: 0,
+ y: 0
+ };
+ }
+
+ };
+
+ dojo._fixIeBiDiScrollLeft = function(/*Integer*/ scrollLeft){
+ // In RTL direction, scrollLeft should be a negative value, but IE < 8
+ // returns a positive one. All codes using documentElement.scrollLeft
+ // must call this function to fix this error, otherwise the position
+ // will offset to right when there is a horizontal scrollbar.
+
+ var dd = d.doc;
+ if(d.isIE < 8 && !d._isBodyLtr()){
+ var de = dd.compatMode == "BackCompat" ? dd.body : dd.documentElement;
+ return scrollLeft + de.clientWidth - de.scrollWidth; // Integer
+ }
+ return scrollLeft; // Integer
+ }
+
+ dojo._abs = function(/*DomNode*/node, /*Boolean?*/includeScroll){
+ // summary:
+ // Gets the position of the passed element relative to
+ // the viewport (if includeScroll==false), or relative to the
+ // document root (if includeScroll==true).
+ //
+ // Returns an object of the form:
+ // { x: 100, y: 300 }
+ // if includeScroll is passed, the x and y values will include any
+ // document offsets that may affect the position relative to the
+ // viewport.
+
+ // FIXME: need to decide in the brave-new-world if we're going to be
+ // margin-box or border-box.
+
+ // targetBoxType == "border-box"
+ var db = d.body(), dh = d.body().parentNode, ret;
+ if(node["getBoundingClientRect"]){
+ // IE6+, FF3+, super-modern WebKit, and Opera 9.6+ all take this branch
+ var client = node.getBoundingClientRect();
+ ret = { x: client.left, y: client.top };
+ if(d.isFF >= 3){
+ // in FF3 you have to subtract the document element margins
+ var cs = gcs(dh);
+ ret.x -= px(dh, cs.marginLeft) + px(dh, cs.borderLeftWidth);
+ ret.y -= px(dh, cs.marginTop) + px(dh, cs.borderTopWidth);
+ }
+ if(d.isIE){
+ // On IE there's a 2px offset that we need to adjust for, see _getIeDocumentElementOffset()
+ var offset = d._getIeDocumentElementOffset();
+
+ // fixes the position in IE, quirks mode
+ ret.x -= offset.x + (d.isQuirks ? db.clientLeft : 0);
+ ret.y -= offset.y + (d.isQuirks ? db.clientTop : 0);
+ }
+ }else{
+ // FF2 and Safari
+ ret = {
+ x: 0,
+ y: 0
+ };
+ if(node["offsetParent"]){
+ ret.x -= _sumAncestorProperties(node, "scrollLeft");
+ ret.y -= _sumAncestorProperties(node, "scrollTop");
+
+ var curnode = node;
+ do{
+ var n = curnode.offsetLeft,
+ t = curnode.offsetTop;
+ ret.x += isNaN(n) ? 0 : n;
+ ret.y += isNaN(t) ? 0 : t;
+
+ cs = gcs(curnode);
+ if(curnode != node){
+ if(d.isFF){
+ // tried left+right with differently sized left/right borders
+ // it really is 2xleft border in FF, not left+right, even in RTL!
+ ret.x += 2 * px(curnode,cs.borderLeftWidth);
+ ret.y += 2 * px(curnode,cs.borderTopWidth);
+ }else{
+ ret.x += px(curnode, cs.borderLeftWidth);
+ ret.y += px(curnode, cs.borderTopWidth);
+ }
+ }
+ // static children in a static div in FF2 are affected by the div's border as well
+ // but offsetParent will skip this div!
+ if(d.isFF && cs.position=="static"){
+ var parent=curnode.parentNode;
+ while(parent!=curnode.offsetParent){
+ var pcs=gcs(parent);
+ if(pcs.position=="static"){
+ ret.x += px(curnode,pcs.borderLeftWidth);
+ ret.y += px(curnode,pcs.borderTopWidth);
+ }
+ parent=parent.parentNode;
+ }
+ }
+ curnode = curnode.offsetParent;
+ }while((curnode != dh) && curnode);
+ }else if(node.x && node.y){
+ ret.x += isNaN(node.x) ? 0 : node.x;
+ ret.y += isNaN(node.y) ? 0 : node.y;
+ }
+ }
+ // account for document scrolling
+ // if offsetParent is used, ret value already includes scroll position
+ // so we may have to actually remove that value if !includeScroll
+ if(includeScroll){
+ var scroll = d._docScroll();
+ ret.x += scroll.x;
+ ret.y += scroll.y;
+ }
+
+ return ret; // Object
+ }
+
+ // FIXME: need a setter for coords or a moveTo!!
+ dojo.coords = function(/*DomNode|String*/node, /*Boolean?*/includeScroll){
+ // summary:
+ // Returns an object that measures margin box width/height and
+ // absolute positioning data from dojo._abs().
+ //
+ // description:
+ // Returns an object that measures margin box width/height and
+ // absolute positioning data from dojo._abs().
+ // Return value will be in the form:
+ // `{ l: 50, t: 200, w: 300: h: 150, x: 100, y: 300 }`
+ // Does not act as a setter. If includeScroll is passed, the x and
+ // y params are affected as one would expect in dojo._abs().
+ var n = d.byId(node), s = gcs(n), mb = d._getMarginBox(n, s);
+ var abs = d._abs(n, includeScroll);
+ mb.x = abs.x;
+ mb.y = abs.y;
+ return mb;
+ }
+
+ // =============================
+ // Element attribute Functions
+ // =============================
+
+ var ieLT8 = d.isIE < 8;
+
+ var _fixAttrName = function(/*String*/name){
+ switch(name.toLowerCase()){
+ // Internet Explorer will only set or remove tabindex/readonly
+ // if it is spelled "tabIndex"/"readOnly"
+ case "tabindex":
+ return ieLT8 ? "tabIndex" : "tabindex";
+ case "readonly":
+ return "readOnly";
+ case "class":
+ return "className";
+ case "for": case "htmlfor":
+ // to pick up for attrib set in markup via getAttribute() IE<8 uses "htmlFor" and others use "for"
+ // get/setAttribute works in all as long use same value for both get/set
+ return ieLT8 ? "htmlFor" : "for";
+ default:
+ return name;
+ }
+ }
+
+ // non-deprecated HTML4 attributes with default values
+ // http://www.w3.org/TR/html401/index/attributes.html
+ // FF and Safari will return the default values if you
+ // access the attributes via a property but not
+ // via getAttribute()
+ var _attrProps = {
+ colspan: "colSpan",
+ enctype: "enctype",
+ frameborder: "frameborder",
+ method: "method",
+ rowspan: "rowSpan",
+ scrolling: "scrolling",
+ shape: "shape",
+ span: "span",
+ type: "type",
+ valuetype: "valueType",
+ // the following attributes don't have the default but should be treated like properties
+ classname: "className",
+ innerhtml: "innerHTML"
+ }
+
+ dojo.hasAttr = function(/*DomNode|String*/node, /*String*/name){
+ // summary:
+ // Returns true if the requested attribute is specified on the
+ // given element, and false otherwise.
+ // node:
+ // id or reference to the element to check
+ // name:
+ // the name of the attribute
+ // returns:
+ // true if the requested attribute is specified on the
+ // given element, and false otherwise
+ node = d.byId(node);
+ var fixName = _fixAttrName(name);
+ fixName = fixName == "htmlFor" ? "for" : fixName; //IE<8 uses htmlFor except in this case
+ var attr = node.getAttributeNode && node.getAttributeNode(fixName);
+ return attr ? attr.specified : false; // Boolean
+ }
+
+ var _evtHdlrMap = {}, _ctr = 0,
+ _attrId = dojo._scopeName + "attrid",
+ // the next dictionary lists elements with read-only innerHTML on IE
+ _roInnerHtml = {col: 1, colgroup: 1,
+ // frameset: 1, head: 1, html: 1, style: 1,
+ table: 1, tbody: 1, tfoot: 1, thead: 1, tr: 1, title: 1};
+
+ dojo.attr = function(/*DomNode|String*/node, /*String|Object*/name, /*String?*/value){
+ // summary:
+ // Gets or sets an attribute on an HTML element.
+ // description:
+ // Handles normalized getting and setting of attributes on DOM
+ // Nodes. If 2 arguments are passed, and a the second argumnt is a
+ // string, acts as a getter.
+ //
+ // If a third argument is passed, or if the second argumnt is a
+ // map of attributes, acts as a setter.
+ //
+ // When passing functions as values, note that they will not be
+ // directly assigned to slots on the node, but rather the default
+ // behavior will be removed and the new behavior will be added
+ // using `dojo.connect()`, meaning that event handler properties
+ // will be normalized and that some caveats with regards to
+ // non-standard behaviors for onsubmit apply. Namely that you
+ // should cancel form submission using `dojo.stopEvent()` on the
+ // passed event object instead of returning a boolean value from
+ // the handler itself.
+ // node:
+ // id or reference to the element to get or set the attribute on
+ // name:
+ // the name of the attribute to get or set.
+ // value:
+ // The value to set for the attribute
+ // returns:
+ // when used as a getter, the value of the requested attribute
+ // or null if that attribute does not have a specified or
+ // default value;
+ //
+ // when used as a setter, undefined
+ //
+ // example:
+ // | // get the current value of the "foo" attribute on a node
+ // | dojo.attr(dojo.byId("nodeId"), "foo");
+ // | // or we can just pass the id:
+ // | dojo.attr("nodeId", "foo");
+ //
+ // example:
+ // | // use attr() to set the tab index
+ // | dojo.attr("nodeId", "tabindex", 3);
+ // |
+ //
+ // example:
+ // Set multiple values at once, including event handlers:
+ // | dojo.attr("formId", {
+ // | "foo": "bar",
+ // | "tabindex": -1,
+ // | "method": "POST",
+ // | "onsubmit": function(e){
+ // | // stop submitting the form. Note that the IE behavior
+ // | // of returning true or false will have no effect here
+ // | // since our handler is connect()ed to the built-in
+ // | // onsubmit behavior and so we need to use
+ // | // dojo.stopEvent() to ensure that the submission
+ // | // doesn't proceed.
+ // | dojo.stopEvent(e);
+ // |
+ // | // submit the form with Ajax
+ // | dojo.xhrPost({ form: "formId" });
+ // | }
+ // | });
+ //
+ // example:
+ // Style is s special case: Only set with an object hash of styles
+ // | dojo.attr("someNode",{
+ // | id:"bar",
+ // | style:{
+ // | width:"200px", height:"100px", color:"#000"
+ // | }
+ // | });
+ //
+ // example:
+ // Again, only set style as an object hash of styles:
+ // | var obj = { color:"#fff", backgroundColor:"#000" };
+ // | dojo.attr("someNode", "style", obj);
+ // |
+ // | // though shorter to use `dojo.style` in this case:
+ // | dojo.style("someNode", obj);
+
+ node = d.byId(node);
+ var args = arguments.length;
+ if(args == 2 && !d.isString(name)){
+ // the object form of setter: the 2nd argument is a dictionary
+ for(var x in name){ d.attr(node, x, name[x]); }
+ // FIXME: return the node in this case? could be useful.
+ return;
+ }
+ name = _fixAttrName(name);
+ if(args == 3){ // setter
+ if(d.isFunction(value)){
+ // clobber if we can
+ var attrId = d.attr(node, _attrId);
+ if(!attrId){
+ attrId = _ctr++;
+ d.attr(node, _attrId, attrId);
+ }
+ if(!_evtHdlrMap[attrId]){
+ _evtHdlrMap[attrId] = {};
+ }
+ var h = _evtHdlrMap[attrId][name];
+ if(h){
+ d.disconnect(h);
+ }else{
+ try{
+ delete node[name];
+ }catch(e){}
+ }
+
+ // ensure that event objects are normalized, etc.
+ _evtHdlrMap[attrId][name] = d.connect(node, name, value);
+
+ }else if(typeof value == "boolean"){ // e.g. onsubmit, disabled
+ node[name] = value;
+ }else if(name === "style" && !d.isString(value)){
+ // when the name is "style" and value is an object, pass along
+ d.style(node, value);
+ }else if(name == "className"){
+ node.className = value;
+ }else if(name === "innerHTML"){
+ if(d.isIE && node.tagName.toLowerCase() in _roInnerHtml){
+ d.empty(node);
+ node.appendChild(d._toDom(value, node.ownerDocument));
+ }else{
+ node[name] = value;
+ }
+ }else{
+ node.setAttribute(name, value);
+ }
+ }else{
+ // getter
+ // should we access this attribute via a property or
+ // via getAttribute()?
+ var prop = _attrProps[name.toLowerCase()];
+ if(prop){
+ return node[prop];
+ }
+ var attrValue = node[name];
+ return (typeof attrValue == 'boolean' || typeof attrValue == 'function') ? attrValue
+ : (d.hasAttr(node, name) ? node.getAttribute(name) : null);
+ }
+ }
+
+ dojo.removeAttr = function(/*DomNode|String*/node, /*String*/name){
+ // summary:
+ // Removes an attribute from an HTML element.
+ // node:
+ // id or reference to the element to remove the attribute from
+ // name:
+ // the name of the attribute to remove
+ d.byId(node).removeAttribute(_fixAttrName(name));
+ }
+
+ dojo.create = function(tag, attrs, refNode, pos){
+ // summary: Create an element, allowing for optional attribute decoration
+ // and placement.
+ //
+ // description:
+ // A DOM Element creation function. A shorthand method for creating a node or
+ // a fragment, and allowing for a convenient optional attribute setting step,
+ // as well as an optional DOM placement reference.
+ //|
+ // Attributes are set by passing the optional object through `dojo.attr`.
+ // See `dojo.attr` for noted caveats and nuances, and API if applicable.
+ //|
+ // Placement is done via `dojo.place`, assuming the new node to be the action
+ // node, passing along the optional reference node and position.
+ //
+ // tag: String|DomNode
+ // A string of the element to create (eg: "div", "a", "p", "li", "script", "br"),
+ // or an existing DOM node to process.
+ //
+ // attrs: Object
+ // An object-hash of attributes to set on the newly created node.
+ // Can be null, if you don't want to set any attributes/styles.
+ // See: `dojo.attr` for a description of available attributes.
+ //
+ // refNode: String?|DomNode?
+ // Optional reference node. Used by `dojo.place` to place the newly created
+ // node somewhere in the dom relative to refNode. Can be a DomNode reference
+ // or String ID of a node.
+ //
+ // pos: String?
+ // Optional positional reference. Defaults to "last" by way of `dojo.place`,
+ // though can be set to "first","after","before","last", "replace" or "only"
+ // to further control the placement of the new node relative to the refNode.
+ // 'refNode' is required if a 'pos' is specified.
+ //
+ // returns: DomNode
+ //
+ // example:
+ // Create a DIV:
+ // | var n = dojo.create("div");
+ //
+ // example:
+ // Create a DIV with content:
+ // | var n = dojo.create("div", { innerHTML:"
hi
" });
+ //
+ // example:
+ // Place a new DIV in the BODY, with no attributes set
+ // | var n = dojo.create("div", null, dojo.body());
+ //
+ // example:
+ // Create an UL, and populate it with LI's. Place the list as the first-child of a
+ // node with id="someId":
+ // | var ul = dojo.create("ul", null, "someId", "first");
+ // | var items = ["one", "two", "three", "four"];
+ // | dojo.forEach(items, function(data){
+ // | dojo.create("li", { innerHTML: data }, ul);
+ // | });
+ //
+ // example:
+ // Create an anchor, with an href. Place in BODY:
+ // | dojo.create("a", { href:"foo.html", title:"Goto FOO!" }, dojo.body());
+ //
+ // example:
+ // Create a `dojo.NodeList` from a new element (for syntatic sugar):
+ // | dojo.query(dojo.create('div'))
+ // | .addClass("newDiv")
+ // | .onclick(function(e){ })
+ // | .place("#someNode"); // redundant, but cleaner.
+
+ var doc = d.doc;
+ if(refNode){
+ refNode = d.byId(refNode);
+ doc = refNode.ownerDocument;
+ }
+ if(d.isString(tag)){
+ tag = doc.createElement(tag);
+ }
+ if(attrs){ d.attr(tag, attrs); }
+ if(refNode){ d.place(tag, refNode, pos); }
+ return tag; // DomNode
+ }
+
+ /*=====
+ dojo.empty = function(node){
+ // summary:
+ // safely removes all children of the node.
+ // node: DOMNode|String
+ // a reference to a DOM node or an id.
+ // example:
+ // Destroy node's children byId:
+ // | dojo.empty("someId");
+ //
+ // example:
+ // Destroy all nodes' children in a list by reference:
+ // | dojo.query(".someNode").forEach(dojo.empty);
+ }
+ =====*/
+
+ d.empty =
+ d.isIE ? function(node){
+ node = d.byId(node);
+ for(var c; c = node.lastChild;){ // intentional assignment
+ d.destroy(c);
+ }
+ } :
+ function(node){
+ d.byId(node).innerHTML = "";
+ };
+
+ /*=====
+ dojo._toDom = function(frag, doc){
+ // summary:
+ // instantiates an HTML fragment returning the corresponding DOM.
+ // frag: String
+ // the HTML fragment
+ // doc: DocumentNode?
+ // optional document to use when creating DOM nodes, defaults to
+ // dojo.doc if not specified.
+ // returns: DocumentFragment
+ //
+ // example:
+ // Create a table row:
+ // | var tr = dojo._toDom("
First!
");
+ }
+ =====*/
+
+ // support stuff for dojo._toDom
+ var tagWrap = {
+ option: ["select"],
+ tbody: ["table"],
+ thead: ["table"],
+ tfoot: ["table"],
+ tr: ["table", "tbody"],
+ td: ["table", "tbody", "tr"],
+ th: ["table", "thead", "tr"],
+ legend: ["fieldset"],
+ caption: ["table"],
+ colgroup: ["table"],
+ col: ["table", "colgroup"],
+ li: ["ul"]
+ },
+ reTag = /<\s*([\w\:]+)/,
+ masterNode = {}, masterNum = 0,
+ masterName = "__" + d._scopeName + "ToDomId";
+
+ // generate start/end tag strings to use
+ // for the injection for each special tag wrap case.
+ for(var param in tagWrap){
+ var tw = tagWrap[param];
+ tw.pre = param == "option" ? '