/*! Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. */ (function (global) { (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else { global.msWriteProfilerMark && msWriteProfilerMark('WinJS.3.0 3.0.0.winjs.2014.10.2 WinJS.js,StartTM'); factory(global.WinJS); global.msWriteProfilerMark && msWriteProfilerMark('WinJS.3.0 3.0.0.winjs.2014.10.2 WinJS.js,StopTM'); } }(function (WinJS) { // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. /*jshint ignore:start */ var require; var define; /*jshint ignore:end */ (function () { "use strict"; var defined = {}; define = function (id, dependencies, factory) { if (!Array.isArray(dependencies)) { factory = dependencies; dependencies = []; } var mod = { dependencies: normalize(id, dependencies), factory: factory }; if (dependencies.indexOf('exports') !== -1) { mod.exports = {}; } defined[id] = mod; }; // WinJS/Core depends on ./Core/_Base // should return WinJS/Core/_Base function normalize(id, dependencies) { var parent = id.split('/'); parent.pop(); return dependencies.map(function (dep) { if (dep[0] === '.') { var parts = dep.split('/'); var current = parent.slice(0); parts.forEach(function (part) { if (part === '..') { current.pop(); } else if (part !== '.') { current.push(part); } }); return current.join('/'); } else { return dep; } }); } function resolve(dependencies, exports) { return dependencies.map(function (depName) { if (depName === 'exports') { return exports; } var dep = defined[depName]; if (!dep) { throw new Error("Undefined dependency: " + depName); } if (!dep.resolved) { dep.resolved = load(dep.dependencies, dep.factory, dep.exports); // exports shadows whatever the module factory returns if (dep.exports) { dep.resolved = dep.exports; } } return dep.resolved; }); } function load(dependencies, factory, exports) { var deps = resolve(dependencies, exports); if (factory && factory.apply) { return factory.apply(null, deps); } else { return factory; } } require = function (dependencies, factory) { //jshint ignore:line if (!Array.isArray(dependencies)) { dependencies = [dependencies]; } load(dependencies, factory); }; })(); define("amd", function(){}); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Core/_WinJS',{}); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. (function (global) { "use strict"; define('WinJS/Core/_Global',global); }(this)); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Core/_BaseCoreUtils',[ './_Global' ], function baseCoreUtilsInit(_Global) { "use strict"; var hasWinRT = !!_Global.Windows; function markSupportedForProcessing(func) { /// /// /// Marks a function as being compatible with declarative processing, such as WinJS.UI.processAll /// or WinJS.Binding.processAll. /// /// /// The function to be marked as compatible with declarative processing. /// /// /// The input function. /// /// func.supportedForProcessing = true; return func; } return { hasWinRT: hasWinRT, markSupportedForProcessing: markSupportedForProcessing, _setImmediate: _Global.setImmediate ? _Global.setImmediate.bind(_Global) : function (handler) { _Global.setTimeout(handler, 0); } }; }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Core/_WriteProfilerMark',[ './_Global' ], function profilerInit(_Global) { "use strict"; return _Global.msWriteProfilerMark || function () { }; }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Core/_Base',[ './_WinJS', './_Global', './_BaseCoreUtils', './_WriteProfilerMark' ], function baseInit(_WinJS, _Global, _BaseCoreUtils, _WriteProfilerMark) { "use strict"; function initializeProperties(target, members, prefix) { var keys = Object.keys(members); var isArray = Array.isArray(target); var properties; var i, len; for (i = 0, len = keys.length; i < len; i++) { var key = keys[i]; var enumerable = key.charCodeAt(0) !== /*_*/95; var member = members[key]; if (member && typeof member === 'object') { if (member.value !== undefined || typeof member.get === 'function' || typeof member.set === 'function') { if (member.enumerable === undefined) { member.enumerable = enumerable; } if (prefix && member.setName && typeof member.setName === 'function') { member.setName(prefix + "." + key); } properties = properties || {}; properties[key] = member; continue; } } if (!enumerable) { properties = properties || {}; properties[key] = { value: member, enumerable: enumerable, configurable: true, writable: true }; continue; } if (isArray) { target.forEach(function (target) { target[key] = member; }); } else { target[key] = member; } } if (properties) { if (isArray) { target.forEach(function (target) { Object.defineProperties(target, properties); }); } else { Object.defineProperties(target, properties); } } } (function () { var _rootNamespace = _WinJS; if (!_rootNamespace.Namespace) { _rootNamespace.Namespace = Object.create(Object.prototype); } function createNamespace(parentNamespace, name) { var currentNamespace = parentNamespace || {}; if (name) { var namespaceFragments = name.split("."); if (currentNamespace === _Global && namespaceFragments[0] === "WinJS") { currentNamespace = _WinJS; namespaceFragments.splice(0, 1); } for (var i = 0, len = namespaceFragments.length; i < len; i++) { var namespaceName = namespaceFragments[i]; if (!currentNamespace[namespaceName]) { Object.defineProperty(currentNamespace, namespaceName, { value: {}, writable: false, enumerable: true, configurable: true } ); } currentNamespace = currentNamespace[namespaceName]; } } return currentNamespace; } function defineWithParent(parentNamespace, name, members) { /// /// /// Defines a new namespace with the specified name under the specified parent namespace. /// /// /// The parent namespace. /// /// /// The name of the new namespace. /// /// /// The members of the new namespace. /// /// /// The newly-defined namespace. /// /// var currentNamespace = createNamespace(parentNamespace, name); if (members) { initializeProperties(currentNamespace, members, name || ""); } return currentNamespace; } function define(name, members) { /// /// /// Defines a new namespace with the specified name. /// /// /// The name of the namespace. This could be a dot-separated name for nested namespaces. /// /// /// The members of the new namespace. /// /// /// The newly-defined namespace. /// /// return defineWithParent(_Global, name, members); } var LazyStates = { uninitialized: 1, working: 2, initialized: 3, }; function lazy(f) { var name; var state = LazyStates.uninitialized; var result; return { setName: function (value) { name = value; }, get: function () { switch (state) { case LazyStates.initialized: return result; case LazyStates.uninitialized: state = LazyStates.working; try { _WriteProfilerMark("WinJS.Namespace._lazy:" + name + ",StartTM"); result = f(); } finally { _WriteProfilerMark("WinJS.Namespace._lazy:" + name + ",StopTM"); state = LazyStates.uninitialized; } f = null; state = LazyStates.initialized; return result; case LazyStates.working: throw "Illegal: reentrancy on initialization"; default: throw "Illegal"; } }, set: function (value) { switch (state) { case LazyStates.working: throw "Illegal: reentrancy on initialization"; default: state = LazyStates.initialized; result = value; break; } }, enumerable: true, configurable: true, }; } // helper for defining AMD module members function moduleDefine(exports, name, members) { var target = [exports]; var publicNS = null; if (name) { publicNS = createNamespace(_Global, name); target.push(publicNS); } initializeProperties(target, members, name || ""); return publicNS; } // Establish members of the "WinJS.Namespace" namespace Object.defineProperties(_rootNamespace.Namespace, { defineWithParent: { value: defineWithParent, writable: true, enumerable: true, configurable: true }, define: { value: define, writable: true, enumerable: true, configurable: true }, _lazy: { value: lazy, writable: true, enumerable: true, configurable: true }, _moduleDefine: { value: moduleDefine, writable: true, enumerable: true, configurable: true } }); })(); (function () { function define(constructor, instanceMembers, staticMembers) { /// /// /// Defines a class using the given constructor and the specified instance members. /// /// /// A constructor function that is used to instantiate this class. /// /// /// The set of instance fields, properties, and methods made available on the class. /// /// /// The set of static fields, properties, and methods made available on the class. /// /// /// The newly-defined class. /// /// constructor = constructor || function () { }; _BaseCoreUtils.markSupportedForProcessing(constructor); if (instanceMembers) { initializeProperties(constructor.prototype, instanceMembers); } if (staticMembers) { initializeProperties(constructor, staticMembers); } return constructor; } function derive(baseClass, constructor, instanceMembers, staticMembers) { /// /// /// Creates a sub-class based on the supplied baseClass parameter, using prototypal inheritance. /// /// /// The class to inherit from. /// /// /// A constructor function that is used to instantiate this class. /// /// /// The set of instance fields, properties, and methods to be made available on the class. /// /// /// The set of static fields, properties, and methods to be made available on the class. /// /// /// The newly-defined class. /// /// if (baseClass) { constructor = constructor || function () { }; var basePrototype = baseClass.prototype; constructor.prototype = Object.create(basePrototype); _BaseCoreUtils.markSupportedForProcessing(constructor); Object.defineProperty(constructor.prototype, "constructor", { value: constructor, writable: true, configurable: true, enumerable: true }); if (instanceMembers) { initializeProperties(constructor.prototype, instanceMembers); } if (staticMembers) { initializeProperties(constructor, staticMembers); } return constructor; } else { return define(constructor, instanceMembers, staticMembers); } } function mix(constructor) { /// /// /// Defines a class using the given constructor and the union of the set of instance members /// specified by all the mixin objects. The mixin parameter list is of variable length. /// /// /// A constructor function that is used to instantiate this class. /// /// /// The newly-defined class. /// /// constructor = constructor || function () { }; var i, len; for (i = 1, len = arguments.length; i < len; i++) { initializeProperties(constructor.prototype, arguments[i]); } return constructor; } // Establish members of "WinJS.Class" namespace _WinJS.Namespace.define("WinJS.Class", { define: define, derive: derive, mix: mix }); })(); return { Namespace: _WinJS.Namespace, Class: _WinJS.Class }; }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Core/_ErrorFromName',[ './_Base' ], function errorsInit(_Base) { "use strict"; var ErrorFromName = _Base.Class.derive(Error, function (name, message) { /// /// /// Creates an Error object with the specified name and message properties. /// /// The name of this error. The name is meant to be consumed programmatically and should not be localized. /// The message for this error. The message is meant to be consumed by humans and should be localized. /// Error instance with .name and .message properties populated /// this.name = name; this.message = message || name; }, { /* empty */ }, { supportedForProcessing: false, }); _Base.Namespace.define("WinJS", { // ErrorFromName establishes a simple pattern for returning error codes. // ErrorFromName: ErrorFromName }); return ErrorFromName; }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Core/_WinRT',[ 'exports', './_Global', './_Base', ], function winrtInit(exports, _Global, _Base) { "use strict"; exports.msGetWeakWinRTProperty = _Global.msGetWeakWinRTProperty; exports.msSetWeakWinRTProperty = _Global.msSetWeakWinRTProperty; var APIs = [ "Windows.ApplicationModel.DesignMode.designModeEnabled", "Windows.ApplicationModel.Resources.Core.ResourceContext", "Windows.ApplicationModel.Resources.Core.ResourceManager", "Windows.ApplicationModel.Search.Core.SearchSuggestionManager", "Windows.ApplicationModel.Search.SearchQueryLinguisticDetails", "Windows.Data.Text.SemanticTextQuery", "Windows.Foundation.Collections.CollectionChange", "Windows.Foundation.Uri", "Windows.Globalization.ApplicationLanguages", "Windows.Globalization.Calendar", "Windows.Globalization.DateTimeFormatting", "Windows.Globalization.Language", "Windows.Phone.UI.Input.HardwareButtons", "Windows.Storage.ApplicationData", "Windows.Storage.CreationCollisionOption", "Windows.Storage.BulkAccess.FileInformationFactory", "Windows.Storage.FileIO", "Windows.Storage.FileProperties.ThumbnailType", "Windows.Storage.FileProperties.ThumbnailMode", "Windows.Storage.FileProperties.ThumbnailOptions", "Windows.Storage.KnownFolders", "Windows.Storage.Search.FolderDepth", "Windows.Storage.Search.IndexerOption", "Windows.Storage.Streams.RandomAccessStreamReference", "Windows.UI.ApplicationSettings.SettingsEdgeLocation", "Windows.UI.ApplicationSettings.SettingsCommand", "Windows.UI.ApplicationSettings.SettingsPane", "Windows.UI.Core.AnimationMetrics", "Windows.UI.Input.EdgeGesture", "Windows.UI.Input.EdgeGestureKind", "Windows.UI.Input.PointerPoint", "Windows.UI.ViewManagement.HandPreference", "Windows.UI.ViewManagement.InputPane", "Windows.UI.ViewManagement.UISettings", "Windows.UI.WebUI.Core.WebUICommandBar", "Windows.UI.WebUI.Core.WebUICommandBarBitmapIcon", "Windows.UI.WebUI.Core.WebUICommandBarClosedDisplayMode", "Windows.UI.WebUI.Core.WebUICommandBarIconButton", "Windows.UI.WebUI.Core.WebUICommandBarSymbolIcon", "Windows.UI.WebUI.WebUIApplication", ]; APIs.forEach(function (api) { var parts = api.split("."); var leaf = {}; leaf[parts[parts.length - 1]] = { get: function () { return parts.reduce(function (current, part) { return current ? current[part] : null; }, _Global); } }; _Base.Namespace.defineWithParent(exports, parts.slice(0, -1).join("."), leaf); }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Core/_Events',[ 'exports', './_Base' ], function eventsInit(exports, _Base) { "use strict"; function createEventProperty(name) { var eventPropStateName = "_on" + name + "state"; return { get: function () { var state = this[eventPropStateName]; return state && state.userHandler; }, set: function (handler) { var state = this[eventPropStateName]; if (handler) { if (!state) { state = { wrapper: function (evt) { return state.userHandler(evt); }, userHandler: handler }; Object.defineProperty(this, eventPropStateName, { value: state, enumerable: false, writable:true, configurable: true }); this.addEventListener(name, state.wrapper, false); } state.userHandler = handler; } else if (state) { this.removeEventListener(name, state.wrapper, false); this[eventPropStateName] = null; } }, enumerable: true }; } function createEventProperties() { /// /// /// Creates an object that has one property for each name passed to the function. /// /// /// A variable list of property names. /// /// /// The object with the specified properties. The names of the properties are prefixed with 'on'. /// /// var props = {}; for (var i = 0, len = arguments.length; i < len; i++) { var name = arguments[i]; props["on" + name] = createEventProperty(name); } return props; } var EventMixinEvent = _Base.Class.define( function EventMixinEvent_ctor(type, detail, target) { this.detail = detail; this.target = target; this.timeStamp = Date.now(); this.type = type; }, { bubbles: { value: false, writable: false }, cancelable: { value: false, writable: false }, currentTarget: { get: function () { return this.target; } }, defaultPrevented: { get: function () { return this._preventDefaultCalled; } }, trusted: { value: false, writable: false }, eventPhase: { value: 0, writable: false }, target: null, timeStamp: null, type: null, preventDefault: function () { this._preventDefaultCalled = true; }, stopImmediatePropagation: function () { this._stopImmediatePropagationCalled = true; }, stopPropagation: function () { } }, { supportedForProcessing: false, } ); var eventMixin = { _listeners: null, addEventListener: function (type, listener, useCapture) { /// /// /// Adds an event listener to the control. /// /// /// The type (name) of the event. /// /// /// The listener to invoke when the event is raised. /// /// /// if true initiates capture, otherwise false. /// /// useCapture = useCapture || false; this._listeners = this._listeners || {}; var eventListeners = (this._listeners[type] = this._listeners[type] || []); for (var i = 0, len = eventListeners.length; i < len; i++) { var l = eventListeners[i]; if (l.useCapture === useCapture && l.listener === listener) { return; } } eventListeners.push({ listener: listener, useCapture: useCapture }); }, dispatchEvent: function (type, details) { /// /// /// Raises an event of the specified type and with the specified additional properties. /// /// /// The type (name) of the event. /// /// /// The set of additional properties to be attached to the event object when the event is raised. /// /// /// true if preventDefault was called on the event. /// /// var listeners = this._listeners && this._listeners[type]; if (listeners) { var eventValue = new EventMixinEvent(type, details, this); // Need to copy the array to protect against people unregistering while we are dispatching listeners = listeners.slice(0, listeners.length); for (var i = 0, len = listeners.length; i < len && !eventValue._stopImmediatePropagationCalled; i++) { listeners[i].listener(eventValue); } return eventValue.defaultPrevented || false; } return false; }, removeEventListener: function (type, listener, useCapture) { /// /// /// Removes an event listener from the control. /// /// /// The type (name) of the event. /// /// /// The listener to remove. /// /// /// Specifies whether to initiate capture. /// /// useCapture = useCapture || false; var listeners = this._listeners && this._listeners[type]; if (listeners) { for (var i = 0, len = listeners.length; i < len; i++) { var l = listeners[i]; if (l.listener === listener && l.useCapture === useCapture) { listeners.splice(i, 1); if (listeners.length === 0) { delete this._listeners[type]; } // Only want to remove one element for each call to removeEventListener break; } } } } }; _Base.Namespace._moduleDefine(exports, "WinJS.Utilities", { _createEventProperty: createEventProperty, createEventProperties: createEventProperties, eventMixin: eventMixin }); }); define('require-json',{load: function(id){throw new Error("Dynamic load not allowed: " + id);}}); define('require-json!en-US/ui.resjson',{ "appBarAriaLabel": "App Bar", "appBarCommandAriaLabel": "App Bar Item", "averageRating": "Average Rating", "backbuttonarialabel": "Back", "clearYourRating" : "Clear your rating", "closeOverlay" : "Close", "datePicker": "Date Picker", "flipViewPanningContainerAriaLabel": "Scrolling Container", "flyoutAriaLabel": "Flyout", "hubViewportAriaLabel": "Scrolling Container", "listViewViewportAriaLabel": "Scrolling Container", "menuCommandAriaLabel": "Menu Item", "menuAriaLabel": "Menu", "navBarContainerViewportAriaLabel": "Scrolling Container", "off" : "Off", "on" : "On", "pivotAriaLabel": "Pivot", "pivotViewportAriaLabel": "Scrolling Container", "searchBoxAriaLabel": "Searchbox", "searchBoxAriaLabelInputNoPlaceHolder": "Searchbox, enter to submit query, esc to clear text", "searchBoxAriaLabelInputPlaceHolder": "Searchbox, {0}, enter to submit query, esc to clear text", "searchBoxAriaLabelButton": "Click to submit query", "searchBoxAriaLabelQuery": "Suggestion: {0}", "_searchBoxAriaLabelQuery.comment": "Suggestion: query text (example: Suggestion: contoso)", "searchBoxAriaLabelSeparator": "Separator: {0}", "_searchBoxAriaLabelSeparator.comment": "Separator: separator text (example: Separator: People or Separator: Apps)", "searchBoxAriaLabelResult": "Result: {0}, {1}", "_searchBoxAriaLabelResult.comment": "Result: text, detailed text (example: Result: contoso, www.contoso.com)", "selectAMPM": "Select A.M P.M", "selectDay": "Select Day", "selectHour": "Select Hour", "selectMinute": "Select Minute", "selectMonth": "Select Month", "selectYear": "Select Year", "settingsFlyoutAriaLabel": "Settings Flyout", "tentativeRating": "Tentative Rating", "timePicker": "Time Picker", "unrated": "Unrated", "userRating": "User Rating", // AppBar Icons follow, the format of the ui.js and ui.resjson differ for // the AppBarIcon namespace. The remainder of the file therefore differs. // Code point comments are the icon glyphs in the 'Segoe UI Symbol' font. "appBarIcons\\previous": "\uE100", //  group:Media "_appBarIcons\\previous.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\next": "\uE101", //  group:Media "_appBarIcons\\next.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\play": "\uE102", //  group:Media "_appBarIcons\\play.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\pause": "\uE103", //  group:Media "_appBarIcons\\pause.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\edit": "\uE104", //  group:File "_appBarIcons\\edit.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\save": "\uE105", //  group:File "_appBarIcons\\save.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\clear": "\uE106", //  group:File "_appBarIcons\\clear.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\delete": "\uE107", //  group:File "_appBarIcons\\delete.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\remove": "\uE108", //  group:File "_appBarIcons\\remove.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\add": "\uE109", //  group:File "_appBarIcons\\add.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\cancel": "\uE10A", //  group:Editing "_appBarIcons\\cancel.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\accept": "\uE10B", //  group:General "_appBarIcons\\accept.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\more": "\uE10C", //  group:General "_appBarIcons\\more.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\redo": "\uE10D", //  group:Editing "_appBarIcons\\redo.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\undo": "\uE10E", //  group:Editing "_appBarIcons\\undo.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\home": "\uE10F", //  group:General "_appBarIcons\\home.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\up": "\uE110", //  group:General "_appBarIcons\\up.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\forward": "\uE111", //  group:General "_appBarIcons\\forward.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\right": "\uE111", //  group:General "_appBarIcons\\right.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\back": "\uE112", //  group:General "_appBarIcons\\back.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\left": "\uE112", //  group:General "_appBarIcons\\left.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\favorite": "\uE113", //  group:Media "_appBarIcons\\favorite.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\camera": "\uE114", //  group:System "_appBarIcons\\camera.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\settings": "\uE115", //  group:System "_appBarIcons\\settings.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\video": "\uE116", //  group:Media "_appBarIcons\\video.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\sync": "\uE117", //  group:Media "_appBarIcons\\sync.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\download": "\uE118", //  group:Media "_appBarIcons\\download.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\mail": "\uE119", //  group:Mail and calendar "_appBarIcons\\mail.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\find": "\uE11A", //  group:Data "_appBarIcons\\find.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\help": "\uE11B", //  group:General "_appBarIcons\\help.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\upload": "\uE11C", //  group:Media "_appBarIcons\\upload.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\emoji": "\uE11D", //  group:Communications "_appBarIcons\\emoji.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\twopage": "\uE11E", //  group:Layout "_appBarIcons\\twopage.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\leavechat": "\uE11F", //  group:Communications "_appBarIcons\\leavechat.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\mailforward": "\uE120", //  group:Mail and calendar "_appBarIcons\\mailforward.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\clock": "\uE121", //  group:General "_appBarIcons\\clock.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\send": "\uE122", //  group:Mail and calendar "_appBarIcons\\send.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\crop": "\uE123", //  group:Editing "_appBarIcons\\crop.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\rotatecamera": "\uE124", //  group:System "_appBarIcons\\rotatecamera.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\people": "\uE125", //  group:Communications "_appBarIcons\\people.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\closepane": "\uE126", //  group:Layout "_appBarIcons\\closepane.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\openpane": "\uE127", //  group:Layout "_appBarIcons\\openpane.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\world": "\uE128", //  group:General "_appBarIcons\\world.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\flag": "\uE129", //  group:Mail and calendar "_appBarIcons\\flag.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\previewlink": "\uE12A", //  group:General "_appBarIcons\\previewlink.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\globe": "\uE12B", //  group:Communications "_appBarIcons\\globe.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\trim": "\uE12C", //  group:Editing "_appBarIcons\\trim.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\attachcamera": "\uE12D", //  group:System "_appBarIcons\\attachcamera.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\zoomin": "\uE12E", //  group:Layout "_appBarIcons\\zoomin.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\bookmarks": "\uE12F", //  group:Editing "_appBarIcons\\bookmarks.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\document": "\uE130", //  group:File "_appBarIcons\\document.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\protecteddocument": "\uE131", //  group:File "_appBarIcons\\protecteddocument.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\page": "\uE132", //  group:Layout "_appBarIcons\\page.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\bullets": "\uE133", //  group:Editing "_appBarIcons\\bullets.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\comment": "\uE134", //  group:Communications "_appBarIcons\\comment.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\mail2": "\uE135", //  group:Mail and calendar "_appBarIcons\\mail2.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\contactinfo": "\uE136", //  group:Communications "_appBarIcons\\contactinfo.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\hangup": "\uE137", //  group:Communications "_appBarIcons\\hangup.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\viewall": "\uE138", //  group:Data "_appBarIcons\\viewall.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\mappin": "\uE139", //  group:General "_appBarIcons\\mappin.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\phone": "\uE13A", //  group:Communications "_appBarIcons\\phone.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\videochat": "\uE13B", //  group:Communications "_appBarIcons\\videochat.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\switch": "\uE13C", //  group:Communications "_appBarIcons\\switch.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\contact": "\uE13D", //  group:Communications "_appBarIcons\\contact.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\rename": "\uE13E", //  group:File "_appBarIcons\\rename.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\pin": "\uE141", //  group:System "_appBarIcons\\pin.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\musicinfo": "\uE142", //  group:Media "_appBarIcons\\musicinfo.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\go": "\uE143", //  group:General "_appBarIcons\\go.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\keyboard": "\uE144", //  group:System "_appBarIcons\\keyboard.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\dockleft": "\uE145", //  group:Layout "_appBarIcons\\dockleft.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\dockright": "\uE146", //  group:Layout "_appBarIcons\\dockright.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\dockbottom": "\uE147", //  group:Layout "_appBarIcons\\dockbottom.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\remote": "\uE148", //  group:System "_appBarIcons\\remote.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\refresh": "\uE149", //  group:Data "_appBarIcons\\refresh.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\rotate": "\uE14A", //  group:Layout "_appBarIcons\\rotate.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\shuffle": "\uE14B", //  group:Media "_appBarIcons\\shuffle.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\list": "\uE14C", //  group:Editing "_appBarIcons\\list.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\shop": "\uE14D", //  group:General "_appBarIcons\\shop.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\selectall": "\uE14E", //  group:Data "_appBarIcons\\selectall.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\orientation": "\uE14F", //  group:Layout "_appBarIcons\\orientation.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\import": "\uE150", //  group:Data "_appBarIcons\\import.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\importall": "\uE151", //  group:Data "_appBarIcons\\importall.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\browsephotos": "\uE155", //  group:Media "_appBarIcons\\browsephotos.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\webcam": "\uE156", //  group:System "_appBarIcons\\webcam.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\pictures": "\uE158", //  group:Media "_appBarIcons\\pictures.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\savelocal": "\uE159", //  group:File "_appBarIcons\\savelocal.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\caption": "\uE15A", //  group:Media "_appBarIcons\\caption.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\stop": "\uE15B", //  group:Media "_appBarIcons\\stop.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\showresults": "\uE15C", //  group:Data "_appBarIcons\\showresults.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\volume": "\uE15D", //  group:Media "_appBarIcons\\volume.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\repair": "\uE15E", //  group:System "_appBarIcons\\repair.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\message": "\uE15F", //  group:Communications "_appBarIcons\\message.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\page2": "\uE160", //  group:Layout "_appBarIcons\\page2.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\calendarday": "\uE161", //  group:Mail and calendar "_appBarIcons\\calendarday.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\calendarweek": "\uE162", //  group:Mail and calendar "_appBarIcons\\calendarweek.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\calendar": "\uE163", //  group:Mail and calendar "_appBarIcons\\calendar.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\characters": "\uE164", //  group:Editing "_appBarIcons\\characters.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\mailreplyall": "\uE165", //  group:Mail and calendar "_appBarIcons\\mailreplyall.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\read": "\uE166", //  group:Mail and calendar "_appBarIcons\\read.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\link": "\uE167", //  group:Communications "_appBarIcons\\link.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\accounts": "\uE168", //  group:Communications "_appBarIcons\\accounts.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\showbcc": "\uE169", //  group:Mail and calendar "_appBarIcons\\showbcc.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\hidebcc": "\uE16A", //  group:Mail and calendar "_appBarIcons\\hidebcc.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\cut": "\uE16B", //  group:Editing "_appBarIcons\\cut.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\attach": "\uE16C", //  group:Mail and calendar "_appBarIcons\\attach.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\paste": "\uE16D", //  group:Editing "_appBarIcons\\paste.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\filter": "\uE16E", //  group:Data "_appBarIcons\\filter.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\copy": "\uE16F", //  group:Editing "_appBarIcons\\copy.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\emoji2": "\uE170", //  group:Mail and calendar "_appBarIcons\\emoji2.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\important": "\uE171", //  group:Mail and calendar "_appBarIcons\\important.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\mailreply": "\uE172", //  group:Mail and calendar "_appBarIcons\\mailreply.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\slideshow": "\uE173", //  group:Media "_appBarIcons\\slideshow.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\sort": "\uE174", //  group:Data "_appBarIcons\\sort.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\manage": "\uE178", //  group:System "_appBarIcons\\manage.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\allapps": "\uE179", //  group:System "_appBarIcons\\allapps.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\disconnectdrive": "\uE17A", //  group:System "_appBarIcons\\disconnectdrive.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\mapdrive": "\uE17B", //  group:System "_appBarIcons\\mapdrive.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\newwindow": "\uE17C", //  group:System "_appBarIcons\\newwindow.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\openwith": "\uE17D", //  group:System "_appBarIcons\\openwith.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\contactpresence": "\uE181", //  group:Communications "_appBarIcons\\contactpresence.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\priority": "\uE182", //  group:Mail and calendar "_appBarIcons\\priority.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\uploadskydrive": "\uE183", //  group:File "_appBarIcons\\uploadskydrive.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\gototoday": "\uE184", //  group:Mail and calendar "_appBarIcons\\gototoday.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\font": "\uE185", //  group:Editing "_appBarIcons\\font.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\fontcolor": "\uE186", //  group:Editing "_appBarIcons\\fontcolor.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\contact2": "\uE187", //  group:Communications "_appBarIcons\\contact2.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\folder": "\uE188", //  group:File "_appBarIcons\\folder.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\audio": "\uE189", //  group:Media "_appBarIcons\\audio.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\placeholder": "\uE18A", //  group:General "_appBarIcons\\placeholder.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\view": "\uE18B", //  group:Layout "_appBarIcons\\view.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\setlockscreen": "\uE18C", //  group:System "_appBarIcons\\setlockscreen.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\settile": "\uE18D", //  group:System "_appBarIcons\\settile.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\cc": "\uE190", //  group:Media "_appBarIcons\\cc.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\stopslideshow": "\uE191", //  group:Media "_appBarIcons\\stopslideshow.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\permissions": "\uE192", //  group:System "_appBarIcons\\permissions.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\highlight": "\uE193", //  group:Editing "_appBarIcons\\highlight.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\disableupdates": "\uE194", //  group:System "_appBarIcons\\disableupdates.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\unfavorite": "\uE195", //  group:Media "_appBarIcons\\unfavorite.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\unpin": "\uE196", //  group:System "_appBarIcons\\unpin.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\openlocal": "\uE197", //  group:File "_appBarIcons\\openlocal.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\mute": "\uE198", //  group:Media "_appBarIcons\\mute.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\italic": "\uE199", //  group:Editing "_appBarIcons\\italic.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\underline": "\uE19A", //  group:Editing "_appBarIcons\\underline.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\bold": "\uE19B", //  group:Editing "_appBarIcons\\bold.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\movetofolder": "\uE19C", //  group:File "_appBarIcons\\movetofolder.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\likedislike": "\uE19D", //  group:Data "_appBarIcons\\likedislike.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\dislike": "\uE19E", //  group:Data "_appBarIcons\\dislike.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\like": "\uE19F", //  group:Data "_appBarIcons\\like.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\alignright": "\uE1A0", //  group:Editing "_appBarIcons\\alignright.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\aligncenter": "\uE1A1", //  group:Editing "_appBarIcons\\aligncenter.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\alignleft": "\uE1A2", //  group:Editing "_appBarIcons\\alignleft.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\zoom": "\uE1A3", //  group:Layout "_appBarIcons\\zoom.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\zoomout": "\uE1A4", //  group:Layout "_appBarIcons\\zoomout.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\openfile": "\uE1A5", //  group:File "_appBarIcons\\openfile.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\otheruser": "\uE1A6", //  group:System "_appBarIcons\\otheruser.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\admin": "\uE1A7", //  group:System "_appBarIcons\\admin.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\street": "\uE1C3", //  group:General "_appBarIcons\\street.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\map": "\uE1C4", //  group:General "_appBarIcons\\map.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\clearselection": "\uE1C5", //  group:Data "_appBarIcons\\clearselection.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\fontdecrease": "\uE1C6", //  group:Editing "_appBarIcons\\fontdecrease.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\fontincrease": "\uE1C7", //  group:Editing "_appBarIcons\\fontincrease.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\fontsize": "\uE1C8", //  group:Editing "_appBarIcons\\fontsize.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\cellphone": "\uE1C9", //  group:Communications "_appBarIcons\\cellphone.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\reshare": "\uE1CA", //  group:Communications "_appBarIcons\\reshare.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\tag": "\uE1CB", //  group:Data "_appBarIcons\\tag.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\repeatone": "\uE1CC", //  group:Media "_appBarIcons\\repeatone.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\repeatall": "\uE1CD", //  group:Media "_appBarIcons\\repeatall.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\outlinestar": "\uE1CE", //  group:Data "_appBarIcons\\outlinestar.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\solidstar": "\uE1CF", //  group:Data "_appBarIcons\\solidstar.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\calculator": "\uE1D0", //  group:General "_appBarIcons\\calculator.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\directions": "\uE1D1", //  group:General "_appBarIcons\\directions.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\target": "\uE1D2", //  group:General "_appBarIcons\\target.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\library": "\uE1D3", //  group:Media "_appBarIcons\\library.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\phonebook": "\uE1D4", //  group:Communications "_appBarIcons\\phonebook.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\memo": "\uE1D5", //  group:Communications "_appBarIcons\\memo.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\microphone": "\uE1D6", //  group:System "_appBarIcons\\microphone.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\postupdate": "\uE1D7", //  group:Communications "_appBarIcons\\postupdate.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\backtowindow": "\uE1D8", //  group:Layout "_appBarIcons\\backtowindow.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\fullscreen": "\uE1D9", //  group:Layout "_appBarIcons\\fullscreen.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\newfolder": "\uE1DA", //  group:File "_appBarIcons\\newfolder.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\calendarreply": "\uE1DB", //  group:Mail and calendar "_appBarIcons\\calendarreply.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\unsyncfolder": "\uE1DD", //  group:File "_appBarIcons\\unsyncfolder.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\reporthacked": "\uE1DE", //  group:Communications "_appBarIcons\\reporthacked.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\syncfolder": "\uE1DF", //  group:File "_appBarIcons\\syncfolder.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\blockcontact": "\uE1E0", //  group:Communications "_appBarIcons\\blockcontact.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\switchapps": "\uE1E1", //  group:System "_appBarIcons\\switchapps.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\addfriend": "\uE1E2", //  group:Communications "_appBarIcons\\addfriend.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\touchpointer": "\uE1E3", //  group:System "_appBarIcons\\touchpointer.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\gotostart": "\uE1E4", //  group:System "_appBarIcons\\gotostart.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\zerobars": "\uE1E5", //  group:System "_appBarIcons\\zerobars.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\onebar": "\uE1E6", //  group:System "_appBarIcons\\onebar.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\twobars": "\uE1E7", //  group:System "_appBarIcons\\twobars.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\threebars": "\uE1E8", //  group:System "_appBarIcons\\threebars.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\fourbars": "\uE1E9", //  group:System "_appBarIcons\\fourbars.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\scan": "\uE294", //  group:General "_appBarIcons\\scan.comment": "{Locked:qps-ploc,qps-plocm}", "appBarIcons\\preview": "\uE295", //  group:General "_appBarIcons\\preview.comment": "{Locked:qps-ploc,qps-plocm}" } ); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Core/_Resources',[ 'exports', './_Global', './_WinRT', './_Base', './_Events', 'require-json!en-US/ui.resjson', ], function resourcesInit(exports, _Global, _WinRT, _Base, _Events, defaultStrings) { "use strict"; var appxVersion = "WinJS.3.0"; var developerPrefix = "Developer."; if (appxVersion.indexOf(developerPrefix) === 0) { appxVersion = appxVersion.substring(developerPrefix.length); } function _getWinJSString(id) { var result = getString("ms-resource://" + appxVersion + "/" + id); if (result.empty) { result = _getStringBuiltIn(id); } return result; } function _getStringBuiltIn(resourceId) { var parts = resourceId.split("/"); parts.shift(); // ignore the leading ui/ var str = defaultStrings[parts.join("\\")]; if (typeof str === "string") { str = { value: str }; } return str || { value: resourceId, empty: true }; } var resourceMap; var mrtEventHook = false; var contextChangedET = "contextchanged"; var resourceContext; var ListenerType = _Base.Class.mix(_Base.Class.define(null, { /* empty */ }, { supportedForProcessing: false }), _Events.eventMixin); var listeners = new ListenerType(); var createEvent = _Events._createEventProperty; var strings = { get malformedFormatStringInput() { return "Malformed, did you mean to escape your '{0}'?"; }, }; _Base.Namespace.define("WinJS.Resources", { _getWinJSString: _getWinJSString }); function formatString(string) { var args = arguments; if (args.length > 1) { string = string.replace(/({{)|(}})|{(\d+)}|({)|(})/g, function (unused, left, right, index, illegalLeft, illegalRight) { if (illegalLeft || illegalRight) { throw formatString(strings.malformedFormatStringInput, illegalLeft || illegalRight); } return (left && "{") || (right && "}") || args[(index | 0) + 1]; }); } return string; } _Base.Namespace._moduleDefine(exports, "WinJS.Resources", { addEventListener: function (type, listener, useCapture) { /// /// /// Registers an event handler for the specified event. /// /// /// The name of the event to handle. /// /// /// The listener to invoke when the event gets raised. /// /// /// Set to true to register the event handler for the capturing phase; set to false to register for the bubbling phase. /// /// if (_WinRT.Windows.ApplicationModel.Resources.Core.ResourceManager && !mrtEventHook) { if (type === contextChangedET) { try { var resContext = exports._getResourceContext(); if (resContext) { resContext.qualifierValues.addEventListener("mapchanged", function (e) { exports.dispatchEvent(contextChangedET, { qualifier: e.key, changed: e.target[e.key] }); }, false); } else { // The API can be called in the Background thread (web worker). _WinRT.Windows.ApplicationModel.Resources.Core.ResourceManager.current.defaultContext.qualifierValues.addEventListener("mapchanged", function (e) { exports.dispatchEvent(contextChangedET, { qualifier: e.key, changed: e.target[e.key] }); }, false); } mrtEventHook = true; } catch (e) { } } } listeners.addEventListener(type, listener, useCapture); }, removeEventListener: listeners.removeEventListener.bind(listeners), dispatchEvent: listeners.dispatchEvent.bind(listeners), _formatString: formatString, _getStringWinRT: function (resourceId) { if (!resourceMap) { var mainResourceMap = _WinRT.Windows.ApplicationModel.Resources.Core.ResourceManager.current.mainResourceMap; try { resourceMap = mainResourceMap.getSubtree('Resources'); } catch (e) { } if (!resourceMap) { resourceMap = mainResourceMap; } } var stringValue; var langValue; var resCandidate; try { var resContext = exports._getResourceContext(); if (resContext) { resCandidate = resourceMap.getValue(resourceId, resContext); } else { resCandidate = resourceMap.getValue(resourceId); } if (resCandidate) { stringValue = resCandidate.valueAsString; if (stringValue === undefined) { stringValue = resCandidate.toString(); } } } catch (e) { } if (!stringValue) { return exports._getStringJS(resourceId); } try { langValue = resCandidate.getQualifierValue("Language"); } catch (e) { return { value: stringValue }; } return { value: stringValue, lang: langValue }; }, _getStringJS: function (resourceId) { var str = _Global.strings && _Global.strings[resourceId]; if (typeof str === "string") { str = { value: str }; } return str || { value: resourceId, empty: true }; }, _getResourceContext: function () { if (_Global.document) { if (typeof (resourceContext) === 'undefined') { var context = _WinRT.Windows.ApplicationModel.Resources.Core.ResourceContext; if (context.getForCurrentView) { resourceContext = context.getForCurrentView(); } else { resourceContext = null; } } } return resourceContext; }, oncontextchanged: createEvent(contextChangedET) }); var getStringImpl = _WinRT.Windows.ApplicationModel.Resources.Core.ResourceManager ? exports._getStringWinRT : exports._getStringJS; var getString = function (resourceId) { /// /// /// Retrieves the resource string that has the specified resource id. /// /// /// The resource id of the string to retrieve. /// /// /// An object that can contain these properties: /// /// value: /// The value of the requested string. This property is always present. /// /// empty: /// A value that specifies whether the requested string wasn't found. /// If its true, the string wasn't found. If its false or undefined, /// the requested string was found. /// /// lang: /// The language of the string, if specified. This property is only present /// for multi-language resources. /// /// /// return getStringImpl(resourceId); }; _Base.Namespace._moduleDefine(exports, null, { _formatString: formatString, _getWinJSString: _getWinJSString }); _Base.Namespace._moduleDefine(exports, "WinJS.Resources", { getString: { get: function () { return getString; }, set: function (value) { getString = value; } } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Core/_Trace',[ './_Global' ], function traceInit(_Global) { "use strict"; function nop(v) { return v; } return { _traceAsyncOperationStarting: (_Global.Debug && _Global.Debug.msTraceAsyncOperationStarting && _Global.Debug.msTraceAsyncOperationStarting.bind(_Global.Debug)) || nop, _traceAsyncOperationCompleted: (_Global.Debug && _Global.Debug.msTraceAsyncOperationCompleted && _Global.Debug.msTraceAsyncOperationCompleted.bind(_Global.Debug)) || nop, _traceAsyncCallbackStarting: (_Global.Debug && _Global.Debug.msTraceAsyncCallbackStarting && _Global.Debug.msTraceAsyncCallbackStarting.bind(_Global.Debug)) || nop, _traceAsyncCallbackCompleted: (_Global.Debug && _Global.Debug.msTraceAsyncCallbackCompleted && _Global.Debug.msTraceAsyncCallbackCompleted.bind(_Global.Debug)) || nop }; }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Promise/_StateMachine',[ '../Core/_Global', '../Core/_BaseCoreUtils', '../Core/_Base', '../Core/_ErrorFromName', '../Core/_Events', '../Core/_Trace' ], function promiseStateMachineInit(_Global, _BaseCoreUtils, _Base, _ErrorFromName, _Events, _Trace) { "use strict"; _Global.Debug && (_Global.Debug.setNonUserCodeExceptions = true); var ListenerType = _Base.Class.mix(_Base.Class.define(null, { /*empty*/ }, { supportedForProcessing: false }), _Events.eventMixin); var promiseEventListeners = new ListenerType(); // make sure there is a listeners collection so that we can do a more trivial check below promiseEventListeners._listeners = {}; var errorET = "error"; var canceledName = "Canceled"; var tagWithStack = false; var tag = { promise: 0x01, thenPromise: 0x02, errorPromise: 0x04, exceptionPromise: 0x08, completePromise: 0x10, }; tag.all = tag.promise | tag.thenPromise | tag.errorPromise | tag.exceptionPromise | tag.completePromise; // // Global error counter, for each error which enters the system we increment this once and then // the error number travels with the error as it traverses the tree of potential handlers. // // When someone has registered to be told about errors (WinJS.Promise.callonerror) promises // which are in error will get tagged with a ._errorId field. This tagged field is the // contract by which nested promises with errors will be identified as chaining for the // purposes of the callonerror semantics. If a nested promise in error is encountered without // a ._errorId it will be assumed to be foreign and treated as an interop boundary and // a new error id will be minted. // var error_number = 1; // // The state machine has a interesting hiccup in it with regards to notification, in order // to flatten out notification and avoid recursion for synchronous completion we have an // explicit set of *_notify states which are responsible for notifying their entire tree // of children. They can do this because they know that immediate children are always // ThenPromise instances and we can therefore reach into their state to access the // _listeners collection. // // So, what happens is that a Promise will be fulfilled through the _completed or _error // messages at which point it will enter a *_notify state and be responsible for to move // its children into an (as appropriate) success or error state and also notify that child's // listeners of the state transition, until leaf notes are reached. // var state_created, // -> working state_working, // -> error | error_notify | success | success_notify | canceled | waiting state_waiting, // -> error | error_notify | success | success_notify | waiting_canceled state_waiting_canceled, // -> error | error_notify | success | success_notify | canceling state_canceled, // -> error | error_notify | success | success_notify | canceling state_canceling, // -> error_notify state_success_notify, // -> success state_success, // -> . state_error_notify, // -> error state_error; // -> . // Noop function, used in the various states to indicate that they don't support a given // message. Named with the somewhat cute name '_' because it reads really well in the states. function _() { } // Initial state // state_created = { name: "created", enter: function (promise) { promise._setState(state_working); }, cancel: _, done: _, then: _, _completed: _, _error: _, _notify: _, _progress: _, _setCompleteValue: _, _setErrorValue: _ }; // Ready state, waiting for a message (completed/error/progress), able to be canceled // state_working = { name: "working", enter: _, cancel: function (promise) { promise._setState(state_canceled); }, done: done, then: then, _completed: completed, _error: error, _notify: _, _progress: progress, _setCompleteValue: setCompleteValue, _setErrorValue: setErrorValue }; // Waiting state, if a promise is completed with a value which is itself a promise // (has a then() method) it signs up to be informed when that child promise is // fulfilled at which point it will be fulfilled with that value. // state_waiting = { name: "waiting", enter: function (promise) { var waitedUpon = promise._value; // We can special case our own intermediate promises which are not in a // terminal state by just pushing this promise as a listener without // having to create new indirection functions if (waitedUpon instanceof ThenPromise && waitedUpon._state !== state_error && waitedUpon._state !== state_success) { pushListener(waitedUpon, { promise: promise }); } else { var error = function (value) { if (waitedUpon._errorId) { promise._chainedError(value, waitedUpon); } else { // Because this is an interop boundary we want to indicate that this // error has been handled by the promise infrastructure before we // begin a new handling chain. // callonerror(promise, value, detailsForHandledError, waitedUpon, error); promise._error(value); } }; error.handlesOnError = true; waitedUpon.then( promise._completed.bind(promise), error, promise._progress.bind(promise) ); } }, cancel: function (promise) { promise._setState(state_waiting_canceled); }, done: done, then: then, _completed: completed, _error: error, _notify: _, _progress: progress, _setCompleteValue: setCompleteValue, _setErrorValue: setErrorValue }; // Waiting canceled state, when a promise has been in a waiting state and receives a // request to cancel its pending work it will forward that request to the child promise // and then waits to be informed of the result. This promise moves itself into the // canceling state but understands that the child promise may instead push it to a // different state. // state_waiting_canceled = { name: "waiting_canceled", enter: function (promise) { // Initiate a transition to canceling. Triggering a cancel on the promise // that we are waiting upon may result in a different state transition // before the state machine pump runs again. promise._setState(state_canceling); var waitedUpon = promise._value; if (waitedUpon.cancel) { waitedUpon.cancel(); } }, cancel: _, done: done, then: then, _completed: completed, _error: error, _notify: _, _progress: progress, _setCompleteValue: setCompleteValue, _setErrorValue: setErrorValue }; // Canceled state, moves to the canceling state and then tells the promise to do // whatever it might need to do on cancelation. // state_canceled = { name: "canceled", enter: function (promise) { // Initiate a transition to canceling. The _cancelAction may change the state // before the state machine pump runs again. promise._setState(state_canceling); promise._cancelAction(); }, cancel: _, done: done, then: then, _completed: completed, _error: error, _notify: _, _progress: progress, _setCompleteValue: setCompleteValue, _setErrorValue: setErrorValue }; // Canceling state, commits to the promise moving to an error state with an error // object whose 'name' and 'message' properties contain the string "Canceled" // state_canceling = { name: "canceling", enter: function (promise) { var error = new Error(canceledName); error.name = error.message; promise._value = error; promise._setState(state_error_notify); }, cancel: _, done: _, then: _, _completed: _, _error: _, _notify: _, _progress: _, _setCompleteValue: _, _setErrorValue: _ }; // Success notify state, moves a promise to the success state and notifies all children // state_success_notify = { name: "complete_notify", enter: function (promise) { promise.done = CompletePromise.prototype.done; promise.then = CompletePromise.prototype.then; if (promise._listeners) { var queue = [promise]; var p; while (queue.length) { p = queue.shift(); p._state._notify(p, queue); } } promise._setState(state_success); }, cancel: _, done: null, /*error to get here */ then: null, /*error to get here */ _completed: _, _error: _, _notify: notifySuccess, _progress: _, _setCompleteValue: _, _setErrorValue: _ }; // Success state, moves a promise to the success state and does NOT notify any children. // Some upstream promise is owning the notification pass. // state_success = { name: "success", enter: function (promise) { promise.done = CompletePromise.prototype.done; promise.then = CompletePromise.prototype.then; promise._cleanupAction(); }, cancel: _, done: null, /*error to get here */ then: null, /*error to get here */ _completed: _, _error: _, _notify: notifySuccess, _progress: _, _setCompleteValue: _, _setErrorValue: _ }; // Error notify state, moves a promise to the error state and notifies all children // state_error_notify = { name: "error_notify", enter: function (promise) { promise.done = ErrorPromise.prototype.done; promise.then = ErrorPromise.prototype.then; if (promise._listeners) { var queue = [promise]; var p; while (queue.length) { p = queue.shift(); p._state._notify(p, queue); } } promise._setState(state_error); }, cancel: _, done: null, /*error to get here*/ then: null, /*error to get here*/ _completed: _, _error: _, _notify: notifyError, _progress: _, _setCompleteValue: _, _setErrorValue: _ }; // Error state, moves a promise to the error state and does NOT notify any children. // Some upstream promise is owning the notification pass. // state_error = { name: "error", enter: function (promise) { promise.done = ErrorPromise.prototype.done; promise.then = ErrorPromise.prototype.then; promise._cleanupAction(); }, cancel: _, done: null, /*error to get here*/ then: null, /*error to get here*/ _completed: _, _error: _, _notify: notifyError, _progress: _, _setCompleteValue: _, _setErrorValue: _ }; // // The statemachine implementation follows a very particular pattern, the states are specified // as static stateless bags of functions which are then indirected through the state machine // instance (a Promise). As such all of the functions on each state have the promise instance // passed to them explicitly as a parameter and the Promise instance members do a little // dance where they indirect through the state and insert themselves in the argument list. // // We could instead call directly through the promise states however then every caller // would have to remember to do things like pumping the state machine to catch state transitions. // var PromiseStateMachine = _Base.Class.define(null, { _listeners: null, _nextState: null, _state: null, _value: null, cancel: function () { /// /// /// Attempts to cancel the fulfillment of a promised value. If the promise hasn't /// already been fulfilled and cancellation is supported, the promise enters /// the error state with a value of Error("Canceled"). /// /// this._state.cancel(this); this._run(); }, done: function Promise_done(onComplete, onError, onProgress) { /// /// /// Allows you to specify the work to be done on the fulfillment of the promised value, /// the error handling to be performed if the promise fails to fulfill /// a value, and the handling of progress notifications along the way. /// /// After the handlers have finished executing, this function throws any error that would have been returned /// from then() as a promise in the error state. /// /// /// The function to be called if the promise is fulfilled successfully with a value. /// The fulfilled value is passed as the single argument. If the value is null, /// the fulfilled value is returned. The value returned /// from the function becomes the fulfilled value of the promise returned by /// then(). If an exception is thrown while executing the function, the promise returned /// by then() moves into the error state. /// /// /// The function to be called if the promise is fulfilled with an error. The error /// is passed as the single argument. If it is null, the error is forwarded. /// The value returned from the function is the fulfilled value of the promise returned by then(). /// /// /// the function to be called if the promise reports progress. Data about the progress /// is passed as the single argument. Promises are not required to support /// progress. /// /// this._state.done(this, onComplete, onError, onProgress); }, then: function Promise_then(onComplete, onError, onProgress) { /// /// /// Allows you to specify the work to be done on the fulfillment of the promised value, /// the error handling to be performed if the promise fails to fulfill /// a value, and the handling of progress notifications along the way. /// /// /// The function to be called if the promise is fulfilled successfully with a value. /// The value is passed as the single argument. If the value is null, the value is returned. /// The value returned from the function becomes the fulfilled value of the promise returned by /// then(). If an exception is thrown while this function is being executed, the promise returned /// by then() moves into the error state. /// /// /// The function to be called if the promise is fulfilled with an error. The error /// is passed as the single argument. If it is null, the error is forwarded. /// The value returned from the function becomes the fulfilled value of the promise returned by then(). /// /// /// The function to be called if the promise reports progress. Data about the progress /// is passed as the single argument. Promises are not required to support /// progress. /// /// /// The promise whose value is the result of executing the complete or /// error function. /// /// return this._state.then(this, onComplete, onError, onProgress); }, _chainedError: function (value, context) { var result = this._state._error(this, value, detailsForChainedError, context); this._run(); return result; }, _completed: function (value) { var result = this._state._completed(this, value); this._run(); return result; }, _error: function (value) { var result = this._state._error(this, value, detailsForError); this._run(); return result; }, _progress: function (value) { this._state._progress(this, value); }, _setState: function (state) { this._nextState = state; }, _setCompleteValue: function (value) { this._state._setCompleteValue(this, value); this._run(); }, _setChainedErrorValue: function (value, context) { var result = this._state._setErrorValue(this, value, detailsForChainedError, context); this._run(); return result; }, _setExceptionValue: function (value) { var result = this._state._setErrorValue(this, value, detailsForException); this._run(); return result; }, _run: function () { while (this._nextState) { this._state = this._nextState; this._nextState = null; this._state.enter(this); } } }, { supportedForProcessing: false }); // // Implementations of shared state machine code. // function completed(promise, value) { var targetState; if (value && typeof value === "object" && typeof value.then === "function") { targetState = state_waiting; } else { targetState = state_success_notify; } promise._value = value; promise._setState(targetState); } function createErrorDetails(exception, error, promise, id, parent, handler) { return { exception: exception, error: error, promise: promise, handler: handler, id: id, parent: parent }; } function detailsForHandledError(promise, errorValue, context, handler) { var exception = context._isException; var errorId = context._errorId; return createErrorDetails( exception ? errorValue : null, exception ? null : errorValue, promise, errorId, context, handler ); } function detailsForChainedError(promise, errorValue, context) { var exception = context._isException; var errorId = context._errorId; setErrorInfo(promise, errorId, exception); return createErrorDetails( exception ? errorValue : null, exception ? null : errorValue, promise, errorId, context ); } function detailsForError(promise, errorValue) { var errorId = ++error_number; setErrorInfo(promise, errorId); return createErrorDetails( null, errorValue, promise, errorId ); } function detailsForException(promise, exceptionValue) { var errorId = ++error_number; setErrorInfo(promise, errorId, true); return createErrorDetails( exceptionValue, null, promise, errorId ); } function done(promise, onComplete, onError, onProgress) { var asyncOpID = _Trace._traceAsyncOperationStarting("WinJS.Promise.done"); pushListener(promise, { c: onComplete, e: onError, p: onProgress, asyncOpID: asyncOpID }); } function error(promise, value, onerrorDetails, context) { promise._value = value; callonerror(promise, value, onerrorDetails, context); promise._setState(state_error_notify); } function notifySuccess(promise, queue) { var value = promise._value; var listeners = promise._listeners; if (!listeners) { return; } promise._listeners = null; var i, len; for (i = 0, len = Array.isArray(listeners) ? listeners.length : 1; i < len; i++) { var listener = len === 1 ? listeners : listeners[i]; var onComplete = listener.c; var target = listener.promise; _Trace._traceAsyncOperationCompleted(listener.asyncOpID, _Global.Debug && _Global.Debug.MS_ASYNC_OP_STATUS_SUCCESS); if (target) { _Trace._traceAsyncCallbackStarting(listener.asyncOpID); try { target._setCompleteValue(onComplete ? onComplete(value) : value); } catch (ex) { target._setExceptionValue(ex); } finally { _Trace._traceAsyncCallbackCompleted(); } if (target._state !== state_waiting && target._listeners) { queue.push(target); } } else { CompletePromise.prototype.done.call(promise, onComplete); } } } function notifyError(promise, queue) { var value = promise._value; var listeners = promise._listeners; if (!listeners) { return; } promise._listeners = null; var i, len; for (i = 0, len = Array.isArray(listeners) ? listeners.length : 1; i < len; i++) { var listener = len === 1 ? listeners : listeners[i]; var onError = listener.e; var target = listener.promise; var errorID = _Global.Debug && (value && value.name === canceledName ? _Global.Debug.MS_ASYNC_OP_STATUS_CANCELED : _Global.Debug.MS_ASYNC_OP_STATUS_ERROR); _Trace._traceAsyncOperationCompleted(listener.asyncOpID, errorID); if (target) { var asyncCallbackStarted = false; try { if (onError) { _Trace._traceAsyncCallbackStarting(listener.asyncOpID); asyncCallbackStarted = true; if (!onError.handlesOnError) { callonerror(target, value, detailsForHandledError, promise, onError); } target._setCompleteValue(onError(value)); } else { target._setChainedErrorValue(value, promise); } } catch (ex) { target._setExceptionValue(ex); } finally { if (asyncCallbackStarted) { _Trace._traceAsyncCallbackCompleted(); } } if (target._state !== state_waiting && target._listeners) { queue.push(target); } } else { ErrorPromise.prototype.done.call(promise, null, onError); } } } function callonerror(promise, value, onerrorDetailsGenerator, context, handler) { if (promiseEventListeners._listeners[errorET]) { if (value instanceof Error && value.message === canceledName) { return; } promiseEventListeners.dispatchEvent(errorET, onerrorDetailsGenerator(promise, value, context, handler)); } } function progress(promise, value) { var listeners = promise._listeners; if (listeners) { var i, len; for (i = 0, len = Array.isArray(listeners) ? listeners.length : 1; i < len; i++) { var listener = len === 1 ? listeners : listeners[i]; var onProgress = listener.p; if (onProgress) { try { onProgress(value); } catch (ex) { } } if (!(listener.c || listener.e) && listener.promise) { listener.promise._progress(value); } } } } function pushListener(promise, listener) { var listeners = promise._listeners; if (listeners) { // We may have either a single listener (which will never be wrapped in an array) // or 2+ listeners (which will be wrapped). Since we are now adding one more listener // we may have to wrap the single listener before adding the second. listeners = Array.isArray(listeners) ? listeners : [listeners]; listeners.push(listener); } else { listeners = listener; } promise._listeners = listeners; } // The difference beween setCompleteValue()/setErrorValue() and complete()/error() is that setXXXValue() moves // a promise directly to the success/error state without starting another notification pass (because one // is already ongoing). function setErrorInfo(promise, errorId, isException) { promise._isException = isException || false; promise._errorId = errorId; } function setErrorValue(promise, value, onerrorDetails, context) { promise._value = value; callonerror(promise, value, onerrorDetails, context); promise._setState(state_error); } function setCompleteValue(promise, value) { var targetState; if (value && typeof value === "object" && typeof value.then === "function") { targetState = state_waiting; } else { targetState = state_success; } promise._value = value; promise._setState(targetState); } function then(promise, onComplete, onError, onProgress) { var result = new ThenPromise(promise); var asyncOpID = _Trace._traceAsyncOperationStarting("WinJS.Promise.then"); pushListener(promise, { promise: result, c: onComplete, e: onError, p: onProgress, asyncOpID: asyncOpID }); return result; } // // Internal implementation detail promise, ThenPromise is created when a promise needs // to be returned from a then() method. // var ThenPromise = _Base.Class.derive(PromiseStateMachine, function (creator) { if (tagWithStack && (tagWithStack === true || (tagWithStack & tag.thenPromise))) { this._stack = Promise._getStack(); } this._creator = creator; this._setState(state_created); this._run(); }, { _creator: null, _cancelAction: function () { if (this._creator) { this._creator.cancel(); } }, _cleanupAction: function () { this._creator = null; } }, { supportedForProcessing: false } ); // // Slim promise implementations for already completed promises, these are created // under the hood on synchronous completion paths as well as by WinJS.Promise.wrap // and WinJS.Promise.wrapError. // var ErrorPromise = _Base.Class.define( function ErrorPromise_ctor(value) { if (tagWithStack && (tagWithStack === true || (tagWithStack & tag.errorPromise))) { this._stack = Promise._getStack(); } this._value = value; callonerror(this, value, detailsForError); }, { cancel: function () { /// /// /// Attempts to cancel the fulfillment of a promised value. If the promise hasn't /// already been fulfilled and cancellation is supported, the promise enters /// the error state with a value of Error("Canceled"). /// /// }, done: function ErrorPromise_done(unused, onError) { /// /// /// Allows you to specify the work to be done on the fulfillment of the promised value, /// the error handling to be performed if the promise fails to fulfill /// a value, and the handling of progress notifications along the way. /// /// After the handlers have finished executing, this function throws any error that would have been returned /// from then() as a promise in the error state. /// /// /// The function to be called if the promise is fulfilled successfully with a value. /// The fulfilled value is passed as the single argument. If the value is null, /// the fulfilled value is returned. The value returned /// from the function becomes the fulfilled value of the promise returned by /// then(). If an exception is thrown while executing the function, the promise returned /// by then() moves into the error state. /// /// /// The function to be called if the promise is fulfilled with an error. The error /// is passed as the single argument. If it is null, the error is forwarded. /// The value returned from the function is the fulfilled value of the promise returned by then(). /// /// /// the function to be called if the promise reports progress. Data about the progress /// is passed as the single argument. Promises are not required to support /// progress. /// /// var value = this._value; if (onError) { try { if (!onError.handlesOnError) { callonerror(null, value, detailsForHandledError, this, onError); } var result = onError(value); if (result && typeof result === "object" && typeof result.done === "function") { // If a promise is returned we need to wait on it. result.done(); } return; } catch (ex) { value = ex; } } if (value instanceof Error && value.message === canceledName) { // suppress cancel return; } // force the exception to be thrown asyncronously to avoid any try/catch blocks // Promise._doneHandler(value); }, then: function ErrorPromise_then(unused, onError) { /// /// /// Allows you to specify the work to be done on the fulfillment of the promised value, /// the error handling to be performed if the promise fails to fulfill /// a value, and the handling of progress notifications along the way. /// /// /// The function to be called if the promise is fulfilled successfully with a value. /// The value is passed as the single argument. If the value is null, the value is returned. /// The value returned from the function becomes the fulfilled value of the promise returned by /// then(). If an exception is thrown while this function is being executed, the promise returned /// by then() moves into the error state. /// /// /// The function to be called if the promise is fulfilled with an error. The error /// is passed as the single argument. If it is null, the error is forwarded. /// The value returned from the function becomes the fulfilled value of the promise returned by then(). /// /// /// The function to be called if the promise reports progress. Data about the progress /// is passed as the single argument. Promises are not required to support /// progress. /// /// /// The promise whose value is the result of executing the complete or /// error function. /// /// // If the promise is already in a error state and no error handler is provided // we optimize by simply returning the promise instead of creating a new one. // if (!onError) { return this; } var result; var value = this._value; try { if (!onError.handlesOnError) { callonerror(null, value, detailsForHandledError, this, onError); } result = new CompletePromise(onError(value)); } catch (ex) { // If the value throw from the error handler is the same as the value // provided to the error handler then there is no need for a new promise. // if (ex === value) { result = this; } else { result = new ExceptionPromise(ex); } } return result; } }, { supportedForProcessing: false } ); var ExceptionPromise = _Base.Class.derive(ErrorPromise, function ExceptionPromise_ctor(value) { if (tagWithStack && (tagWithStack === true || (tagWithStack & tag.exceptionPromise))) { this._stack = Promise._getStack(); } this._value = value; callonerror(this, value, detailsForException); }, { /* empty */ }, { supportedForProcessing: false } ); var CompletePromise = _Base.Class.define( function CompletePromise_ctor(value) { if (tagWithStack && (tagWithStack === true || (tagWithStack & tag.completePromise))) { this._stack = Promise._getStack(); } if (value && typeof value === "object" && typeof value.then === "function") { var result = new ThenPromise(null); result._setCompleteValue(value); return result; } this._value = value; }, { cancel: function () { /// /// /// Attempts to cancel the fulfillment of a promised value. If the promise hasn't /// already been fulfilled and cancellation is supported, the promise enters /// the error state with a value of Error("Canceled"). /// /// }, done: function CompletePromise_done(onComplete) { /// /// /// Allows you to specify the work to be done on the fulfillment of the promised value, /// the error handling to be performed if the promise fails to fulfill /// a value, and the handling of progress notifications along the way. /// /// After the handlers have finished executing, this function throws any error that would have been returned /// from then() as a promise in the error state. /// /// /// The function to be called if the promise is fulfilled successfully with a value. /// The fulfilled value is passed as the single argument. If the value is null, /// the fulfilled value is returned. The value returned /// from the function becomes the fulfilled value of the promise returned by /// then(). If an exception is thrown while executing the function, the promise returned /// by then() moves into the error state. /// /// /// The function to be called if the promise is fulfilled with an error. The error /// is passed as the single argument. If it is null, the error is forwarded. /// The value returned from the function is the fulfilled value of the promise returned by then(). /// /// /// the function to be called if the promise reports progress. Data about the progress /// is passed as the single argument. Promises are not required to support /// progress. /// /// if (!onComplete) { return; } try { var result = onComplete(this._value); if (result && typeof result === "object" && typeof result.done === "function") { result.done(); } } catch (ex) { // force the exception to be thrown asynchronously to avoid any try/catch blocks Promise._doneHandler(ex); } }, then: function CompletePromise_then(onComplete) { /// /// /// Allows you to specify the work to be done on the fulfillment of the promised value, /// the error handling to be performed if the promise fails to fulfill /// a value, and the handling of progress notifications along the way. /// /// /// The function to be called if the promise is fulfilled successfully with a value. /// The value is passed as the single argument. If the value is null, the value is returned. /// The value returned from the function becomes the fulfilled value of the promise returned by /// then(). If an exception is thrown while this function is being executed, the promise returned /// by then() moves into the error state. /// /// /// The function to be called if the promise is fulfilled with an error. The error /// is passed as the single argument. If it is null, the error is forwarded. /// The value returned from the function becomes the fulfilled value of the promise returned by then(). /// /// /// The function to be called if the promise reports progress. Data about the progress /// is passed as the single argument. Promises are not required to support /// progress. /// /// /// The promise whose value is the result of executing the complete or /// error function. /// /// try { // If the value returned from the completion handler is the same as the value // provided to the completion handler then there is no need for a new promise. // var newValue = onComplete ? onComplete(this._value) : this._value; return newValue === this._value ? this : new CompletePromise(newValue); } catch (ex) { return new ExceptionPromise(ex); } } }, { supportedForProcessing: false } ); // // Promise is the user-creatable WinJS.Promise object. // function timeout(timeoutMS) { var id; return new Promise( function (c) { if (timeoutMS) { id = _Global.setTimeout(c, timeoutMS); } else { _BaseCoreUtils._setImmediate(c); } }, function () { if (id) { _Global.clearTimeout(id); } } ); } function timeoutWithPromise(timeout, promise) { var cancelPromise = function () { promise.cancel(); }; var cancelTimeout = function () { timeout.cancel(); }; timeout.then(cancelPromise); promise.then(cancelTimeout, cancelTimeout); return promise; } var staticCanceledPromise; var Promise = _Base.Class.derive(PromiseStateMachine, function Promise_ctor(init, oncancel) { /// /// /// A promise provides a mechanism to schedule work to be done on a value that /// has not yet been computed. It is a convenient abstraction for managing /// interactions with asynchronous APIs. /// /// /// The function that is called during construction of the promise. The function /// is given three arguments (complete, error, progress). Inside this function /// you should add event listeners for the notifications supported by this value. /// /// /// The function to call if a consumer of this promise wants /// to cancel its undone work. Promises are not required to /// support cancellation. /// /// if (tagWithStack && (tagWithStack === true || (tagWithStack & tag.promise))) { this._stack = Promise._getStack(); } this._oncancel = oncancel; this._setState(state_created); this._run(); try { var complete = this._completed.bind(this); var error = this._error.bind(this); var progress = this._progress.bind(this); init(complete, error, progress); } catch (ex) { this._setExceptionValue(ex); } }, { _oncancel: null, _cancelAction: function () { if (this._oncancel) { try { this._oncancel(); } catch (ex) { } } }, _cleanupAction: function () { this._oncancel = null; } }, { addEventListener: function Promise_addEventListener(eventType, listener, capture) { /// /// /// Adds an event listener to the control. /// /// /// The type (name) of the event. /// /// /// The listener to invoke when the event is raised. /// /// /// Specifies whether or not to initiate capture. /// /// promiseEventListeners.addEventListener(eventType, listener, capture); }, any: function Promise_any(values) { /// /// /// Returns a promise that is fulfilled when one of the input promises /// has been fulfilled. /// /// /// An array that contains promise objects or objects whose property /// values include promise objects. /// /// /// A promise that on fulfillment yields the value of the input (complete or error). /// /// return new Promise( function (complete, error) { var keys = Object.keys(values); if (keys.length === 0) { complete(); } var canceled = 0; keys.forEach(function (key) { Promise.as(values[key]).then( function () { complete({ key: key, value: values[key] }); }, function (e) { if (e instanceof Error && e.name === canceledName) { if ((++canceled) === keys.length) { complete(Promise.cancel); } return; } error({ key: key, value: values[key] }); } ); }); }, function () { var keys = Object.keys(values); keys.forEach(function (key) { var promise = Promise.as(values[key]); if (typeof promise.cancel === "function") { promise.cancel(); } }); } ); }, as: function Promise_as(value) { /// /// /// Returns a promise. If the object is already a promise it is returned; /// otherwise the object is wrapped in a promise. /// /// /// The value to be treated as a promise. /// /// /// A promise. /// /// if (value && typeof value === "object" && typeof value.then === "function") { return value; } return new CompletePromise(value); }, /// /// Canceled promise value, can be returned from a promise completion handler /// to indicate cancelation of the promise chain. /// cancel: { get: function () { return (staticCanceledPromise = staticCanceledPromise || new ErrorPromise(new _ErrorFromName(canceledName))); } }, dispatchEvent: function Promise_dispatchEvent(eventType, details) { /// /// /// Raises an event of the specified type and properties. /// /// /// The type (name) of the event. /// /// /// The set of additional properties to be attached to the event object. /// /// /// Specifies whether preventDefault was called on the event. /// /// return promiseEventListeners.dispatchEvent(eventType, details); }, is: function Promise_is(value) { /// /// /// Determines whether a value fulfills the promise contract. /// /// /// A value that may be a promise. /// /// /// true if the specified value is a promise, otherwise false. /// /// return value && typeof value === "object" && typeof value.then === "function"; }, join: function Promise_join(values) { /// /// /// Creates a promise that is fulfilled when all the values are fulfilled. /// /// /// An object whose fields contain values, some of which may be promises. /// /// /// A promise whose value is an object with the same field names as those of the object in the values parameter, where /// each field value is the fulfilled value of a promise. /// /// return new Promise( function (complete, error, progress) { var keys = Object.keys(values); var errors = Array.isArray(values) ? [] : {}; var results = Array.isArray(values) ? [] : {}; var undefineds = 0; var pending = keys.length; var argDone = function (key) { if ((--pending) === 0) { var errorCount = Object.keys(errors).length; if (errorCount === 0) { complete(results); } else { var canceledCount = 0; keys.forEach(function (key) { var e = errors[key]; if (e instanceof Error && e.name === canceledName) { canceledCount++; } }); if (canceledCount === errorCount) { complete(Promise.cancel); } else { error(errors); } } } else { progress({ Key: key, Done: true }); } }; keys.forEach(function (key) { var value = values[key]; if (value === undefined) { undefineds++; } else { Promise.then(value, function (value) { results[key] = value; argDone(key); }, function (value) { errors[key] = value; argDone(key); } ); } }); pending -= undefineds; if (pending === 0) { complete(results); return; } }, function () { Object.keys(values).forEach(function (key) { var promise = Promise.as(values[key]); if (typeof promise.cancel === "function") { promise.cancel(); } }); } ); }, removeEventListener: function Promise_removeEventListener(eventType, listener, capture) { /// /// /// Removes an event listener from the control. /// /// /// The type (name) of the event. /// /// /// The listener to remove. /// /// /// Specifies whether or not to initiate capture. /// /// promiseEventListeners.removeEventListener(eventType, listener, capture); }, supportedForProcessing: false, then: function Promise_then(value, onComplete, onError, onProgress) { /// /// /// A static version of the promise instance method then(). /// /// /// the value to be treated as a promise. /// /// /// The function to be called if the promise is fulfilled with a value. /// If it is null, the promise simply /// returns the value. The value is passed as the single argument. /// /// /// The function to be called if the promise is fulfilled with an error. The error /// is passed as the single argument. /// /// /// The function to be called if the promise reports progress. Data about the progress /// is passed as the single argument. Promises are not required to support /// progress. /// /// /// A promise whose value is the result of executing the provided complete function. /// /// return Promise.as(value).then(onComplete, onError, onProgress); }, thenEach: function Promise_thenEach(values, onComplete, onError, onProgress) { /// /// /// Performs an operation on all the input promises and returns a promise /// that has the shape of the input and contains the result of the operation /// that has been performed on each input. /// /// /// A set of values (which could be either an array or an object) of which some or all are promises. /// /// /// The function to be called if the promise is fulfilled with a value. /// If the value is null, the promise returns the value. /// The value is passed as the single argument. /// /// /// The function to be called if the promise is fulfilled with an error. The error /// is passed as the single argument. /// /// /// The function to be called if the promise reports progress. Data about the progress /// is passed as the single argument. Promises are not required to support /// progress. /// /// /// A promise that is the result of calling Promise.join on the values parameter. /// /// var result = Array.isArray(values) ? [] : {}; Object.keys(values).forEach(function (key) { result[key] = Promise.as(values[key]).then(onComplete, onError, onProgress); }); return Promise.join(result); }, timeout: function Promise_timeout(time, promise) { /// /// /// Creates a promise that is fulfilled after a timeout. /// /// /// The timeout period in milliseconds. If this value is zero or not specified /// setImmediate is called, otherwise setTimeout is called. /// /// /// A promise that will be canceled if it doesn't complete before the /// timeout has expired. /// /// /// A promise that is completed asynchronously after the specified timeout. /// /// var to = timeout(time); return promise ? timeoutWithPromise(to, promise) : to; }, wrap: function Promise_wrap(value) { /// /// /// Wraps a non-promise value in a promise. You can use this function if you need /// to pass a value to a function that requires a promise. /// /// /// Some non-promise value to be wrapped in a promise. /// /// /// A promise that is successfully fulfilled with the specified value /// /// return new CompletePromise(value); }, wrapError: function Promise_wrapError(error) { /// /// /// Wraps a non-promise error value in a promise. You can use this function if you need /// to pass an error to a function that requires a promise. /// /// /// A non-promise error value to be wrapped in a promise. /// /// /// A promise that is in an error state with the specified value. /// /// return new ErrorPromise(error); }, _veryExpensiveTagWithStack: { get: function () { return tagWithStack; }, set: function (value) { tagWithStack = value; } }, _veryExpensiveTagWithStack_tag: tag, _getStack: function () { if (_Global.Debug && _Global.Debug.debuggerEnabled) { try { throw new Error(); } catch (e) { return e.stack; } } }, _cancelBlocker: function Promise__cancelBlocker(input) { // // Returns a promise which on cancelation will still result in downstream cancelation while // protecting the promise 'input' from being canceled which has the effect of allowing // 'input' to be shared amoung various consumers. // if (!Promise.is(input)) { return Promise.wrap(input); } var complete; var error; var output = new Promise( function (c, e) { complete = c; error = e; }, function () { complete = null; error = null; } ); input.then( function (v) { complete && complete(v); }, function (e) { error && error(e); } ); return output; }, } ); Object.defineProperties(Promise, _Events.createEventProperties(errorET)); Promise._doneHandler = function (value) { _BaseCoreUtils._setImmediate(function Promise_done_rethrow() { throw value; }); }; return { PromiseStateMachine: PromiseStateMachine, Promise: Promise, state_created: state_created }; }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Promise',[ './Core/_Base', './Promise/_StateMachine' ], function promiseInit( _Base, _StateMachine) { "use strict"; _Base.Namespace.define("WinJS", { Promise: _StateMachine.Promise }); return _StateMachine.Promise; }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Core/_Log',[ 'exports', './_Global', './_Base', ], function logInit(exports, _Global, _Base) { "use strict"; var spaceR = /\s+/g; var typeR = /^(error|warn|info|log)$/; var WinJSLog = null; function format(message, tag, type) { /// /// /// Adds tags and type to a logging message. /// /// The message to format. /// /// The tag(s) to apply to the message. Separate multiple tags with spaces. /// /// The type of the message. /// The formatted message. /// var m = message; if (typeof (m) === "function") { m = m(); } return ((type && typeR.test(type)) ? ("") : (type ? (type + ": ") : "")) + (tag ? tag.replace(spaceR, ":") + ": " : "") + m; } function defAction(message, tag, type) { var m = exports.formatLog(message, tag, type); if (_Global.console) { _Global.console[(type && typeR.test(type)) ? type : "log"](m); } } function escape(s) { // \s (whitespace) is used as separator, so don't escape it return s.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\$&"); } _Base.Namespace._moduleDefine(exports, "WinJS.Utilities", { startLog: function (options) { /// /// /// Configures a logger that writes messages containing the specified tags from WinJS.log to console.log. /// /// /// The tags for messages to log. Separate multiple tags with spaces. /// /// /// /// /// Configure a logger to write WinJS.log output. /// /// /// May contain .type, .tags, .excludeTags and .action properties. /// - .type is a required tag. /// - .excludeTags is a space-separated list of tags, any of which will result in a message not being logged. /// - .tags is a space-separated list of tags, any of which will result in a message being logged. /// - .action is a function that, if present, will be called with the log message, tags and type. The default is to log to the console. /// /// options = options || {}; if (typeof options === "string") { options = { tags: options }; } var el = options.type && new RegExp("^(" + escape(options.type).replace(spaceR, " ").split(" ").join("|") + ")$"); var not = options.excludeTags && new RegExp("(^|\\s)(" + escape(options.excludeTags).replace(spaceR, " ").split(" ").join("|") + ")(\\s|$)", "i"); var has = options.tags && new RegExp("(^|\\s)(" + escape(options.tags).replace(spaceR, " ").split(" ").join("|") + ")(\\s|$)", "i"); var action = options.action || defAction; if (!el && !not && !has && !exports.log) { exports.log = action; return; } var result = function (message, tag, type) { if (!((el && !el.test(type)) // if the expected log level is not satisfied || (not && not.test(tag)) // if any of the excluded categories exist || (has && !has.test(tag)))) { // if at least one of the included categories doesn't exist action(message, tag, type); } result.next && result.next(message, tag, type); }; result.next = exports.log; exports.log = result; }, stopLog: function () { /// /// /// Removes the previously set up logger. /// /// exports.log = null; }, formatLog: format }); _Base.Namespace._moduleDefine(exports, "WinJS", { log: { get: function () { return WinJSLog; }, set: function (value) { WinJSLog = value; } } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Scheduler',[ 'exports', './Core/_Global', './Core/_Base', './Core/_ErrorFromName', './Core/_Log', './Core/_Resources', './Core/_Trace', './Core/_WriteProfilerMark', './Promise' ], function schedulerInit(exports, _Global, _Base, _ErrorFromName, _Log, _Resources, _Trace, _WriteProfilerMark, Promise) { "use strict"; function linkedListMixin(name) { var mixin = {}; var PREV = "_prev" + name; var NEXT = "_next" + name; mixin["_remove" + name] = function () { // Assumes we always have a static head and tail. // var prev = this[PREV]; var next = this[NEXT]; // PREV <-> NEXT // next && (next[PREV] = prev); prev && (prev[NEXT] = next); // null <- this -> null // this[PREV] = null; this[NEXT] = null; }; mixin["_insert" + name + "Before"] = function (node) { var prev = this[PREV]; // PREV -> node -> this // prev && (prev[NEXT] = node); node[NEXT] = this; // PREV <- node <- this // node[PREV] = prev; this[PREV] = node; return node; }; mixin["_insert" + name + "After"] = function (node) { var next = this[NEXT]; // this -> node -> NEXT // this[NEXT] = node; node[NEXT] = next; // this <- node <- NEXT // node[PREV] = this; next && (next[PREV] = node); return node; }; return mixin; } _Base.Namespace.define("WinJS.Utilities", { _linkedListMixin: linkedListMixin }); var strings = { get jobInfoIsNoLongerValid() { return "The job info object can only be used while the job is running"; } }; // // Profiler mark helpers // // markerType must be one of the following: info, StartTM, StopTM // function profilerMarkArgs(arg0, arg1, arg2) { if (arg2 !== undefined) { return "(" + arg0 + ";" + arg1 + ";" + arg2 + ")"; } else if (arg1 !== undefined) { return "(" + arg0 + ";" + arg1 + ")"; } else if (arg0 !== undefined) { return "(" + arg0 + ")"; } else { return ""; } } function schedulerProfilerMark(operation, markerType, arg0, arg1) { _WriteProfilerMark( "WinJS.Scheduler:" + operation + profilerMarkArgs(arg0, arg1) + "," + markerType ); } function jobProfilerMark(job, operation, markerType, arg0, arg1) { var argProvided = job.name || arg0 !== undefined || arg1 !== undefined; _WriteProfilerMark( "WinJS.Scheduler:" + operation + ":" + job.id + (argProvided ? profilerMarkArgs(job.name, arg0, arg1) : "") + "," + markerType ); } // // Job type. This cannot be instantiated by developers and is instead handed back by the scheduler // schedule method. Its public interface is what is used when interacting with a job. // var JobNode = _Base.Class.define(function (id, work, priority, context, name, asyncOpID) { this._id = id; this._work = work; this._context = context; this._name = name; this._asyncOpID = asyncOpID; this._setPriority(priority); this._setState(state_created); jobProfilerMark(this, "job-scheduled", "info"); }, { /// /// Gets a value that indicates whether the job has completed. This value is true if job has run to completion /// and false if it hasn't yet run or was canceled. /// completed: { get: function () { return !!this._state.completed; } }, /// /// Gets the unique identifier for this job. /// id: { get: function () { return this._id; } }, /// /// Gets or sets a string that specifies the diagnostic name for this job. /// name: { get: function () { return this._name; }, set: function (value) { this._name = value; } }, /// /// Gets an owner token for the job. You can use this owner token's cancelAll method to cancel related jobs. /// owner: { get: function () { return this._owner; }, set: function (value) { this._owner && this._owner._remove(this); this._owner = value; this._owner && this._owner._add(this); } }, /// /// Gets or sets the priority at which this job is executed by the scheduler. /// priority: { get: function () { return this._priority; }, set: function (value) { value = clampPriority(value); this._state.setPriority(this, value); } }, cancel: function () { /// /// Cancels the job. /// this._state.cancel(this); }, pause: function () { /// /// Pauses the job. /// this._state.pause(this); }, resume: function () { /// /// Resumes the job if it's been paused. /// this._state.resume(this); }, _execute: function (shouldYield) { this._state.execute(this, shouldYield); }, _executeDone: function (result) { return this._state.executeDone(this, result); }, _blockedDone: function (result) { return this._state.blockedDone(this, result); }, _setPriority: function (value) { if (+this._priority === this._priority && this._priority !== value) { jobProfilerMark(this, "job-priority-changed", "info", markerFromPriority(this._priority).name, markerFromPriority(value).name); } this._priority = value; }, _setState: function (state, arg0, arg1) { if (this._state) { _Log.log && _Log.log("Transitioning job (" + this.id + ") from: " + this._state.name + " to: " + state.name, "winjs scheduler", "log"); } this._state = state; this._state.enter(this, arg0, arg1); }, }); _Base.Class.mix(JobNode, linkedListMixin("Job")); var YieldPolicy = { complete: 1, continue: 2, block: 3, }; // // JobInfo object is passed to a work item when it is executed and allows the work to ask whether it // should cooperatively yield and in that event provide a continuation work function to run the // next time this job is scheduled. The JobInfo object additionally allows access to the job itself // and the ability to provide a Promise for a future continuation work function in order to have // jobs easily block on async work. // var JobInfo = _Base.Class.define(function (shouldYield, job) { this._job = job; this._result = null; this._yieldPolicy = YieldPolicy.complete; this._shouldYield = shouldYield; }, { /// /// The job instance for which the work is currently being executed. /// job: { get: function () { this._throwIfDisabled(); return this._job; } }, /// /// A boolean which will become true when the work item is requested to cooperatively yield by the scheduler. /// shouldYield: { get: function () { this._throwIfDisabled(); return this._shouldYield(); } }, setPromise: function (promise) { /// /// /// Called when the work item is blocked on asynchronous work. /// The scheduler waits for the specified Promise to complete before rescheduling the job. /// /// /// A Promise value which, when completed, provides a work item function to be re-scheduled. /// /// this._throwIfDisabled(); this._result = promise; this._yieldPolicy = YieldPolicy.block; }, setWork: function (work) { /// /// /// Called when the work item is cooperatively yielding to the scheduler and has more work to complete in the future. /// Use this method to schedule additonal work for when the work item is about to yield. /// /// /// The work function which will be re-scheduled. /// /// this._throwIfDisabled(); this._result = work; this._yieldPolicy = YieldPolicy.continue; }, _disablePublicApi: function () { // _disablePublicApi should be called as soon as the job yields. This // says that the job info object should no longer be used by the // job and if the job tries to use it, job info will throw. // this._publicApiDisabled = true; }, _throwIfDisabled: function () { if (this._publicApiDisabled) { throw new _ErrorFromName("WinJS.Utilities.Scheduler.JobInfoIsNoLongerValid", strings.jobInfoIsNoLongerValid); } } }); // // Owner type. Made available to developers through the createOwnerToken method. // Allows cancelation of jobs in bulk. // var OwnerToken = _Base.Class.define(function OwnerToken_ctor() { this._jobs = {}; }, { cancelAll: function OwnerToken_cancelAll() { /// /// /// Cancels all jobs that are associated with this owner token. /// /// var jobs = this._jobs, jobIds = Object.keys(jobs); this._jobs = {}; for (var i = 0, len = jobIds.length; i < len; i++) { jobs[jobIds[i]].cancel(); } }, _add: function OwnerToken_add(job) { this._jobs[job.id] = job; }, _remove: function OwnerToken_remove(job) { delete this._jobs[job.id]; } }); function _() { // Noop function, used in the various states to indicate that they don't support a given // message. Named with the somewhat cute name '_' because it reads really well in the states. // return false; } function illegal(job) { /*jshint validthis: true */ throw "Illegal call by job(" + job.id + ") in state: " + this.name; } // // Scheduler job state machine. // // A job normally goes through a lifecycle which is created -> scheduled -> running -> complete. The // Scheduler decides when to transition a job from scheduled to running based on its policies and // the other work which is scheduled. // // Additionally there are various operations which can be performed on a job which will change its // state like: cancel, pause, resume and setting the job's priority. // // Additionally when in the running state a job may either cooperatively yield, or block. // // The job state machine accounts for these various states and interactions. // var State = _Base.Class.define(function (name) { this.name = name; this.enter = illegal; this.execute = illegal; this.executeDone = illegal; this.blockedDone = illegal; this.cancel = illegal; this.pause = illegal; this.resume = illegal; this.setPriority = illegal; }); var state_created = new State("created"), // -> scheduled state_scheduled = new State("scheduled"), // -> running | canceled | paused state_paused = new State("paused"), // -> canceled | scheduled state_canceled = new State("canceled"), // -> . state_running = new State("running"), // -> cooperative_yield | blocked | complete | running_canceled | running_paused state_running_paused = new State("running_paused"), // -> cooperative_yield_paused | blocked_paused | complete | running_canceled | running_resumed state_running_resumed = new State("running_resumed"), // -> cooperative_yield | blocked | complete | running_canceled | running_paused state_running_canceled = new State("running_canceled"), // -> canceled | running_canceled_blocked state_running_canceled_blocked = new State("running_canceled_blocked"), // -> canceled state_cooperative_yield = new State("cooperative_yield"), // -> scheduled state_cooperative_yield_paused = new State("cooperative_yield_paused"), // -> paused state_blocked = new State("blocked"), // -> blocked_waiting state_blocked_waiting = new State("blocked_waiting"), // -> cooperative_yield | complete | blocked_canceled | blocked_paused_waiting state_blocked_paused = new State("blocked_paused"), // -> blocked_paused_waiting state_blocked_paused_waiting = new State("blocked_paused_waiting"), // -> cooperative_yield_paused | complete | blocked_canceled | blocked_waiting state_blocked_canceled = new State("blocked_canceled"), // -> canceled state_complete = new State("complete"); // -> . // A given state may include implementations for the following operations: // // - enter(job, arg0, arg1) // - execute(job, shouldYield) // - executeDone(job, result) --> next state // - blockedDone(job, result, initialPriority) // - cancel(job) // - pause(job) // - resume(job) // - setPriority(job, priority) // // Any functions which are not implemented are illegal in that state. // Any functions which have an implementation of _ are a nop in that state. // // Helper which yields a function that transitions to the specified state // function setState(state) { return function (job, arg0, arg1) { job._setState(state, arg0, arg1); }; } // Helper which sets the priority of a job. // function changePriority(job, priority) { job._setPriority(priority); } // Created // state_created.enter = function (job) { addJobAtTailOfPriority(job, job.priority); job._setState(state_scheduled); }; // Scheduled // state_scheduled.enter = function () { startRunning(); }; state_scheduled.execute = setState(state_running); state_scheduled.cancel = setState(state_canceled); state_scheduled.pause = setState(state_paused); state_scheduled.resume = _; state_scheduled.setPriority = function (job, priority) { if (job.priority !== priority) { job._setPriority(priority); job.pause(); job.resume(); } }; // Paused // state_paused.enter = function (job) { jobProfilerMark(job, "job-paused", "info"); job._removeJob(); }; state_paused.cancel = setState(state_canceled); state_paused.pause = _; state_paused.resume = function (job) { jobProfilerMark(job, "job-resumed", "info"); addJobAtTailOfPriority(job, job.priority); job._setState(state_scheduled); }; state_paused.setPriority = changePriority; // Canceled // state_canceled.enter = function (job) { jobProfilerMark(job, "job-canceled", "info"); _Trace._traceAsyncOperationCompleted(job._asyncOpID, _Global.Debug && _Global.Debug.MS_ASYNC_OP_STATUS_CANCELED); job._removeJob(); job._work = null; job._context = null; job.owner = null; }; state_canceled.cancel = _; state_canceled.pause = _; state_canceled.resume = _; state_canceled.setPriority = _; // Running // state_running.enter = function (job, shouldYield) { // Remove the job from the list in case it throws an exception, this means in the // yield case we have to add it back. // job._removeJob(); var priority = job.priority; var work = job._work; var context = job._context; // Null out the work and context so they aren't leaked if the job throws an exception. // job._work = null; job._context = null; var jobInfo = new JobInfo(shouldYield, job); _Trace._traceAsyncCallbackStarting(job._asyncOpID); try { MSApp.execAtPriority(function () { work.call(context, jobInfo); }, toWwaPriority(priority)); } finally { _Trace._traceAsyncCallbackCompleted(); jobInfo._disablePublicApi(); } // Restore the context in case it is needed due to yielding or blocking. // job._context = context; var targetState = job._executeDone(jobInfo._yieldPolicy); job._setState(targetState, jobInfo._result, priority); }; state_running.executeDone = function (job, yieldPolicy) { switch (yieldPolicy) { case YieldPolicy.complete: return state_complete; case YieldPolicy.continue: return state_cooperative_yield; case YieldPolicy.block: return state_blocked; } }; state_running.cancel = function (job) { // Interaction with the singleton scheduler. The act of canceling a job pokes the scheduler // and tells it to start asking the job to yield. // immediateYield = true; job._setState(state_running_canceled); }; state_running.pause = function (job) { // Interaction with the singleton scheduler. The act of pausing a job pokes the scheduler // and tells it to start asking the job to yield. // immediateYield = true; job._setState(state_running_paused); }; state_running.resume = _; state_running.setPriority = changePriority; // Running paused // state_running_paused.enter = _; state_running_paused.executeDone = function (job, yieldPolicy) { switch (yieldPolicy) { case YieldPolicy.complete: return state_complete; case YieldPolicy.continue: return state_cooperative_yield_paused; case YieldPolicy.block: return state_blocked_paused; } }; state_running_paused.cancel = setState(state_running_canceled); state_running_paused.pause = _; state_running_paused.resume = setState(state_running_resumed); state_running_paused.setPriority = changePriority; // Running resumed // state_running_resumed.enter = _; state_running_resumed.executeDone = function (job, yieldPolicy) { switch (yieldPolicy) { case YieldPolicy.complete: return state_complete; case YieldPolicy.continue: return state_cooperative_yield; case YieldPolicy.block: return state_blocked; } }; state_running_resumed.cancel = setState(state_running_canceled); state_running_resumed.pause = setState(state_running_paused); state_running_resumed.resume = _; state_running_resumed.setPriority = changePriority; // Running canceled // state_running_canceled.enter = _; state_running_canceled.executeDone = function (job, yieldPolicy) { switch (yieldPolicy) { case YieldPolicy.complete: case YieldPolicy.continue: return state_canceled; case YieldPolicy.block: return state_running_canceled_blocked; } }; state_running_canceled.cancel = _; state_running_canceled.pause = _; state_running_canceled.resume = _; state_running_canceled.setPriority = _; // Running canceled -> blocked // state_running_canceled_blocked.enter = function (job, work) { work.cancel(); job._setState(state_canceled); }; // Cooperative yield // state_cooperative_yield.enter = function (job, work, initialPriority) { jobProfilerMark(job, "job-yielded", "info"); if (initialPriority === job.priority) { addJobAtHeadOfPriority(job, job.priority); } else { addJobAtTailOfPriority(job, job.priority); } job._work = work; job._setState(state_scheduled); }; // Cooperative yield paused // state_cooperative_yield_paused.enter = function (job, work) { jobProfilerMark(job, "job-yielded", "info"); job._work = work; job._setState(state_paused); }; // Blocked // state_blocked.enter = function (job, work, initialPriority) { jobProfilerMark(job, "job-blocked", "StartTM"); job._work = work; job._setState(state_blocked_waiting); // Sign up for a completion from the provided promise, after the completion occurs // transition from the current state at the completion time to the target state // depending on the completion value. // work.done( function (newWork) { jobProfilerMark(job, "job-blocked", "StopTM"); var targetState = job._blockedDone(newWork); job._setState(targetState, newWork, initialPriority); }, function (error) { if (!(error && error.name === "Canceled")) { jobProfilerMark(job, "job-error", "info"); } jobProfilerMark(job, "job-blocked", "StopTM"); job._setState(state_canceled); return Promise.wrapError(error); } ); }; // Blocked waiting // state_blocked_waiting.enter = _; state_blocked_waiting.blockedDone = function (job, result) { if (typeof result === "function") { return state_cooperative_yield; } else { return state_complete; } }; state_blocked_waiting.cancel = setState(state_blocked_canceled); state_blocked_waiting.pause = setState(state_blocked_paused_waiting); state_blocked_waiting.resume = _; state_blocked_waiting.setPriority = changePriority; // Blocked paused // state_blocked_paused.enter = function (job, work, initialPriority) { jobProfilerMark(job, "job-blocked", "StartTM"); job._work = work; job._setState(state_blocked_paused_waiting); // Sign up for a completion from the provided promise, after the completion occurs // transition from the current state at the completion time to the target state // depending on the completion value. // work.done( function (newWork) { jobProfilerMark(job, "job-blocked", "StopTM"); var targetState = job._blockedDone(newWork); job._setState(targetState, newWork, initialPriority); }, function (error) { if (!(error && error.name === "Canceled")) { jobProfilerMark(job, "job-error", "info"); } jobProfilerMark(job, "job-blocked", "StopTM"); job._setState(state_canceled); return Promise.wrapError(error); } ); }; // Blocked paused waiting // state_blocked_paused_waiting.enter = _; state_blocked_paused_waiting.blockedDone = function (job, result) { if (typeof result === "function") { return state_cooperative_yield_paused; } else { return state_complete; } }; state_blocked_paused_waiting.cancel = setState(state_blocked_canceled); state_blocked_paused_waiting.pause = _; state_blocked_paused_waiting.resume = setState(state_blocked_waiting); state_blocked_paused_waiting.setPriority = changePriority; // Blocked canceled // state_blocked_canceled.enter = function (job) { // Cancel the outstanding promise and then eventually it will complete, presumably with a 'canceled' // error at which point we will transition to the canceled state. // job._work.cancel(); job._work = null; }; state_blocked_canceled.blockedDone = function () { return state_canceled; }; state_blocked_canceled.cancel = _; state_blocked_canceled.pause = _; state_blocked_canceled.resume = _; state_blocked_canceled.setPriority = _; // Complete // state_complete.completed = true; state_complete.enter = function (job) { _Trace._traceAsyncOperationCompleted(job._asyncOpID, _Global.Debug && _Global.Debug.MS_ASYNC_OP_STATUS_SUCCESS); job._work = null; job._context = null; job.owner = null; jobProfilerMark(job, "job-completed", "info"); }; state_complete.cancel = _; state_complete.pause = _; state_complete.resume = _; state_complete.setPriority = _; // Private Priority marker node in the Job list. The marker nodes are linked both into the job // list and a separate marker list. This is used so that jobs can be easily added into a given // priority level by simply traversing to the next marker in the list and inserting before it. // // Markers may either be "static" or "dynamic". Static markers are the set of things which are // named and are always in the list, they may exist with or without jobs at their priority // level. Dynamic markers are added as needed. // // @NOTE: Dynamic markers are NYI // var MarkerNode = _Base.Class.define(function (priority, name) { this.priority = priority; this.name = name; }, { // NYI // //dynamic: { // get: function () { return !this.name; } //}, }); _Base.Class.mix(MarkerNode, linkedListMixin("Job"), linkedListMixin("Marker")); // // Scheduler state // // Unique ID per job. // var globalJobId = 0; // Unique ID per drain request. var globalDrainId = 0; // Priority is: -15 ... 0 ... 15 where that maps to: 'min' ... 'normal' ... 'max' // var MIN_PRIORITY = -15; var MAX_PRIORITY = 15; // Named priorities // var Priority = { max: 15, high: 13, aboveNormal: 9, normal: 0, belowNormal: -9, idle: -13, min: -15, }; // Definition of the priorities, named have static markers. // var priorities = [ new MarkerNode(15, "max"), // Priority.max new MarkerNode(14, "14"), new MarkerNode(13, "high"), // Priority.high new MarkerNode(12, "12"), new MarkerNode(11, "11"), new MarkerNode(10, "10"), new MarkerNode(9, "aboveNormal"), // Priority.aboveNormal new MarkerNode(8, "8"), new MarkerNode(7, "7"), new MarkerNode(6, "6"), new MarkerNode(5, "5"), new MarkerNode(4, "4"), new MarkerNode(3, "3"), new MarkerNode(2, "2"), new MarkerNode(1, "1"), new MarkerNode(0, "normal"), // Priority.normal new MarkerNode(-1, "-1"), new MarkerNode(-2, "-2"), new MarkerNode(-3, "-3"), new MarkerNode(-4, "-4"), new MarkerNode(-5, "-5"), new MarkerNode(-6, "-6"), new MarkerNode(-7, "-7"), new MarkerNode(-8, "-8"), new MarkerNode(-9, "belowNormal"), // Priority.belowNormal new MarkerNode(-10, "-10"), new MarkerNode(-11, "-11"), new MarkerNode(-12, "-12"), new MarkerNode(-13, "idle"), // Priority.idle new MarkerNode(-14, "-14"), new MarkerNode(-15, "min"), // Priority.min new MarkerNode(-16, "") ]; function dumpList(type, reverse) { function dumpMarker(marker, pos) { _Log.log && _Log.log(pos + ": MARKER: " + marker.name, "winjs scheduler", "log"); } function dumpJob(job, pos) { _Log.log && _Log.log(pos + ": JOB(" + job.id + "): state: " + (job._state ? job._state.name : "") + (job.name ? ", name: " + job.name : ""), "winjs scheduler", "log"); } _Log.log && _Log.log("highWaterMark: " + highWaterMark, "winjs scheduler", "log"); var pos = 0; var head = reverse ? priorities[priorities.length - 1] : priorities[0]; var current = head; do { if (current instanceof MarkerNode) { dumpMarker(current, pos); } if (current instanceof JobNode) { dumpJob(current, pos); } pos++; current = reverse ? current["_prev" + type] : current["_next" + type]; } while (current); } function retrieveState() { /// /// /// Returns a string representation of the scheduler's state for diagnostic /// purposes. The jobs and drain requests are displayed in the order in which /// they are currently expected to be processed. The current job and drain /// request are marked by an asterisk. /// /// var output = ""; function logJob(job, isRunning) { output += " " + (isRunning ? "*" : " ") + "id: " + job.id + ", priority: " + markerFromPriority(job.priority).name + (job.name ? ", name: " + job.name : "") + "\n"; } output += "Jobs:\n"; var current = markerFromPriority(highWaterMark); var jobCount = 0; if (runningJob) { logJob(runningJob, true); jobCount++; } while (current.priority >= Priority.min) { if (current instanceof JobNode) { logJob(current, false); jobCount++; } current = current._nextJob; } if (jobCount === 0) { output += " None\n"; } output += "Drain requests:\n"; for (var i = 0, len = drainQueue.length; i < len; i++) { output += " " + (i === 0 ? "*" : " ") + "priority: " + markerFromPriority(drainQueue[i].priority).name + ", name: " + drainQueue[i].name + "\n"; } if (drainQueue.length === 0) { output += " None\n"; } return output; } function isEmpty() { var current = priorities[0]; do { if (current instanceof JobNode) { return false; } current = current._nextJob; } while (current); return true; } // The WWA priority at which the pump is currently scheduled on the WWA scheduler. // null when the pump is not scheduled. // var scheduledWwaPriority = null; // Whether the scheduler pump is currently on the stack // var pumping; // What priority is currently being pumped // var pumpingPriority; // A reference to the job object that is currently running. // null when no job is running. // var runningJob = null; // Whether we are using the WWA scheduler. // var usingWwaScheduler = !!(_Global.MSApp && _Global.MSApp.execAtPriority); // Queue of drain listeners // var drainQueue = []; // Bit indicating that we should yield immediately // var immediateYield; // time slice for scheduler // var TIME_SLICE = 30; // high-water-mark is maintained any time priorities are adjusted, new jobs are // added or the scheduler pumps itself down through a priority marker. The goal // of the high-water-mark is to be a fast check as to whether a job may exist // at a higher priority level than we are currently at. It may be wrong but it // may only be wrong by being higher than the current highest priority job, not // lower as that would cause the system to pump things out of order. // var highWaterMark = Priority.min; // // Initialize the scheduler // // Wire up the markers // priorities.reduce(function (prev, current) { if (prev) { prev._insertJobAfter(current); prev._insertMarkerAfter(current); } return current; }); // // Draining mechanism // // For each active drain request, there is a unique drain listener in the // drainQueue. Requests are processed in FIFO order. The scheduler is in // drain mode precisely when the drainQueue is non-empty. // // Returns priority of the current drain request // function currentDrainPriority() { return drainQueue.length === 0 ? null : drainQueue[0].priority; } function drainStarting(listener) { schedulerProfilerMark("drain", "StartTM", listener.name, markerFromPriority(listener.priority).name); } function drainStopping(listener, canceled) { if (canceled) { schedulerProfilerMark("drain-canceled", "info", listener.name, markerFromPriority(listener.priority).name); } schedulerProfilerMark("drain", "StopTM", listener.name, markerFromPriority(listener.priority).name); } function addDrainListener(priority, complete, name) { drainQueue.push({ priority: priority, complete: complete, name: name }); if (drainQueue.length === 1) { drainStarting(drainQueue[0]); if (priority > highWaterMark) { highWaterMark = priority; immediateYield = true; } } } function removeDrainListener(complete, canceled) { var i, len = drainQueue.length; for (i = 0; i < len; i++) { if (drainQueue[i].complete === complete) { if (i === 0) { drainStopping(drainQueue[0], canceled); drainQueue[1] && drainStarting(drainQueue[1]); } drainQueue.splice(i, 1); break; } } } // Notifies and removes the current drain listener // function notifyCurrentDrainListener() { var listener = drainQueue.shift(); if (listener) { drainStopping(listener); drainQueue[0] && drainStarting(drainQueue[0]); listener.complete(); } } // Notifies all drain listeners which are at a priority > highWaterMark. // Returns whether or not any drain listeners were notified. This // function sets pumpingPriority and reads highWaterMark. Note that // it may call into user code which may call back into the scheduler. // function notifyDrainListeners() { var notifiedSomebody = false; if (!!drainQueue.length) { // As we exhaust priority levels, notify the appropriate drain listeners. // var drainPriority = currentDrainPriority(); while (+drainPriority === drainPriority && drainPriority > highWaterMark) { pumpingPriority = drainPriority; notifyCurrentDrainListener(); notifiedSomebody = true; drainPriority = currentDrainPriority(); } } return notifiedSomebody; } // // Interfacing with the WWA Scheduler // // The purpose of yielding to the host is to give the host the opportunity to do some work. // setImmediate has this guarantee built-in so we prefer that. Otherwise, we do setTimeout 16 // which should give the host a decent amount of time to do work. // var scheduleWithHost = _Global.setImmediate ? _Global.setImmediate.bind(_Global) : function (callback) { _Global.setTimeout(callback, 16); }; // Stubs for the parts of the WWA scheduler APIs that we use. These stubs are // used in contexts where the WWA scheduler is not available. // var MSAppStubs = { execAsyncAtPriority: function (callback, priority) { // If it's a high priority callback then we additionally schedule using setTimeout(0) // if (priority === MSApp.HIGH) { _Global.setTimeout(callback, 0); } // We always schedule using setImmediate // scheduleWithHost(callback); }, execAtPriority: function (callback) { return callback(); }, getCurrentPriority: function () { return MSAppStubs.NORMAL; }, isTaskScheduledAtPriorityOrHigher: function () { return false; }, HIGH: "high", NORMAL: "normal", IDLE: "idle" }; var MSApp = (usingWwaScheduler ? _Global.MSApp : MSAppStubs); function toWwaPriority(winjsPriority) { if (winjsPriority >= Priority.aboveNormal + 1) { return MSApp.HIGH; } if (winjsPriority >= Priority.belowNormal) { return MSApp.NORMAL; } return MSApp.IDLE; } var wwaPriorityToInt = {}; wwaPriorityToInt[MSApp.IDLE] = 1; wwaPriorityToInt[MSApp.NORMAL] = 2; wwaPriorityToInt[MSApp.HIGH] = 3; function isEqualOrHigherWwaPriority(priority1, priority2) { return wwaPriorityToInt[priority1] >= wwaPriorityToInt[priority2]; } function isHigherWwaPriority(priority1, priority2) { return wwaPriorityToInt[priority1] > wwaPriorityToInt[priority2]; } function wwaTaskScheduledAtPriorityHigherThan(wwaPriority) { switch (wwaPriority) { case MSApp.HIGH: return false; case MSApp.NORMAL: return MSApp.isTaskScheduledAtPriorityOrHigher(MSApp.HIGH); case MSApp.IDLE: return MSApp.isTaskScheduledAtPriorityOrHigher(MSApp.NORMAL); } } // // Mechanism for the scheduler // function addJobAtHeadOfPriority(node, priority) { var marker = markerFromPriority(priority); if (marker.priority > highWaterMark) { highWaterMark = marker.priority; immediateYield = true; } marker._insertJobAfter(node); } function addJobAtTailOfPriority(node, priority) { var marker = markerFromPriority(priority); if (marker.priority > highWaterMark) { highWaterMark = marker.priority; immediateYield = true; } marker._nextMarker._insertJobBefore(node); } function clampPriority(priority) { priority = priority | 0; priority = Math.max(priority, MIN_PRIORITY); priority = Math.min(priority, MAX_PRIORITY); return priority; } function markerFromPriority(priority) { priority = clampPriority(priority); // The priority skip list is from high -> idle, add the offset and then make it positive. // return priorities[-1 * (priority - MAX_PRIORITY)]; } // Performance.now is not defined in web workers. // var now = (_Global.performance && _Global.performance.now && _Global.performance.now.bind(_Global.performance)) || Date.now.bind(Date); // Main scheduler pump. // function run(scheduled) { pumping = true; schedulerProfilerMark("timeslice", "StartTM"); var didWork; var ranJobSuccessfully = true; var current; var lastLoggedPriority; var timesliceExhausted = false; var yieldForPriorityBoundary = false; // Reset per-run state // immediateYield = false; try { var start = now(); var end = start + TIME_SLICE; // Yielding policy // // @TODO, should we have a different scheduler policy when the debugger is attached. Today if you // break in user code we will generally yield immediately after that job due to the fact that any // breakpoint will take longer than TIME_SLICE to process. // var shouldYield = function () { timesliceExhausted = false; if (immediateYield) { return true; } if (wwaTaskScheduledAtPriorityHigherThan(toWwaPriority(highWaterMark))) { return true; } if (!!drainQueue.length) { return false; } if (now() > end) { timesliceExhausted = true; return true; } return false; }; // Run until we run out of jobs or decide it is time to yield // while (highWaterMark >= Priority.min && !shouldYield() && !yieldForPriorityBoundary) { didWork = false; current = markerFromPriority(highWaterMark)._nextJob; do { // Record the priority currently being pumped // pumpingPriority = current.priority; if (current instanceof JobNode) { if (lastLoggedPriority !== current.priority) { if (+lastLoggedPriority === lastLoggedPriority) { schedulerProfilerMark("priority", "StopTM", markerFromPriority(lastLoggedPriority).name); } schedulerProfilerMark("priority", "StartTM", markerFromPriority(current.priority).name); lastLoggedPriority = current.priority; } // Important that we update this state before calling execute because the // job may throw an exception and we don't want to stall the queue. // didWork = true; ranJobSuccessfully = false; runningJob = current; jobProfilerMark(runningJob, "job-running", "StartTM", markerFromPriority(pumpingPriority).name); current._execute(shouldYield); jobProfilerMark(runningJob, "job-running", "StopTM", markerFromPriority(pumpingPriority).name); runningJob = null; ranJobSuccessfully = true; } else { // As we pass marker nodes update our high water mark. It's important to do // this before notifying drain listeners because they may schedule new jobs // which will affect the highWaterMark. // var wwaPrevHighWaterMark = toWwaPriority(highWaterMark); highWaterMark = current.priority; didWork = notifyDrainListeners(); var wwaHighWaterMark = toWwaPriority(highWaterMark); if (isHigherWwaPriority(wwaPrevHighWaterMark, wwaHighWaterMark) && (!usingWwaScheduler || MSApp.isTaskScheduledAtPriorityOrHigher(wwaHighWaterMark))) { // Timeslice is moving to a lower WWA priority and the host // has equally or more important work to do. Time to yield. // yieldForPriorityBoundary = true; } } current = current._nextJob; // When didWork is true we exit the loop because: // - We've called into user code which may have modified the // scheduler's queue. We need to restart at the high water mark. // - We need to check if it's time for the scheduler to yield. // } while (current && !didWork && !yieldForPriorityBoundary && !wwaTaskScheduledAtPriorityHigherThan(toWwaPriority(highWaterMark))); // Reset per-item state // immediateYield = false; } } finally { runningJob = null; // If a job was started and did not run to completion due to an exception // we should transition it to a terminal state. // if (!ranJobSuccessfully) { jobProfilerMark(current, "job-error", "info"); jobProfilerMark(current, "job-running", "StopTM", markerFromPriority(pumpingPriority).name); current.cancel(); } if (+lastLoggedPriority === lastLoggedPriority) { schedulerProfilerMark("priority", "StopTM", markerFromPriority(lastLoggedPriority).name); } // Update high water mark to be the priority of the highest priority job. // var foundAJob = false; while (highWaterMark >= Priority.min && !foundAJob) { didWork = false; current = markerFromPriority(highWaterMark)._nextJob; do { if (current instanceof JobNode) { // We found a job. High water mark is now set to the priority // of this job. // foundAJob = true; } else { // As we pass marker nodes update our high water mark. It's important to do // this before notifying drain listeners because they may schedule new jobs // which will affect the highWaterMark. // highWaterMark = current.priority; didWork = notifyDrainListeners(); } current = current._nextJob; // When didWork is true we exit the loop because: // - We've called into user code which may have modified the // scheduler's queue. We need to restart at the high water mark. // } while (current && !didWork && !foundAJob); } var reasonForYielding; if (!ranJobSuccessfully) { reasonForYielding = "job error"; } else if (timesliceExhausted) { reasonForYielding = "timeslice exhausted"; } else if (highWaterMark < Priority.min) { reasonForYielding = "jobs exhausted"; } else if (yieldForPriorityBoundary) { reasonForYielding = "reached WWA priority boundary"; } else { reasonForYielding = "WWA host work"; } // If this was a scheduled call to the pump, then the pump is no longer // scheduled to be called and we should clear its scheduled priority. // if (scheduled) { scheduledWwaPriority = null; } // If the high water mark has not reached the end of the queue then // we re-queue in order to see if there are more jobs to run. // pumping = false; if (highWaterMark >= Priority.min) { startRunning(); } schedulerProfilerMark("yielding", "info", reasonForYielding); schedulerProfilerMark("timeslice", "StopTM"); } } // When we schedule the pump we assign it a version. When we start executing one we check // to see what the max executed version is. If we have superseded it then we skip the call. // var scheduledVersion = 0; var executedVersion = 0; function startRunning(priority) { if (+priority !== priority) { priority = highWaterMark; } var priorityWwa = toWwaPriority(priority); // Don't schedule the pump while pumping. The pump will be scheduled // immediately before yielding if necessary. // if (pumping) { return; } // If the pump is already scheduled at priority or higher, then there // is no need to schedule the pump again. // However, when we're not using the WWA scheduler, we fallback to immediate/timeout // which do not have a notion of priority. In this case, if the pump is scheduled, // there is no need to schedule another pump. // if (scheduledWwaPriority && (!usingWwaScheduler || isEqualOrHigherWwaPriority(scheduledWwaPriority, priorityWwa))) { return; } var current = ++scheduledVersion; var runner = function () { if (executedVersion < current) { executedVersion = scheduledVersion; run(true); } }; MSApp.execAsyncAtPriority(runner, priorityWwa); scheduledWwaPriority = priorityWwa; } function requestDrain(priority, name) { /// /// /// Runs jobs in the scheduler without timeslicing until all jobs at the /// specified priority and higher have executed. /// /// /// The priority to which the scheduler should drain. The default is Priority.min, which drains all jobs in the queue. /// /// /// An optional description of the drain request for diagnostics. /// /// /// A promise which completes when the drain has finished. Canceling this /// promise cancels the drain request. This promise will never enter an error state. /// /// var id = globalDrainId++; if (name === undefined) { name = "Drain Request " + id; } priority = (+priority === priority) ? priority : Priority.min; priority = clampPriority(priority); var complete; var promise = new Promise(function (c) { complete = c; addDrainListener(priority, complete, name); }, function () { removeDrainListener(complete, true); }); if (!pumping) { startRunning(); } return promise; } function execHigh(callback) { /// /// /// Runs the specified callback in a high priority context. /// /// /// The callback to run in a high priority context. /// /// /// The return value of the callback. /// /// return MSApp.execAtPriority(callback, MSApp.HIGH); } function createOwnerToken() { /// /// /// Creates and returns a new owner token which can be set to the owner property of one or more jobs. /// It can then be used to cancel all jobs it "owns". /// /// /// The new owner token. You can use this token to control jobs that it owns. /// /// return new OwnerToken(); } function schedule(work, priority, thisArg, name) { /// /// /// Schedules the specified function to execute asynchronously. /// /// /// A function that represents the work item to be scheduled. When called the work item will receive as its first argument /// a JobInfo object which allows the work item to ask the scheduler if it should yield cooperatively and if so allows the /// work item to either provide a function to be run as a continuation or a WinJS.Promise which will when complete /// provide a function to run as a continuation. /// /// /// The priority at which to schedule the work item. The default value is Priority.normal. /// /// /// A 'this' instance to be bound into the work item. The default value is null. /// /// /// A description of the work item for diagnostics. The default value is an empty string. /// /// /// The Job instance which represents this work item. /// /// priority = priority || Priority.normal; thisArg = thisArg || null; var jobId = ++globalJobId; var asyncOpID = _Trace._traceAsyncOperationStarting("WinJS.Utilities.Scheduler.schedule: " + jobId + profilerMarkArgs(name)); name = name || ""; return new JobNode(jobId, work, priority, thisArg, name, asyncOpID); } function getCurrentPriority() { if (pumping) { return pumpingPriority; } else { switch (MSApp.getCurrentPriority()) { case MSApp.HIGH: return Priority.high; case MSApp.NORMAL: return Priority.normal; case MSApp.IDLE: return Priority.idle; } } } function makeSchedulePromise(priority) { return function (promiseValue, jobName) { /// /// /// Schedules a job to complete a returned Promise. /// There are four versions of this method for different commonly used priorities: schedulePromiseHigh, /// schedulePromiseAboveNormal, schedulePromiseNormal, schedulePromiseBelowNormal, /// and schedulePromiseIdle. /// Example usage which shows how to /// ensure that the last link in a promise chain is run on the scheduler at high priority: /// asyncOp().then(Scheduler.schedulePromiseHigh).then(function (valueOfAsyncOp) { }); /// /// /// The value with which the returned promise will complete. /// /// /// A string that describes the job for diagnostic purposes. /// /// /// A promise which completes within a job of the desired priority. /// /// var job; return new Promise( function (c) { job = schedule(function schedulePromise() { c(promiseValue); }, priority, null, jobName); }, function () { job.cancel(); } ); }; } _Base.Namespace._moduleDefine(exports, "WinJS.Utilities.Scheduler", { Priority: Priority, schedule: schedule, createOwnerToken: createOwnerToken, execHigh: execHigh, requestDrain: requestDrain, /// /// Gets the current priority at which the caller is executing. /// currentPriority: { get: getCurrentPriority }, // Promise helpers // schedulePromiseHigh: makeSchedulePromise(Priority.high), schedulePromiseAboveNormal: makeSchedulePromise(Priority.aboveNormal), schedulePromiseNormal: makeSchedulePromise(Priority.normal), schedulePromiseBelowNormal: makeSchedulePromise(Priority.belowNormal), schedulePromiseIdle: makeSchedulePromise(Priority.idle), retrieveState: retrieveState, _JobNode: JobNode, _JobInfo: JobInfo, _OwnerToken: OwnerToken, _dumpList: dumpList, _isEmpty: { get: isEmpty }, // The properties below are used for testing. // _usingWwaScheduler: { get: function () { return usingWwaScheduler; }, set: function (value) { usingWwaScheduler = value; MSApp = (usingWwaScheduler ? _Global.MSApp : MSAppStubs); } }, _MSApp: { get: function () { return MSApp; }, set: function (value) { MSApp = value; } }, _TIME_SLICE: TIME_SLICE }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Core/_BaseUtils',[ 'exports', './_Global', './_Base', './_BaseCoreUtils', './_ErrorFromName', './_Resources', './_Trace', '../Promise', '../Scheduler' ], function baseUtilsInit(exports, _Global, _Base, _BaseCoreUtils, _ErrorFromName, _Resources, _Trace, Promise, Scheduler) { "use strict"; var strings = { get notSupportedForProcessing() { return "Value is not supported within a declarative processing context, if you want it to be supported mark it using WinJS.Utilities.markSupportedForProcessing. The value was: '{0}'"; } }; var requestAnimationWorker; var requestAnimationId = 0; var requestAnimationHandlers = {}; var isPhone = false; var validation = false; var platform = _Global.navigator.platform; var isiOS = platform === "iPhone" || platform === "iPad" || platform === "iPod"; function nop(v) { return v; } function getMemberFiltered(name, root, filter) { return name.split(".").reduce(function (currentNamespace, name) { if (currentNamespace) { return filter(currentNamespace[name]); } return null; }, root); } function getMember(name, root) { /// /// /// Gets the leaf-level type or namespace specified by the name parameter. /// /// /// The name of the member. /// /// /// The root to start in. Defaults to the global object. /// /// /// The leaf-level type or namespace in the specified parent namespace. /// /// if (!name) { return null; } return getMemberFiltered(name, root || _Global, nop); } function getCamelCasedName(styleName) { // special case -moz prefixed styles because their JS property name starts with Moz if (styleName.length > 0 && styleName.indexOf("-moz") !== 0 && styleName.charAt(0) === "-") { styleName = styleName.slice(1); } return styleName.replace(/\-[a-z]/g, function (x) { return x[1].toUpperCase(); }); } function addPrefixToCamelCasedName(prefix, name) { if (prefix === "") { return name; } return prefix + name.charAt(0).toUpperCase() + name.slice(1); } function addPrefixToCSSName(prefix, name) { return (prefix !== "" ? "-" + prefix.toLowerCase() + "-" : "") + name; } function getBrowserStyleEquivalents() { // not supported in WebWorker if (!_Global.document) { return {}; } var equivalents = {}, docStyle = _Global.document.documentElement.style, stylePrefixesToTest = ["", "webkit", "ms", "Moz"], styles = ["animation", "transition", "transform", "animation-name", "animation-duration", "animation-delay", "animation-timing-function", "animation-iteration-count", "animation-direction", "animation-fill-mode", "grid-column", "grid-columns", "grid-column-span", "grid-row", "grid-rows", "grid-row-span", "transform-origin", "transition-property", "transition-duration", "transition-delay", "transition-timing-function", "scroll-snap-points-x", "scroll-snap-points-y", "scroll-chaining", "scroll-limit", "scroll-limit-x-max", "scroll-limit-x-min", "scroll-limit-y-max", "scroll-limit-y-min", "scroll-snap-type", "scroll-snap-x", "scroll-snap-y", "touch-action", "overflow-style", "user-select" // used for Template Compiler test ], prefixesUsedOnStyles = {}; for (var i = 0, len = styles.length; i < len; i++) { var originalName = styles[i], styleToTest = getCamelCasedName(originalName); for (var j = 0, prefixLen = stylePrefixesToTest.length; j < prefixLen; j++) { var prefix = stylePrefixesToTest[j]; var styleName = addPrefixToCamelCasedName(prefix, styleToTest); if (styleName in docStyle) { // Firefox doesn't support dashed style names being get/set via script. (eg, something like element.style["transform-origin"] = "" wouldn't work). // For each style translation we create, we'll make a CSS name version and a script name version for it so each can be used where appropriate. var cssName = addPrefixToCSSName(prefix, originalName); equivalents[originalName] = { cssName: cssName, scriptName: styleName }; prefixesUsedOnStyles[originalName] = prefix; break; } } } // Special cases: equivalents.animationPrefix = addPrefixToCSSName(prefixesUsedOnStyles["animation"], ""); equivalents.keyframes = addPrefixToCSSName(prefixesUsedOnStyles["animation"], "keyframes"); return equivalents; } function getBrowserEventEquivalents() { var equivalents = {}; var animationEventPrefixes = ["", "WebKit"], animationEvents = [ { eventObject: "TransitionEvent", events: ["transitionStart", "transitionEnd"] }, { eventObject: "AnimationEvent", events: ["animationStart", "animationEnd"] } ]; for (var i = 0, len = animationEvents.length; i < len; i++) { var eventToTest = animationEvents[i], chosenPrefix = ""; for (var j = 0, prefixLen = animationEventPrefixes.length; j < prefixLen; j++) { var prefix = animationEventPrefixes[j]; if ((prefix + eventToTest.eventObject) in _Global) { chosenPrefix = prefix.toLowerCase(); break; } } for (var j = 0, eventsLen = eventToTest.events.length; j < eventsLen; j++) { var eventName = eventToTest.events[j]; equivalents[eventName] = addPrefixToCamelCasedName(chosenPrefix, eventName); if (chosenPrefix === "") { // Transition and animation events are case sensitive. When there's no prefix, the event name should be in lowercase. // In IE, Chrome and Firefox, an event handler listening to transitionend will be triggered properly, but transitionEnd will not. // When a prefix is provided, though, the event name needs to be case sensitive. // IE and Firefox will trigger an animationend event handler correctly, but Chrome won't trigger webkitanimationend -- it has to be webkitAnimationEnd. equivalents[eventName] = equivalents[eventName].toLowerCase(); } } } // Non-standardized events equivalents["manipulationStateChanged"] = ("MSManipulationEvent" in _Global ? "ManipulationEvent" : null); return equivalents; } _Base.Namespace._moduleDefine(exports, "WinJS.Utilities", { // Used for mocking in tests _setHasWinRT: { value: function (value) { _BaseCoreUtils.hasWinRT = value; }, configurable: false, writable: false, enumerable: false }, /// Determine if WinRT is accessible in this script context. hasWinRT: { get: function () { return _BaseCoreUtils.hasWinRT; }, configurable: false, enumerable: true }, // Used for mocking in tests _setIsiOS: { value: function (value) { isiOS = value; }, configurable: false, writable: false, enumerable: false }, _isiOS: { get: function () { return isiOS; }, configurable: false, enumerable: true }, _getMemberFiltered: getMemberFiltered, getMember: getMember, _browserStyleEquivalents: getBrowserStyleEquivalents(), _browserEventEquivalents: getBrowserEventEquivalents(), _getCamelCasedName: getCamelCasedName, ready: function ready(callback, async) { /// /// /// Ensures that the specified function executes only after the DOMContentLoaded event has fired /// for the current page. /// /// A promise that completes after DOMContentLoaded has occurred. /// /// A function that executes after DOMContentLoaded has occurred. /// /// /// If true, the callback is executed asynchronously. /// /// return new Promise(function (c, e) { function complete() { if (callback) { try { callback(); c(); } catch (err) { e(err); } } else { c(); } } var readyState = ready._testReadyState; if (!readyState) { if (_Global.document) { readyState = _Global.document.readyState; } else { readyState = "complete"; } } if (readyState === "complete" || (_Global.document && _Global.document.body !== null)) { if (async) { Scheduler.schedule(function WinJS_Utilities_ready() { complete(); }, Scheduler.Priority.normal, null, "WinJS.Utilities.ready"); } else { complete(); } } else { _Global.addEventListener("DOMContentLoaded", complete, false); } }); }, /// Determines if strict declarative processing is enabled in this script context. strictProcessing: { get: function () { return true; }, configurable: false, enumerable: true, }, markSupportedForProcessing: { value: _BaseCoreUtils.markSupportedForProcessing, configurable: false, writable: false, enumerable: true }, requireSupportedForProcessing: { value: function (value) { /// /// /// Asserts that the value is compatible with declarative processing, such as WinJS.UI.processAll /// or WinJS.Binding.processAll. If it is not compatible an exception will be thrown. /// /// /// The value to be tested for compatibility with declarative processing. If the /// value is a function it must be marked with a property 'supportedForProcessing' /// with a value of true. /// /// /// The input value. /// /// var supportedForProcessing = true; supportedForProcessing = supportedForProcessing && value !== _Global; supportedForProcessing = supportedForProcessing && value !== _Global.location; supportedForProcessing = supportedForProcessing && !(value instanceof _Global.HTMLIFrameElement); supportedForProcessing = supportedForProcessing && !(typeof value === "function" && !value.supportedForProcessing); switch (_Global.frames.length) { case 0: break; case 1: supportedForProcessing = supportedForProcessing && value !== _Global.frames[0]; break; default: for (var i = 0, len = _Global.frames.length; supportedForProcessing && i < len; i++) { supportedForProcessing = supportedForProcessing && value !== _Global.frames[i]; } break; } if (supportedForProcessing) { return value; } throw new _ErrorFromName("WinJS.Utilities.requireSupportedForProcessing", _Resources._formatString(strings.notSupportedForProcessing, value)); }, configurable: false, writable: false, enumerable: true }, _setImmediate: _BaseCoreUtils._setImmediate, _requestAnimationFrame: _Global.requestAnimationFrame ? _Global.requestAnimationFrame.bind(_Global) : function (handler) { var handle = ++requestAnimationId; requestAnimationHandlers[handle] = handler; requestAnimationWorker = requestAnimationWorker || _Global.setTimeout(function () { var toProcess = requestAnimationHandlers; var now = Date.now(); requestAnimationHandlers = {}; requestAnimationWorker = null; Object.keys(toProcess).forEach(function (key) { toProcess[key](now); }); }, 16); return handle; }, _cancelAnimationFrame: _Global.cancelAnimationFrame ? _Global.cancelAnimationFrame.bind(_Global) : function (handle) { delete requestAnimationHandlers[handle]; }, // Allows the browser to finish dispatching its current set of events before running // the callback. _yieldForEvents: _Global.setImmediate ? _Global.setImmediate.bind(_Global) : function (handler) { _Global.setTimeout(handler, 0); }, // Allows the browser to notice a DOM modification before running the callback. _yieldForDomModification: _Global.setImmediate ? _Global.setImmediate.bind(_Global) : function (handler) { _Global.setTimeout(handler, 0); }, _shallowCopy: function _shallowCopy(a) { // Shallow copy a single object. return this._mergeAll([a]); }, _merge: function _merge(a, b) { // Merge 2 objects together into a new object return this._mergeAll([a, b]); }, _mergeAll: function _mergeAll(list) { // Merge a list of objects together var o = {}; list.forEach(function (part) { Object.keys(part).forEach(function (k) { o[k] = part[k]; }); }); return o; }, _getProfilerMarkIdentifier: function _getProfilerMarkIdentifier(element) { var profilerMarkIdentifier = ""; if (element.id) { profilerMarkIdentifier += " id='" + element.id + "'"; } if (element.className) { profilerMarkIdentifier += " class='" + element.className + "'"; } return profilerMarkIdentifier; }, _now: function _now() { return (_Global.performance && _Global.performance.now && _Global.performance.now()) || Date.now(); }, _traceAsyncOperationStarting: _Trace._traceAsyncOperationStarting, _traceAsyncOperationCompleted: _Trace._traceAsyncOperationCompleted, _traceAsyncCallbackStarting: _Trace._traceAsyncCallbackStarting, _traceAsyncCallbackCompleted: _Trace._traceAsyncCallbackCompleted, /// Determine if we are currently running in the Phone. isPhone: { get: function () { return isPhone; }, configurable: false, enumerable: true }, _setIsPhone: { set: function (value) { isPhone = value; } } }); _Base.Namespace._moduleDefine(exports, "WinJS", { validation: { get: function () { return validation; }, set: function (value) { validation = value; } } }); // strictProcessing also exists as a module member _Base.Namespace.define("WinJS", { strictProcessing: { value: function () { /// /// /// Strict processing is always enforced, this method has no effect. /// /// }, configurable: false, writable: false, enumerable: false } }); }); define('WinJS/Core',[ './Core/_Base', './Core/_BaseCoreUtils', './Core/_BaseUtils', './Core/_ErrorFromName', './Core/_Events', './Core/_Global', './Core/_Log', './Core/_Resources', './Core/_Trace', './Core/_WinRT', './Core/_WriteProfilerMark' ], function () { // Wrapper module }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/_Signal',[ './Core/_Base', './Promise/_StateMachine' ], function signalInit(_Base, _StateMachine) { "use strict"; var SignalPromise = _Base.Class.derive(_StateMachine.PromiseStateMachine, function (cancel) { this._oncancel = cancel; this._setState(_StateMachine.state_created); this._run(); }, { _cancelAction: function () { this._oncancel && this._oncancel(); }, _cleanupAction: function () { this._oncancel = null; } }, { supportedForProcessing: false } ); var Signal = _Base.Class.define( function Signal_ctor(oncancel) { this._promise = new SignalPromise(oncancel); }, { promise: { get: function () { return this._promise; } }, cancel: function Signal_cancel() { this._promise.cancel(); }, complete: function Signal_complete(value) { this._promise._completed(value); }, error: function Signal_error(value) { this._promise._error(value); }, progress: function Signal_progress(value) { this._promise._progress(value); } }, { supportedForProcessing: false, } ); _Base.Namespace.define("WinJS", { _Signal: Signal }); return Signal; }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_Control',[ 'exports', '../Core/_Global', '../Core/_Base' ], function controlInit(exports, _Global, _Base) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } function setOptions(control, options) { /// /// /// Adds the set of declaratively specified options (properties and events) to the specified control. /// If name of the options property begins with "on", the property value is a function and the control /// supports addEventListener. The setOptions method calls the addEventListener method on the control. /// /// /// The control on which the properties and events are to be applied. /// /// /// The set of options that are specified declaratively. /// /// _setOptions(control, options); } function _setOptions(control, options, eventsOnly) { if (typeof options === "object") { var keys = Object.keys(options); for (var i = 0, len = keys.length; i < len; i++) { var key = keys[i]; var value = options[key]; if (key.length > 2) { var ch1 = key[0]; var ch2 = key[1]; if ((ch1 === 'o' || ch1 === 'O') && (ch2 === 'n' || ch2 === 'N')) { if (typeof value === "function") { if (control.addEventListener) { control.addEventListener(key.substr(2), value); continue; } } } } if (!eventsOnly) { control[key] = value; } } } } _Base.Namespace._moduleDefine(exports, "WinJS.UI", { DOMEventMixin: _Base.Namespace._lazy(function () { return { _domElement: null, addEventListener: function (type, listener, useCapture) { /// /// /// Adds an event listener to the control. /// /// /// The type (name) of the event. /// /// /// The listener to invoke when the event gets raised. /// /// /// true to initiate capture; otherwise, false. /// /// (this.element || this._domElement).addEventListener(type, listener, useCapture || false); }, dispatchEvent: function (type, eventProperties) { /// /// /// Raises an event of the specified type, adding the specified additional properties. /// /// /// The type (name) of the event. /// /// /// The set of additional properties to be attached to the event object when the event is raised. /// /// /// true if preventDefault was called on the event, otherwise false. /// /// var eventValue = _Global.document.createEvent("Event"); eventValue.initEvent(type, false, false); eventValue.detail = eventProperties; if (typeof eventProperties === "object") { Object.keys(eventProperties).forEach(function (key) { eventValue[key] = eventProperties[key]; }); } return (this.element || this._domElement).dispatchEvent(eventValue); }, removeEventListener: function (type, listener, useCapture) { /// /// /// Removes an event listener from the control. /// /// /// The type (name) of the event. /// /// /// The listener to remove. /// /// /// true to initiate capture; otherwise, false. /// /// (this.element || this._domElement).removeEventListener(type, listener, useCapture || false); } }; }), setOptions: setOptions, _setOptions: _setOptions }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_ElementUtilities',[ 'exports', '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', '../Promise', '../Scheduler' ], function elementUtilities(exports, _Global, _Base, _BaseUtils, Promise, Scheduler) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } var _zoomToDuration = 167; function removeEmpties(arr) { var len = arr.length; for (var i = len - 1; i >= 0; i--) { if (!arr[i]) { arr.splice(i, 1); len--; } } return len; } function getClassName(e) { var name = e.className || ""; if (typeof (name) === "string") { return name; } else { return name.baseVal || ""; } } function setClassName(e, value) { // SVG elements (which use e.className.baseVal) are never undefined, // so this logic makes the comparison a bit more compact. // var name = e.className || ""; if (typeof (name) === "string") { e.className = value; } else { e.className.baseVal = value; } return e; } function addClass(e, name) { /// /// /// Adds the specified class(es) to the specified element. Multiple classes can be added using space delimited names. /// /// /// The element to which to add the class. /// /// /// The name of the class to add, multiple classes can be added using space delimited names /// /// /// The element. /// /// if (e.classList) { // Fastpath: adding a single class, no need to string split the argument if (name.indexOf(" ") < 0) { e.classList.add(name); } else { var namesToAdd = name.split(" "); removeEmpties(namesToAdd); for (var i = 0, len = namesToAdd.length; i < len; i++) { e.classList.add(namesToAdd[i]); } } return e; } else { var className = getClassName(e); var names = className.split(" "); var l = removeEmpties(names); var toAdd; // we have a fast path for the common case of a single name in the class name // if (name.indexOf(" ") >= 0) { var namesToAdd = name.split(" "); removeEmpties(namesToAdd); for (var i = 0; i < l; i++) { var found = namesToAdd.indexOf(names[i]); if (found >= 0) { namesToAdd.splice(found, 1); } } if (namesToAdd.length > 0) { toAdd = namesToAdd.join(" "); } } else { var saw = false; for (var i = 0; i < l; i++) { if (names[i] === name) { saw = true; break; } } if (!saw) { toAdd = name; } } if (toAdd) { if (l > 0 && names[0].length > 0) { setClassName(e, className + " " + toAdd); } else { setClassName(e, toAdd); } } return e; } } function removeClass(e, name) { /// /// /// Removes the specified class from the specified element. /// /// /// The element from which to remove the class. /// /// /// The name of the class to remove. /// /// /// The element. /// /// if (e.classList) { // Fastpath: Nothing to remove if (e.classList.length === 0) { return e; } var namesToRemove = name.split(" "); removeEmpties(namesToRemove); for (var i = 0, len = namesToRemove.length; i < len; i++) { e.classList.remove(namesToRemove[i]); } return e; } else { var original = getClassName(e); var namesToRemove; var namesToRemoveLen; if (name.indexOf(" ") >= 0) { namesToRemove = name.split(" "); namesToRemoveLen = removeEmpties(namesToRemove); } else { // early out for the case where you ask to remove a single // name and that name isn't found. // if (original.indexOf(name) < 0) { return e; } namesToRemove = [name]; namesToRemoveLen = 1; } var removed; var names = original.split(" "); var namesLen = removeEmpties(names); for (var i = namesLen - 1; i >= 0; i--) { if (namesToRemove.indexOf(names[i]) >= 0) { names.splice(i, 1); removed = true; } } if (removed) { setClassName(e, names.join(" ")); } return e; } } function toggleClass(e, name) { /// /// /// Toggles (adds or removes) the specified class on the specified element. /// If the class is present, it is removed; if it is absent, it is added. /// /// /// The element on which to toggle the class. /// /// /// The name of the class to toggle. /// /// /// The element. /// /// if (e.classList) { e.classList.toggle(name); return e; } else { var className = getClassName(e); var names = className.trim().split(" "); var l = names.length; var found = false; for (var i = 0; i < l; i++) { if (names[i] === name) { found = true; } } if (!found) { if (l > 0 && names[0].length > 0) { setClassName(e, className + " " + name); } else { setClassName(e, className + name); } } else { setClassName(e, names.reduce(function (r, e) { if (e === name) { return r; } else if (r && r.length > 0) { return r + " " + e; } else { return e; } }, "")); } return e; } } // Only set the attribute if its value has changed function setAttribute(element, attribute, value) { if (element.getAttribute(attribute) !== "" + value) { element.setAttribute(attribute, value); } } function _clamp(value, lowerBound, upperBound, defaultValue) { var n = Math.max(lowerBound, Math.min(upperBound, +value)); return n === 0 ? 0 : n || Math.max(lowerBound, Math.min(upperBound, defaultValue)); } var _pixelsRE = /^-?\d+\.?\d*(px)?$/i; var _numberRE = /^-?\d+/i; function convertToPixels(element, value) { /// /// /// Converts a CSS positioning string for the specified element to pixels. /// /// /// The element. /// /// /// The CSS positioning string. /// /// /// The number of pixels. /// /// if (!_pixelsRE.test(value) && _numberRE.test(value)) { var previousValue = element.style.left; element.style.left = value; value = element.style.pixelLeft; element.style.left = previousValue; return value; } else { return Math.round(parseFloat(value)) || 0; } } function getDimension(element, property) { return convertToPixels(element, _Global.getComputedStyle(element, null)[property]); } var _MSGestureEvent = _Global.MSGestureEvent || { MSGESTURE_FLAG_BEGIN: 1, MSGESTURE_FLAG_CANCEL: 4, MSGESTURE_FLAG_END: 2, MSGESTURE_FLAG_INERTIA: 8, MSGESTURE_FLAG_NONE: 0 }; var _MSManipulationEvent = _Global.MSManipulationEvent || { MS_MANIPULATION_STATE_ACTIVE: 1, MS_MANIPULATION_STATE_CANCELLED: 6, MS_MANIPULATION_STATE_COMMITTED: 7, MS_MANIPULATION_STATE_DRAGGING: 5, MS_MANIPULATION_STATE_INERTIA: 2, MS_MANIPULATION_STATE_PRESELECT: 3, MS_MANIPULATION_STATE_SELECTING: 4, MS_MANIPULATION_STATE_STOPPED: 0 }; var _MSPointerEvent = _Global.MSPointerEvent || { MSPOINTER_TYPE_TOUCH: "touch", MSPOINTER_TYPE_PEN: "pen", MSPOINTER_TYPE_MOUSE: "mouse", }; // Helpers for managing element._eventsMap for custom events // function addListenerToEventMap(element, type, listener, useCapture, data) { var eventNameLowercase = type.toLowerCase(); if (!element._eventsMap) { element._eventsMap = {}; } if (!element._eventsMap[eventNameLowercase]) { element._eventsMap[eventNameLowercase] = []; } element._eventsMap[eventNameLowercase].push({ listener: listener, useCapture: useCapture, data: data }); } function removeListenerFromEventMap(element, type, listener, useCapture) { var eventNameLowercase = type.toLowerCase(); var mappedEvents = element._eventsMap && element._eventsMap[eventNameLowercase]; if (mappedEvents) { for (var i = mappedEvents.length - 1; i >= 0; i--) { var mapping = mappedEvents[i]; if (mapping.listener === listener && (!!useCapture === !!mapping.useCapture)) { mappedEvents.splice(i, 1); return mapping; } } } return null; } function lookupListeners(element, type) { var eventNameLowercase = type.toLowerCase(); return element._eventsMap && element._eventsMap[eventNameLowercase] && element._eventsMap[eventNameLowercase].slice(0) || []; } // Custom focusin/focusout events // Generally, use these instead of using the browser's blur/focus/focusout/focusin events directly. // However, this doesn't support the window object. If you need to listen to focus events on the window, // use the browser's events directly. // function bubbleEvent(element, type, eventObject) { while (element) { var handlers = lookupListeners(element, type); for (var i = 0, len = handlers.length; i < len; i++) { handlers[i].listener.call(element, eventObject); } element = element.parentNode; } } function prepareFocusEvent(eventObject) { // If an iframe is involved, then relatedTarget should be null. if (eventObject.relatedTarget && eventObject.relatedTarget.tagName === "IFRAME" || eventObject.target && eventObject.target.tagName === "IFRAME") { eventObject.relatedTarget = null; } return eventObject; } var activeElement = null; _Global.addEventListener("blur", function () { // Fires focusout when focus move to another window or into an iframe. var previousActiveElement = activeElement; if (previousActiveElement) { bubbleEvent(previousActiveElement, "focusout", prepareFocusEvent({ type: "focusout", target: previousActiveElement, relatedTarget: null })); } activeElement = null; }); _Global.document.documentElement.addEventListener("focus", function (eventObject) { var previousActiveElement = activeElement; activeElement = eventObject.target; if (previousActiveElement) { bubbleEvent(previousActiveElement, "focusout", prepareFocusEvent({ type: "focusout", target: previousActiveElement, relatedTarget: activeElement })); } if (activeElement) { bubbleEvent(activeElement, "focusin", prepareFocusEvent({ type: "focusin", target: activeElement, relatedTarget: previousActiveElement })); } }, true); function registerBubbleListener(element, type, listener, useCapture) { if (useCapture) { throw "This custom WinJS event only supports bubbling"; } addListenerToEventMap(element, type, listener, useCapture); } // Custom pointer events // // Sets the properties in *overrideProperties* on the object. Delegates all other // property accesses to *eventObject*. // // The purpose of PointerEventProxy is that it allows us to customize properties on // an eventObject despite those properties being unwritable and unconfigurable. var PointerEventProxy = function (eventObject, overrideProperties) { overrideProperties = overrideProperties || {}; this.__eventObject = eventObject; var that = this; Object.keys(overrideProperties).forEach(function (propertyName) { Object.defineProperty(that, propertyName, { value: overrideProperties[propertyName] }); }); }; // Define PointerEventProxy properties which should be delegated to the original eventObject. [ "altKey", "AT_TARGET", "bubbles", "BUBBLING_PHASE", "button", "buttons", "cancelable", "cancelBubble", "CAPTURING_PHASE", "clientX", "clientY", "ctrlKey", "currentTarget", "defaultPrevented", "detail", "eventPhase", "fromElement", "getModifierState", "height", "hwTimestamp", "initEvent", "initMouseEvent", "initPointerEvent", "initUIEvent", "isPrimary", "isTrusted", "layerX", "layerY", "metaKey", "offsetX", "offsetY", "pageX", "pageY", "pointerId", "pointerType", "pressure", "preventDefault", "relatedTarget", "rotation", "screenX", "screenY", "shiftKey", "srcElement", "stopImmediatePropagation", "stopPropagation", "target", "tiltX", "tiltY", "timeStamp", "toElement", "type", "view", "which", "width", "x", "y", "_normalizedType", "_fakedBySemanticZoom" ].forEach(function (propertyName) { Object.defineProperty(PointerEventProxy.prototype, propertyName, { get: function () { var value = this.__eventObject[propertyName]; return typeof value === "function" ? value.bind(this.__eventObject) : value; }, configurable: true }); }); function touchEventTranslator(callback, eventObject) { var changedTouches = eventObject.changedTouches, retVal = null; if (!changedTouches) { return retVal; } for (var i = 0, len = changedTouches.length; i < len; i++) { var touchObject = changedTouches[i]; var pointerEventObject = new PointerEventProxy(eventObject, { pointerType: _MSPointerEvent.MSPOINTER_TYPE_TOUCH, pointerId: touchObject.identifier, screenX: touchObject.screenX, screenY: touchObject.screenY, clientX: touchObject.clientX, clientY: touchObject.clientY, radiusX: touchObject.radiusX, radiusY: touchObject.radiusY, rotationAngle: touchObject.rotationAngle, force: touchObject.force, _currentTouch: touchObject }); var newRetVal = callback(pointerEventObject); retVal = retVal || newRetVal; } return retVal; } function mouseEventTranslator(callback, eventObject) { eventObject.pointerType = _MSPointerEvent.MSPOINTER_TYPE_MOUSE; eventObject.pointerId = -1; return callback(eventObject); } function mspointerEventTranslator(callback, eventObject) { return callback(eventObject); } var eventTranslations = { pointerdown: { touch: "touchstart", mspointer: "MSPointerDown", mouse: "mousedown" }, pointerup: { touch: "touchend", mspointer: "MSPointerUp", mouse: "mouseup" }, pointermove: { touch: "touchmove", mspointer: "MSPointerMove", mouse: "mousemove" }, pointerenter: { touch: "touchenter", mspointer: "MSPointerEnter", mouse: "mouseenter" }, pointerover: { touch: null, mspointer: "MSPointerOver", mouse: "mouseover" }, pointerout: { touch: "touchleave", mspointer: "MSPointerOut", mouse: "mouseout" }, pointercancel: { touch: "touchcancel", mspointer: "MSPointerCancel", mouse: null } }; function registerPointerEvent(element, type, callback, capture) { var eventNameLowercase = type.toLowerCase(); var mouseWrapper, touchWrapper, mspointerWrapper; var translations = eventTranslations[eventNameLowercase]; // Browsers fire a touch event and then a mouse event when the input is touch. touchHandled is used to prevent invoking the pointer callback twice. var touchHandled; // If we are in IE10, we should use MSPointer as it provides a better interface than touch events if (_Global.MSPointerEvent) { mspointerWrapper = function (eventObject) { eventObject._normalizedType = eventNameLowercase; touchHandled = true; return mspointerEventTranslator(callback, eventObject); }; element.addEventListener(translations.mspointer, mspointerWrapper, capture); } else { // Otherwise, use a mouse and touch event if (translations.mouse) { mouseWrapper = function (eventObject) { eventObject._normalizedType = eventNameLowercase; if (!touchHandled) { return mouseEventTranslator(callback, eventObject); } touchHandled = false; }; element.addEventListener(translations.mouse, mouseWrapper, capture); } if (translations.touch) { touchWrapper = function (eventObject) { eventObject._normalizedType = eventNameLowercase; touchHandled = true; return touchEventTranslator(callback, eventObject); }; element.addEventListener(translations.touch, touchWrapper, capture); } } addListenerToEventMap(element, type, callback, capture, { mouseWrapper: mouseWrapper, touchWrapper: touchWrapper, mspointerWrapper: mspointerWrapper }); } function unregisterPointerEvent(element, type, callback, capture) { var eventNameLowercase = type.toLowerCase(); var mapping = removeListenerFromEventMap(element, type, callback, capture); if (mapping) { var translations = eventTranslations[eventNameLowercase]; if (mapping.data.mouseWrapper) { element.removeEventListener(translations.mouse, mapping.data.mouseWrapper, capture); } if (mapping.data.touchWrapper) { element.removeEventListener(translations.touch, mapping.data.touchWrapper, capture); } if (mapping.data.mspointerWrapper) { element.removeEventListener(translations.mspointer, mapping.data.mspointerWrapper, capture); } } } // Custom events dispatch table. Event names should be lowercased. // var customEvents = { focusout: { register: registerBubbleListener, unregister: removeListenerFromEventMap }, focusin: { register: registerBubbleListener, unregister: removeListenerFromEventMap } }; if (!_Global.PointerEvent) { var pointerEventEntry = { register: registerPointerEvent, unregister: unregisterPointerEvent }; customEvents.pointerdown = pointerEventEntry; customEvents.pointerup = pointerEventEntry; customEvents.pointermove = pointerEventEntry; customEvents.pointerenter = pointerEventEntry; customEvents.pointerover = pointerEventEntry; customEvents.pointerout = pointerEventEntry; customEvents.pointercancel = pointerEventEntry; } // The MutationObserverShim only supports the following configuration: // attributes // attributeFilter var MutationObserverShim = _Base.Class.define( function MutationObserverShim_ctor(callback) { this._callback = callback; this._toDispose = []; this._attributeFilter = []; this._scheduled = false; this._pendingChanges = []; this._observerCount = 0; this._handleCallback = this._handleCallback.bind(this); this._targetElements = []; }, { observe: function MutationObserverShim_observe(element, configuration) { if (this._targetElements.indexOf(element) === -1) { this._targetElements.push(element); } this._observerCount++; if (configuration.attributes) { this._addRemovableListener(element, "DOMAttrModified", this._handleCallback); } if (configuration.attributeFilter) { this._attributeFilter = configuration.attributeFilter; } }, disconnect: function MutationObserverShim_disconnect() { this._observerCount = 0; this._targetElements = []; this._toDispose.forEach(function (disposeFunc) { disposeFunc(); }); }, _addRemovableListener: function MutationObserverShim_addRemovableListener(target, event, listener) { target.addEventListener(event, listener); this._toDispose.push(function () { target.removeEventListener(event, listener); }); }, _handleCallback: function MutationObserverShim_handleCallback(evt) { // prevent multiple events from firing when nesting observers evt.stopPropagation(); if (this._attributeFilter.length && this._attributeFilter.indexOf(evt.attrName) === -1) { return; } // subtree:true is not currently supported if (this._targetElements.indexOf(evt.target) === -1) { return; } var attrName = evt.attrName; // DOM mutation events use different naming for this attribute if (attrName === 'tabindex') { attrName = 'tabIndex'; } this._pendingChanges.push({ type: 'attributes', target: evt.target, attributeName: attrName }); if (this._observerCount === 1) { this._dispatchEvent(); } else if (this._scheduled === false) { this._scheduled = true; _BaseUtils._setImmediate(this._dispatchEvent.bind(this)); } }, _dispatchEvent: function MutationObserverShim_dispatchEvent() { try { this._callback(this._pendingChanges); } finally { this._pendingChanges = []; this._scheduled = false; } } }, { _isShim: true } ); var _MutationObserver = _Global.MutationObserver || MutationObserverShim; // Lazily init singleton on first access. var _resizeNotifier = null; // Class to provide a global listener for window.onresize events. // This keeps individual elements from having to listen to window.onresize // and having to dispose themselves to avoid leaks. var ResizeNotifier = _Base.Class.define( function ElementResizer_ctor() { _Global.addEventListener("resize", this._handleResize.bind(this)); }, { subscribe: function ElementResizer_subscribe(element, handler) { element.addEventListener(this._resizeEvent, handler); addClass(element, this._resizeClass); }, unsubscribe: function ElementResizer_unsubscribe(element, handler) { removeClass(element, this._resizeClass); element.removeEventListener(this._resizeEvent, handler); }, _handleResize: function ElementResizer_handleResize() { var resizables = _Global.document.querySelectorAll('.' + this._resizeClass); var length = resizables.length; for (var i = 0; i < length; i++) { var event = _Global.document.createEvent("Event"); event.initEvent(this._resizeEvent, false, true); resizables[i].dispatchEvent(event); } }, _resizeClass: { get: function () { return 'win-element-resize'; } }, _resizeEvent: { get: function () { return 'WinJSElementResize'; } } } ); var GlobalListener = new (_Base.Class.define( function GlobalListener_ctor() { this.capture = {}; this.bubble = {}; }, { addEventListener: function GlobalListener_addEventListener(element, name, listener, capture) { name = name.toLowerCase(); var handlers = this._getHandlers(capture); var handler = handlers[name]; if (!handler) { handler = this._getListener(name, capture); handler.refCount = 0; handlers[name] = handler; exports._addEventListener(_Global, name, handler, capture); } handler.refCount++; element.addEventListener(this._getEventName(name, capture), listener); addClass(element, this._getClassName(name, capture)); }, removeEventListener: function GlobalListener_removeEventListener(element, name, listener, capture) { name = name.toLowerCase(); var handlers = this._getHandlers(capture); var handler = handlers[name]; if (handler) { handler.refCount--; if (handler.refCount === 0) { exports._removeEventListener(_Global, name, handler, capture); delete handlers[name]; } } removeClass(element, this._getClassName(name, capture)); element.removeEventListener(this._getEventName(name, capture), listener); }, _getHandlers: function GlobalListener_getHandlers(capture) { if (capture) { return this.capture; } else { return this.bubble; } }, _getClassName: function GlobalListener_getClassName(name, capture) { var captureSuffix = capture ? 'capture' : 'bubble'; return 'win-global-event-' + name + captureSuffix; }, _getEventName: function GlobalListener_getEventName(name, capture) { var captureSuffix = capture ? 'capture' : 'bubble'; return 'WinJSGlobalEvent-' + name + captureSuffix; }, _getListener: function GlobalListener_getListener(name, capture) { var listener = function GlobalListener_generatedListener(ev) { var targets = _Global.document.querySelectorAll('.' + this._getClassName(name, capture)); var length = targets.length; for (var i = 0; i < length; i++) { var event = _Global.document.createEvent("Event"); event.initEvent(this._getEventName(name, capture), false, true); event.detail = { originalEvent: ev }; targets[i].dispatchEvent(event); } }; return listener.bind(this); } } ))(); var determinedRTLEnvironment = false, usingWebkitScrollCoordinates = false, usingFirefoxScrollCoordinates = false; function determineRTLEnvironment() { var element = _Global.document.createElement("div"); element.style.direction = "rtl"; element.innerHTML = "" + "
" + "
" + "
"; _Global.document.body.appendChild(element); var elementScroller = element.firstChild; if (elementScroller.scrollLeft > 0) { usingWebkitScrollCoordinates = true; } elementScroller.scrollLeft += 100; if (elementScroller.scrollLeft === 0) { usingFirefoxScrollCoordinates = true; } _Global.document.body.removeChild(element); determinedRTLEnvironment = true; } function getAdjustedScrollPosition(element) { var computedStyle = _Global.getComputedStyle(element), scrollLeft = element.scrollLeft; if (computedStyle.direction === "rtl") { if (!determinedRTLEnvironment) { determineRTLEnvironment(); } if (usingWebkitScrollCoordinates) { scrollLeft = element.scrollWidth - element.clientWidth - scrollLeft; } scrollLeft = Math.abs(scrollLeft); } return { scrollLeft: scrollLeft, scrollTop: element.scrollTop }; } function setAdjustedScrollPosition(element, scrollLeft, scrollTop) { if (scrollLeft !== undefined) { var computedStyle = _Global.getComputedStyle(element); if (computedStyle.direction === "rtl") { if (!determinedRTLEnvironment) { determineRTLEnvironment(); } if (usingFirefoxScrollCoordinates) { scrollLeft = -scrollLeft; } else if (usingWebkitScrollCoordinates) { scrollLeft = element.scrollWidth - element.clientWidth - scrollLeft; } } element.scrollLeft = scrollLeft; } if (scrollTop !== undefined) { element.scrollTop = scrollTop; } } function getScrollPosition(element) { /// /// /// Gets the scrollLeft and scrollTop of the specified element, adjusting the scrollLeft to change from browser specific coordinates to logical coordinates when in RTL. /// /// /// The element. /// /// /// An object with two properties: scrollLeft and scrollTop /// /// return getAdjustedScrollPosition(element); } function setScrollPosition(element, position) { /// /// /// Sets the scrollLeft and scrollTop of the specified element, changing the scrollLeft from logical coordinates to browser-specific coordinates when in RTL. /// /// /// The element. /// /// /// The element. /// /// position = position || {}; setAdjustedScrollPosition(element, position.scrollLeft, position.scrollTop); } // navigator.msManipulationViewsEnabled tells us whether snap points work or not regardless of whether the style properties exist, however, // on Phone WWAs, this check returns false even though snap points are supported. To work around this bug, we check for the presence of // 'MSAppHost' in the user agent string which indicates that we are in a WWA environment; all WWA environments support snap points. var supportsSnapPoints = _Global.navigator.msManipulationViewsEnabled || _Global.navigator.userAgent.indexOf("MSAppHost") >= 0; var supportsTouchDetection = !!(_Global.MSPointerEvent || _Global.TouchEvent); var uniqueElementIDCounter = 0; function uniqueID(e) { if (!(e.uniqueID || e._uniqueID)) { e._uniqueID = "element__" + (++uniqueElementIDCounter); } return e.uniqueID || e._uniqueID; } function ensureId(element) { if (!element.id) { element.id = uniqueID(element); } } function _getCursorPos(eventObject) { var docElement = _Global.document.documentElement; var docScrollPos = getScrollPosition(docElement); return { left: eventObject.clientX + (_Global.document.body.dir === "rtl" ? -docScrollPos.scrollLeft : docScrollPos.scrollLeft), top: eventObject.clientY + docElement.scrollTop }; } function _getElementsByClasses(parent, classes) { var retVal = []; for (var i = 0, len = classes.length; i < len; i++) { var element = parent.querySelector("." + classes[i]); if (element) { retVal.push(element); } } return retVal; } var _selectionPartsSelector = ".win-selectionborder, .win-selectionbackground, .win-selectioncheckmark, .win-selectioncheckmarkbackground"; var _dataKey = "_msDataKey"; _Base.Namespace._moduleDefine(exports, "WinJS.Utilities", { _dataKey: _dataKey, _supportsSnapPoints: { get: function () { return supportsSnapPoints; } }, _supportsTouchDetection: { get: function () { return supportsTouchDetection; } }, _uniqueID: uniqueID, _ensureId: ensureId, _clamp: _clamp, _getCursorPos: _getCursorPos, _getElementsByClasses: _getElementsByClasses, _createGestureRecognizer: function () { if (_Global.MSGesture) { return new _Global.MSGesture(); } var doNothing = function () { }; return { addEventListener: doNothing, removeEventListener: doNothing, addPointer: doNothing, stop: doNothing }; }, _supportsTouchActionCrossSlide: { get: function () { if (this._supportsTouchActionCrossSlideValue === undefined) { this._supportsTouchActionCrossSlideValue = false; var touchActionStyle = _BaseUtils._browserStyleEquivalents["touch-action"]; if (touchActionStyle) { var div = _Global.document.createElement("div"); div.style.touchAction = "cross-slide-x"; _Global.document.body.appendChild(div); this._supportsTouchActionCrossSlideValue = _Global.getComputedStyle(div).touchAction === "cross-slide-x"; _Global.document.body.removeChild(div); } } return this._supportsTouchActionCrossSlideValue; } }, _MSGestureEvent: _MSGestureEvent, _MSManipulationEvent: _MSManipulationEvent, _elementsFromPoint: function (x, y) { if (_Global.document.msElementsFromPoint) { return _Global.document.msElementsFromPoint(x, y); } else { var element = _Global.document.elementFromPoint(x, y); return element ? [element] : null; } }, _matchesSelector: function _matchesSelector(element, selectorString) { var matchesSelector = element.matches || element.msMatchesSelector || element.mozMatchesSelector || element.webkitMatchesSelector; return matchesSelector.call(element, selectorString); }, _selectionPartsSelector: _selectionPartsSelector, _isSelectionRendered: function _isSelectionRendered(itemBox) { // The tree is changed at pointerDown but _selectedClass is added only when the user drags an item below the selection threshold so checking for _selectedClass is not reliable. return itemBox.querySelectorAll(_selectionPartsSelector).length > 0; }, _addEventListener: function _addEventListener(element, type, listener, useCapture) { var eventNameLower = type && type.toLowerCase(); var entry = customEvents[eventNameLower]; var equivalentEvent = _BaseUtils._browserEventEquivalents[type]; if (entry) { entry.register(element, type, listener, useCapture); } else if (equivalentEvent) { element.addEventListener(equivalentEvent, listener, useCapture); } else { element.addEventListener(type, listener, useCapture); } }, _removeEventListener: function _removeEventListener(element, type, listener, useCapture) { var eventNameLower = type && type.toLowerCase(); var entry = customEvents[eventNameLower]; var equivalentEvent = _BaseUtils._browserEventEquivalents[type]; if (entry) { entry.unregister(element, type, listener, useCapture); } else if (equivalentEvent) { element.removeEventListener(equivalentEvent, listener, useCapture); } else { element.removeEventListener(type, listener, useCapture); } }, _initEventImpl: function (initType, event, eventType) { eventType = eventType.toLowerCase(); var mapping = eventTranslations[eventType]; if (mapping) { switch (initType.toLowerCase()) { case "pointer": arguments[2] = mapping.mspointer; break; default: arguments[2] = mapping[initType.toLowerCase()]; break; } } event["init" + initType + "Event"].apply(event, Array.prototype.slice.call(arguments, 2)); }, _initMouseEvent: function (event) { this._initEventImpl.apply(this, ["Mouse", event].concat(Array.prototype.slice.call(arguments, 1))); }, _initPointerEvent: function (event) { this._initEventImpl.apply(this, ["Pointer", event].concat(Array.prototype.slice.call(arguments, 1))); }, _PointerEventProxy: PointerEventProxy, _bubbleEvent: bubbleEvent, _setPointerCapture: function (element, pointerId) { if (element.setPointerCapture) { element.setPointerCapture(pointerId); } }, _releasePointerCapture: function (element, pointerId) { if (element.releasePointerCapture) { element.releasePointerCapture(pointerId); } }, _MSPointerEvent: _MSPointerEvent, _zoomToDuration: _zoomToDuration, _zoomTo: function _zoomTo(element, args) { if (element.msZoomTo) { element.msZoomTo(args); } else { // Schedule to ensure that we're not running from within an event handler. For example, if running // within a focus handler triggered by WinJS.Utilities._setActive, scroll position will not yet be // restored. Scheduler.schedule(function () { var initialPos = getAdjustedScrollPosition(element); var effectiveScrollLeft = (typeof element._zoomToDestX === "number" ? element._zoomToDestX : initialPos.scrollLeft); var effectiveScrollTop = (typeof element._zoomToDestY === "number" ? element._zoomToDestY : initialPos.scrollTop); var cs = _Global.getComputedStyle(element); var scrollLimitX = element.scrollWidth - parseInt(cs.width, 10) - parseInt(cs.paddingLeft, 10) - parseInt(cs.paddingRight, 10); var scrollLimitY = element.scrollHeight - parseInt(cs.height, 10) - parseInt(cs.paddingTop, 10) - parseInt(cs.paddingBottom, 10); if (typeof args.contentX !== "number") { args.contentX = effectiveScrollLeft; } if (typeof args.contentY !== "number") { args.contentY = effectiveScrollTop; } var zoomToDestX = _clamp(args.contentX, 0, scrollLimitX); var zoomToDestY = _clamp(args.contentY, 0, scrollLimitY); if (zoomToDestX === effectiveScrollLeft && zoomToDestY === effectiveScrollTop) { // Scroll position is already in the proper state. This zoomTo is a no-op. return; } element._zoomToId = element._zoomToId || 0; element._zoomToId++; element._zoomToDestX = zoomToDestX; element._zoomToDestY = zoomToDestY; var thisZoomToId = element._zoomToId; var start = _BaseUtils._now(); var xFactor = (element._zoomToDestX - initialPos.scrollLeft) / _zoomToDuration; var yFactor = (element._zoomToDestY - initialPos.scrollTop) / _zoomToDuration; var update = function () { var t = _BaseUtils._now() - start; if (element._zoomToId !== thisZoomToId) { return; } else if (t > _zoomToDuration) { setAdjustedScrollPosition(element, element._zoomToDestX, element._zoomToDestY); element._zoomToDestX = null; element._zoomToDestY = null; } else { setAdjustedScrollPosition(element, initialPos.scrollLeft + t * xFactor, initialPos.scrollTop + t * yFactor); _BaseUtils._requestAnimationFrame(update); } }; _BaseUtils._requestAnimationFrame(update); }, Scheduler.Priority.high, null, "WinJS.Utilities._zoomTo"); } }, _setActive: function _setActive(element, scroller) { var success = true; try { if (_Global.HTMLElement && _Global.HTMLElement.prototype.setActive) { element.setActive(); } else { // We are aware the unlike setActive(), focus() will scroll to the element that gets focus. However, this is // our current cross-browser solution until there is an equivalent for setActive() in other browsers. // // This _setActive polyfill does have limited support for preventing scrolling: via the scroller parameter, it // can prevent one scroller from scrolling. This functionality is necessary in some scenarios. For example, when using // _zoomTo and _setActive together. var scrollLeft, scrollTop; if (scroller) { scrollLeft = scroller.scrollLeft; scrollTop = scroller.scrollTop; } element.focus(); if (scroller) { scroller.scrollLeft = scrollLeft; scroller.scrollTop = scrollTop; } } } catch (e) { // setActive() raises an exception when trying to focus an invisible item. Checking visibility is non-trivial, so it's best // just to catch the exception and ignore it. focus() on the other hand, does not raise exceptions. success = false; } return success; }, _MutationObserver: _MutationObserver, _resizeNotifier: { get: function () { if (!_resizeNotifier) { _resizeNotifier = new ResizeNotifier(); } return _resizeNotifier; } }, _globalListener: GlobalListener, // Appends a hidden child to the given element that will listen for being added // to the DOM. When the hidden element is added to the DOM, it will dispatch a // "WinJSNodeInserted" event on the provided element. _addInsertedNotifier: function (element) { var hiddenElement = _Global.document.createElement("div"); hiddenElement.style[_BaseUtils._browserStyleEquivalents["animation-name"].scriptName] = "WinJS-node-inserted"; hiddenElement.style[_BaseUtils._browserStyleEquivalents["animation-duration"].scriptName] = "0.01s"; hiddenElement.style["position"] = "absolute"; element.appendChild(hiddenElement); exports._addEventListener(hiddenElement, "animationStart", function (e) { if (e.animationName === "WinJS-node-inserted") { var e = _Global.document.createEvent("Event"); e.initEvent("WinJSNodeInserted", false, true); element.dispatchEvent(e); } }, false); return hiddenElement; }, // Browser agnostic method to set element flex style // Param is an object in the form {grow: flex-grow, shrink: flex-shrink, basis: flex-basis} // All fields optional _setFlexStyle: function (element, flexParams) { var styleObject = element.style; if (typeof flexParams.grow !== "undefined") { styleObject.msFlexPositive = flexParams.grow; styleObject.webkitFlexGrow = flexParams.grow; styleObject.flexGrow = flexParams.grow; } if (typeof flexParams.shrink !== "undefined") { styleObject.msFlexNegative = flexParams.shrink; styleObject.webkitFlexShrink = flexParams.shrink; styleObject.flexShrink = flexParams.shrink; } if (typeof flexParams.basis !== "undefined") { styleObject.msFlexPreferredSize = flexParams.basis; styleObject.webkitFlexBasis = flexParams.basis; styleObject.flexBasis = flexParams.basis; } }, /// /// Defines a set of keyboard values. /// Key: { /// /// BACKSPACE key. /// backspace: 8, /// /// TAB key. /// tab: 9, /// /// ENTER key. /// enter: 13, /// /// Shift key. /// shift: 16, /// /// CTRL key. /// ctrl: 17, /// /// ALT key /// alt: 18, /// /// Pause key. /// pause: 19, /// /// CAPS LOCK key. /// capsLock: 20, /// /// ESCAPE key. /// escape: 27, /// /// SPACE key. /// space: 32, /// /// PAGE UP key. /// pageUp: 33, /// /// PAGE DOWN key. /// pageDown: 34, /// /// END key. /// end: 35, /// /// HOME key. /// home: 36, /// /// Left arrow key. /// leftArrow: 37, /// /// Up arrow key. /// upArrow: 38, /// /// Right arrow key. /// rightArrow: 39, /// /// Down arrow key. /// downArrow: 40, /// /// INSERT key. /// insert: 45, /// /// DELETE key. /// deleteKey: 46, /// /// Number 0 key. /// num0: 48, /// /// Number 1 key. /// num1: 49, /// /// Number 2 key. /// num2: 50, /// /// Number 3 key. /// num3: 51, /// /// Number 4 key. /// num4: 52, /// /// Number 5 key. /// num5: 53, /// /// Number 6 key. /// num6: 54, /// /// Number 7 key. /// num7: 55, /// /// Number 8 key. /// num8: 56, /// /// Number 9 key. /// num9: 57, /// /// A key. /// a: 65, /// /// B key. /// b: 66, /// /// C key. /// c: 67, /// /// D key. /// d: 68, /// /// E key. /// e: 69, /// /// F key. /// f: 70, /// /// G key. /// g: 71, /// /// H key. /// h: 72, /// /// I key. /// i: 73, /// /// J key. /// j: 74, /// /// K key. /// k: 75, /// /// L key. /// l: 76, /// /// M key. /// m: 77, /// /// N key. /// n: 78, /// /// O key. /// o: 79, /// /// P key. /// p: 80, /// /// Q key. /// q: 81, /// /// R key. /// r: 82, /// /// S key. /// s: 83, /// /// T key. /// t: 84, /// /// U key. /// u: 85, /// /// V key. /// v: 86, /// /// W key. /// w: 87, /// /// X key. /// x: 88, /// /// Y key. /// y: 89, /// /// Z key. /// z: 90, /// /// Left Windows key. /// leftWindows: 91, /// /// Right Windows key. /// rightWindows: 92, /// /// Menu key. /// menu: 93, /// /// Number pad 0 key. /// numPad0: 96, /// /// Number pad 1 key. /// numPad1: 97, /// /// Number pad 2 key. /// numPad2: 98, /// /// Number pad 3 key. /// numPad3: 99, /// /// Number pad 4 key. /// numPad4: 100, /// /// Number pad 5 key. /// numPad5: 101, /// /// Number pad 6 key. /// numPad6: 102, /// /// Number pad 7 key. /// numPad7: 103, /// /// Number pad 8 key. /// numPad8: 104, /// /// Number pad 9 key. /// numPad9: 105, /// /// Multiplication key. /// multiply: 106, /// /// Addition key. /// add: 107, /// /// Subtraction key. /// subtract: 109, /// /// Decimal point key. /// decimalPoint: 110, /// /// Division key. /// divide: 111, /// /// F1 key. /// F1: 112, /// /// F2 key. /// F2: 113, /// /// F3 key. /// F3: 114, /// /// F4 key. /// F4: 115, /// /// F5 key. /// F5: 116, /// /// F6 key. /// F6: 117, /// /// F7 key. /// F7: 118, /// /// F8 key. /// F8: 119, /// /// F9 key. /// F9: 120, /// /// F10 key. /// F10: 121, /// /// F11 key. /// F11: 122, /// /// F12 key. /// F12: 123, /// /// NUMBER LOCK key. /// numLock: 144, /// /// SCROLL LOCK key. /// scrollLock: 145, /// /// Browser back key. /// browserBack: 166, /// /// Browser forward key. /// browserForward: 167, /// /// SEMICOLON key. /// semicolon: 186, /// /// EQUAL key. /// equal: 187, /// /// COMMA key. /// comma: 188, /// /// DASH key. /// dash: 189, /// /// PERIOD key. /// period: 190, /// /// FORWARD SLASH key. /// forwardSlash: 191, /// /// Accent grave key. /// graveAccent: 192, /// /// OPEN BRACKET key. /// openBracket: 219, /// /// BACKSLASH key. /// backSlash: 220, /// /// CLOSE BRACKET key. /// closeBracket: 221, /// /// SINGLE QUOTE key. /// singleQuote: 222, /// /// Any IME input. /// IME: 229 }, data: function (element) { /// /// /// Gets the data value associated with the specified element. /// /// /// The element. /// /// /// The value associated with the element. /// /// if (!element[_dataKey]) { element[_dataKey] = {}; } return element[_dataKey]; }, hasClass: function (e, name) { /// /// /// Determines whether the specified element has the specified class. /// /// /// The element. /// /// /// The name of the class. /// /// /// true if the specified element contains the specified class; otherwise, false. /// /// if (e.classList) { return e.classList.contains(name); } else { var className = getClassName(e); var names = className.trim().split(" "); var l = names.length; for (var i = 0; i < l; i++) { if (names[i] === name) { return true; } } return false; } }, addClass: addClass, removeClass: removeClass, toggleClass: toggleClass, _setAttribute: setAttribute, getRelativeLeft: function (element, parent) { /// /// /// Gets the left coordinate of the specified element relative to the specified parent. /// /// /// The element. /// /// /// The parent element. /// /// /// The relative left coordinate. /// /// if (!element) { return 0; } var left = element.offsetLeft; var e = element.parentNode; while (e) { left -= e.offsetLeft; if (e === parent) { break; } e = e.parentNode; } return left; }, getRelativeTop: function (element, parent) { /// /// /// Gets the top coordinate of the element relative to the specified parent. /// /// /// The element. /// /// /// The parent element. /// /// /// The relative top coordinate. /// /// if (!element) { return 0; } var top = element.offsetTop; var e = element.parentNode; while (e) { top -= e.offsetTop; if (e === parent) { break; } e = e.parentNode; } return top; }, getScrollPosition: getScrollPosition, setScrollPosition: setScrollPosition, empty: function (element) { /// /// /// Removes all the child nodes from the specified element. /// /// /// The element. /// /// /// The element. /// /// if (element.childNodes && element.childNodes.length > 0) { for (var i = element.childNodes.length - 1; i >= 0; i--) { element.removeChild(element.childNodes.item(i)); } } return element; }, _isDOMElement: function (element) { return element && typeof element === "object" && typeof element.tagName === "string"; }, getContentWidth: function (element) { /// /// /// Gets the width of the content of the specified element. The content width does not include borders or padding. /// /// /// The element. /// /// /// The content width of the element. /// /// var border = getDimension(element, "borderLeftWidth") + getDimension(element, "borderRightWidth"), padding = getDimension(element, "paddingLeft") + getDimension(element, "paddingRight"); return element.offsetWidth - border - padding; }, getTotalWidth: function (element) { /// /// /// Gets the width of the element, including margins. /// /// /// The element. /// /// /// The width of the element including margins. /// /// var margin = getDimension(element, "marginLeft") + getDimension(element, "marginRight"); return element.offsetWidth + margin; }, getContentHeight: function (element) { /// /// /// Gets the height of the content of the specified element. The content height does not include borders or padding. /// /// /// The element. /// /// /// The content height of the element. /// /// var border = getDimension(element, "borderTopWidth") + getDimension(element, "borderBottomWidth"), padding = getDimension(element, "paddingTop") + getDimension(element, "paddingBottom"); return element.offsetHeight - border - padding; }, getTotalHeight: function (element) { /// /// /// Gets the height of the element, including its margins. /// /// /// The element. /// /// /// The height of the element including margins. /// /// var margin = getDimension(element, "marginTop") + getDimension(element, "marginBottom"); return element.offsetHeight + margin; }, getPosition: function (element) { /// /// /// Gets the position of the specified element. /// /// /// The element. /// /// /// An object that contains the left, top, width and height properties of the element. /// /// var fromElement = element, offsetParent = element.offsetParent, top = element.offsetTop, left = element.offsetLeft; while ((element = element.parentNode) && element !== _Global.document.body && element !== _Global.document.documentElement) { top -= element.scrollTop; var dir = _Global.document.defaultView.getComputedStyle(element, null).direction; left -= dir !== "rtl" ? element.scrollLeft : -getAdjustedScrollPosition(element).scrollLeft; if (element === offsetParent) { top += element.offsetTop; left += element.offsetLeft; offsetParent = element.offsetParent; } } return { left: left, top: top, width: fromElement.offsetWidth, height: fromElement.offsetHeight }; }, getTabIndex: function (element) { /// /// /// Gets the tabIndex of the specified element. /// /// /// The element. /// /// /// The tabIndex of the element. Returns -1 if the element cannot be tabbed to /// /// // For reference: http://www.w3.org/html/wg/drafts/html/master/single-page.html#specially-focusable var tabbableElementsRE = /BUTTON|COMMAND|MENUITEM|OBJECT|SELECT|TEXTAREA/; if (element.disabled) { return -1; } var tabIndex = element.getAttribute("tabindex"); if (tabIndex === null || tabIndex === undefined) { var name = element.tagName; if (tabbableElementsRE.test(name) || (element.href && (name === "A" || name === "AREA" || name === "LINK")) || (name === "INPUT" && element.type !== "hidden") || (name === "TH" && element.sorted)) { return 0; } return -1; } return parseInt(tabIndex, 10); }, convertToPixels: convertToPixels, eventWithinElement: function (element, event) { /// /// /// Determines whether the specified event occurred within the specified element. /// /// /// The element. /// /// /// The event. /// /// /// true if the event occurred within the element; otherwise, false. /// /// var related = event.relatedTarget; if (related && related !== element) { return element.contains(related); } return false; }, //UI Utilities _deprecated: function (message) { _Global.console && _Global.console.warn(message); }, // Take a renderer which may be a function (signature: (data) => element) or a WinJS.Binding.Template // and return a function with a unified synchronous contract which is: // // (data, container) => element // // Where: // // 1) if you pass container the content will be rendered into the container and the // container will be returned. // // 2) if you don't pass a container the content will be rendered and returned. // _syncRenderer: function (renderer, tagName) { tagName = tagName || "div"; if (typeof renderer === "function") { return function (data, container) { if (container) { container.appendChild(renderer(data)); return container; } else { return renderer(data); } }; } var template; if (typeof renderer.render === "function") { template = renderer; } else if (renderer.winControl && typeof renderer.winControl.render === "function") { template = renderer.winControl; } return function (data, container) { var host = container || _Global.document.createElement(tagName); template.render(data, host); if (container) { return container; } else { // The expectation is that the creation of the DOM elements happens synchronously // and as such we steal the first child and make it the root element. // var element = host.firstElementChild; // Because we have changed the "root" we may need to move the dispose method // created by the template to the child and do a little switcheroo on dispose. // if (element && host.dispose) { var prev = element.dispose; element.dispose = function () { element.dispose = prev; host.appendChild(element); host.dispose(); }; } return element; } }; }, _getLowestTabIndexInList: function Utilities_getLowestTabIndexInList(elements) { // Returns the lowest positive tabIndex in a list of elements. // Returns 0 if there are no positive tabIndices. var lowestTabIndex = 0; var elmTabIndex; for (var i = 0; i < elements.length; i++) { elmTabIndex = parseInt(elements[i].getAttribute("tabIndex"), 10); if ((0 < elmTabIndex) && ((elmTabIndex < lowestTabIndex) || !lowestTabIndex)) { lowestTabIndex = elmTabIndex; } } return lowestTabIndex; }, _getHighestTabIndexInList: function Utilities_getHighestTabIndexInList(elements) { // Returns 0 if any element is explicitly set to 0. (0 is the highest tabIndex) // Returns the highest tabIndex in the list of elements. // Returns 0 if there are no positive tabIndices. var highestTabIndex = 0; var elmTabIndex; for (var i = 0; i < elements.length; i++) { elmTabIndex = parseInt(elements[i].getAttribute("tabIndex"), 10); if (elmTabIndex === 0) { return elmTabIndex; } else if (highestTabIndex < elmTabIndex) { highestTabIndex = elmTabIndex; } } return highestTabIndex; }, _trySetActive: function Utilities_trySetActive(elem, scroller) { return this._tryFocus(elem, true, scroller); }, _tryFocus: function Utilities_tryFocus(elem, useSetActive, scroller) { var previousActiveElement = _Global.document.activeElement; if (elem === previousActiveElement) { return true; } var simpleLogicForValidTabStop = (exports.getTabIndex(elem) >= 0); if (!simpleLogicForValidTabStop) { return false; } if (useSetActive) { exports._setActive(elem, scroller); } else { elem.focus(); } if (previousActiveElement !== _Global.document.activeElement) { return true; } return false; }, _setActiveFirstFocusableElement: function Utilities_setActiveFirstFocusableElement(rootEl, scroller) { return this._focusFirstFocusableElement(rootEl, true, scroller); }, _focusFirstFocusableElement: function Utilities_focusFirstFocusableElement(rootEl, useSetActive, scroller) { var _elms = rootEl.getElementsByTagName("*"); // Get the tabIndex set to the firstDiv (which is the lowest) var _lowestTabIndex = this._getLowestTabIndexInList(_elms); var _nextLowestTabIndex = 0; // If there are positive tabIndices, set focus to the element with the lowest tabIndex. // Keep trying with the next lowest tabIndex until all tabIndices have been exhausted. // Otherwise set focus to the first focusable element in DOM order. var i; while (_lowestTabIndex) { for (i = 0; i < _elms.length; i++) { if (_elms[i].tabIndex === _lowestTabIndex) { if (this._tryFocus(_elms[i], useSetActive, scroller)) { return true; } } else if ((_lowestTabIndex < _elms[i].tabIndex) && ((_elms[i].tabIndex < _nextLowestTabIndex) || (_nextLowestTabIndex === 0))) { // Here if _lowestTabIndex < _elms[i].tabIndex < _nextLowestTabIndex _nextLowestTabIndex = _elms[i].tabIndex; } } // We weren't able to set focus to anything at that tabIndex // If we found a higher valid tabIndex, try that now _lowestTabIndex = _nextLowestTabIndex; _nextLowestTabIndex = 0; } // Wasn't able to set focus to anything with a positive tabIndex, try everything now. // This is where things with tabIndex of 0 will be tried. for (i = 0; i < _elms.length; i++) { if (this._tryFocus(_elms[i], useSetActive, scroller)) { return true; } } return false; }, _setActiveLastFocusableElement: function Utilities_setActiveLastFocusableElement(rootEl, scroller) { return this._focusLastFocusableElement(rootEl, true, scroller); }, _focusLastFocusableElement: function Utilities_focusLastFocusableElement(rootEl, useSetActive, scroller) { var _elms = rootEl.getElementsByTagName("*"); // Get the tabIndex set to the finalDiv (which is the highest) var _highestTabIndex = this._getHighestTabIndexInList(_elms); var _nextHighestTabIndex = 0; // Try all tabIndex 0 first. After this conditional the _highestTabIndex // should be equal to the highest positive tabIndex. var i; if (_highestTabIndex === 0) { for (i = _elms.length - 1; i >= 0; i--) { if (_elms[i].tabIndex === _highestTabIndex) { if (this._tryFocus(_elms[i], useSetActive, scroller)) { return true; } } else if (_nextHighestTabIndex < _elms[i].tabIndex) { _nextHighestTabIndex = _elms[i].tabIndex; } } _highestTabIndex = _nextHighestTabIndex; _nextHighestTabIndex = 0; } // If there are positive tabIndices, set focus to the element with the highest tabIndex. // Keep trying with the next highest tabIndex until all tabIndices have been exhausted. // Otherwise set focus to the last focusable element in DOM order. while (_highestTabIndex) { for (i = _elms.length - 1; i >= 0; i--) { if (_elms[i].tabIndex === _highestTabIndex) { if (this._tryFocus(_elms[i], useSetActive, scroller)) { return true; } } else if ((_nextHighestTabIndex < _elms[i].tabIndex) && (_elms[i].tabIndex < _highestTabIndex)) { // Here if _nextHighestTabIndex < _elms[i].tabIndex < _highestTabIndex _nextHighestTabIndex = _elms[i].tabIndex; } } // We weren't able to set focus to anything at that tabIndex // If we found a lower valid tabIndex, try that now _highestTabIndex = _nextHighestTabIndex; _nextHighestTabIndex = 0; } // Wasn't able to set focus to anything with a tabIndex, try everything now for (i = _elms.length - 2; i > 0; i--) { if (this._tryFocus(_elms[i], useSetActive, scroller)) { return true; } } return false; } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_Dispose',[ 'exports', '../Core/_Base', '../Core/_WriteProfilerMark', './_ElementUtilities' ], function (exports, _Base, _WriteProfilerMark, _ElementUtilities) { "use strict"; function markDisposable(element, disposeImpl) { /// /// /// Adds the specified dispose implementation to the specified element and marks it as disposable. /// /// /// The element to mark as disposable. /// /// /// The function containing the element-specific dispose logic that will be called by the dispose function. /// /// var disposed = false; _ElementUtilities.addClass(element, "win-disposable"); var disposable = element.winControl || element; disposable.dispose = function () { if (disposed) { return; } disposed = true; disposeSubTree(element); if (disposeImpl) { disposeImpl(); } }; } function disposeSubTree(element) { /// /// /// Disposes all first-generation disposable elements that are descendents of the specified element. /// The specified element itself is not disposed. /// /// /// The root element whose sub-tree is to be disposed. /// /// if (!element) { return; } _WriteProfilerMark("WinJS.Utilities.disposeSubTree,StartTM"); var query = element.querySelectorAll(".win-disposable"); var index = 0; var length = query.length; while (index < length) { var disposable = query[index]; if (disposable.winControl && disposable.winControl.dispose) { disposable.winControl.dispose(); } if (disposable.dispose) { disposable.dispose(); } // Skip over disposable's descendants since they are this disposable's responsibility to clean up. index += disposable.querySelectorAll(".win-disposable").length + 1; } _WriteProfilerMark("WinJS.Utilities.disposeSubTree,StopTM"); } function _disposeElement(element) { // This helper should only be used for supporting dispose scenarios predating the dispose pattern. // The specified element should be well enough defined so we don't have to check whether it // a) has a disposable winControl, // b) is disposable itself, // or has disposable descendants in which case either a) or b) must have been true when designed correctly. if (!element) { return; } var disposed = false; if (element.winControl && element.winControl.dispose) { element.winControl.dispose(); disposed = true; } if (element.dispose) { element.dispose(); disposed = true; } if (!disposed) { disposeSubTree(element); } } _Base.Namespace._moduleDefine(exports, "WinJS.Utilities", { markDisposable: markDisposable, disposeSubTree: disposeSubTree, _disposeElement: _disposeElement }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/ControlProcessor/_OptionsLexer',[ 'exports', '../Core/_Base' ], function optionsLexerInit(exports, _Base) { "use strict"; /* Lexical grammar is defined in ECMA-262-5, section 7. Lexical productions used in this grammar defined in ECMA-262-5: Production Section -------------------------------- Identifier 7.6 NullLiteral 7.8.1 BooleanLiteral 7.8.2 NumberLiteral 7.8.3 StringLiteral 7.8.4 */ _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _optionsLexer: _Base.Namespace._lazy(function () { var tokenType = { leftBrace: 1, // { rightBrace: 2, // } leftBracket: 3, // [ rightBracket: 4, // ] separator: 5, // ECMA-262-5, 7.2 colon: 6, // : semicolon: 7, // ; comma: 8, // , dot: 9, // . nullLiteral: 10, // ECMA-262-5, 7.8.1 (null) trueLiteral: 11, // ECMA-262-5, 7.8.2 (true) falseLiteral: 12, // ECMA-262-5, 7.8.2 (false) numberLiteral: 13, // ECMA-262-5, 7.8.3 stringLiteral: 14, // ECMA-262-5, 7.8.4 identifier: 15, // ECMA-262-5, 7.6 reservedWord: 16, thisKeyword: 17, leftParentheses: 18, // ( rightParentheses: 19, // ) eof: 20, error: 21 }; // debugging - this costs something like 20% // //Object.keys(tokenType).forEach(function (key) { // tokenType[key] = key.toString(); //}); var tokens = { leftBrace: { type: tokenType.leftBrace, length: 1 }, rightBrace: { type: tokenType.rightBrace, length: 1 }, leftBracket: { type: tokenType.leftBracket, length: 1 }, rightBracket: { type: tokenType.rightBracket, length: 1 }, colon: { type: tokenType.colon, length: 1 }, semicolon: { type: tokenType.semicolon, length: 1 }, comma: { type: tokenType.comma, length: 1 }, dot: { type: tokenType.dot, length: 1 }, nullLiteral: { type: tokenType.nullLiteral, length: 4, value: null, keyword: true }, trueLiteral: { type: tokenType.trueLiteral, length: 4, value: true, keyword: true }, falseLiteral: { type: tokenType.falseLiteral, length: 5, value: false, keyword: true }, thisKeyword: { type: tokenType.thisKeyword, length: 4, value: "this", keyword: true }, leftParentheses: { type: tokenType.leftParentheses, length: 1 }, rightParentheses: { type: tokenType.rightParentheses, length: 1 }, eof: { type: tokenType.eof, length: 0 } }; function reservedWord(word) { return { type: tokenType.reservedWord, value: word, length: word.length, keyword: true }; } function reservedWordLookup(identifier) { // Moving from a simple object literal lookup for reserved words to this // switch was worth a non-trivial performance increase (5-7%) as this path // gets taken for any identifier. // switch (identifier.charCodeAt(0)) { case /*b*/98: switch (identifier) { case 'break': return reservedWord(identifier); } break; case /*c*/99: switch (identifier) { case 'case': case 'catch': case 'class': case 'const': case 'continue': return reservedWord(identifier); } break; case /*d*/100: switch (identifier) { case 'debugger': case 'default': case 'delete': case 'do': return reservedWord(identifier); } break; case /*e*/101: switch (identifier) { case 'else': case 'enum': case 'export': case 'extends': return reservedWord(identifier); } break; case /*f*/102: switch (identifier) { case 'false': return tokens.falseLiteral; case 'finally': case 'for': case 'function': return reservedWord(identifier); } break; case /*i*/105: switch (identifier) { case 'if': case 'import': case 'in': case 'instanceof': return reservedWord(identifier); } break; case /*n*/110: switch (identifier) { case 'null': return tokens.nullLiteral; case 'new': return reservedWord(identifier); } break; case /*r*/114: switch (identifier) { case 'return': return reservedWord(identifier); } break; case /*s*/115: switch (identifier) { case 'super': case 'switch': return reservedWord(identifier); } break; case /*t*/116: switch (identifier) { case 'true': return tokens.trueLiteral; case 'this': return tokens.thisKeyword; case 'throw': case 'try': case 'typeof': return reservedWord(identifier); } break; case /*v*/118: switch (identifier) { case 'var': case 'void': return reservedWord(identifier); } break; case /*w*/119: switch (identifier) { case 'while': case 'with': return reservedWord(identifier); } break; } return; } var lexer = (function () { function isIdentifierStartCharacter(code, text, offset, limit) { // The ES5 spec decalares that identifiers consist of a bunch of unicode classes, without // WinRT support for determining unicode class membership we are looking at 2500+ lines of // javascript code to encode the relevant class tables. Instead we look for everything // which is legal and < 0x7f, we exclude whitespace and line terminators, and then accept // everything > 0x7f. // // Here's the ES5 production: // // Lu | Ll | Lt | Lm | Lo | Nl // $ // _ // \ UnicodeEscapeSequence // switch (code) { case (code >= /*a*/97 && code <= /*z*/122) && code: case (code >= /*A*/65 && code <= /*Z*/90) && code: case /*$*/36: case /*_*/95: return true; case isWhitespace(code) && code: case isLineTerminator(code) && code: return false; case (code > 0x7f) && code: return true; case /*\*/92: if (offset + 4 < limit) { if (text.charCodeAt(offset) === /*u*/117 && isHexDigit(text.charCodeAt(offset + 1)) && isHexDigit(text.charCodeAt(offset + 2)) && isHexDigit(text.charCodeAt(offset + 3)) && isHexDigit(text.charCodeAt(offset + 4))) { return true; } } return false; default: return false; } } /* // Hand-inlined into readIdentifierPart function isIdentifierPartCharacter(code) { // See comment in isIdentifierStartCharacter. // // Mn | Mc | Nd | Pc // | // switch (code) { case isIdentifierStartCharacter(code) && code: case isDecimalDigit(code) && code: return true; default: return false; } } */ function readIdentifierPart(text, offset, limit) { var hasEscape = false; while (offset < limit) { var code = text.charCodeAt(offset); switch (code) { //case isIdentifierStartCharacter(code) && code: case (code >= /*a*/97 && code <= /*z*/122) && code: case (code >= /*A*/65 && code <= /*Z*/90) && code: case /*$*/36: case /*_*/95: break; case isWhitespace(code) && code: case isLineTerminator(code) && code: return hasEscape ? -offset : offset; case (code > 0x7f) && code: break; //case isDecimalDigit(code) && code: case (code >= /*0*/48 && code <= /*9*/57) && code: break; case /*\*/92: if (offset + 5 < limit) { if (text.charCodeAt(offset + 1) === /*u*/117 && isHexDigit(text.charCodeAt(offset + 2)) && isHexDigit(text.charCodeAt(offset + 3)) && isHexDigit(text.charCodeAt(offset + 4)) && isHexDigit(text.charCodeAt(offset + 5))) { offset += 5; hasEscape = true; break; } } return hasEscape ? -offset : offset; default: return hasEscape ? -offset : offset; } offset++; } return hasEscape ? -offset : offset; } function readIdentifierToken(text, offset, limit) { var startOffset = offset; offset = readIdentifierPart(text, offset, limit); var hasEscape = false; if (offset < 0) { offset = -offset; hasEscape = true; } var identifier = text.substr(startOffset, offset - startOffset); if (hasEscape) { identifier = "" + JSON.parse('"' + identifier + '"'); } var wordToken = reservedWordLookup(identifier); if (wordToken) { return wordToken; } return { type: tokenType.identifier, length: offset - startOffset, value: identifier }; } function isHexDigit(code) { switch (code) { case (code >= /*0*/48 && code <= /*9*/57) && code: case (code >= /*a*/97 && code <= /*f*/102) && code: case (code >= /*A*/65 && code <= /*F*/70) && code: return true; default: return false; } } function readHexIntegerLiteral(text, offset, limit) { while (offset < limit && isHexDigit(text.charCodeAt(offset))) { offset++; } return offset; } function isDecimalDigit(code) { switch (code) { case (code >= /*0*/48 && code <= /*9*/57) && code: return true; default: return false; } } function readDecimalDigits(text, offset, limit) { while (offset < limit && isDecimalDigit(text.charCodeAt(offset))) { offset++; } return offset; } function readDecimalLiteral(text, offset, limit) { offset = readDecimalDigits(text, offset, limit); if (offset < limit && text.charCodeAt(offset) === /*.*/46 && offset + 1 < limit && isDecimalDigit(text.charCodeAt(offset + 1))) { offset = readDecimalDigits(text, offset + 2, limit); } if (offset < limit) { var code = text.charCodeAt(offset); if (code === /*e*/101 || code === /*E*/69) { var tempOffset = offset + 1; if (tempOffset < limit) { code = text.charCodeAt(tempOffset); if (code === /*+*/43 || code === /*-*/45) { tempOffset++; } offset = readDecimalDigits(text, tempOffset, limit); } } } return offset; } function readDecimalLiteralToken(text, start, offset, limit) { var offset = readDecimalLiteral(text, offset, limit); var length = offset - start; return { type: tokenType.numberLiteral, length: length, value: +text.substr(start, length) }; } function isLineTerminator(code) { switch (code) { case 0x000A: // line feed case 0x000D: // carriage return case 0x2028: // line separator case 0x2029: // paragraph separator return true; default: return false; } } function readStringLiteralToken(text, offset, limit) { var startOffset = offset; var quoteCharCode = text.charCodeAt(offset); var hasEscape = false; offset++; while (offset < limit && !isLineTerminator(text.charCodeAt(offset))) { if (offset + 1 < limit && text.charCodeAt(offset) === /*\*/92) { hasEscape = true; switch (text.charCodeAt(offset + 1)) { case quoteCharCode: case 0x005C: // \ case 0x000A: // line feed case 0x2028: // line separator case 0x2029: // paragraph separator offset += 2; continue; case 0x000D: // carriage return if (offset + 2 < limit && text.charCodeAt(offset + 2) === 0x000A) { // Skip \r\n offset += 3; } else { offset += 2; } continue; } } offset++; if (text.charCodeAt(offset - 1) === quoteCharCode) { break; } } var length = offset - startOffset; // If we don't have a terminating quote go through the escape path. hasEscape = hasEscape || length === 1 || text.charCodeAt(offset - 1) !== quoteCharCode; var stringValue; if (hasEscape) { stringValue = eval(text.substr(startOffset, length)); // jshint ignore:line } else { stringValue = text.substr(startOffset + 1, length - 2); } return { type: tokenType.stringLiteral, length: length, value: stringValue }; } function isWhitespace(code) { switch (code) { case 0x0009: // tab case 0x000B: // vertical tab case 0x000C: // form feed case 0x0020: // space case 0x00A0: // no-breaking space case 0xFEFF: // BOM return true; // There are no category Zs between 0x00A0 and 0x1680. // case (code < 0x1680) && code: return false; // Unicode category Zs // case 0x1680: case 0x180e: case (code >= 0x2000 && code <= 0x200a) && code: case 0x202f: case 0x205f: case 0x3000: return true; default: return false; } } // Hand-inlined isWhitespace. function readWhitespace(text, offset, limit) { while (offset < limit) { var code = text.charCodeAt(offset); switch (code) { case 0x0009: // tab case 0x000B: // vertical tab case 0x000C: // form feed case 0x0020: // space case 0x00A0: // no-breaking space case 0xFEFF: // BOM break; // There are no category Zs between 0x00A0 and 0x1680. // case (code < 0x1680) && code: return offset; // Unicode category Zs // case 0x1680: case 0x180e: case (code >= 0x2000 && code <= 0x200a) && code: case 0x202f: case 0x205f: case 0x3000: break; default: return offset; } offset++; } return offset; } function lex(result, text, offset, limit) { while (offset < limit) { var startOffset = offset; var code = text.charCodeAt(offset++); var token; switch (code) { case isWhitespace(code) && code: case isLineTerminator(code) && code: offset = readWhitespace(text, offset, limit); token = { type: tokenType.separator, length: offset - startOffset }; // don't include whitespace in the token stream. continue; case /*"*/34: case /*'*/39: token = readStringLiteralToken(text, offset - 1, limit); break; case /*(*/40: token = tokens.leftParentheses; break; case /*)*/41: token = tokens.rightParentheses; break; case /*+*/43: case /*-*/45: if (offset < limit) { var afterSign = text.charCodeAt(offset); if (afterSign === /*.*/46) { var signOffset = offset + 1; if (signOffset < limit && isDecimalDigit(text.charCodeAt(signOffset))) { token = readDecimalLiteralToken(text, startOffset, signOffset, limit); break; } } else if (isDecimalDigit(afterSign)) { token = readDecimalLiteralToken(text, startOffset, offset, limit); break; } } token = { type: tokenType.error, length: offset - startOffset, value: text.substring(startOffset, offset) }; break; case /*,*/44: token = tokens.comma; break; case /*.*/46: token = tokens.dot; if (offset < limit && isDecimalDigit(text.charCodeAt(offset))) { token = readDecimalLiteralToken(text, startOffset, offset, limit); } break; case /*0*/48: var ch2 = (offset < limit ? text.charCodeAt(offset) : 0); if (ch2 === /*x*/120 || ch2 === /*X*/88) { var hexOffset = readHexIntegerLiteral(text, offset + 1, limit); token = { type: tokenType.numberLiteral, length: hexOffset - startOffset, value: +text.substr(startOffset, hexOffset - startOffset) }; } else { token = readDecimalLiteralToken(text, startOffset, offset, limit); } break; case (code >= /*1*/49 && code <= /*9*/57) && code: token = readDecimalLiteralToken(text, startOffset, offset, limit); break; case /*:*/58: token = tokens.colon; break; case /*;*/59: token = tokens.semicolon; break; case /*[*/91: token = tokens.leftBracket; break; case /*]*/93: token = tokens.rightBracket; break; case /*{*/123: token = tokens.leftBrace; break; case /*}*/125: token = tokens.rightBrace; break; default: if (isIdentifierStartCharacter(code, text, offset, limit)) { token = readIdentifierToken(text, offset - 1, limit); break; } token = { type: tokenType.error, length: offset - startOffset, value: text.substring(startOffset, offset) }; break; } offset += (token.length - 1); result.push(token); } } return function (text) { var result = []; lex(result, text, 0, text.length); result.push(tokens.eof); return result; }; })(); lexer.tokenType = tokenType; return lexer; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/ControlProcessor/_OptionsParser',[ 'exports', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Resources', './_OptionsLexer' ], function optionsParserInit(exports, _Base, _BaseUtils, _ErrorFromName, _Resources, _OptionsLexer) { "use strict"; var strings = { get invalidOptionsRecord() { return "Invalid options record: '{0}', expected to be in the format of an object literal. {1}"; }, get unexpectedTokenExpectedToken() { return "Unexpected token: {0}, expected token: {1}, at offset {2}"; }, get unexpectedTokenExpectedTokens() { return "Unexpected token: {0}, expected one of: {1}, at offset {2}"; }, get unexpectedTokenGeneric() { return "Unexpected token: {0}, at offset {1}"; }, }; /* Notation is described in ECMA-262-5 (ECMAScript Language Specification, 5th edition) section 5. Lexical grammar is defined in ECMA-262-5, section 7. Lexical productions used in this grammar defined in ECMA-262-5: Production Section -------------------------------- Identifier 7.6 NullLiteral 7.8.1 BooleanLiteral 7.8.2 NumberLiteral 7.8.3 StringLiteral 7.8.4 Syntactic grammar for the value of the data-win-options attribute. OptionsLiteral: ObjectLiteral ObjectLiteral: { } { ObjectProperties } { ObjectProperties , } ObjectProperties: ObjectProperty ObjectProperties, ObjectProperty ObjectProperty: PropertyName : Value PropertyName: (from ECMA-262-6, 11.1.5) StringLiteral NumberLiteral Identifier ArrayLiteral: [ ] [ Elision ] [ ArrayElements ] [ ArrayElements , ] [ ArrayElements , Elision ] ArrayElements: Value Elision Value ArrayElements , Value ArrayElements , Elision Value Elision: , Elision , Value: NullLiteral NumberLiteral BooleanLiteral StringLiteral ArrayLiteral ObjectLiteral IdentifierExpression ObjectQueryExpression AccessExpression: [ Value ] . Identifier AccessExpressions: AccessExpression AccessExpressions AccessExpression IdentifierExpression: Identifier Identifier AccessExpressions ObjectQueryExpression: Identifier ( StringLiteral ) Identifier ( StringLiteral ) AccessExpressions NOTE: We have factored the above grammar to allow the infrastructure to be used by the BindingInterpreter as well. The BaseInterpreter does NOT provide an implementation of _evaluateValue(), this is expected to be provided by the derived class since right now the two have different grammars for Value AccessExpression: [ Value ] . Identifier AccessExpressions: AccessExpression AccessExpressions AccessExpression Identifier: Identifier (from ECMA-262-6, 7.6) IdentifierExpression: Identifier Identifier AccessExpressions Value: *** Provided by concrete interpreter *** */ function illegal() { throw "Illegal"; } var imports = _Base.Namespace.defineWithParent(null, null, { lexer: _Base.Namespace._lazy(function () { return _OptionsLexer._optionsLexer; }), tokenType: _Base.Namespace._lazy(function () { return _OptionsLexer._optionsLexer.tokenType; }), }); var requireSupportedForProcessing = _BaseUtils.requireSupportedForProcessing; function tokenTypeName(type) { var keys = Object.keys(imports.tokenType); for (var i = 0, len = keys.length; i < len; i++) { if (type === imports.tokenType[keys[i]]) { return keys[i]; } } return ""; } var local = _Base.Namespace.defineWithParent(null, null, { BaseInterpreter: _Base.Namespace._lazy(function () { return _Base.Class.define(null, { _error: function (message) { throw new _ErrorFromName("WinJS.UI.ParseError", message); }, _currentOffset: function () { var p = this._pos; var offset = 0; for (var i = 0; i < p; i++) { offset += this._tokens[i].length; } return offset; }, _evaluateAccessExpression: function (value) { switch (this._current.type) { case imports.tokenType.dot: this._read(); switch (this._current.type) { case imports.tokenType.identifier: case this._current.keyword && this._current.type: var id = this._current.value; this._read(); return value[id]; default: this._unexpectedToken(imports.tokenType.identifier, imports.tokenType.reservedWord); break; } return; case imports.tokenType.leftBracket: this._read(); var index = this._evaluateValue(); this._read(imports.tokenType.rightBracket); return value[index]; // default: is unreachable because all the callers are conditional on // the next token being either a . or { // } }, _evaluateAccessExpressions: function (value) { while (true) { switch (this._current.type) { case imports.tokenType.dot: case imports.tokenType.leftBracket: value = this._evaluateAccessExpression(value); break; default: return value; } } }, _evaluateIdentifier: function (nested, value) { var id = this._readIdentifier(); value = nested ? value[id] : this._context[id]; return value; }, _evaluateIdentifierExpression: function () { var value = this._evaluateIdentifier(false); switch (this._current.type) { case imports.tokenType.dot: case imports.tokenType.leftBracket: return this._evaluateAccessExpressions(value); default: return value; } }, _initialize: function (tokens, originalSource, context, functionContext) { this._originalSource = originalSource; this._tokens = tokens; this._context = context; this._functionContext = functionContext; this._pos = 0; this._current = this._tokens[0]; }, _read: function (expected) { if (expected && this._current.type !== expected) { this._unexpectedToken(expected); } if (this._current !== imports.tokenType.eof) { this._current = this._tokens[++this._pos]; } }, _peek: function (expected) { if (expected && this._current.type !== expected) { return; } if (this._current !== imports.tokenType.eof) { return this._tokens[this._pos + 1]; } }, _readAccessExpression: function (parts) { switch (this._current.type) { case imports.tokenType.dot: this._read(); switch (this._current.type) { case imports.tokenType.identifier: case this._current.keyword && this._current.type: parts.push(this._current.value); this._read(); break; default: this._unexpectedToken(imports.tokenType.identifier, imports.tokenType.reservedWord); break; } return; case imports.tokenType.leftBracket: this._read(); parts.push(this._evaluateValue()); this._read(imports.tokenType.rightBracket); return; // default: is unreachable because all the callers are conditional on // the next token being either a . or { // } }, _readAccessExpressions: function (parts) { while (true) { switch (this._current.type) { case imports.tokenType.dot: case imports.tokenType.leftBracket: this._readAccessExpression(parts); break; default: return; } } }, _readIdentifier: function () { var id = this._current.value; this._read(imports.tokenType.identifier); return id; }, _readIdentifierExpression: function () { var parts = []; if (this._peek(imports.tokenType.thisKeyword) && parts.length === 0) { this._read(); } else { parts.push(this._readIdentifier()); } switch (this._current.type) { case imports.tokenType.dot: case imports.tokenType.leftBracket: this._readAccessExpressions(parts); break; } return parts; }, _unexpectedToken: function (expected) { var unexpected = (this._current.type === imports.tokenType.error ? "'" + this._current.value + "'" : tokenTypeName(this._current.type)); if (expected) { if (arguments.length === 1) { expected = tokenTypeName(expected); this._error(_Resources._formatString(strings.unexpectedTokenExpectedToken, unexpected, expected, this._currentOffset())); } else { var names = []; for (var i = 0, len = arguments.length; i < len; i++) { names.push(tokenTypeName(arguments[i])); } expected = names.join(", "); this._error(_Resources._formatString(strings.unexpectedTokenExpectedTokens, unexpected, expected, this._currentOffset())); } } else { this._error(_Resources._formatString(strings.unexpectedTokenGeneric, unexpected, this._currentOffset())); } } }, { supportedForProcessing: false, }); }), OptionsInterpreter: _Base.Namespace._lazy(function () { return _Base.Class.derive(local.BaseInterpreter, function (tokens, originalSource, context, functionContext) { this._initialize(tokens, originalSource, context, functionContext); }, { _error: function (message) { throw new _ErrorFromName("WinJS.UI.ParseError", _Resources._formatString(strings.invalidOptionsRecord, this._originalSource, message)); }, _evaluateArrayLiteral: function () { var a = []; this._read(imports.tokenType.leftBracket); this._readArrayElements(a); this._read(imports.tokenType.rightBracket); return a; }, _evaluateObjectLiteral: function () { var o = {}; this._read(imports.tokenType.leftBrace); this._readObjectProperties(o); this._tryReadComma(); this._read(imports.tokenType.rightBrace); return o; }, _evaluateOptionsLiteral: function () { var value = this._evaluateValue(); if (this._current.type !== imports.tokenType.eof) { this._unexpectedToken(imports.tokenType.eof); } return value; }, _peekValue: function () { switch (this._current.type) { case imports.tokenType.falseLiteral: case imports.tokenType.nullLiteral: case imports.tokenType.stringLiteral: case imports.tokenType.trueLiteral: case imports.tokenType.numberLiteral: case imports.tokenType.leftBrace: case imports.tokenType.leftBracket: case imports.tokenType.identifier: return true; default: return false; } }, _evaluateValue: function () { switch (this._current.type) { case imports.tokenType.falseLiteral: case imports.tokenType.nullLiteral: case imports.tokenType.stringLiteral: case imports.tokenType.trueLiteral: case imports.tokenType.numberLiteral: var value = this._current.value; this._read(); return value; case imports.tokenType.leftBrace: return this._evaluateObjectLiteral(); case imports.tokenType.leftBracket: return this._evaluateArrayLiteral(); case imports.tokenType.identifier: if (this._peek(imports.tokenType.identifier).type === imports.tokenType.leftParentheses) { return requireSupportedForProcessing(this._evaluateObjectQueryExpression()); } return requireSupportedForProcessing(this._evaluateIdentifierExpression()); default: this._unexpectedToken(imports.tokenType.falseLiteral, imports.tokenType.nullLiteral, imports.tokenType.stringLiteral, imports.tokenType.trueLiteral, imports.tokenType.numberLiteral, imports.tokenType.leftBrace, imports.tokenType.leftBracket, imports.tokenType.identifier); break; } }, _tryReadElement: function (a) { if (this._peekValue()) { a.push(this._evaluateValue()); return true; } else { return false; } }, _tryReadComma: function () { if (this._peek(imports.tokenType.comma)) { this._read(); return true; } return false; }, _tryReadElision: function (a) { var found = false; while (this._tryReadComma()) { a.push(undefined); found = true; } return found; }, _readArrayElements: function (a) { while (!this._peek(imports.tokenType.rightBracket)) { var elision = this._tryReadElision(a); var element = this._tryReadElement(a); var comma = this._peek(imports.tokenType.comma); if (element && comma) { // if we had a element followed by a comma, eat the comma and try to read the next element this._read(); } else if (element || elision) { // if we had a element without a trailing comma or if all we had were commas we're done break; } else { // if we didn't have a element or elision then we are done and in error this._unexpectedToken(imports.tokenType.falseLiteral, imports.tokenType.nullLiteral, imports.tokenType.stringLiteral, imports.tokenType.trueLiteral, imports.tokenType.numberLiteral, imports.tokenType.leftBrace, imports.tokenType.leftBracket, imports.tokenType.identifier); break; } } }, _readObjectProperties: function (o) { while (!this._peek(imports.tokenType.rightBrace)) { var property = this._tryReadObjectProperty(o); var comma = this._peek(imports.tokenType.comma); if (property && comma) { // if we had a property followed by a comma, eat the comma and try to read the next property this._read(); } else if (property) { // if we had a property without a trailing comma we're done break; } else { // if we didn't have a property then we are done and in error this._unexpectedToken(imports.tokenType.numberLiteral, imports.tokenType.stringLiteral, imports.tokenType.identifier); break; } } }, _tryReadObjectProperty: function (o) { switch (this._current.type) { case imports.tokenType.numberLiteral: case imports.tokenType.stringLiteral: case imports.tokenType.identifier: case this._current.keyword && this._current.type: var propertyName = this._current.value; this._read(); this._read(imports.tokenType.colon); o[propertyName] = this._evaluateValue(); return true; default: return false; } }, _failReadObjectProperty: function () { this._unexpectedToken(imports.tokenType.numberLiteral, imports.tokenType.stringLiteral, imports.tokenType.identifier, imports.tokenType.reservedWord); }, _evaluateObjectQueryExpression: function () { var functionName = this._current.value; this._read(imports.tokenType.identifier); this._read(imports.tokenType.leftParentheses); var queryExpression = this._current.value; this._read(imports.tokenType.stringLiteral); this._read(imports.tokenType.rightParentheses); var value = requireSupportedForProcessing(this._functionContext[functionName])(queryExpression); switch (this._current.type) { case imports.tokenType.dot: case imports.tokenType.leftBracket: return this._evaluateAccessExpressions(value); default: return value; } }, run: function () { return this._evaluateOptionsLiteral(); } }, { supportedForProcessing: false, }); }), OptionsParser: _Base.Namespace._lazy(function () { return _Base.Class.derive(local.OptionsInterpreter, function (tokens, originalSource) { this._initialize(tokens, originalSource); }, { // When parsing it is illegal to get to any of these "evaluate" RHS productions because // we will always instead go to the "read" version // _evaluateAccessExpression: illegal, _evaluateAccessExpressions: illegal, _evaluateIdentifier: illegal, _evaluateIdentifierExpression: illegal, _evaluateObjectQueryExpression: illegal, _evaluateValue: function () { switch (this._current.type) { case imports.tokenType.falseLiteral: case imports.tokenType.nullLiteral: case imports.tokenType.stringLiteral: case imports.tokenType.trueLiteral: case imports.tokenType.numberLiteral: var value = this._current.value; this._read(); return value; case imports.tokenType.leftBrace: return this._evaluateObjectLiteral(); case imports.tokenType.leftBracket: return this._evaluateArrayLiteral(); case imports.tokenType.identifier: if (this._peek(imports.tokenType.identifier).type === imports.tokenType.leftParentheses) { return this._readObjectQueryExpression(); } return this._readIdentifierExpression(); default: this._unexpectedToken(imports.tokenType.falseLiteral, imports.tokenType.nullLiteral, imports.tokenType.stringLiteral, imports.tokenType.trueLiteral, imports.tokenType.numberLiteral, imports.tokenType.leftBrace, imports.tokenType.leftBracket, imports.tokenType.identifier); break; } }, _readIdentifierExpression: function () { var parts = local.BaseInterpreter.prototype._readIdentifierExpression.call(this); return new IdentifierExpression(parts); }, _readObjectQueryExpression: function () { var functionName = this._current.value; this._read(imports.tokenType.identifier); this._read(imports.tokenType.leftParentheses); var queryExpressionLiteral = this._current.value; this._read(imports.tokenType.stringLiteral); this._read(imports.tokenType.rightParentheses); var call = new CallExpression(functionName, queryExpressionLiteral); switch (this._current.type) { case imports.tokenType.dot: case imports.tokenType.leftBracket: var parts = [call]; this._readAccessExpressions(parts); return new IdentifierExpression(parts); default: return call; } }, }, { supportedForProcessing: false, }); }) }); var parser = function (text, context, functionContext) { var tokens = imports.lexer(text); var interpreter = new local.OptionsInterpreter(tokens, text, context || {}, functionContext || {}); return interpreter.run(); }; Object.defineProperty(parser, "_BaseInterpreter", { get: function () { return local.BaseInterpreter; } }); var parser2 = function (text) { var tokens = imports.lexer(text); var parser = new local.OptionsParser(tokens, text); return parser.run(); }; // Consumers of parser2 need to be able to see the AST for RHS expression in order to emit // code representing these portions of the options record // var CallExpression = _Base.Class.define(function (target, arg0Value) { this.target = target; this.arg0Value = arg0Value; }); CallExpression.supportedForProcessing = false; var IdentifierExpression = _Base.Class.define(function (parts) { this.parts = parts; }); IdentifierExpression.supportedForProcessing = false; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { // This is the mis-named interpreter version of the options record processor. // optionsParser: parser, // This is the actual parser version of the options record processor. // _optionsParser: parser2, _CallExpression: CallExpression, _IdentifierExpression: IdentifierExpression, }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/ControlProcessor',[ 'exports', './Core/_Global', './Core/_Base', './Core/_BaseUtils', './Core/_Log', './Core/_Resources', './Core/_WriteProfilerMark', './ControlProcessor/_OptionsParser', './Promise', './Utilities/_ElementUtilities' ], function declarativeControlsInit(exports, _Global, _Base, _BaseUtils, _Log, _Resources, _WriteProfilerMark, _OptionsParser, Promise, _ElementUtilities) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } var strings = { get errorActivatingControl() { return "Error activating control: {0}"; }, }; var markSupportedForProcessing = _BaseUtils.markSupportedForProcessing; var requireSupportedForProcessing = _BaseUtils.requireSupportedForProcessing; var processedAllCalled = false; function createSelect(element) { var result = function select(selector) { /// /// /// Walks the DOM tree from the given element to the root of the document, whenever /// a selector scope is encountered select performs a lookup within that scope for /// the given selector string. The first matching element is returned. /// /// The selector string. /// The target element, if found. /// var current = element; var selected; while (current) { if (current.msParentSelectorScope) { var scope = current.parentNode; if (scope) { selected = _ElementUtilities._matchesSelector(scope, selector) ? scope : scope.querySelector(selector); if (selected) { break; } } } current = current.parentNode; } return selected || _Global.document.querySelector(selector); }; return markSupportedForProcessing(result); } function activate(element, Handler) { return new Promise(function activate2(complete, error) { try { var options; var optionsAttribute = element.getAttribute("data-win-options"); if (optionsAttribute) { options = _OptionsParser.optionsParser(optionsAttribute, _Global, { select: createSelect(element) }); } var ctl; var count = 1; // handler is required to call complete if it takes that parameter // if (Handler.length > 2) { count++; } var checkComplete = function checkComplete() { count--; if (count === 0) { element.winControl = element.winControl || ctl; complete(ctl); } }; // async exceptions from the handler get dropped on the floor... // ctl = new Handler(element, options, checkComplete); checkComplete(); } catch (err) { _Log.log && _Log.log(_Resources._formatString(strings.errorActivatingControl, err && err.message), "winjs controls", "error"); error(err); } }); } function processAllImpl(rootElement, skipRootElement) { return new Promise(function processAllImpl2(complete, error) { _WriteProfilerMark("WinJS.UI:processAll,StartTM"); rootElement = rootElement || _Global.document.body; var pending = 0; var selector = "[data-win-control]"; var allElements = rootElement.querySelectorAll(selector); var elements = []; if (!skipRootElement && getControlHandler(rootElement)) { elements.push(rootElement); } for (var i = 0, len = allElements.length; i < len; i++) { elements.push(allElements[i]); } // bail early if there is nothing to process // if (elements.length === 0) { _WriteProfilerMark("WinJS.UI:processAll,StopTM"); complete(rootElement); return; } var checkAllComplete = function () { pending = pending - 1; if (pending < 0) { _WriteProfilerMark("WinJS.UI:processAll,StopTM"); complete(rootElement); } }; // First go through and determine which elements to activate // var controls = new Array(elements.length); for (var i = 0, len = elements.length; i < len; i++) { var element = elements[i]; var control; var instance = element.winControl; if (instance) { control = instance.constructor; // already activated, don't need to add to controls array } else { controls[i] = control = getControlHandler(element); } if (control && control.isDeclarativeControlContainer) { i += element.querySelectorAll(selector).length; } } // Now go through and activate those // _WriteProfilerMark("WinJS.UI:processAllActivateControls,StartTM"); for (var i = 0, len = elements.length; i < len; i++) { var ctl = controls[i]; var element = elements[i]; if (ctl && !element.winControl) { pending++; activate(element, ctl).then(checkAllComplete, function (e) { _WriteProfilerMark("WinJS.UI:processAll,StopTM"); error(e); }); if (ctl.isDeclarativeControlContainer && typeof ctl.isDeclarativeControlContainer === "function") { var idcc = requireSupportedForProcessing(ctl.isDeclarativeControlContainer); idcc(element.winControl, processAll); } } } _WriteProfilerMark("WinJS.UI:processAllActivateControls,StopTM"); checkAllComplete(); }); } function getControlHandler(element) { if (element.getAttribute) { var evaluator = element.getAttribute("data-win-control"); if (evaluator) { return _BaseUtils._getMemberFiltered(evaluator.trim(), _Global, requireSupportedForProcessing); } } } function scopedSelect(selector, element) { /// /// /// Walks the DOM tree from the given element to the root of the document, whenever /// a selector scope is encountered select performs a lookup within that scope for /// the given selector string. The first matching element is returned. /// /// The selector string. /// The target element, if found. /// return createSelect(element)(selector); } function processAll(rootElement, skipRoot) { /// /// /// Applies declarative control binding to all elements, starting at the specified root element. /// /// /// The element at which to start applying the binding. If this parameter is not specified, the binding is applied to the entire document. /// /// /// If true, the elements to be bound skip the specified root element and include only the children. /// /// /// A promise that is fulfilled when binding has been applied to all the controls. /// /// if (!processedAllCalled) { return _BaseUtils.ready().then(function () { processedAllCalled = true; return processAllImpl(rootElement, skipRoot); }); } else { return processAllImpl(rootElement, skipRoot); } } function process(element) { /// /// /// Applies declarative control binding to the specified element. /// /// /// The element to bind. /// /// /// A promise that is fulfilled after the control is activated. The value of the /// promise is the control that is attached to element. /// /// if (element && element.winControl) { return Promise.as(element.winControl); } var handler = getControlHandler(element); if (!handler) { return Promise.as(); // undefined, no handler } else { return activate(element, handler); } } _Base.Namespace._moduleDefine(exports, "WinJS.UI", { scopedSelect: scopedSelect, processAll: processAll, process: process }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_ElementListUtilities',[ 'exports', '../Core/_Global', '../Core/_Base', '../ControlProcessor', '../Promise', '../Utilities/_Control', '../Utilities/_ElementUtilities' ], function elementListUtilities(exports, _Global, _Base, ControlProcessor, Promise, _Control, _ElementUtilities) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } _Base.Namespace._moduleDefine(exports, "WinJS.Utilities", { QueryCollection: _Base.Class.derive(Array, function (items) { /// /// /// Represents the result of a query selector, and provides /// various operations that perform actions over the elements of /// the collection. /// /// /// The items resulting from the query. /// /// if (items) { this.include(items); } }, { forEach: function (callbackFn, thisArg) { /// /// /// Performs an action on each item in the QueryCollection /// /// /// Action to perform on each item. /// /// /// Argument to bind to callbackFn /// /// /// Returns the QueryCollection /// /// Array.prototype.forEach.apply(this, [callbackFn, thisArg]); return this; }, get: function (index) { /// /// /// Gets an item from the QueryCollection. /// /// /// The index of the item to return. /// /// /// A single item from the collection. /// /// return this[index]; }, setAttribute: function (name, value) { /// /// /// Sets an attribute value on all the items in the collection. /// /// /// The name of the attribute to be set. /// /// /// The value of the attribute to be set. /// /// /// This QueryCollection object. /// /// this.forEach(function (item) { item.setAttribute(name, value); }); return this; }, getAttribute: function (name) { /// /// /// Gets an attribute value from the first element in the collection. /// /// /// The name of the attribute. /// /// /// The value of the attribute. /// /// if (this.length > 0) { return this[0].getAttribute(name); } }, addClass: function (name) { /// /// /// Adds the specified class to all the elements in the collection. /// /// /// The name of the class to add. /// /// /// This QueryCollection object. /// /// this.forEach(function (item) { _ElementUtilities.addClass(item, name); }); return this; }, hasClass: function (name) { /// /// /// Determines whether the specified class exists on the first element of the collection. /// /// /// The name of the class. /// /// /// true if the element has the specified class; otherwise, false. /// /// if (this.length > 0) { return _ElementUtilities.hasClass(this[0], name); } return false; }, removeClass: function (name) { /// /// /// Removes the specified class from all the elements in the collection. /// /// /// The name of the class to be removed. /// /// /// This QueryCollection object. /// /// this.forEach(function (item) { _ElementUtilities.removeClass(item, name); }); return this; }, toggleClass: function (name) { /// /// /// Toggles (adds or removes) the specified class on all the elements in the collection. /// If the class is present, it is removed; if it is absent, it is added. /// /// /// The name of the class to be toggled. /// /// /// This QueryCollection object. /// /// this.forEach(function (item) { _ElementUtilities.toggleClass(item, name); }); return this; }, listen: function (eventType, listener, capture) { /// /// /// Registers the listener for the specified event on all the elements in the collection. /// /// /// The name of the event. /// /// /// The event handler function to be called when the event occurs. /// /// /// true if capture == true is to be passed to addEventListener; otherwise, false. /// /// /// This QueryCollection object. /// /// this.forEach(function (item) { item.addEventListener(eventType, listener, capture); }); return this; }, removeEventListener: function (eventType, listener, capture) { /// /// /// Unregisters the listener for the specified event on all the elements in the collection. /// /// /// The name of the event. /// /// /// The event handler function. /// /// /// true if capture == true; otherwise, false. /// /// /// This QueryCollection object. /// /// this.forEach(function (item) { item.removeEventListener(eventType, listener, capture); }); return this; }, setStyle: function (name, value) { /// /// /// Sets the specified style property for all the elements in the collection. /// /// /// The name of the style property. /// /// /// The value for the property. /// /// /// This QueryCollection object. /// /// this.forEach(function (item) { item.style[name] = value; }); return this; }, clearStyle: function (name) { /// /// /// Clears the specified style property for all the elements in the collection. /// /// /// The name of the style property to be cleared. /// /// /// This QueryCollection object. /// /// this.forEach(function (item) { item.style[name] = ""; }); return this; }, query: function (query) { /// /// /// Executes a query selector on all the elements in the collection /// and aggregates the result into a QueryCollection. /// /// /// The query selector string. /// /// /// A QueryCollection object containing the aggregate results of /// executing the query on all the elements in the collection. /// /// var newCollection = new exports.QueryCollection(); this.forEach(function (item) { newCollection.include(item.querySelectorAll(query)); }); return newCollection; }, include: function (items) { /// /// /// Adds a set of items to this QueryCollection. /// /// /// The items to add to the QueryCollection. This may be an /// array-like object, a document fragment, or a single item. /// /// if (typeof items.length === "number") { for (var i = 0; i < items.length; i++) { this.push(items[i]); } } else if (items.DOCUMENT_FRAGMENT_NODE && items.nodeType === items.DOCUMENT_FRAGMENT_NODE) { this.include(items.childNodes); } else { this.push(items); } }, control: function (Ctor, options) { /// /// /// Creates controls that are attached to the elements in this QueryCollection. /// /// /// A constructor function that is used to create controls to attach to the elements. /// /// /// The options passed to the newly-created controls. /// /// /// This QueryCollection object. /// /// /// /// /// Configures the controls that are attached to the elements in this QueryCollection. /// /// /// The options passed to the controls. /// /// /// This QueryCollection object. /// /// if (Ctor && typeof (Ctor) === "function") { this.forEach(function (element) { element.winControl = new Ctor(element, options); }); } else { options = Ctor; this.forEach(function (element) { ControlProcessor.process(element).done(function (control) { control && _Control.setOptions(control, options); }); }); } return this; }, template: function (templateElement, data, renderDonePromiseCallback) { /// /// /// Renders a template that is bound to the given data /// and parented to the elements included in the QueryCollection. /// If the QueryCollection contains multiple elements, the template /// is rendered multiple times, once at each element in the QueryCollection /// per item of data passed. /// /// /// The DOM element to which the template control is attached to. /// /// /// The data to render. If the data is an array (or any other object /// that has a forEach method) then the template is rendered /// multiple times, once for each item in the collection. /// /// /// If supplied, this function is called /// each time the template gets rendered, and is passed a promise /// that is fulfilled when the template rendering is complete. /// /// /// The QueryCollection. /// /// if (templateElement instanceof exports.QueryCollection) { templateElement = templateElement[0]; } var template = templateElement.winControl; if (data === null || data === undefined || !data.forEach) { data = [data]; } renderDonePromiseCallback = renderDonePromiseCallback || function () { }; var that = this; var donePromises = []; data.forEach(function (datum) { that.forEach(function (element) { donePromises.push(template.render(datum, element)); }); }); renderDonePromiseCallback(Promise.join(donePromises)); return this; } }, { supportedForProcessing: false, }), query: function (query, element) { /// /// /// Executes a query selector on the specified element or the entire document. /// /// /// The query selector to be executed. /// /// /// The element on which to execute the query. If this parameter is not specified, the /// query is executed on the entire document. /// /// /// The QueryCollection that contains the results of the query. /// /// return new exports.QueryCollection((element || _Global.document).querySelectorAll(query)); }, id: function (id) { /// /// /// Looks up an element by ID and wraps the result in a QueryCollection. /// /// /// The ID of the element. /// /// /// A QueryCollection that contains the element, if it is found. /// /// var e = _Global.document.getElementById(id); return new exports.QueryCollection(e ? [e] : []); }, children: function (element) { /// /// /// Creates a QueryCollection that contains the children of the specified parent element. /// /// /// The parent element. /// /// /// The QueryCollection that contains the children of the element. /// /// return new exports.QueryCollection(element.children); } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_Hoverable',[ 'exports', '../Core/_Global' ], function hoverable(exports, _Global) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } _Global.document.documentElement.classList.add("win-hoverable"); exports.isHoverable = true; if (!_Global.MSPointerEvent) { var touchStartHandler = function () { _Global.document.removeEventListener("touchstart", touchStartHandler); // Remove win-hoverable CSS class fromstartt . to avoid :hover styles in webkit when there is // touch support. _Global.document.documentElement.classList.remove("win-hoverable"); exports.isHoverable = false; }; _Global.document.addEventListener("touchstart", touchStartHandler); } }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_ParallelWorkQueue',[ 'exports', '../Core/_Base', '../Promise', '../Scheduler' ], function parallelWorkQueueInit(exports, _Base, Promise, Scheduler) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _ParallelWorkQueue : _Base.Namespace._lazy(function () { return _Base.Class.define(function ParallelWorkQueue_ctor(maxRunning) { var workIndex = 0; var workItems = {}; var workQueue = []; maxRunning = maxRunning || 3; var running = 0; var processing = 0; function runNext() { running--; // if we have fallen out of this loop, then we know we are already // async, so "post" is OK. If we are still in the loop, then the // loop will continue to run, so we don't need to "post" or // recurse. This avoids stack overflow in the sync case. // if (!processing) { Scheduler.schedule(run, Scheduler.Priority.normal, null, "WinJS._ParallelWorkQueue.runNext"); } } function run() { processing++; for (; running < maxRunning; running++) { var next; var nextWork; do { next = workQueue.shift(); nextWork = next && workItems[next]; } while (next && !nextWork); if (nextWork) { delete workItems[next]; try { nextWork().then(runNext, runNext); } catch (err) { // this will only get hit if there is a queued item that // fails to return something that conforms to the Promise // contract // runNext(); } } else { break; } } processing--; } function queue(f, data, first) { var id = "w" + (workIndex++); var workPromise; return new Promise( function (c, e, p) { var w = function () { workPromise = f().then(c, e, p); return workPromise; }; w.data = data; workItems[id] = w; if (first) { workQueue.unshift(id); } else { workQueue.push(id); } run(); }, function () { delete workItems[id]; if (workPromise) { workPromise.cancel(); } } ); } this.sort = function (f) { workQueue.sort(function (a, b) { a = workItems[a]; b = workItems[b]; return a === undefined && b === undefined ? 0 : a === undefined ? 1 : b === undefined ? -1 : f(a.data, b.data); }); }; this.queue = queue; }, { /* empty */ }, { supportedForProcessing: false, }); }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_VersionManager',[ 'exports', '../Core/_Base', '../_Signal' ], function versionManagerInit(exports, _Base, _Signal) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _VersionManager: _Base.Namespace._lazy(function () { return _Base.Class.define(function _VersionManager_ctor() { this._unlocked = new _Signal(); this._unlocked.complete(); }, { _cancelCount: 0, _notificationCount: 0, _updateCount: 0, _version: 0, // This should be used generally for all logic that should be suspended while data changes are happening // locked: { get: function () { return this._notificationCount !== 0 || this._updateCount !== 0; } }, // this should only be accessed by the update logic in ListViewImpl.js // noOutstandingNotifications: { get: function () { return this._notificationCount === 0; } }, version: { get: function () { return this._version; } }, unlocked: { get: function () { return this._unlocked.promise; } }, _dispose: function () { if (this._unlocked) { this._unlocked.cancel(); this._unlocked = null; } }, beginUpdating: function () { this._checkLocked(); this._updateCount++; }, endUpdating: function () { this._updateCount--; this._checkUnlocked(); }, beginNotifications: function () { this._checkLocked(); this._notificationCount++; }, endNotifications: function () { this._notificationCount--; this._checkUnlocked(); }, _checkLocked: function () { if (!this.locked) { this._dispose(); this._unlocked = new _Signal(); } }, _checkUnlocked: function () { if (!this.locked) { this._unlocked.complete(); } }, receivedNotification: function () { this._version++; if (this._cancel) { var cancel = this._cancel; this._cancel = null; cancel.forEach(function (p) { p && p.cancel(); }); } }, cancelOnNotification: function (promise) { if (!this._cancel) { this._cancel = []; this._cancelCount = 0; } this._cancel[this._cancelCount++] = promise; return this._cancelCount - 1; }, clearCancelOnNotification: function (token) { if (this._cancel) { delete this._cancel[token]; } } }, { supportedForProcessing: false, }); }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Items Manager define('WinJS/Utilities/_ItemsManager',[ 'exports', '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Resources', '../Core/_WriteProfilerMark', '../Promise', '../_Signal', '../Scheduler', '../Utilities/_ElementUtilities', './_ParallelWorkQueue', './_VersionManager' ], function itemsManagerInit(exports, _Global, _Base, _BaseUtils, _ErrorFromName, _Resources, _WriteProfilerMark, Promise, _Signal, Scheduler, _ElementUtilities, _ParallelWorkQueue, _VersionManager) { "use strict"; var markSupportedForProcessing = _BaseUtils.markSupportedForProcessing; var uniqueID = _ElementUtilities._uniqueID; function simpleItemRenderer(f) { return markSupportedForProcessing(function (itemPromise, element) { return itemPromise.then(function (item) { return (item ? f(item, element) : null); }); }); } var trivialHtmlRenderer = simpleItemRenderer(function (item) { if (_ElementUtilities._isDOMElement(item.data)) { return item.data; } var data = item.data; if (data === undefined) { data = "undefined"; } else if (data === null) { data = "null"; } else if (typeof data === "object") { data = JSON.stringify(data); } var element = _Global.document.createElement("span"); element.textContent = data.toString(); return element; }); _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _normalizeRendererReturn: function (v) { if (v) { if (typeof v === "object" && v.element) { var elementPromise = Promise.as(v.element); return elementPromise.then(function (e) { return { element: e, renderComplete: Promise.as(v.renderComplete) }; }); } else { var elementPromise = Promise.as(v); return elementPromise.then(function (e) { return { element: e, renderComplete: Promise.as() }; }); } } else { return { element: null, renderComplete: Promise.as() }; } }, simpleItemRenderer: simpleItemRenderer, _trivialHtmlRenderer: trivialHtmlRenderer }); // Private statics var strings = { get listDataSourceIsInvalid() { return "Invalid argument: dataSource must be an object."; }, get itemRendererIsInvalid() { return "Invalid argument: itemRenderer must be a function."; }, get itemIsInvalid() { return "Invalid argument: item must be a DOM element that was returned by the Items Manager, and has not been replaced or released."; }, }; var imageLoader; var lastSort = new Date(); var minDurationBetweenImageSort = 64; // This optimization is good for a couple of reasons: // - It is a global optimizer, which means that all on screen images take precedence over all off screen images. // - It avoids resorting too frequently by only resorting when a new image loads and it has been at least 64 ms since // the last sort. // Also, it is worth noting that "sort" on an empty queue does no work (besides the function call). function compareImageLoadPriority(a, b) { var aon = false; var bon = false; // Currently isOnScreen is synchronous and fast for list view a.isOnScreen().then(function (v) { aon = v; }); b.isOnScreen().then(function (v) { bon = v; }); return (aon ? 0 : 1) - (bon ? 0 : 1); } var nextImageLoaderId = 0; var seenUrls = {}; var seenUrlsMRU = []; var SEEN_URLS_MAXSIZE = 250; var SEEN_URLS_MRU_MAXSIZE = 1000; function seenUrl(srcUrl) { if ((/^blob:/i).test(srcUrl)) { return; } seenUrls[srcUrl] = true; seenUrlsMRU.push(srcUrl); if (seenUrlsMRU.length > SEEN_URLS_MRU_MAXSIZE) { var mru = seenUrlsMRU; seenUrls = {}; seenUrlsMRU = []; for (var count = 0, i = mru.length - 1; i >= 0 && count < SEEN_URLS_MAXSIZE; i--) { var url = mru[i]; if (!seenUrls[url]) { seenUrls[url] = true; count++; } } } } // Exposing the seenUrl related members to use them in unit tests _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _seenUrl: seenUrl, _getSeenUrls: function () { return seenUrls; }, _getSeenUrlsMRU: function () { return seenUrlsMRU; }, _seenUrlsMaxSize: SEEN_URLS_MAXSIZE, _seenUrlsMRUMaxSize: SEEN_URLS_MRU_MAXSIZE }); function loadImage(srcUrl, image, data) { var imageId = nextImageLoaderId++; imageLoader = imageLoader || new _ParallelWorkQueue._ParallelWorkQueue(6); return imageLoader.queue(function () { return new Promise(function (c, e) { Scheduler.schedule(function ImageLoader_async_loadImage(jobInfo) { if (!image) { image = _Global.document.createElement("img"); } var seen = seenUrls[srcUrl]; if (!seen) { jobInfo.setPromise(new Promise(function (imageLoadComplete) { var tempImage = _Global.document.createElement("img"); var cleanup = function () { tempImage.removeEventListener("load", loadComplete, false); tempImage.removeEventListener("error", loadError, false); // One time use blob images are cleaned up as soon as they are not referenced by images any longer. // We set the image src before clearing the tempImage src to make sure the blob image is always // referenced. image.src = srcUrl; var currentDate = new Date(); if (currentDate - lastSort > minDurationBetweenImageSort) { lastSort = currentDate; imageLoader.sort(compareImageLoadPriority); } }; var loadComplete = function () { imageLoadComplete(jobComplete); }; var loadError = function () { imageLoadComplete(jobError); }; var jobComplete = function () { seenUrl(srcUrl); cleanup(); c(image); }; var jobError = function () { cleanup(); e(image); }; tempImage.addEventListener("load", loadComplete, false); tempImage.addEventListener("error", loadError, false); tempImage.src = srcUrl; })); } else { seenUrl(srcUrl); image.src = srcUrl; c(image); } }, Scheduler.Priority.normal, null, "WinJS.UI._ImageLoader._image" + imageId); }); }, data); } function isImageCached(srcUrl) { return seenUrls[srcUrl]; } function defaultRenderer() { return _Global.document.createElement("div"); } // Public definitions _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _createItemsManager: _Base.Namespace._lazy(function () { var ListNotificationHandler = _Base.Class.define(function ListNotificationHandler_ctor(itemsManager) { // Constructor this._itemsManager = itemsManager; }, { // Public methods beginNotifications: function () { this._itemsManager._versionManager.beginNotifications(); this._itemsManager._beginNotifications(); }, // itemAvailable: not implemented inserted: function (itemPromise, previousHandle, nextHandle) { this._itemsManager._versionManager.receivedNotification(); this._itemsManager._inserted(itemPromise, previousHandle, nextHandle); }, changed: function (newItem, oldItem) { this._itemsManager._versionManager.receivedNotification(); this._itemsManager._changed(newItem, oldItem); }, moved: function (itemPromise, previousHandle, nextHandle) { this._itemsManager._versionManager.receivedNotification(); this._itemsManager._moved(itemPromise, previousHandle, nextHandle); }, removed: function (handle, mirage) { this._itemsManager._versionManager.receivedNotification(); this._itemsManager._removed(handle, mirage); }, countChanged: function (newCount, oldCount) { this._itemsManager._versionManager.receivedNotification(); this._itemsManager._countChanged(newCount, oldCount); }, indexChanged: function (handle, newIndex, oldIndex) { this._itemsManager._versionManager.receivedNotification(); this._itemsManager._indexChanged(handle, newIndex, oldIndex); }, affectedRange: function (range) { this._itemsManager._versionManager.receivedNotification(); this._itemsManager._affectedRange(range); }, endNotifications: function () { this._itemsManager._versionManager.endNotifications(); this._itemsManager._endNotifications(); }, reload: function () { this._itemsManager._versionManager.receivedNotification(); this._itemsManager._reload(); } }, { // Static Members supportedForProcessing: false, }); var ItemsManager = _Base.Class.define(function ItemsManager_ctor(listDataSource, itemRenderer, elementNotificationHandler, options) { // Constructor if (!listDataSource) { throw new _ErrorFromName("WinJS.UI.ItemsManager.ListDataSourceIsInvalid", strings.listDataSourceIsInvalid); } if (!itemRenderer) { throw new _ErrorFromName("WinJS.UI.ItemsManager.ItemRendererIsInvalid", strings.itemRendererIsInvalid); } this.$pipeline_callbacksMap = {}; this._listDataSource = listDataSource; this.dataSource = this._listDataSource; this._elementNotificationHandler = elementNotificationHandler; this._listBinding = this._listDataSource.createListBinding(new ListNotificationHandler(this)); if (options) { if (options.ownerElement) { this._ownerElement = options.ownerElement; } this._profilerId = options.profilerId; this._versionManager = options.versionManager || new _VersionManager._VersionManager(); } this._indexInView = options && options.indexInView; this._itemRenderer = itemRenderer; this._viewCallsReady = options && options.viewCallsReady; // Map of (the uniqueIDs of) elements to records for items this._elementMap = {}; // Map of handles to records for items this._handleMap = {}; // Owner for use with jobs on the scheduler. Allows for easy cancellation of jobs during clean up. this._jobOwner = Scheduler.createOwnerToken(); // Boolean to track whether endNotifications needs to be called on the ElementNotificationHandler this._notificationsSent = false; // Only enable the lastItem method if the data source implements the itemsFromEnd method if (this._listBinding.last) { this.lastItem = function () { return this._elementForItem(this._listBinding.last()); }; } }, { _itemFromItemPromise: function (itemPromise) { return this._waitForElement(this._elementForItem(itemPromise)); }, // If stage 0 is not yet complete, caller is responsible for transitioning the item from stage 0 to stage 1 _itemFromItemPromiseThrottled: function (itemPromise) { return this._waitForElement(this._elementForItem(itemPromise, true)); }, _itemAtIndex: function (index) { var itemPromise = this._itemPromiseAtIndex(index); this._itemFromItemPromise(itemPromise).then(null, function (e) { itemPromise.cancel(); return Promise.wrapError(e); }); }, _itemPromiseAtIndex: function (index) { return this._listBinding.fromIndex(index); }, _waitForElement: function (possiblePlaceholder) { var that = this; return new Promise(function (c) { if (possiblePlaceholder) { if (!that.isPlaceholder(possiblePlaceholder)) { c(possiblePlaceholder); } else { var placeholderID = uniqueID(possiblePlaceholder); var callbacks = that.$pipeline_callbacksMap[placeholderID]; if (!callbacks) { that.$pipeline_callbacksMap[placeholderID] = [c]; } else { callbacks.push(c); } } } else { c(possiblePlaceholder); } }); }, _updateElement: function (newElement, oldElement) { var placeholderID = uniqueID(oldElement); var callbacks = this.$pipeline_callbacksMap[placeholderID]; if (callbacks) { delete this.$pipeline_callbacksMap[placeholderID]; callbacks.forEach(function (c) { c(newElement); }); } }, _firstItem: function () { return this._waitForElement(this._elementForItem(this._listBinding.first())); }, _lastItem: function () { return this._waitForElement(this._elementForItem(this._listBinding.last())); }, _previousItem: function (element) { this._listBinding.jumpToItem(this._itemFromElement(element)); return this._waitForElement(this._elementForItem(this._listBinding.previous())); }, _nextItem: function (element) { this._listBinding.jumpToItem(this._itemFromElement(element)); return this._waitForElement(this._elementForItem(this._listBinding.next())); }, _itemFromPromise: function (itemPromise) { return this._waitForElement(this._elementForItem(itemPromise)); }, isPlaceholder: function (item) { return !!this._recordFromElement(item).elementIsPlaceholder; }, itemObject: function (element) { return this._itemFromElement(element); }, release: function () { this._listBinding.release(); this._elementNotificationHandler = null; this._listBinding = null; this._jobOwner.cancelAll(); this._released = true; }, releaseItemPromise: function (itemPromise) { var handle = itemPromise.handle; var record = this._handleMap[handle]; if (!record) { // The item promise is not in our handle map so we didn't even try to render it yet. itemPromise.cancel(); } else { this._releaseRecord(record); } }, releaseItem: function (element) { var record = this._elementMap[uniqueID(element)]; this._releaseRecord(record); }, _releaseRecord: function (record) { if (!record) { return; } if (record.renderPromise) { record.renderPromise.cancel(); } if (record.itemPromise) { record.itemPromise.cancel(); } if (record.imagePromises) { record.imagePromises.forEach(function (promise) { promise.cancel(); }); } if (record.itemReadyPromise) { record.itemReadyPromise.cancel(); } if (record.renderComplete) { record.renderComplete.cancel(); } this._removeEntryFromElementMap(record.element); this._removeEntryFromHandleMap(record.itemPromise.handle, record); if (record.item) { this._listBinding.releaseItem(record.item); } }, refresh: function () { return this._listDataSource.invalidateAll(); }, // Private members _handlerToNotifyCaresAboutItemAvailable: function () { return !!(this._elementNotificationHandler && this._elementNotificationHandler.itemAvailable); }, _handlerToNotify: function () { if (!this._notificationsSent) { this._notificationsSent = true; if (this._elementNotificationHandler && this._elementNotificationHandler.beginNotifications) { this._elementNotificationHandler.beginNotifications(); } } return this._elementNotificationHandler; }, _defineIndexProperty: function (itemForRenderer, item, record) { record.indexObserved = false; Object.defineProperty(itemForRenderer, "index", { get: function () { record.indexObserved = true; return item.index; } }); }, _renderPlaceholder: function (record) { var itemForRenderer = {}; var elementPlaceholder = defaultRenderer(itemForRenderer); record.elementIsPlaceholder = true; return elementPlaceholder; }, _renderItem: function (itemPromise, record, callerThrottlesStage1) { var that = this; var indexInView = that._indexInView || function () { return true; }; var stage1Signal = new _Signal(); var readySignal = new _Signal(); var perfItemPromiseId = "_renderItem(" + record.item.index + "):itemPromise"; var stage0RunningSync = true; var stage0Ran = false; itemPromise.then(function (item) { stage0Ran = true; if (stage0RunningSync) { stage1Signal.complete(item); } }); stage0RunningSync = false; var itemForRendererPromise = stage1Signal.promise.then(function (item) { if (item) { var itemForRenderer = Object.create(item); // Derive a new item and override its index property, to track whether it is read that._defineIndexProperty(itemForRenderer, item, record); itemForRenderer.ready = readySignal.promise; itemForRenderer.isOnScreen = function () { return Promise.wrap(indexInView(item.index)); }; itemForRenderer.loadImage = function (srcUrl, image) { var loadImagePromise = loadImage(srcUrl, image, itemForRenderer); if (record.imagePromises) { record.imagePromises.push(loadImagePromise); } else { record.imagePromises = [loadImagePromise]; } return loadImagePromise; }; itemForRenderer.isImageCached = isImageCached; return itemForRenderer; } else { return Promise.cancel; } }); function queueAsyncStage1() { itemPromise.then(function (item) { that._writeProfilerMark(perfItemPromiseId + ",StartTM"); stage1Signal.complete(item); that._writeProfilerMark(perfItemPromiseId + ",StopTM"); }); } if (!stage0Ran) { if (callerThrottlesStage1) { record.stage0 = itemPromise; record.startStage1 = function () { record.startStage1 = null; queueAsyncStage1(); }; } else { queueAsyncStage1(); } } itemForRendererPromise.handle = itemPromise.handle; record.itemPromise = itemForRendererPromise; record.itemReadyPromise = readySignal.promise; record.readyComplete = false; // perfRendererWorkId = stage 1 rendering (if itemPromise is async) or stage 1+2 (if itemPromise is sync and ran inline) // perfItemPromiseId = stage 2 rendering only (should only be emitted if itemPromise was async) // perfItemReadyId = stage 3 rendering var perfRendererWorkId = "_renderItem(" + record.item.index + (stage0Ran ? "):syncItemPromise" : "):placeholder"); var perfItemReadyId = "_renderItem(" + record.item.index + "):itemReady"; this._writeProfilerMark(perfRendererWorkId + ",StartTM"); var rendererPromise = Promise.as(that._itemRenderer(itemForRendererPromise, record.element)). then(exports._normalizeRendererReturn). then(function (v) { if (that._released) { return Promise.cancel; } itemForRendererPromise.then(function (item) { // Store pending ready callback off record so ScrollView can call it during realizePage. Otherwise // call it ourselves. record.pendingReady = function () { if (record.pendingReady) { record.pendingReady = null; record.readyComplete = true; that._writeProfilerMark(perfItemReadyId + ",StartTM"); readySignal.complete(item); that._writeProfilerMark(perfItemReadyId + ",StopTM"); } }; if (!that._viewCallsReady) { var job = Scheduler.schedule(record.pendingReady, Scheduler.Priority.normal, record, "WinJS.UI._ItemsManager._pendingReady"); job.owner = that._jobOwner; } }); return v; }); this._writeProfilerMark(perfRendererWorkId + ",StopTM"); return rendererPromise; }, _replaceElement: function (record, elementNew) { this._removeEntryFromElementMap(record.element); record.element = elementNew; this._addEntryToElementMap(elementNew, record); }, _changeElement: function (record, elementNew, elementNewIsPlaceholder) { record.renderPromise = null; var elementOld = record.element, itemOld = record.item; if (record.newItem) { record.item = record.newItem; record.newItem = null; } this._replaceElement(record, elementNew); if (record.item && record.elementIsPlaceholder && !elementNewIsPlaceholder) { record.elementDelayed = null; record.elementIsPlaceholder = false; this._updateElement(record.element, elementOld); if (this._handlerToNotifyCaresAboutItemAvailable()) { this._handlerToNotify().itemAvailable(record.element, elementOld); } } else { this._handlerToNotify().changed(elementNew, elementOld, itemOld); } }, _elementForItem: function (itemPromise, callerThrottlesStage1) { var handle = itemPromise.handle, record = this._recordFromHandle(handle, true), element; if (!handle) { return null; } if (record) { element = record.element; } else { // Create a new record for this item record = { item: itemPromise, itemPromise: itemPromise }; this._addEntryToHandleMap(handle, record); var that = this; var mirage = false; var synchronous = false; var renderPromise = that._renderItem(itemPromise, record, callerThrottlesStage1). then(function (v) { var elementNew = v.element; record.renderComplete = v.renderComplete; itemPromise.then(function (item) { record.item = item; if (!item) { mirage = true; element = null; } }); synchronous = true; record.renderPromise = null; if (elementNew) { if (element) { that._presentElements(record, elementNew); } else { element = elementNew; } } }); if (!mirage) { if (!synchronous) { record.renderPromise = renderPromise; } if (!element) { element = this._renderPlaceholder(record); } record.element = element; this._addEntryToElementMap(element, record); itemPromise.retain(); } } return element; }, _addEntryToElementMap: function (element, record) { this._elementMap[uniqueID(element)] = record; }, _removeEntryFromElementMap: function (element) { delete this._elementMap[uniqueID(element)]; }, _recordFromElement: function (element) { var record = this._elementMap[uniqueID(element)]; if (!record) { this._writeProfilerMark("_recordFromElement:ItemIsInvalidError,info"); throw new _ErrorFromName("WinJS.UI.ItemsManager.ItemIsInvalid", strings.itemIsInvalid); } return record; }, _addEntryToHandleMap: function (handle, record) { this._handleMap[handle] = record; }, _removeEntryFromHandleMap: function (handle) { delete this._handleMap[handle]; }, _handleInHandleMap: function (handle) { return !!this._handleMap[handle]; }, _recordFromHandle: function (handle, ignoreFailure) { var record = this._handleMap[handle]; if (!record && !ignoreFailure) { throw new _ErrorFromName("WinJS.UI.ItemsManager.ItemIsInvalid", strings.itemIsInvalid); } return record; }, _foreachRecord: function (callback) { var records = this._handleMap; for (var property in records) { var record = records[property]; callback(record); } }, _itemFromElement: function (element) { return this._recordFromElement(element).item; }, _elementFromHandle: function (handle) { if (handle) { var record = this._recordFromHandle(handle, true); if (record && record.element) { return record.element; } } return null; }, _inserted: function (itemPromise, previousHandle, nextHandle) { this._handlerToNotify().inserted(itemPromise, previousHandle, nextHandle); }, _changed: function (newItem, oldItem) { if (!this._handleInHandleMap(oldItem.handle)) { return; } var record = this._recordFromHandle(oldItem.handle); if (record.renderPromise) { record.renderPromise.cancel(); } if (record.itemPromise) { record.itemPromise.cancel(); } if (record.imagePromises) { record.imagePromises.forEach(function (promise) { promise.cancel(); }); } if (record.itemReadyPromise) { record.itemReadyPromise.cancel(); } if (record.renderComplete) { record.renderComplete.cancel(); } record.newItem = newItem; var that = this; var newItemPromise = Promise.as(newItem); newItemPromise.handle = record.itemPromise.handle; record.renderPromise = this._renderItem(newItemPromise, record). then(function (v) { record.renderComplete = v.renderComplete; that._changeElement(record, v.element, false); that._presentElements(record); }); }, _moved: function (itemPromise, previousHandle, nextHandle) { // no check for haveHandle, as we get move notification for items we // are "next" to, so we handle the "null element" cases below // var element = this._elementFromHandle(itemPromise.handle); var previous = this._elementFromHandle(previousHandle); var next = this._elementFromHandle(nextHandle); this._handlerToNotify().moved(element, previous, next, itemPromise); this._presentAllElements(); }, _removed: function (handle, mirage) { if (this._handleInHandleMap(handle)) { var element = this._elementFromHandle(handle); this._handlerToNotify().removed(element, mirage, handle); this.releaseItem(element); this._presentAllElements(); } else { this._handlerToNotify().removed(null, mirage, handle); } }, _countChanged: function (newCount, oldCount) { if (this._elementNotificationHandler && this._elementNotificationHandler.countChanged) { this._handlerToNotify().countChanged(newCount, oldCount); } }, _indexChanged: function (handle, newIndex, oldIndex) { var element; if (this._handleInHandleMap(handle)) { var record = this._recordFromHandle(handle); if (record.indexObserved) { if (!record.elementIsPlaceholder) { if (record.item.index !== newIndex) { if (record.renderPromise) { record.renderPromise.cancel(); } if (record.renderComplete) { record.renderComplete.cancel(); } var itemToRender = record.newItem || record.item; itemToRender.index = newIndex; var newItemPromise = Promise.as(itemToRender); newItemPromise.handle = record.itemPromise.handle; var that = this; record.renderPromise = this._renderItem(newItemPromise, record). then(function (v) { record.renderComplete = v.renderComplete; that._changeElement(record, v.element, false); that._presentElements(record); }); } } else { this._changeElement(record, this._renderPlaceholder(record), true); } } element = record.element; } if (this._elementNotificationHandler && this._elementNotificationHandler.indexChanged) { this._handlerToNotify().indexChanged(element, newIndex, oldIndex); } }, _affectedRange: function (range) { if (this._elementNotificationHandler && this._elementNotificationHandler.updateAffectedRange) { this._handlerToNotify().updateAffectedRange(range); } }, _beginNotifications: function () { // accessing _handlerToNotify will force the call to beginNotifications on the client // this._externalBegin = true; this._handlerToNotify(); }, _endNotifications: function () { if (this._notificationsSent) { this._notificationsSent = false; this._externalBegin = false; if (this._elementNotificationHandler && this._elementNotificationHandler.endNotifications) { this._elementNotificationHandler.endNotifications(); } } }, _reload: function () { if (this._elementNotificationHandler && this._elementNotificationHandler.reload) { this._elementNotificationHandler.reload(); } }, // Some functions may be called synchronously or asynchronously, so it's best to post _endNotifications to avoid // calling it prematurely. _postEndNotifications: function () { if (this._notificationsSent && !this._externalBegin && !this._endNotificationsPosted) { this._endNotificationsPosted = true; var that = this; Scheduler.schedule(function ItemsManager_async_endNotifications() { that._endNotificationsPosted = false; that._endNotifications(); }, Scheduler.Priority.high, null, "WinJS.UI._ItemsManager._postEndNotifications"); } }, _presentElement: function (record) { var elementOld = record.element; // Finish modifying the slot before calling back into user code, in case there is a reentrant call this._replaceElement(record, record.elementDelayed); record.elementDelayed = null; record.elementIsPlaceholder = false; this._updateElement(record.element, elementOld); if (this._handlerToNotifyCaresAboutItemAvailable()) { this._handlerToNotify().itemAvailable(record.element, elementOld); } }, _presentElements: function (record, elementDelayed) { if (elementDelayed) { record.elementDelayed = elementDelayed; } this._listBinding.jumpToItem(record.item); if (record.elementDelayed) { this._presentElement(record); } this._postEndNotifications(); }, // Presents all delayed elements _presentAllElements: function () { var that = this; this._foreachRecord(function (record) { if (record.elementDelayed) { that._presentElement(record); } }); }, _writeProfilerMark: function (text) { var message = "WinJS.UI._ItemsManager:" + (this._profilerId ? (this._profilerId + ":") : ":") + text; _WriteProfilerMark(message); } }, { // Static Members supportedForProcessing: false, }); return function (dataSource, itemRenderer, elementNotificationHandler, options) { return new ItemsManager(dataSource, itemRenderer, elementNotificationHandler, options); }; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_TabContainer',[ 'exports', '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', './_ElementUtilities' ], function tabManagerInit(exports, _Global, _Base, _BaseUtils, _ElementUtilities) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } function fireEvent(element, name, forward, cancelable) { var event = _Global.document.createEvent('UIEvent'); event.initUIEvent(name, false, !!cancelable, _Global, forward ? 1 : 0); return !element.dispatchEvent(event); } var getTabIndex = _ElementUtilities.getTabIndex; // tabbableElementsNodeFilter works with the TreeWalker to create a view of the DOM tree that is built up of what we want the focusable tree to look like. // When it runs into a tab contained area, it rejects anything except the childFocus element so that any potentially tabbable things that the TabContainer // doesn't want tabbed to get ignored. function tabbableElementsNodeFilter(node) { var nodeStyle = _Global.getComputedStyle(node); if (nodeStyle.display === "none" || nodeStyle.visibility === "hidden") { return _Global.NodeFilter.FILTER_REJECT; } if (node._tabContainer) { return _Global.NodeFilter.FILTER_ACCEPT; } if (node.parentNode && node.parentNode._tabContainer) { var managedTarget = node.parentNode._tabContainer.childFocus; // Ignore subtrees that are under a tab manager but not the child focus. If a node is contained in the child focus, either accept it (if it's tabbable itself), or skip it and find tabbable content inside of it. if (managedTarget && node.contains(managedTarget)) { return (getTabIndex(node) >= 0 ? _Global.NodeFilter.FILTER_ACCEPT : _Global.NodeFilter.FILTER_SKIP); } return _Global.NodeFilter.FILTER_REJECT; } var tabIndex = getTabIndex(node); if (tabIndex >= 0) { return _Global.NodeFilter.FILTER_ACCEPT; } return _Global.NodeFilter.FILTER_SKIP; } // We've got to manually scrape the results the walker generated, since the walker will have generated a fairly good representation of the tabbable tree, but // won't have a perfect image. Trees like this cause a problem for the walker: // [ tabContainer element ] // [ element containing childFocus ] // [ childFocus ] [ sibling of child focus that has tabIndex >= 0 ] // We can't tell the tree walker to jump right to the childFocus, so it'll collect the childFocus but also that sibling element. We don't want that sibling element // to appear in our version of the tabOrder, so scrapeTabManagedSubtree will take the pretty accurate representation we get from the TreeWalker, and do a little // more pruning to give us only the nodes we're interested in. function scrapeTabManagedSubtree(walker) { var tabManagedElement = walker.currentNode, childFocus = tabManagedElement._tabContainer.childFocus, elementsFound = []; if (!childFocus) { return []; } walker.currentNode = childFocus; function scrapeSubtree() { if (walker.currentNode._tabContainer) { elementsFound = elementsFound.concat(scrapeTabManagedSubtree(walker)); } else { // A child focus can have tabIndex = -1, so check the tabIndex before marking it as valid if (getTabIndex(walker.currentNode) >= 0) { elementsFound.push(walker.currentNode); } if (walker.firstChild()) { do { scrapeSubtree(); } while (walker.nextSibling()); walker.parentNode(); } } } scrapeSubtree(); walker.currentNode = tabManagedElement; return elementsFound; } function TabHelperObject(element, tabIndex) { function createCatcher() { var fragment = _Global.document.createElement("DIV"); fragment.tabIndex = (tabIndex ? tabIndex : 0); fragment.setAttribute("aria-hidden", true); return fragment; } var parent = element.parentNode; // Insert prefix focus catcher var catcherBegin = createCatcher(); parent.insertBefore(catcherBegin, element); // Insert postfix focus catcher var catcherEnd = createCatcher(); parent.insertBefore(catcherEnd, element.nextSibling); catcherBegin.addEventListener("focus", function () { fireEvent(element, "onTabEnter", true); }, true); catcherEnd.addEventListener("focus", function () { fireEvent(element, "onTabEnter", false); }, true); this._catcherBegin = catcherBegin; this._catcherEnd = catcherEnd; var refCount = 1; this.addRef = function () { refCount++; }; this.release = function () { if (--refCount === 0) { if (catcherBegin.parentElement) { parent.removeChild(catcherBegin); } if (catcherEnd.parentElement) { parent.removeChild(catcherEnd); } } return refCount; }; this.updateTabIndex = function (tabIndex) { catcherBegin.tabIndex = tabIndex; catcherEnd.tabIndex = tabIndex; }; } var TrackTabBehavior = { attach: function (element, tabIndex) { /// if (!element["win-trackTabHelperObject"]) { element["win-trackTabHelperObject"] = new TabHelperObject(element, tabIndex); } else { element["win-trackTabHelperObject"].addRef(); } return element["win-trackTabHelperObject"]; }, detach: function (element) { /// if (!element["win-trackTabHelperObject"].release()) { delete element["win-trackTabHelperObject"]; } } }; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { TrackTabBehavior: TrackTabBehavior, TabContainer: _Base.Class.define(function TabContainer_ctor(element) { /// /// /// Constructs the TabContainer. /// /// /// The DOM element to be associated with the TabContainer. /// /// /// The set of options to be applied initially to the TabContainer. /// /// /// A constructed TabContainer. /// /// this._element = element; this._tabIndex = 0; element._tabContainer = this; if (element.getAttribute("tabindex") === null) { element.tabIndex = -1; } var that = this; element.addEventListener("onTabEnter", function (e) { var skipDefaultBehavior = fireEvent(that._element, "onTabEntered", e.detail, true); if (skipDefaultBehavior) { return; } if (that.childFocus) { that.childFocus.focus(); } else { element.focus(); } }); element.addEventListener("keydown", function (e) { var targetElement = e.target; if (e.keyCode === _ElementUtilities.Key.tab) { var forwardTab = !e.shiftKey; var canKeepTabbing = that._hasMoreElementsInTabOrder(targetElement, forwardTab); if (!canKeepTabbing) { var skipTabExitHandling = fireEvent(that._element, "onTabExiting", forwardTab, true); if (skipTabExitHandling) { e.stopPropagation(); e.preventDefault(); return; } var allTabbableElements = that._element.querySelectorAll("a[href],area[href],button,command,input,link,menuitem,object,select,textarea,th[sorted],[tabindex]"), len = allTabbableElements.length, originalTabIndices = []; for (var i = 0; i < len; i++) { var element = allTabbableElements[i]; originalTabIndices.push(element.tabIndex); element.tabIndex = -1; } // If there's nothing else that can be tabbed to on the page, tab should wrap around back to the tab contained area. // We'll disable the sentinel node that's directly in the path of the tab order (catcherEnd for forward tabs, and // catcherBegin for shift+tabs), but leave the other sentinel node untouched so tab can wrap around back into the region. that._elementTabHelper[forwardTab ? "_catcherEnd" : "_catcherBegin"].tabIndex = -1; var restoreTabIndicesOnBlur = function () { targetElement.removeEventListener("blur", restoreTabIndicesOnBlur, false); for (var i = 0; i < len; i++) { if (originalTabIndices[i] !== -1) { // When the original tabIndex was -1, don't try restoring to -1 again. A nested TabContainer might also be in the middle of handling this same code, // and so would have set tabIndex = -1 on this element. The nested tab container will restore the element's tabIndex properly. allTabbableElements[i].tabIndex = originalTabIndices[i]; } } that._elementTabHelper._catcherBegin.tabIndex = that._tabIndex; that._elementTabHelper._catcherEnd.tabIndex = that._tabIndex; }; targetElement.addEventListener("blur", restoreTabIndicesOnBlur, false); _BaseUtils._yieldForEvents(function () { fireEvent(that._element, "onTabExit", forwardTab); }); } } }); this._elementTabHelper = TrackTabBehavior.attach(element, this._tabIndex); this._elementTabHelper._catcherBegin.tabIndex = 0; this._elementTabHelper._catcherEnd.tabIndex = 0; }, { // Public members /// /// /// Disposes the Tab Container. /// /// dispose: function () { TrackTabBehavior.detach(this._element, this._tabIndex); }, /// /// Gets or sets the child element that has focus. /// childFocus: { set: function (e) { if (e !== this._focusElement) { if (e && e.parentNode) { this._focusElement = e; } else { this._focusElement = null; } } }, get: function () { return this._focusElement; } }, /// /// Gets or sets the tab order of the control within its container. /// tabIndex: { set: function (tabIndex) { this._tabIndex = tabIndex; this._elementTabHelper.updateTabIndex(tabIndex); }, get: function () { return this._tabIndex; } }, // Private members _element: null, _skipper: function (e) { e.stopPropagation(); e.preventDefault(); }, _hasMoreElementsInTabOrder: function (currentFocus, movingForwards) { if (!this.childFocus) { return false; } var walker = _Global.document.createTreeWalker(this._element, _Global.NodeFilter.SHOW_ELEMENT, tabbableElementsNodeFilter, false); var tabStops = scrapeTabManagedSubtree(walker); for (var i = 0; i < tabStops.length; i++) { if (tabStops[i] === currentFocus) { return (movingForwards ? (i < tabStops.length - 1) : (i > 0)); } } return false; }, _focusElement: null }, { // Static Members supportedForProcessing: false, }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_KeyboardBehavior',[ 'exports', '../Core/_Global', '../Core/_Base', './_Control', './_ElementUtilities', './_TabContainer' ], function KeyboardBehaviorInit(exports, _Global, _Base, _Control, _ElementUtilities, _TabContainer) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } var _keyboardSeenLast = false; _ElementUtilities._addEventListener(_Global, "pointerdown", function () { if (_keyboardSeenLast) { _keyboardSeenLast = false; } }, true); _Global.addEventListener("keydown", function () { if (!_keyboardSeenLast) { _keyboardSeenLast = true; } }, true); _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _keyboardSeenLast : { get: function _keyboardSeenLast_get() { return _keyboardSeenLast; }, set: function _keyboardSeenLast_set(value) { _keyboardSeenLast = value; } }, _WinKeyboard: function (element) { // Win Keyboard behavior is a solution that would be similar to -ms-keyboard-focus. // It monitors the last input (keyboard/mouse) and adds/removes a win-keyboard class // so that you can style .foo.win-keyboard:focus vs .foo:focus to add a keyboard rect // on an item only when the last input method was keyboard. // Reminder: Touch edgy does not count as an input method. _ElementUtilities._addEventListener(element, "pointerdown", function (ev) { // In case pointer down came on the active element. _ElementUtilities.removeClass(ev.target, "win-keyboard"); }, true); element.addEventListener("keydown", function (ev) { _ElementUtilities.addClass(ev.target, "win-keyboard"); }, true); _ElementUtilities._addEventListener(element, "focusin", function (ev) { exports._keyboardSeenLast && _ElementUtilities.addClass(ev.target, "win-keyboard"); }, false); _ElementUtilities._addEventListener(element, "focusout", function (ev) { _ElementUtilities.removeClass(ev.target, "win-keyboard"); }, false); }, _KeyboardBehavior: _Base.Namespace._lazy(function () { var Key = _ElementUtilities.Key; var _KeyboardBehavior = _Base.Class.define(function KeyboardBehavior_ctor(element, options) { // KeyboardBehavior allows you to easily convert a bunch of tabable elements into a single tab stop with // navigation replaced by keyboard arrow (Up/Down/Left/Right) + Home + End + Custom keys. // // Example use cases: // // 1 Dimensional list: FixedDirection = height and FixedSize = 1; // [1] [ 2 ] [ 3 ] [4] [ 5 ]... // // 2 Dimensional list: FixedDirection = height and FixedSize = 2; // [1] [3] [5] [7] ... // [2] [4] [6] [8] // // 1 Dimensional list: FixedDirection = width and FixedSize = 1; // [ 1 ] // - - // | | // | 2 | // | | // - - // [ 3 ] // [ 4 ] // ... // // 2 Dimensional list: FixedDirection = width and FixedSize = 2; // [1][2] // [3][4] // [5][6] // ... // // Currently it is a "behavior" instead of a "control" so it can be attached to the same element as a // winControl. The main scenario for this would be to attach it to the same element as a repeater. // // It also blocks "Portaling" where you go off the end of one column and wrap around to the other // column. It also blocks "Carousel" where you go from the end of the list to the beginning. // // Keyboarding behavior supports nesting. It supports your tab stops having sub tab stops. If you want // an interactive element within the tab stop you need to use the win-interactive classname or during the // keydown event stop propogation so that the event is skipped. // // If you have custom keyboarding the getAdjacent API is provided. This can be used to enable keyboarding // in multisize 2d lists or custom keyboard commands. PageDown and PageUp are the most common since this // behavior does not detect scrollers. // // It also allows developers to show/hide keyboard focus rectangles themselves. // // It has an API called currentIndex so that Tab (or Shift+Tab) or a developer imitating Tab will result in // the correct item having focus. // // It also allows an element to be represented as 2 arrow stops (commonly used for a split button) by calling // the _getFocusInto API on the child element's winControl if it exists. element = element || _Global.document.createElement("DIV"); options = options || {}; element._keyboardBehavior = this; this._element = element; this._fixedDirection = _KeyboardBehavior.FixedDirection.width; this._fixedSize = 1; this._currentIndex = 0; _Control.setOptions(this, options); // If there's a scroller, the TabContainer can't be inside of the scroller. Otherwise, tabbing into the // TabContainer will cause the scroller to scroll. this._tabContainer = new _TabContainer.TabContainer(this.scroller || this._element); this._tabContainer.tabIndex = 0; if (this._element.children.length > 0) { this._tabContainer.childFocus = this._getFocusInto(this._element.children[0]); } this._element.addEventListener('keydown', this._keyDownHandler.bind(this)); _ElementUtilities._addEventListener(this._element, 'pointerdown', this._MSPointerDownHandler.bind(this)); }, { element: { get: function () { return this._element; } }, fixedDirection: { get: function () { return this._fixedDirection; }, set: function (value) { this._fixedDirection = value; } }, fixedSize: { get: function () { return this._fixedSize; }, set: function (value) { if (+value === value) { value = Math.max(1, value); this._fixedSize = value; } } }, currentIndex: { get: function () { if (this._element.children.length > 0) { return this._currentIndex; } return -1; }, set: function (value) { if (+value === value) { var length = this._element.children.length; value = Math.max(0, Math.min(length - 1, value)); this._currentIndex = value; this._tabContainer.childFocus = this._getFocusInto(this._element.children[value]); } } }, getAdjacent: { get: function () { return this._getAdjacent; }, set: function (value) { this._getAdjacent = value; } }, // If set, KeyboardBehavior will prevent *scroller* from scrolling when moving focus scroller: { get: function () { return this._scroller; }, set: function (value) { this._scroller = value; } }, _keyDownHandler: function _KeyboardBehavior_keyDownHandler(ev) { if (!ev.altKey) { if (_ElementUtilities._matchesSelector(ev.target, ".win-interactive, .win-interactive *")) { return; } var blockScrolling = false; var newIndex = this.currentIndex; var maxIndex = this._element.children.length - 1; var rtl = _Global.getComputedStyle(this._element).direction === "rtl"; var leftStr = rtl ? Key.rightArrow : Key.leftArrow; var rightStr = rtl ? Key.leftArrow : Key.rightArrow; var targetIndex = this.getAdjacent && this.getAdjacent(newIndex, ev.keyCode); if (+targetIndex === targetIndex) { blockScrolling = true; newIndex = targetIndex; } else { var modFixedSize = newIndex % this.fixedSize; if (ev.keyCode === leftStr) { blockScrolling = true; if (this.fixedDirection === _KeyboardBehavior.FixedDirection.width) { if (modFixedSize !== 0) { newIndex--; } } else { if (newIndex >= this.fixedSize) { newIndex -= this.fixedSize; } } } else if (ev.keyCode === rightStr) { blockScrolling = true; if (this.fixedDirection === _KeyboardBehavior.FixedDirection.width) { if (modFixedSize !== this.fixedSize - 1) { newIndex++; } } else { if (newIndex + this.fixedSize - modFixedSize <= maxIndex) { newIndex += this.fixedSize; } } } else if (ev.keyCode === Key.upArrow) { blockScrolling = true; if (this.fixedDirection === _KeyboardBehavior.FixedDirection.height) { if (modFixedSize !== 0) { newIndex--; } } else { if (newIndex >= this.fixedSize) { newIndex -= this.fixedSize; } } } else if (ev.keyCode === Key.downArrow) { blockScrolling = true; if (this.fixedDirection === _KeyboardBehavior.FixedDirection.height) { if (modFixedSize !== this.fixedSize - 1) { newIndex++; } } else { if (newIndex + this.fixedSize - modFixedSize <= maxIndex) { newIndex += this.fixedSize; } } } else if (ev.keyCode === Key.home) { blockScrolling = true; newIndex = 0; } else if (ev.keyCode === Key.end) { blockScrolling = true; newIndex = this._element.children.length - 1; } else if (ev.keyCode === Key.pageUp) { blockScrolling = true; } else if (ev.keyCode === Key.pageDown) { blockScrolling = true; } } newIndex = Math.max(0, Math.min(this._element.children.length - 1, newIndex)); if (newIndex !== this.currentIndex) { this._focus(newIndex, ev.keyCode); // Allow KeyboardBehavior to be nested if (ev.keyCode === leftStr || ev.keyCode === rightStr || ev.keyCode === Key.upArrow || ev.keyCode === Key.downArrow) { ev.stopPropagation(); } } if (blockScrolling) { ev.preventDefault(); } } }, _getFocusInto: function _KeyboardBehavior_getFocusInto(elementToFocus, keyCode) { return elementToFocus && elementToFocus.winControl && elementToFocus.winControl._getFocusInto ? elementToFocus.winControl._getFocusInto(keyCode) : elementToFocus; }, _focus: function _KeyboardBehavior_focus(index, keyCode) { index = (+index === index) ? index : this.currentIndex; var elementToFocus = this._element.children[index]; if (elementToFocus) { elementToFocus = this._getFocusInto(elementToFocus, keyCode); this.currentIndex = index; _ElementUtilities._setActive(elementToFocus, this.scroller); } }, _MSPointerDownHandler: function _KeyboardBehavior_MSPointerDownHandler(ev) { var srcElement = ev.target; if (srcElement === this.element) { return; } while (srcElement.parentNode !== this.element) { srcElement = srcElement.parentNode; } var index = -1; while (srcElement) { index++; srcElement = srcElement.previousElementSibling; } this.currentIndex = index; } }, { FixedDirection: { height: "height", width: "width" } }); return _KeyboardBehavior; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_SafeHtml',[ 'exports', '../Core/_Global', '../Core/_Base', '../Core/_ErrorFromName', '../Core/_Resources' ], function safeHTMLInit(exports, _Global, _Base, _ErrorFromName, _Resources) { "use strict"; var setInnerHTML, setInnerHTMLUnsafe, setOuterHTML, setOuterHTMLUnsafe, insertAdjacentHTML, insertAdjacentHTMLUnsafe; var strings = { get nonStaticHTML() { return "Unable to add dynamic content. A script attempted to inject dynamic content, or elements previously modified dynamically, that might be unsafe. For example, using the innerHTML property or the document.write method to add a script element will generate this exception. If the content is safe and from a trusted source, use a method to explicitly manipulate elements and attributes, such as createElement, or use setInnerHTMLUnsafe (or other unsafe method)."; }, }; setInnerHTML = setInnerHTMLUnsafe = function (element, text) { /// /// /// Sets the innerHTML property of the specified element to the specified text. /// /// /// The element on which the innerHTML property is to be set. /// /// /// The value to be set to the innerHTML property. /// /// element.innerHTML = text; }; setOuterHTML = setOuterHTMLUnsafe = function (element, text) { /// /// /// Sets the outerHTML property of the specified element to the specified text. /// /// /// The element on which the outerHTML property is to be set. /// /// /// The value to be set to the outerHTML property. /// /// element.outerHTML = text; }; insertAdjacentHTML = insertAdjacentHTMLUnsafe = function (element, position, text) { /// /// /// Calls insertAdjacentHTML on the specified element. /// /// /// The element on which insertAdjacentHTML is to be called. /// /// /// The position relative to the element at which to insert the HTML. /// /// /// The value to be provided to insertAdjacentHTML. /// /// element.insertAdjacentHTML(position, text); }; var msApp = _Global.MSApp; if (msApp) { setInnerHTMLUnsafe = function (element, text) { /// /// /// Sets the innerHTML property of the specified element to the specified text. /// /// /// The element on which the innerHTML property is to be set. /// /// /// The value to be set to the innerHTML property. /// /// msApp.execUnsafeLocalFunction(function () { element.innerHTML = text; }); }; setOuterHTMLUnsafe = function (element, text) { /// /// /// Sets the outerHTML property of the specified element to the specified text /// in the context of msWWA.execUnsafeLocalFunction. /// /// /// The element on which the outerHTML property is to be set. /// /// /// The value to be set to the outerHTML property. /// /// msApp.execUnsafeLocalFunction(function () { element.outerHTML = text; }); }; insertAdjacentHTMLUnsafe = function (element, position, text) { /// /// /// Calls insertAdjacentHTML on the specified element in the context /// of msWWA.execUnsafeLocalFunction. /// /// /// The element on which insertAdjacentHTML is to be called. /// /// /// The position relative to the element at which to insert the HTML. /// /// /// Value to be provided to insertAdjacentHTML. /// /// msApp.execUnsafeLocalFunction(function () { element.insertAdjacentHTML(position, text); }); }; } else if (_Global.msIsStaticHTML) { var check = function (str) { if (!_Global.msIsStaticHTML(str)) { throw new _ErrorFromName("WinJS.Utitilies.NonStaticHTML", strings.nonStaticHTML); } }; // If we ever get isStaticHTML we can attempt to recreate the behavior we have in the local // compartment, in the mean-time all we can do is sanitize the input. // setInnerHTML = function (element, text) { /// /// /// Sets the innerHTML property of a element to the specified text /// if it passes a msIsStaticHTML check. /// /// /// The element on which the innerHTML property is to be set. /// /// /// The value to be set to the innerHTML property. /// /// check(text); element.innerHTML = text; }; setOuterHTML = function (element, text) { /// /// /// Sets the outerHTML property of a element to the specified text /// if it passes a msIsStaticHTML check. /// /// /// The element on which the outerHTML property is to be set. /// /// /// The value to be set to the outerHTML property. /// /// check(text); element.outerHTML = text; }; insertAdjacentHTML = function (element, position, text) { /// /// /// Calls insertAdjacentHTML on the element if it passes /// a msIsStaticHTML check. /// /// /// The element on which insertAdjacentHTML is to be called. /// /// /// The position relative to the element at which to insert the HTML. /// /// /// The value to be provided to insertAdjacentHTML. /// /// check(text); element.insertAdjacentHTML(position, text); }; } _Base.Namespace._moduleDefine(exports, "WinJS.Utilities", { setInnerHTML: setInnerHTML, setInnerHTMLUnsafe: setInnerHTMLUnsafe, setOuterHTML: setOuterHTML, setOuterHTMLUnsafe: setOuterHTMLUnsafe, insertAdjacentHTML: insertAdjacentHTML, insertAdjacentHTMLUnsafe: insertAdjacentHTMLUnsafe }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_Select',[ 'exports', '../Core/_Base', './_SafeHtml' ], function selectInit(exports, _Base, _SafeHtml) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _Select: _Base.Namespace._lazy(function () { var encodeHtmlRegEx = /[&<>'"]/g; var encodeHtmlEscapeMap = { "&": "&", "<": "<", ">": ">", "'": "'", '"': """ }; var stringDirectionRegEx = /[\u200e\u200f]/g; function encodeHtml(str) { return str.replace(encodeHtmlRegEx, function (m) { return encodeHtmlEscapeMap[m] || ""; }); } function stripDirectionMarker(str) { return str.replace(stringDirectionRegEx, ""); } function stockGetValue(index) { /*jshint validthis: true */ return this[index]; } function stockGetLength() { /*jshint validthis: true */ return this.length; } function fixDataSource(dataSource) { if (!dataSource.getValue) { dataSource.getValue = stockGetValue; } if (!dataSource.getLength) { dataSource.getLength = stockGetLength; } return dataSource; } return _Base.Class.define(function _Select_ctor(element, options) { // This is an implementation detail of the TimePicker and DatePicker, designed // to provide a primitive "data bound" select control. This is not designed to // be used outside of the TimePicker and DatePicker controls. // this._dataSource = fixDataSource(options.dataSource); this._index = options.index || 0; this._domElement = element; // Mark this as a tab stop this._domElement.tabIndex = 0; if (options.disabled) { this.setDisabled(options.disabled); } var that = this; this._domElement.addEventListener("change", function () { //Should be set to _index to prevent events from firing twice that._index = that._domElement.selectedIndex; }, false); //update runtime accessibility value after initialization this._createSelectElement(); }, { _index: 0, _dataSource: null, dataSource: { get: function () { return this._dataSource; }, set: function (value) { this._dataSource = fixDataSource(value); //Update layout as data source change if (this._domElement) { this._createSelectElement(); } } }, setDisabled: function (disabled) { if (disabled) { this._domElement.setAttribute("disabled", "disabled"); } else { this._domElement.removeAttribute("disabled"); } }, _createSelectElement: function () { var dataSourceLength = this._dataSource.getLength(); var text = ""; for (var i = 0; i < dataSourceLength; i++) { var value = "" + this._dataSource.getValue(i); var escaped = encodeHtml(value); // WinRT localization often tags the strings with reading direction. We want this // for display text (escaped), but don't want this in the value space, as it // only present for display. // var stripped = stripDirectionMarker(escaped); text += ""; } _SafeHtml.setInnerHTMLUnsafe(this._domElement, text); this._domElement.selectedIndex = this._index; }, index: { get: function () { return Math.max(0, Math.min(this._index, this._dataSource.getLength() - 1)); }, set: function (value) { if (this._index !== value) { this._index = value; var d = this._domElement; if (d && d.selectedIndex !== value) { d.selectedIndex = value; } } } }, value: { get: function () { return this._dataSource.getValue(this.index); } } }); }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_UI',[ 'exports', '../Core/_BaseCoreUtils', '../Core/_Base' ], function uiInit(exports, _BaseCoreUtils, _Base) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { eventHandler: function (handler) { /// /// /// Marks a event handler function as being compatible with declarative processing. /// /// /// The handler to be marked as compatible with declarative processing. /// /// /// The input handler. /// /// return _BaseCoreUtils.markSupportedForProcessing(handler); }, /// /// Orientation options for a control's property /// Orientation: { /// /// Horizontal /// horizontal: "horizontal", /// /// Vertical /// vertical: "vertical" }, CountResult: { unknown: "unknown" }, CountError: { noResponse: "noResponse" }, DataSourceStatus: { ready: "ready", waiting: "waiting", failure: "failure" }, FetchError: { noResponse: "noResponse", doesNotExist: "doesNotExist" }, EditError: { noResponse: "noResponse", canceled: "canceled", notPermitted: "notPermitted", noLongerMeaningful: "noLongerMeaningful" }, /// /// Specifies the type of an IListViewEntity. /// ObjectType: { /// /// This value represents a ListView item. /// item: "item", /// /// This value represents a ListView group header. /// groupHeader: "groupHeader" }, /// /// Specifies the selection mode for a ListView. /// SelectionMode: { /// /// Items cannot be selected. /// none: "none", /// /// A single item may be selected. /// /// single: "single", /// /// Multiple items may be selected. /// multi: "multi" }, /// /// Specifies how an ItemContainer or items in a ListView respond to the tap interaction. /// TapBehavior: { /// /// Tapping the item invokes it and selects it. Navigating to the item with the keyboard changes the /// the selection so that the focused item is the only item that is selected. /// /// directSelect: "directSelect", /// /// Tapping the item invokes it. If the item was selected, tapping it clears the selection. If the item wasn't /// selected, tapping the item selects it. /// Navigating to the item with the keyboard does not select or invoke it. /// toggleSelect: "toggleSelect", /// /// Tapping the item invokes it. Navigating to the item with keyboard does not select it or invoke it. /// invokeOnly: "invokeOnly", /// /// Nothing happens. /// none: "none" }, /// /// Specifies whether items are selected when the user performs a swipe interaction. /// /// SwipeBehavior: { /// /// The swipe interaction selects the items touched by the swipe. /// select: "select", /// /// The swipe interaction does not change which items are selected. /// none: "none" }, /// /// Specifies how group headers in a ListView respond to the tap interaction. /// GroupHeaderTapBehavior: { /// /// Tapping the group header invokes it. /// invoke: "invoke", /// /// Nothing happens. /// none: "none" } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities/_Xhr',[ '../Core/_Global', '../Core/_Base', '../Promise', '../Scheduler' ], function xhrInit(_Global, _Base, Promise, Scheduler) { "use strict"; function schedule(f, arg, priority) { Scheduler.schedule(function xhr_callback() { f(arg); }, priority, null, "WinJS.xhr"); } function noop() { } function xhr(options) { /// /// /// Wraps calls to XMLHttpRequest in a promise. /// /// /// The options that are applied to the XMLHttpRequest object. They are: type, /// url, user, password, headers, responseType, data, and customRequestInitializer. /// /// /// A promise that returns the XMLHttpRequest object when it completes. /// /// var req; return new Promise( function (c, e, p) { /// var priority = Scheduler.currentPriority; req = new _Global.XMLHttpRequest(); req.onreadystatechange = function () { if (req._canceled) { req.onreadystatechange = noop; return; } if (req.readyState === 4) { if ((req.status >= 200 && req.status < 300) || req.status === 0) { schedule(c, req, priority); } else { schedule(e, req, priority); } req.onreadystatechange = noop; } else { schedule(p, req, priority); } }; req.open( options.type || "GET", options.url, // Promise based XHR does not support sync. // true, options.user, options.password ); req.responseType = options.responseType || ""; Object.keys(options.headers || {}).forEach(function (k) { req.setRequestHeader(k, options.headers[k]); }); if (options.customRequestInitializer) { options.customRequestInitializer(req); } if (options.data === undefined) { req.send(); } else { req.send(options.data); } }, function () { req.onreadystatechange = noop; req._canceled = true; req.abort(); } ); } _Base.Namespace.define("WinJS", { xhr: xhr }); return xhr; }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Utilities',[ './Utilities/_Control', './Utilities/_Dispose', './Utilities/_ElementListUtilities', './Utilities/_ElementUtilities', './Utilities/_Hoverable', './Utilities/_ItemsManager', './Utilities/_KeyboardBehavior', './Utilities/_ParallelWorkQueue', './Utilities/_SafeHtml', './Utilities/_Select', './Utilities/_TabContainer', './Utilities/_UI', './Utilities/_VersionManager', './Utilities/_Xhr' ], function () { //wrapper module }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Fragments',[ 'exports', './Core/_Global', './Core/_WinRT', './Core/_Base', './Core/_BaseUtils', './Core/_ErrorFromName', './Core/_Resources', './Core/_WriteProfilerMark', './Promise', './Utilities/_ElementUtilities', './Utilities/_SafeHtml', './Utilities/_Xhr' ], function fragmentLoaderInit(exports, _Global, _WinRT, _Base, _BaseUtils, _ErrorFromName, _Resources, _WriteProfilerMark, Promise, _ElementUtilities, _SafeHtml, _Xhr) { "use strict"; var strings = { get invalidFragmentUri() { return "Unsupported uri for fragment loading. Fragments in the local context can only load from package content or local sources. To load fragments from other sources, use a web context."; }, }; // not supported in WebWorker if (!_Global.document) { return; } var forEach = function (arrayLikeValue, action) { for (var i = 0, l = arrayLikeValue.length; i < l; i++) { action(arrayLikeValue[i], i); } }; var head = _Global.document.head || _Global.document.getElementsByTagName("head")[0]; var scripts = {}; var styles = {}; var links = {}; var initialized = false; var cacheStore = {}; var uniqueId = 1; function addScript(scriptTag, fragmentHref, position, lastNonInlineScriptPromise) { // We synthesize a name for inline scripts because today we put the // inline scripts in the same processing pipeline as src scripts. If // we seperated inline scripts into their own logic, we could simplify // this somewhat. // var src = scriptTag.src; var inline = !src; if (inline) { src = fragmentHref + "script[" + position + "]"; } src = src.toLowerCase(); if (!(src in scripts)) { var promise = null; scripts[src] = true; var n = _Global.document.createElement("script"); if (scriptTag.language) { n.setAttribute("language", "javascript"); } n.setAttribute("type", scriptTag.type); n.setAttribute("async", "false"); if (scriptTag.id) { n.setAttribute("id", scriptTag.id); } if (inline) { var text = scriptTag.text; promise = lastNonInlineScriptPromise.then(function () { n.text = text; }).then(null, function () { // eat error }); } else { promise = new Promise(function (c) { n.onload = n.onerror = function () { c(); }; // Using scriptTag.src to maintain the original casing n.setAttribute("src", scriptTag.src); }); } head.appendChild(n); return { promise: promise, inline: inline, }; } } function addStyle(styleTag, fragmentHref, position) { var src = (fragmentHref + "script[" + position + "]").toLowerCase(); if (!(src in styles)) { styles[src] = true; head.appendChild(styleTag.cloneNode(true)); } } function addLink(styleTag) { var src = styleTag.href.toLowerCase(); if (!(src in links)) { links[src] = true; var n = styleTag.cloneNode(false); // Using scriptTag.href to maintain the original casing n.href = styleTag.href; head.appendChild(n); } } function getStateRecord(href, removeFromCache) { if (typeof href === "string") { return loadFromCache(href, removeFromCache); } else { var state = { docfrag: _ElementUtilities.data(href).docFragment }; if (!state.docfrag) { var fragment = _Global.document.createDocumentFragment(); while (href.childNodes.length > 0) { fragment.appendChild(href.childNodes[0]); } state.docfrag = _ElementUtilities.data(href).docFragment = fragment; href.setAttribute("data-win-hasfragment", ""); } if (removeFromCache) { clearCache(href); } return Promise.as(state); } } function createEntry(state, href) { return populateDocument(state, href). then(function () { if (state.document) { return processDocument(href, state); } else { return state; } }). then(function () { if (state.document) { delete state.document; } return state; }); } function loadFromCache(href, removeFromCache) { var fragmentId = href.toLowerCase(); var state = cacheStore[fragmentId]; if (state) { if (removeFromCache) { delete cacheStore[fragmentId]; } if (state.promise) { return state.promise; } else { return Promise.as(state); } } else { state = {}; if (!removeFromCache) { cacheStore[fragmentId] = state; } var result = state.promise = createEntry(state, href); state.promise.then(function () { delete state.promise; }); return result; } } function processDocument(href, state) { // Once the control's static state has been loaded in the temporary iframe, // this method spelunks the iframe's document to retrieve all relevant information. Also, // this performs any needed fixups on the DOM (like adjusting relative URLs). var cd = state.document; var b = cd.body; var sp = []; forEach(cd.querySelectorAll('link[rel="stylesheet"], link[type="text/css"]'), addLink); forEach(cd.getElementsByTagName('style'), function (e, i) { addStyle(e, href, i); }); // In DOCMODE 11 IE moved to the standards based script loading behavior of // having out-of-line script elements which are dynamically added to the DOM // asynchronously load. This raises two problems for our fragment loader, // // 1) out-of-line scripts need to execute in order // // 2) so do in-line scripts. // // In order to mitigate this behavior we do two things: // // A) We mark all scripts with the attribute async='false' which makes // out-of-line scripts respect DOM append order for execution when they // are eventually retrieved // // B) We chain the setting of in-line script element's 'text' property // on the completion of the previous out-of-line script's execution. // This relies on the fact that the out-of-line script elements will // synchronously run their onload handler immediately after executing // thus assuring that the in-line script will run before the next // trailing out-of-line script. // var lastNonInlineScriptPromise = Promise.as(); forEach(cd.getElementsByTagName('script'), function (e, i) { var result = addScript(e, href, i, lastNonInlineScriptPromise); if (result) { if (!result.inline) { lastNonInlineScriptPromise = result.promise; } sp.push(result.promise); } }); forEach(b.getElementsByTagName('img'), function (e) { e.src = e.src; }); forEach(b.getElementsByTagName('a'), function (e) { // for # only anchor tags, we don't update the href // if (e.href !== "") { var href = e.getAttribute("href"); if (href && href[0] !== "#") { e.href = e.href; } } }); // strip inline scripts from the body, they got copied to the // host document with the rest of the scripts above... // var localScripts = b.getElementsByTagName("script"); while (localScripts.length > 0) { var s = localScripts[0]; s.parentNode.removeChild(s); } return Promise.join(sp).then(function () { // Create the docfrag which is just the body children // var fragment = _Global.document.createDocumentFragment(); var imported = _Global.document.importNode(cd.body, true); while (imported.childNodes.length > 0) { fragment.appendChild(imported.childNodes[0]); } state.docfrag = fragment; return state; }); } function initialize() { if (initialized) { return; } initialized = true; forEach(head.querySelectorAll("script"), function (e) { scripts[e.src.toLowerCase()] = true; }); forEach(head.querySelectorAll('link[rel="stylesheet"], link[type="text/css"]'), function (e) { links[e.href.toLowerCase()] = true; }); } function renderCopy(href, target) { /// /// /// Copies the contents of the specified URI into the specified element. /// /// /// The URI that contains the fragment to copy. /// /// /// The element to which the fragment is appended. /// /// /// A promise that is fulfilled when the fragment has been loaded. /// If a target element is not specified, the copied fragment is the /// completed value. /// /// return renderImpl(href, target, true); } function renderImpl(href, target, copy) { var profilerMarkIdentifier = (href instanceof _Global.HTMLElement ? _BaseUtils._getProfilerMarkIdentifier(href) : " href='" + href + "'") + "[" + (++uniqueId) + "]"; writeProfilerMark("WinJS.UI.Fragments:render" + profilerMarkIdentifier + ",StartTM"); initialize(); return getStateRecord(href, !copy).then(function (state) { var frag = state.docfrag; if (copy) { frag = frag.cloneNode(true); } var child = frag.firstChild; while (child) { if (child.nodeType === 1 /*Element node*/) { child.msParentSelectorScope = true; } child = child.nextSibling; } var retVal; if (target) { target.appendChild(frag); retVal = target; } else { retVal = frag; } writeProfilerMark("WinJS.UI.Fragments:render" + profilerMarkIdentifier + ",StopTM"); return retVal; }); } function render(href, target) { /// /// /// Copies the contents of the specified URI into the specified element. /// /// /// The URI that contains the fragment to copy. /// /// /// The element to which the fragment is appended. /// /// /// A promise that is fulfilled when the fragment has been loaded. /// If a target element is not specified, the copied fragment is the /// completed value. /// /// return renderImpl(href, target, false); } function cache(href) { /// /// /// Starts loading the fragment at the specified location. The returned promise completes /// when the fragment is ready to be copied. /// /// /// The URI that contains the fragment to be copied. /// /// /// A promise that is fulfilled when the fragment has been prepared for copying. /// /// initialize(); return getStateRecord(href).then(function (state) { return state.docfrag; }); } function clearCache(href) { /// /// /// Removes any cached information about the specified fragment. This method does not unload any scripts /// or styles that are referenced by the fragment. /// /// /// The URI that contains the fragment to be cleared. If no URI is provided, the entire contents of the cache are cleared. /// /// if (!href) { cacheStore = {}; } else if (typeof (href) === "string") { delete cacheStore[href.toLowerCase()]; } else { delete _ElementUtilities.data(href).docFragment; href.removeAttribute("data-win-hasfragment"); } } function forceLocal(uri) { if (_BaseUtils.hasWinRT) { // we force the URI to be cannonicalized and made absolute by IE // var a = _Global.document.createElement("a"); a.href = uri; var absolute = a.href; // WinRT Uri class doesn't provide URI construction, but can crack the URI // appart to let us reliably discover the scheme. // var wuri = new _WinRT.Windows.Foundation.Uri(absolute); // Only "ms-appx" (local package content) are allowed when running in the local // context. Both strings are known to be safe to compare in any culture (including Turkish). // var scheme = wuri.schemeName; if (scheme !== "ms-appx") { throw new _ErrorFromName("WinJS.UI.Fragments.InvalidUri", strings.invalidFragmentUri); } return absolute; } return uri; } function populateDocument(state, href) { // Because we later use "setInnerHTMLUnsafe" ("Unsafe" is the magic word here), we // want to force the href to only support local package content when running // in the local context. When running in the web context, this will be a no-op. // href = forceLocal(href); var htmlDoc = _Global.document.implementation.createHTMLDocument("frag"); var base = htmlDoc.createElement("base"); htmlDoc.head.appendChild(base); var anchor = htmlDoc.createElement("a"); htmlDoc.body.appendChild(anchor); base.href = _Global.document.location.href; // Initialize base URL to primary document URL anchor.setAttribute("href", href); // Resolve the relative path to an absolute path base.href = anchor.href; // Update the base URL to be the resolved absolute path // 'anchor' is no longer needed at this point and will be removed by the innerHTML call state.document = htmlDoc; return getFragmentContents(href).then(function (text) { _SafeHtml.setInnerHTMLUnsafe(htmlDoc.documentElement, text); htmlDoc.head.appendChild(base); }); } var writeProfilerMark = _WriteProfilerMark; var getFragmentContents = getFragmentContentsXHR; function getFragmentContentsXHR(href) { return _Xhr({ url: href }).then(function (req) { return req.responseText; }); } _Base.Namespace._moduleDefine(exports, "WinJS.UI.Fragments", { renderCopy: renderCopy, render: render, cache: cache, clearCache: clearCache, _cacheStore: { get: function () { return cacheStore; } }, _forceLocal: forceLocal, _getFragmentContents: { get: function () { return getFragmentContents; }, set: function (value) { getFragmentContents = value; } }, _writeProfilerMark: { get: function () { return writeProfilerMark; }, set: function (value) { writeProfilerMark = value; } } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Application/_State',[ 'exports', '../Core/_Global', '../Core/_WinRT', '../Core/_Base', '../Core/_BaseUtils', '../Promise' ], function stateInit(exports, _Global, _WinRT, _Base, _BaseUtils, Promise) { "use strict"; function initWithWinRT() { var local, temp, roaming; var IOHelper = _Base.Class.define( function IOHelper_ctor(folder) { this.folder = folder; this._path = folder.path; if (folder.tryGetItemAsync) { this._tryGetItemAsync = folder.tryGetItemAsync.bind(folder); } }, { _tryGetItemAsync: function (fileName) { return this.folder.getFileAsync(fileName).then(null, function () { return false; }); }, exists: function (fileName) { /// /// /// Determines if the specified file exists in the container /// /// /// The file which may exist within this folder /// /// /// Promise with either true (file exists) or false. /// /// return this._tryGetItemAsync(fileName).then(function (fileItem) { return fileItem ? true : false; }); }, remove: function (fileName) { /// /// /// Delets a file in the container /// /// /// The file to be deleted /// /// /// Promise which is fulfilled when the file has been deleted /// /// return this._tryGetItemAsync(fileName).then(function (fileItem) { return fileItem ? fileItem.deleteAsync() : false; }).then(null, function () { return false; }); }, writeText: function (fileName, str) { /// /// /// Writes a file to the container with the specified text /// /// /// The file to write to /// /// /// Content to be written to the file /// /// /// Promise with the count of characters written /// /// var sto = _WinRT.Windows.Storage; var that = this; return that.folder.createFileAsync(fileName, sto.CreationCollisionOption.openIfExists). then(function (fileItem) { return sto.FileIO.writeTextAsync(fileItem, str); }); }, readText: function (fileName, def) { /// /// /// Reads the contents of a file from the container, if the file /// doesn't exist, def is returned. /// /// /// The file to read from /// /// /// Default value to be returned if the file failed to open /// /// /// Promise containing the contents of the file, or def. /// /// var sto = _WinRT.Windows.Storage; return this._tryGetItemAsync(fileName).then(function (fileItem) { return fileItem ? sto.FileIO.readTextAsync(fileItem) : def; }).then(null, function () { return def; }); } }, { supportedForProcessing: false, }); _Base.Namespace._moduleDefine(exports, "WinJS.Application", { /// /// Allows access to create files in the application local storage, which is preserved across runs /// of an application and does not roam. /// local: { get: function () { if (!local) { local = new IOHelper(_WinRT.Windows.Storage.ApplicationData.current.localFolder); } return local; } }, /// /// Allows access to create files in the application temp storage, which may be reclaimed /// by the system between application runs. /// temp: { get: function () { if (!temp) { temp = new IOHelper(_WinRT.Windows.Storage.ApplicationData.current.temporaryFolder); } return temp; } }, /// /// Allows access to create files in the application roaming storage, which is preserved across runs /// of an application and roams with the user across multiple machines. /// roaming: { get: function () { if (!roaming) { roaming = new IOHelper(_WinRT.Windows.Storage.ApplicationData.current.roamingFolder); } return roaming; } } }); } function initWithStub() { var InMemoryHelper = _Base.Class.define( function InMemoryHelper_ctor() { this.storage = {}; }, { exists: function (fileName) { /// /// /// Determines if the specified file exists in the container /// /// /// The filename which may exist within this folder /// /// /// Promise with either true (file exists) or false. /// /// // force conversion to boolean // return Promise.as(this.storage[fileName] !== undefined); }, remove: function (fileName) { /// /// /// Deletes a file in the container /// /// /// The file to be deleted /// /// /// Promise which is fulfilled when the file has been deleted /// /// delete this.storage[fileName]; return Promise.as(); }, writeText: function (fileName, str) { /// /// /// Writes a file to the container with the specified text /// /// /// The filename to write to /// /// /// Content to be written to the file /// /// /// Promise with the count of characters written /// /// this.storage[fileName] = str; return Promise.as(str.length); }, readText: function (fileName, def) { /// /// /// Reads the contents of a file from the container, if the file /// doesn't exist, def is returned. /// /// /// The filename to read from /// /// /// Default value to be returned if the file failed to open /// /// /// Promise containing the contents of the file, or def. /// /// var result = this.storage[fileName]; return Promise.as(typeof result === "string" ? result : def); } }, { supportedForProcessing: false, } ); _Base.Namespace._moduleDefine(exports, "WinJS.Application", { /// /// Allows access to create files in the application local storage, which is preserved across runs /// of an application and does not roam. /// local: new InMemoryHelper(), /// /// Allows access to create files in the application temp storage, which may be reclaimed /// by the system between application runs. /// temp: new InMemoryHelper(), /// /// Allows access to create files in the application roaming storage, which is preserved across runs /// of an application and roams with the user across multiple machines. /// roaming: new InMemoryHelper() }); } if (_WinRT.Windows.Storage.FileIO && _WinRT.Windows.Storage.ApplicationData && _WinRT.Windows.Storage.CreationCollisionOption) { initWithWinRT(); } else { initWithStub(); } var sessionState = {}; _Base.Namespace._moduleDefine(exports, "WinJS.Application", { sessionState: { get: function () { return sessionState; }, set: function (value) { sessionState = value; } }, _loadState: function (e) { // we only restore state if we are coming back from a clear termination from PLM // if (e.previousExecutionState === 3 /* ApplicationExecutionState.Terminated */) { return exports.local.readText("_sessionState.json", "{}"). then(function (str) { var sessionState = JSON.parse(str); if (sessionState && Object.keys(sessionState).length > 0) { exports._sessionStateLoaded = true; } exports.sessionState = sessionState; }). then(null, function () { exports.sessionState = {}; }); } else { return Promise.as(); } }, _oncheckpoint: function (event, Application) { if (_Global.MSApp && _Global.MSApp.getViewOpener && _Global.MSApp.getViewOpener()) { // don't save state in child windows. return; } var sessionState = exports.sessionState; if ((sessionState && Object.keys(sessionState).length > 0) || exports._sessionStateLoaded) { var stateString; try { stateString = JSON.stringify(sessionState); } catch (e) { stateString = ""; Application.queueEvent({ type: "error", detail: e }); } event.setPromise( exports.local.writeText("_sessionState.json", stateString). then(null, function (err) { Application.queueEvent({ type: "error", detail: err }); }) ); } } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Navigation',[ 'exports', './Core/_Base', './Core/_Events', './Core/_WriteProfilerMark', './Promise' ], function navigationInit(exports, _Base, _Events, _WriteProfilerMark, Promise) { "use strict"; var navigatedEventName = "navigated"; var navigatingEventName = "navigating"; var beforenavigateEventName = "beforenavigate"; var ListenerType = _Base.Class.mix(_Base.Class.define(null, { /* empty */ }, { supportedForProcessing: false }), _Events.eventMixin); var listeners = new ListenerType(); var history = { backStack: [], current: { location: "", initialPlaceholder: true }, forwardStack: [] }; var createEvent = _Events._createEventProperty; var raiseBeforeNavigate = function (proposed) { _WriteProfilerMark("WinJS.Navigation:navigation,StartTM"); return Promise.as(). then(function () { var waitForPromise = Promise.as(); var defaultPrevented = listeners.dispatchEvent(beforenavigateEventName, { setPromise: function (promise) { /// /// /// Used to inform the ListView that asynchronous work is being performed, and that this /// event handler should not be considered complete until the promise completes. /// /// /// The promise to wait for. /// /// waitForPromise = waitForPromise.then(function () { return promise; }); }, location: proposed.location, state: proposed.state }); return waitForPromise.then(function beforeNavComplete(cancel) { return defaultPrevented || cancel; }); }); }; var raiseNavigating = function (delta) { return Promise.as(). then(function () { var waitForPromise = Promise.as(); listeners.dispatchEvent(navigatingEventName, { setPromise: function (promise) { /// /// /// Used to inform the ListView that asynchronous work is being performed, and that this /// event handler should not be considered complete until the promise completes. /// /// /// The promise to wait for. /// /// waitForPromise = waitForPromise.then(function () { return promise; }); }, location: history.current.location, state: history.current.state, delta: delta }); return waitForPromise; }); }; var raiseNavigated = function (value, err) { _WriteProfilerMark("WinJS.Navigation:navigation,StopTM"); var waitForPromise = Promise.as(); var detail = { value: value, location: history.current.location, state: history.current.state, setPromise: function (promise) { /// /// /// Used to inform the ListView that asynchronous work is being performed, and that this /// event handler should not be considered complete until the promise completes. /// /// /// The promise to wait for. /// /// waitForPromise = waitForPromise.then(function () { return promise; }); } }; if (!value && err) { detail.error = err; } listeners.dispatchEvent(navigatedEventName, detail); return waitForPromise; }; var go = function (distance, fromStack, toStack, delta) { distance = Math.min(distance, fromStack.length); if (distance > 0) { return raiseBeforeNavigate(fromStack[fromStack.length - distance]). then(function goBeforeCompleted(cancel) { if (!cancel) { toStack.push(history.current); while (distance - 1 > 0) { distance--; toStack.push(fromStack.pop()); } history.current = fromStack.pop(); return raiseNavigating(delta).then( raiseNavigated, function (err) { raiseNavigated(undefined, err || true); throw err; }).then(function () { return true; }); } else { return false; } }); } return Promise.wrap(false); }; _Base.Namespace._moduleDefine(exports, "WinJS.Navigation", { /// /// Determines whether it is possible to navigate forwards. /// canGoForward: { get: function () { return history.forwardStack.length > 0; } }, /// /// Determines whether it is possible to navigate backwards. /// canGoBack: { get: function () { return history.backStack.length > 0; } }, /// /// Gets the current location. /// location: { get: function () { return history.current.location; } }, /// /// Gets or sets the navigation state. /// state: { get: function () { return history.current.state; }, set: function (value) { history.current.state = value; } }, /// /// Gets or sets the navigation history. /// history: { get: function () { return history; }, set: function (value) { history = value; // ensure the require fields are present // history.backStack = history.backStack || []; history.forwardStack = history.forwardStack || []; history.current = history.current || { location: "", initialPlaceholder: true }; history.current.location = history.current.location || ""; } }, forward: function (distance) { /// /// /// Navigates forwards. /// /// /// The number of entries to go forward. /// /// /// A promise that is completed with a value that indicates whether or not /// the navigation was successful. /// /// distance = distance || 1; return go(distance, history.forwardStack, history.backStack, distance); }, back: function (distance) { /// /// /// Navigates backwards. /// /// /// The number of entries to go back into the history. /// /// /// A promise that is completed with a value that indicates whether or not /// the navigation was successful. /// /// distance = distance || 1; return go(distance, history.backStack, history.forwardStack, -distance); }, navigate: function (location, initialState) { /// /// /// Navigates to a location. /// /// /// The location to navigate to. Generally the location is a string, but /// it may be anything. /// /// /// The navigation state that may be accessed through WinJS.Navigation.state. /// /// /// A promise that is completed with a value that indicates whether or not /// the navigation was successful. /// /// var proposed = { location: location, state: initialState }; return raiseBeforeNavigate(proposed). then(function navBeforeCompleted(cancel) { if (!cancel) { if (!history.current.initialPlaceholder) { history.backStack.push(history.current); } history.forwardStack = []; history.current = proposed; // error or no, we go from navigating -> navigated // cancelation should be handled with "beforenavigate" // return raiseNavigating().then( raiseNavigated, function (err) { raiseNavigated(undefined, err || true); throw err; }).then(function () { return true; }); } else { return false; } }); }, addEventListener: function (eventType, listener, capture) { /// /// /// Adds an event listener to the control. /// /// /// The type (name) of the event. /// /// /// The listener to invoke when the event gets raised. /// /// /// Specifies whether or not to initiate capture. /// /// listeners.addEventListener(eventType, listener, capture); }, removeEventListener: function (eventType, listener, capture) { /// /// /// Removes an event listener from the control. /// /// /// The type (name) of the event. /// /// /// The listener to remove. /// /// /// Specifies whether or not to initiate capture. /// /// listeners.removeEventListener(eventType, listener, capture); }, /// /// A page navigation event that occurs after onbeforenavigate and onnavigating. This event can be used to perform other actions after navigation is complete. /// onnavigated: createEvent(navigatedEventName), /// /// A page navigation event that occurs after onbeforenavigate and before onnavigated. This event can be used to perform other actions during navigation. /// onnavigating: createEvent(navigatingEventName), /// /// A page navigation event that occurs before onnavigating and onnavigated. This event can be used to cancel navigation or perform other actions prior to navigation. /// onbeforenavigate: createEvent(beforenavigateEventName) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Application',[ 'exports', './Core/_Global', './Core/_WinRT', './Core/_Base', './Core/_Events', './Core/_Log', './Core/_WriteProfilerMark', './Application/_State', './Navigation', './Promise', './_Signal', './Scheduler' ], function applicationInit(exports, _Global, _WinRT, _Base, _Events, _Log, _WriteProfilerMark, _State, Navigation, Promise, _Signal, Scheduler) { "use strict"; _Global.Debug && (_Global.Debug.setNonUserCodeExceptions = true); var checkpointET = "checkpoint", unloadET = "unload", activatedET = "activated", loadedET = "loaded", readyET = "ready", errorET = "error", settingsET = "settings", backClickET = "backclick"; var outstandingPromiseErrors; var eventQueue = []; var eventQueueJob = null; var eventQueuedSignal = null; var running = false; var registered = false; var ListenerType = _Base.Class.mix(_Base.Class.define(null, { /* empty */ }, { supportedForProcessing: false }), _Events.eventMixin); var listeners = new ListenerType(); var createEvent = _Events._createEventProperty; var pendingDeferrals = {}; var pendingDeferralID = 0; function safeSerialize(obj) { var str; try { var seenObjects = []; str = JSON.stringify(obj, function (key, value) { if (value === _Global) { return "[window]"; } else if (value instanceof _Global.HTMLElement) { return "[HTMLElement]"; } else if (typeof value === "function") { return "[function]"; } else if (typeof value === "object") { if (value === null) { return value; } else if (seenObjects.indexOf(value) === -1) { seenObjects.push(value); return value; } else { return "[circular]"; } } else { return value; } }); } catch (err) { // primitives, undefined, null, etc, all get serialized fine. In the // case that stringify fails (typically due to circular graphs) we // just show "[object]". While we may be able to tighten the condition // for the exception, we never way this serialize to fail. // // Note: we make this be a JSON string, so that consumers of the log // can always call JSON.parse. str = JSON.stringify("[object]"); } return str; } function defaultTerminateAppHandler(data, e) { /*jshint unused: false*/ // This is the unhandled exception handler in WinJS. This handler is invoked whenever a promise // has an exception occur that is not handled (via an error handler passed to then() or a call to done()). // // To see the original exception stack, look at data.stack. // For more information on debugging and exception handling go to http://go.microsoft.com/fwlink/p/?LinkId=253583. debugger; // jshint ignore:line if (_Global.MSApp) { _Global.MSApp.terminateApp(data); } } var terminateAppHandler = defaultTerminateAppHandler; function captureDeferral(obj) { var id = "def" + (pendingDeferralID++); return { deferral: pendingDeferrals[id] = obj.getDeferral(), id: id }; } function completeDeferral(deferral, deferralID) { // If we have a deferralID we our table to find the // deferral. Since we remove it on completion, this // ensures that we never double notify a deferral // in the case of a user call "Application.stop" in // the middle of processing an event // if (deferralID) { deferral = pendingDeferrals[deferralID]; delete pendingDeferrals[deferralID]; } if (deferral) { deferral.complete(); } } function cleanupAllPendingDeferrals() { if (pendingDeferrals) { Object.keys(pendingDeferrals).forEach(function (k) { pendingDeferrals[k].complete(); }); pendingDeferrals = {}; } } function dispatchEvent(eventRecord) { _WriteProfilerMark("WinJS.Application:Event_" + eventRecord.type + ",StartTM"); var waitForPromise = Promise.as(); eventRecord.setPromise = function (promise) { /// /// /// Used to inform the application object that asynchronous work is being performed, and that this /// event handler should not be considered complete until the promise completes. /// /// /// The promise to wait for. /// /// waitForPromise = waitForPromise.then(function () { return promise; }); }; eventRecord.detail = eventRecord.detail || {}; if (typeof (eventRecord.detail) === "object") { eventRecord.detail.setPromise = eventRecord.setPromise; } try { if (listeners._listeners) { var handled = false; l = listeners._listeners[eventRecord.type]; if (l) { l.forEach(function dispatchOne(e) { handled = e.listener(eventRecord) || handled; }); } } // Fire built in listeners last, for checkpoint this is important // as it lets our built in serialization see any mutations to // app.sessionState // var l = builtInListeners[eventRecord.type]; if (l) { l.forEach(function dispatchOne(e) { e(eventRecord, handled); }); } } catch (err) { queueEvent({ type: errorET, detail: err }); } function cleanup(r) { _WriteProfilerMark("WinJS.Application:Event_" + eventRecord.type + ",StopTM"); if (eventRecord._deferral) { completeDeferral(eventRecord._deferral, eventRecord._deferralID); } return r; } return waitForPromise.then(cleanup, function (r) { r = cleanup(r); if (r && r.name === "Canceled") { return; } return Promise.wrapError(r); }); } function createEventQueuedSignal() { if (!eventQueuedSignal) { eventQueuedSignal = new _Signal(); eventQueuedSignal.promise.done(function () { eventQueuedSignal = null; }, function () { eventQueuedSignal = null; }); } return eventQueuedSignal; } function drainOneEvent(queue) { function drainError(err) { queueEvent({ type: errorET, detail: err }); } if (queue.length === 0) { return createEventQueuedSignal().promise; } else { return dispatchEvent(queue.shift()).then(null, drainError); } } // Drains the event queue via the scheduler // function drainQueue(jobInfo) { function drainNext() { return drainQueue; } var queue = jobInfo.job._queue; if (queue.length === 0 && eventQueue.length > 0) { queue = jobInfo.job._queue = copyAndClearQueue(); } jobInfo.setPromise(drainOneEvent(queue).then(drainNext, drainNext)); } function startEventQueue() { function markSync() { sync = true; } var queue = []; var sync = true; var promise; // Drain the queue as long as there are events and they complete synchronously // while (sync) { if (queue.length === 0 && eventQueue.length > 0) { queue = copyAndClearQueue(); } sync = false; promise = drainOneEvent(queue); promise.done(markSync, markSync); } // Schedule a job which will be responsible for draining events for the // lifetime of the application. // eventQueueJob = Scheduler.schedule(function Application_pumpEventQueue(jobInfo) { function drainNext() { return drainQueue; } jobInfo.setPromise(promise.then(drainNext, drainNext)); }, Scheduler.Priority.high, null, "WinJS.Application._pumpEventQueue"); eventQueueJob._queue = queue; } function queueEvent(eventRecord) { /// /// /// Queues an event to be processed by the WinJS.Application event queue. /// /// /// The event object is expected to have a type property that is /// used as the event name when dispatching on the WinJS.Application /// event queue. The entire object is provided to event listeners /// in the detail property of the event. /// /// _WriteProfilerMark("WinJS.Application:Event_" + eventRecord.type + " queued,Info"); eventQueue.push(eventRecord); if (running && eventQueuedSignal) { eventQueuedSignal.complete(drainQueue); } } function copyAndClearQueue() { var queue = eventQueue; eventQueue = []; return queue; } var builtInListeners = { activated: [ function Application_activatedHandler() { queueEvent({ type: readyET }); } ], checkpoint: [ function Application_checkpointHandler(e) { _State._oncheckpoint(e, exports); } ], error: [ function Application_errorHandler(e, handled) { if (handled) { return; } _Log.log && _Log.log(safeSerialize(e), "winjs", "error"); if (_Global.document && exports._terminateApp) { var data = e.detail; var number = data && (data.number || (data.exception && (data.exception.number || data.exception.code)) || (data.error && data.error.number) || data.errorCode || 0); var terminateData = { description: safeSerialize(data), // note: because of how we listen to events, we rarely get a stack stack: data && (data.stack || (data.exception && (data.exception.stack || data.exception.message)) || (data.error && data.error.stack) || null), errorNumber: number, number: number }; exports._terminateApp(terminateData, e); } } ], backclick: [ function Application_backClickHandler(e, handled) { if (handled) { e._winRTBackPressedEvent.handled = true; } else if (Navigation.canGoBack) { Navigation.back(); e._winRTBackPressedEvent.handled = true; } } ], }; // loaded == DOMContentLoaded // activated == after WinRT Activated // ready == after all of the above // function activatedHandler(e) { var def = captureDeferral(e.activatedOperation); _State._loadState(e).then(function () { queueEvent({ type: activatedET, detail: e, _deferral: def.deferral, _deferralID: def.id }); }); } function suspendingHandler(e) { var def = captureDeferral(e.suspendingOperation); queueEvent({ type: checkpointET, _deferral: def.deferral, _deferralID: def.id }); } function domContentLoadedHandler() { queueEvent({ type: loadedET }); if (!(_Global.document && _WinRT.Windows.UI.WebUI.WebUIApplication)) { var activatedArgs = { arguments: "", kind: "Windows.Launch", previousExecutionState: 0 //_WinRT.Windows.ApplicationModel.Activation.ApplicationExecutionState.NotRunning }; _State._loadState(activatedArgs).then(function () { queueEvent({ type: activatedET, detail: activatedArgs }); }); } } function beforeUnloadHandler() { cleanupAllPendingDeferrals(); queueEvent({ type: unloadET }); } function errorHandler(e) { var flattenedError = {}; for (var key in e) { flattenedError[key] = e[key]; } var data; var handled = true; var prev = exports._terminateApp; try { exports._terminateApp = function (d, e) { handled = false; data = d; if (prev !== defaultTerminateAppHandler) { prev(d, e); } }; dispatchEvent({ type: errorET, detail: { error: flattenedError, errorLine: e.lineno, errorCharacter: e.colno, errorUrl: e.filename, errorMessage: e.message } }); } finally { exports._terminateApp = prev; } return handled; } function promiseErrorHandler(e) { // // e.detail looks like: { exception, error, promise, handler, id, parent } // var details = e.detail; var id = details.id; // If the error has a parent promise then this is not the origination of the // error so we check if it has a handler, and if so we mark that the error // was handled by removing it from outstandingPromiseErrors // if (details.parent) { if (details.handler && outstandingPromiseErrors) { delete outstandingPromiseErrors[id]; } return; } // Work around browsers that don't serialize exceptions if (details.exception instanceof Error) { var error = { stack: details.exception.stack, message: details.exception.message }; details.exception = error; } // If this is the first promise error to occur in this period we need to schedule // a helper to come along after a setImmediate that propagates any remaining // errors to the application's queue. // var shouldScheduleErrors = !outstandingPromiseErrors; // Indicate that this error was orignated and needs to be handled // outstandingPromiseErrors = outstandingPromiseErrors || []; outstandingPromiseErrors[id] = details; if (shouldScheduleErrors) { Scheduler.schedule(function Application_async_promiseErrorHandler() { var errors = outstandingPromiseErrors; outstandingPromiseErrors = null; errors.forEach(function (error) { queueEvent({ type: errorET, detail: error }); }); }, Scheduler.Priority.high, null, "WinJS.Application._queuePromiseErrors"); } } // capture this early // if (_Global.document) { _Global.document.addEventListener("DOMContentLoaded", domContentLoadedHandler, false); } function commandsRequested(e) { var event = { e: e, applicationcommands: undefined }; listeners.dispatchEvent(settingsET, event); } function hardwareButtonBackPressed(winRTBackPressedEvent) { // Fire WinJS.Application 'backclick' event. If the winRTBackPressedEvent is not handled, the app will get suspended. var eventRecord = { type: backClickET }; Object.defineProperty(eventRecord, "_winRTBackPressedEvent", { value: winRTBackPressedEvent, enumerable: false }); dispatchEvent(eventRecord); } function register() { if (!registered) { registered = true; _Global.addEventListener("beforeunload", beforeUnloadHandler, false); // None of these are enabled in web worker if (_Global.document) { _Global.addEventListener("error", errorHandler, false); if (_WinRT.Windows.UI.WebUI.WebUIApplication) { var wui = _WinRT.Windows.UI.WebUI.WebUIApplication; wui.addEventListener("activated", activatedHandler, false); wui.addEventListener("suspending", suspendingHandler, false); } if (_WinRT.Windows.UI.ApplicationSettings.SettingsPane) { var settingsPane = _WinRT.Windows.UI.ApplicationSettings.SettingsPane.getForCurrentView(); settingsPane.addEventListener("commandsrequested", commandsRequested); } // Code in WinJS.Application for phone. This integrates WinJS.Application into the hardware back button. if (_WinRT.Windows.Phone.UI.Input.HardwareButtons) { _WinRT.Windows.Phone.UI.Input.HardwareButtons.addEventListener("backpressed", hardwareButtonBackPressed); } } Promise.addEventListener("error", promiseErrorHandler); } } function unregister() { if (registered) { registered = false; _Global.removeEventListener("beforeunload", beforeUnloadHandler, false); // None of these are enabled in web worker if (_Global.document) { if (_WinRT.Windows.UI.WebUI.WebUIApplication) { _Global.removeEventListener("error", errorHandler, false); var wui = _WinRT.Windows.UI.WebUI.WebUIApplication; wui.removeEventListener("activated", activatedHandler, false); wui.removeEventListener("suspending", suspendingHandler, false); } if (_WinRT.Windows.UI.ApplicationSettings.SettingsPane) { var settingsPane = _WinRT.Windows.UI.ApplicationSettings.SettingsPane.getForCurrentView(); settingsPane.removeEventListener("commandsrequested", commandsRequested); } // Code in WinJS.Application for phone. This integrates WinJS.Application into the hardware back button. if (_WinRT.Windows.Phone.UI.Input.HardwareButtons) { _WinRT.Windows.Phone.UI.Input.HardwareButtons.removeEventListener("backpressed", hardwareButtonBackPressed); } } Promise.removeEventListener("error", promiseErrorHandler); } } var publicNS = _Base.Namespace._moduleDefine(exports, "WinJS.Application", { stop: function () { /// /// /// Stops application event processing and resets WinJS.Application /// to its initial state. /// /// // Need to clear out the event properties explicitly to clear their backing // state. // publicNS.onactivated = null; publicNS.oncheckpoint = null; publicNS.onerror = null; publicNS.onloaded = null; publicNS.onready = null; publicNS.onsettings = null; publicNS.onunload = null; publicNS.onbackclick = null; listeners = new ListenerType(); _State.sessionState = {}; running = false; copyAndClearQueue(); eventQueueJob && eventQueueJob.cancel(); eventQueueJob = null; eventQueuedSignal = null; unregister(); cleanupAllPendingDeferrals(); }, addEventListener: function (eventType, listener, capture) { /// /// /// Adds an event listener to the control. /// /// /// The type (name) of the event. /// /// /// The listener to invoke when the event is raised. /// /// /// true to initiate capture; otherwise, false. /// /// listeners.addEventListener(eventType, listener, capture); }, removeEventListener: function (eventType, listener, capture) { /// /// /// Removes an event listener from the control. /// /// /// The type (name) of the event. /// /// /// The listener to remove. /// /// /// Specifies whether or not to initiate capture. /// /// listeners.removeEventListener(eventType, listener, capture); }, checkpoint: function () { /// /// /// Queues a checkpoint event. /// /// queueEvent({ type: checkpointET }); }, start: function () { /// /// /// Starts processing events in the WinJS.Application event queue. /// /// register(); running = true; startEventQueue(); }, queueEvent: queueEvent, _terminateApp: { get: function () { return terminateAppHandler; }, set: function (value) { terminateAppHandler = value; } }, /// /// Occurs when receiving Process Lifetime Management (PLM) notification or when the checkpoint function is called. /// oncheckpoint: createEvent(checkpointET), /// /// Occurs when the application is about to be unloaded. /// onunload: createEvent(unloadET), /// /// Occurs when Windows Runtime activation has occurred. /// The name of this event is "activated" (and also "mainwindowactivated".) /// This event occurs after the loaded event and before the ready event. /// onactivated: createEvent(activatedET), /// /// Occurs after the DOMContentLoaded event, which fires after the page has been parsed but before all the resources are loaded. /// This event occurs before the activated event and the ready event. /// onloaded: createEvent(loadedET), /// /// Occurs when the application is ready. This event occurs after the onloaded event and the onactivated event. /// onready: createEvent(readyET), /// /// Occurs when the settings charm is invoked. /// onsettings: createEvent(settingsET), /// /// Occurs when an unhandled error has been raised. /// onerror: createEvent(errorET), /// /// Raised when the users clicks the backbutton on a Windows Phone. /// onbackclick: createEvent(backClickET) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Animations/_Constants',[ 'exports', '../Core/_Base' ], function animationsConstantsInit(exports, _Base) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { /// /// Specifies what animation type should be returned by WinJS.UI.Animation.createPageNavigationAnimations. /// PageNavigationAnimation: { /// /// The pages will exit and enter using a turnstile animation. /// turnstile: "turnstile", /// /// The pages will exit and enter using an animation that slides up/down. /// slide: "slide", /// /// The pages will enter using an enterPage animation, and exit with no animation. /// enterPage: "enterPage", /// /// The pages will exit and enter using a continuum animation. /// continuum: "continuum" } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Animations/_TransitionAnimation',[ 'exports', '../Core/_Global', '../Core/_WinRT', '../Core/_Base', '../Core/_BaseUtils', '../Promise', '../Scheduler', '../Utilities/_ElementUtilities' ], function transitionAnimationInit(exports, _Global, _WinRT, _Base, _BaseUtils, Promise, Scheduler, _ElementUtilities) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } var browserStyleEquivalents = _BaseUtils._browserStyleEquivalents; function makeArray(elements) { if (Array.isArray(elements) || elements instanceof _Global.NodeList || elements instanceof _Global.HTMLCollection) { return elements; } else if (elements) { return [elements]; } else { return []; } } var keyframeCounter = 0; function getUniqueKeyframeName() { ++keyframeCounter; return "WinJSUIAnimation" + keyframeCounter; } function isUniqueKeyframeName(s) { return "WinJSUIAnimation" === s.substring(0, 16); } function resolveStyles(elem) { _Global.getComputedStyle(elem, null).opacity; } function copyWithEvaluation(iElem, elem) { return function (obj) { var newObj = {}; for (var p in obj) { var v = obj[p]; if (typeof v === "function") { v = v(iElem, elem); } newObj[p] = v; } if (!newObj.exactTiming) { newObj.delay += exports._libraryDelay; } return newObj; }; } var activeActions = []; var reason_interrupted = 1; var reason_canceled = 2; function stopExistingAction(id, prop) { var key = id + "|" + prop; var finish = activeActions[key]; if (finish) { finish(reason_interrupted); } } function registerAction(id, prop, finish) { activeActions[id + "|" + prop] = finish; } function unregisterAction(id, prop) { delete activeActions[id + "|" + prop]; } var StyleCache = _Base.Class.define( // Constructor function StyleCache_ctor(id, desc, style) { this.cref = 0; this.id = id; this.desc = desc; this.removed = {}; this.prevStyles = desc.props.map(function (p) { return style[p[0]]; }); this.prevNames = this.names = style[desc.nameProp]; desc.styleCaches[id] = this; }, { // Members destroy: function StyleCache_destroy(style, skipStylesReset) { var desc = this.desc; delete desc.styleCaches[this.id]; if (!skipStylesReset) { if (this.prevNames === "" && this.prevStyles.every(function (s) { return s === ""; })) { style[desc.shorthandProp] = ""; } else { desc.props.forEach(function (p, i) { style[p[0]] = this.prevStyles[i]; }, this); style[desc.nameProp] = this.prevNames; } } }, removeName: function StyleCache_removeName(style, name, elem, skipStylesReset) { var nameValue = this.names; var names = nameValue.split(", "); var index = names.lastIndexOf(name); if (index >= 0) { names.splice(index, 1); this.names = nameValue = names.join(", "); if (nameValue === "" && this.desc.isTransition) { nameValue = "none"; } } if (--this.cref) { style[this.desc.nameProp] = nameValue; if (!isUniqueKeyframeName(name)) { this.removed[name] = true; } } else { if (elem && nameValue === "none") { style[this.desc.nameProp] = nameValue; resolveStyles(elem); } this.destroy(style, skipStylesReset); } } }); function setTemporaryStyles(elem, id, style, actions, desc) { var styleCache = desc.styleCaches[id] || new StyleCache(id, desc, style); styleCache.cref += actions.length; actions.forEach(function (action) { stopExistingAction(id, action.property); }); if (desc.isTransition || actions.some(function (action) { return styleCache.removed[action[desc.nameField]]; })) { resolveStyles(elem); styleCache.removed = {}; } var newShorthand = actions.map(function (action) { return action[desc.nameField] + " " + desc.props.map(function (p) { return (p[1] ? action[p[1]] : "") + p[2]; }).join(" "); }).join(", "); var newNames = actions.map(function (action) { return action[desc.nameField]; }).join(", "); if (styleCache.names !== "") { newShorthand = styleCache.names + ", " + newShorthand; newNames = styleCache.names + ", " + newNames; } style[desc.shorthandProp] = newShorthand; styleCache.names = newNames; return styleCache; } var elementTransitionProperties = { shorthandProp: browserStyleEquivalents["transition"].scriptName, nameProp: browserStyleEquivalents["transition-property"].scriptName, nameField: "property", props: [ [browserStyleEquivalents["transition-duration"].scriptName, "duration", "ms"], [browserStyleEquivalents["transition-timing-function"].scriptName, "timing", ""], [browserStyleEquivalents["transition-delay"].scriptName, "delay", "ms"] ], isTransition: true, styleCaches: [] }; function completePromise(c, synchronous) { if (synchronous) { c(); } else { Scheduler.schedule(function _Animation_completeAnimationPromise() { c(); }, Scheduler.Priority.normal, null, "WinJS.UI._Animation._completeAnimationPromise"); } } var uniformizeStyle; function executeElementTransition(elem, index, transitions, promises, animate) { if (transitions.length > 0) { var style = elem.style; var id = _ElementUtilities._uniqueID(elem); if (!uniformizeStyle) { uniformizeStyle = _Global.document.createElement("DIV").style; } transitions = transitions.map(copyWithEvaluation(index, elem)); transitions.forEach(function (transition) { var scriptNameOfProperty = _BaseUtils._getCamelCasedName(transition.property); if (transition.hasOwnProperty("from")) { style[scriptNameOfProperty] = transition.from; } uniformizeStyle[scriptNameOfProperty] = transition.to; transition.to = uniformizeStyle[scriptNameOfProperty]; transition.propertyScriptName = scriptNameOfProperty; }); if (animate) { var styleCache = setTemporaryStyles(elem, id, style, transitions, elementTransitionProperties); var listener = elem.disabled ? _Global.document : elem; transitions.forEach(function (transition) { var finish; promises.push(new Promise(function (c) { finish = function (reason) { if (onTransitionEnd) { listener.removeEventListener(_BaseUtils._browserEventEquivalents["transitionEnd"], onTransitionEnd, false); unregisterAction(id, transition.property); styleCache.removeName(style, transition.propertyScriptName, reason ? elem : null, transition.skipStylesReset); _Global.clearTimeout(timeoutId); onTransitionEnd = null; } completePromise(c, reason === reason_canceled); }; var onTransitionEnd = function (event) { if (event.target === elem && event.propertyName === transition.property) { finish(); } }; registerAction(id, transition.property, finish); listener.addEventListener(_BaseUtils._browserEventEquivalents["transitionEnd"], onTransitionEnd, false); var padding = 0; if (style[transition.propertyScriptName] !== transition.to) { style[transition.propertyScriptName] = transition.to; padding = 50; } var timeoutId = _Global.setTimeout(function () { timeoutId = _Global.setTimeout(finish, transition.delay + transition.duration); }, padding); }, function () { finish(reason_canceled); })); }); } else { transitions.forEach(function (transition) { style[transition.propertyScriptName] = transition.to; }); } } } var elementAnimationProperties = { shorthandProp: browserStyleEquivalents["animation"].scriptName, nameProp: browserStyleEquivalents["animation-name"].scriptName, nameField: "keyframe", props: [ [browserStyleEquivalents["animation-duration"].scriptName, "duration", "ms"], [browserStyleEquivalents["animation-timing-function"].scriptName, "timing", ""], [browserStyleEquivalents["animation-delay"].scriptName, "delay", "ms"], [browserStyleEquivalents["animation-iteration-count"].scriptName, "", "1"], [browserStyleEquivalents["animation-direction"].scriptName, "", "normal"], [browserStyleEquivalents["animation-fill-mode"].scriptName, "", "both"] ], isTransition: false, styleCaches: [] }; function executeElementAnimation(elem, index, anims, promises, animate) { if (animate && anims.length > 0) { var style = elem.style; var id = _ElementUtilities._uniqueID(elem); anims = anims.map(copyWithEvaluation(index, elem)); var styleElem; var listener = elem.disabled ? _Global.document : elem; anims.forEach(function (anim) { if (!anim.keyframe) { if (!styleElem) { styleElem = _Global.document.createElement("STYLE"); _Global.document.documentElement.appendChild(styleElem); } anim.keyframe = getUniqueKeyframeName(); var kf = "@" + browserStyleEquivalents["keyframes"] + " " + anim.keyframe + " { from {" + anim.property + ":" + anim.from + ";} to {" + anim.property + ":" + anim.to + ";}}"; styleElem.sheet.insertRule(kf, 0); } else { anim.keyframe = browserStyleEquivalents.animationPrefix + anim.keyframe; } }); var styleCache = setTemporaryStyles(elem, id, style, anims, elementAnimationProperties), animationsToCleanUp = [], animationPromises = []; anims.forEach(function (anim) { var finish; animationPromises.push(new Promise(function (c) { finish = function (reason) { if (onAnimationEnd) { listener.removeEventListener(_BaseUtils._browserEventEquivalents["animationEnd"], onAnimationEnd, false); _Global.clearTimeout(timeoutId); onAnimationEnd = null; } completePromise(c, reason === reason_canceled); }; var onAnimationEnd = function (event) { if (event.target === elem && event.animationName === anim.keyframe) { finish(); } }; registerAction(id, anim.property, finish); // Firefox will stop all animations if we clean up that animation's properties when there're other CSS animations still running // on an element. To work around this, we delay animation style cleanup until all parts of an animation finish. animationsToCleanUp.push({ id: id, property: anim.property, style: style, keyframe: anim.keyframe }); var timeoutId = _Global.setTimeout(function () { timeoutId = _Global.setTimeout(finish, anim.delay + anim.duration); }, 50); listener.addEventListener(_BaseUtils._browserEventEquivalents["animationEnd"], onAnimationEnd, false); }, function () { finish(reason_canceled); })); }); if (styleElem) { _Global.setTimeout(function () { var parentElement = styleElem.parentElement; if (parentElement) { parentElement.removeChild(styleElem); } }, 50); } var cleanupAnimations = function () { for (var i = 0; i < animationsToCleanUp.length; i++) { var anim = animationsToCleanUp[i]; unregisterAction(anim.id, anim.property); styleCache.removeName(anim.style, anim.keyframe); } }; promises.push(Promise.join(animationPromises).then(cleanupAnimations, cleanupAnimations)); } } var enableCount = 0; var animationSettings; function initAnimations() { if (!animationSettings) { if (_WinRT.Windows.UI.ViewManagement.UISettings) { animationSettings = new _WinRT.Windows.UI.ViewManagement.UISettings(); } else { animationSettings = { animationsEnabled: true }; } } } var isAnimationEnabled = function isAnimationEnabledImpl() { /// /// /// Determines whether the WinJS Animation Library will perform animations. /// /// /// true if WinJS animations will be performed. /// false if WinJS animations are suppressed. /// /// initAnimations(); return enableCount + animationSettings.animationsEnabled > 0; }; function applyAction(element, action, execAction) { try { var animate = exports.isAnimationEnabled(); var elems = makeArray(element); var actions = makeArray(action); var promises = []; for (var i = 0; i < elems.length; i++) { if (Array.isArray(elems[i])) { for (var j = 0; j < elems[i].length; j++) { execAction(elems[i][j], i, actions, promises, animate); } } else { execAction(elems[i], i, actions, promises, animate); } } if (promises.length) { return Promise.join(promises); } else { return Scheduler.schedulePromiseNormal(null, "WinJS.UI._Animation._completeActionPromise").then(null, function () { // Convert a cancelation to the success path }); } } catch (e) { return Promise.wrapError(e); } } function fastAnimation(animation) { if (Array.isArray(animation)) { return animation.map(function (animation) { return fastAnimation(animation); }); } else if (animation) { animation.delay = animationTimeAdjustment(animation.delay); animation.duration = animationTimeAdjustment(animation.duration); return animation; } else { return; } } function animationAdjustment(animation) { if (fastAnimations) { return fastAnimation(animation); } else { return animation; } } var animationTimeAdjustment = function _animationTimeAdjustmentImpl(v) { if (fastAnimations) { return v / 20; } else { return v; } }; var fastAnimations = false; var libraryDelay = 0; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { disableAnimations: function () { /// /// /// Disables animations in the WinJS Animation Library /// by decrementing the animation enable count. /// /// enableCount--; }, enableAnimations: function () { /// /// /// Enables animations in the WinJS Animation Library /// by incrementing the animation enable count. /// /// enableCount++; }, isAnimationEnabled: { get: function () { return isAnimationEnabled; }, set: function (value) { isAnimationEnabled = value; } }, _libraryDelay: { get: function () { return libraryDelay; }, set: function (value) { libraryDelay = value; } }, executeAnimation: function (element, animation) { /// /// /// Perform a CSS animation that can coexist with other /// Animation Library animations. Applications are not expected /// to call this function directly; they should prefer to use /// the high-level animations in the Animation Library. /// /// /// Single element or collection of elements on which /// to perform a CSS animation. /// /// /// Single animation description or array of animation descriptions. /// /// /// Promise object that completes when the CSS animation is complete. /// /// return applyAction(element, animationAdjustment(animation), executeElementAnimation); }, executeTransition: function (element, transition) { /// /// /// Perform a CSS transition that can coexist with other /// Animation Library animations. Applications are not expected /// to call this function directly; they should prefer to use /// the high-level animations in the Animation Library. /// /// /// Single element or collection of elements on which /// to perform a CSS transition. /// /// /// Single transition description or array of transition descriptions. /// /// /// Promise object that completes when the CSS transition is complete. /// /// return applyAction(element, animationAdjustment(transition), executeElementTransition); }, _animationTimeAdjustment: { get: function () { return animationTimeAdjustment; }, set: function (value) { animationTimeAdjustment = value; } } }); _Base.Namespace._moduleDefine(exports, "WinJS.Utilities", { _fastAnimations: { get: function () { return fastAnimations; }, set: function (value) { fastAnimations = value; } } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Animations',[ 'exports', './Core/_Global', './Core/_Base', './Core/_BaseUtils', './Core/_WriteProfilerMark', './Animations/_Constants', './Animations/_TransitionAnimation', './Promise' ], function animationsInit(exports, _Global, _Base, _BaseUtils, _WriteProfilerMark, _Constants, _TransitionAnimation, Promise) { "use strict"; var transformNames = _BaseUtils._browserStyleEquivalents["transform"]; // Default to 11 pixel from the left (or right if RTL) var defaultOffset = [{ top: "0px", left: "11px", rtlflip: true }]; var OffsetArray = _Base.Class.define(function OffsetArray_ctor(offset, keyframe, defOffset) { // Constructor defOffset = defOffset || defaultOffset; if (Array.isArray(offset) && offset.length > 0) { this.offsetArray = offset; if (offset.length === 1) { this.keyframe = checkKeyframe(offset[0], defOffset[0], keyframe); } } else if (offset && offset.hasOwnProperty("top") && offset.hasOwnProperty("left")) { this.offsetArray = [offset]; this.keyframe = checkKeyframe(offset, defOffset[0], keyframe); } else { this.offsetArray = defOffset; this.keyframe = chooseKeyframe(defOffset[0], keyframe); } }, { // Public Members getOffset: function (i) { if (i >= this.offsetArray.length) { i = this.offsetArray.length - 1; } return this.offsetArray[i]; } }, { // Static Members supportedForProcessing: false, }); function checkKeyframe(offset, defOffset, keyframe) { if (offset.keyframe) { return offset.keyframe; } if (!keyframe || offset.left !== defOffset.left || offset.top !== defOffset.top || (offset.rtlflip && !defOffset.rtlflip)) { return null; } if (!offset.rtlflip) { return keyframe; } return keyframeCallback(keyframe); } function chooseKeyframe(defOffset, keyframe) { if (!keyframe || !defOffset.rtlflip) { return keyframe; } return keyframeCallback(keyframe); } function keyframeCallback(keyframe) { var keyframeRtl = keyframe + "-rtl"; return function (i, elem) { return _Global.getComputedStyle(elem).direction === "ltr" ? keyframe : keyframeRtl; }; } function makeArray(elements) { if (Array.isArray(elements) || elements instanceof _Global.NodeList || elements instanceof _Global.HTMLCollection) { return elements; } else if (elements) { return [elements]; } else { return []; } } function collectOffsetArray(elemArray) { var offsetArray = []; for (var i = 0; i < elemArray.length; i++) { var offset = { top: elemArray[i].offsetTop, left: elemArray[i].offsetLeft }; var matrix = _Global.getComputedStyle(elemArray[i], null)[transformNames.scriptName].split(","); if (matrix.length === 6) { offset.left += parseFloat(matrix[4]); offset.top += parseFloat(matrix[5]); } offsetArray.push(offset); } return offsetArray; } function staggerDelay(initialDelay, extraDelay, delayFactor, delayCap) { return function (i) { var ret = initialDelay; for (var j = 0; j < i; j++) { extraDelay *= delayFactor; ret += extraDelay; } if (delayCap) { ret = Math.min(ret, delayCap); } return ret; }; } function makeOffsetsRelative(elemArray, offsetArray) { for (var i = 0; i < offsetArray.length; i++) { offsetArray[i].top -= elemArray[i].offsetTop; offsetArray[i].left -= elemArray[i].offsetLeft; } } function animTranslate2DTransform(elemArray, offsetArray, transition) { makeOffsetsRelative(elemArray, offsetArray); for (var i = 0; i < elemArray.length; i++) { if (offsetArray[i].top !== 0 || offsetArray[i].left !== 0) { elemArray[i].style[transformNames.scriptName] = "translate(" + offsetArray[i].left + "px, " + offsetArray[i].top + "px)"; } } return _TransitionAnimation.executeTransition(elemArray, transition); } function animStaggeredSlide(curve, start, end, fadeIn, page, first, second, third) { var elementArray = [], startOffsetArray = [], endOffsetArray = []; function prepareSlide(elements, start, end) { if (!elements) { return; } var startOffset = { left: start + "px", top: "0px" }, endOffset = { left: end + "px", top: "0px" }; if (+elements.length === elements.length) { for (var i = 0, len = elements.length; i < len; i++) { elementArray.push(elements[i]); startOffsetArray.push(startOffset); endOffsetArray.push(endOffset); } } else { elementArray.push(elements); startOffsetArray.push(startOffset); endOffsetArray.push(endOffset); } } var horizontalOffset = 200, startOffset = (start !== 0 ? (start < 0 ? -horizontalOffset : horizontalOffset) : 0), endOffset = (end !== 0 ? (end < 0 ? -horizontalOffset : horizontalOffset) : 0); prepareSlide(page, start, end); prepareSlide(first, startOffset, endOffset); prepareSlide(second, startOffset * 2, endOffset * 2); prepareSlide(third, startOffset * 3, endOffset * 3); startOffsetArray = new OffsetArray(startOffsetArray); endOffsetArray = new OffsetArray(endOffsetArray); return _TransitionAnimation.executeTransition( elementArray, [{ property: transformNames.cssName, delay: 0, duration: 350, timing: curve, from: translateCallback(startOffsetArray), to: translateCallback(endOffsetArray) }, { property: "opacity", delay: 0, duration: 350, timing: fadeIn ? "steps(1, start)" : "steps(1, end)", from: fadeIn ? 0 : 1, to: fadeIn ? 1 : 0 }]); } function animRotationTransform(elemArray, origins, transition) { elemArray = makeArray(elemArray); origins = makeArray(origins); for (var i = 0, len = elemArray.length; i < len; i++) { var rtl = _Global.getComputedStyle(elemArray[i]).direction === "rtl"; elemArray[i].style[_BaseUtils._browserStyleEquivalents["transform-origin"].scriptName] = origins[Math.min(origins.length - 1, i)][rtl ? "rtl" : "ltr"]; } function onComplete() { clearAnimRotationTransform(elemArray); } return _TransitionAnimation.executeTransition(elemArray, transition).then(onComplete, onComplete); } function clearAnimRotationTransform(elemArray) { for (var i = 0, len = elemArray.length; i < len; i++) { elemArray[i].style[_BaseUtils._browserStyleEquivalents["transform-origin"].scriptName] = ""; elemArray[i].style[transformNames.scriptName] = ""; elemArray[i].style.opacity = ""; } } function translateCallback(offsetArray, prefix) { prefix = prefix || ""; return function (i, elem) { var offset = offsetArray.getOffset(i); var left = offset.left; if (offset.rtlflip && _Global.getComputedStyle(elem).direction === "rtl") { left = left.toString(); if (left.charAt(0) === "-") { left = left.substring(1); } else { left = "-" + left; } } return prefix + "translate(" + left + ", " + offset.top + ")"; }; } function translateCallbackAnimate(offsetArray, suffix) { suffix = suffix || ""; return function (i) { var offset = offsetArray[i]; return "translate(" + offset.left + "px, " + offset.top + "px) " + suffix; }; } function keyframeCallbackAnimate(offsetArray, keyframe) { return function (i) { var offset = offsetArray[i]; return (offset.left === 0 && offset.top === 0) ? keyframe : null; }; } function layoutTransition(LayoutTransition, target, affected, extra) { var targetArray = makeArray(target); var affectedArray = makeArray(affected); var offsetArray = collectOffsetArray(affectedArray); return new LayoutTransition(targetArray, affectedArray, offsetArray, extra); } function collectTurnstileTransformOrigins(elements) { var origins = []; for (var i = 0, len = elements.length; i < len; i++) { var itemBoundingBox = elements[i].getBoundingClientRect(); var offsetLeftLTR = -(40 + itemBoundingBox.left); var offsetLeftRTL = 40 + (_Global.innerWidth - itemBoundingBox.right); var totalOffsetY = ((_Global.innerHeight / 2) - itemBoundingBox.top); origins.push( { ltr: offsetLeftLTR + "px " + totalOffsetY + "px", rtl: offsetLeftRTL + "px " + totalOffsetY + "px" } ); } return origins; } function writeAnimationProfilerMark(text) { _WriteProfilerMark("WinJS.UI.Animation:" + text); } var ExpandAnimation = _Base.Class.define(function ExpandAnimation_ctor(revealedArray, affectedArray, offsetArray) { // Constructor this.revealedArray = revealedArray; this.affectedArray = affectedArray; this.offsetArray = offsetArray; }, { // Public Members execute: function () { writeAnimationProfilerMark("expandAnimation,StartTM"); var promise1 = _TransitionAnimation.executeAnimation( this.revealedArray, { keyframe: "WinJS-opacity-in", property: "opacity", delay: this.affectedArray.length > 0 ? 200 : 0, duration: 167, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: 0, to: 1 }); var promise2 = animTranslate2DTransform( this.affectedArray, this.offsetArray, { property: transformNames.cssName, delay: 0, duration: 367, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("expandAnimation,StopTM"); }); } }, { // Static Members supportedForProcessing: false, }); var CollapseAnimation = _Base.Class.define(function CollapseAnimation_ctor(hiddenArray, affectedArray, offsetArray) { // Constructor this.hiddenArray = hiddenArray; this.affectedArray = affectedArray; this.offsetArray = offsetArray; }, { // Public Members execute: function () { writeAnimationProfilerMark("collapseAnimation,StartTM"); var promise1 = _TransitionAnimation.executeAnimation( this.hiddenArray, { keyframe: "WinJS-opacity-out", property: "opacity", delay: 0, duration: 167, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: 1, to: 0 }); var promise2 = animTranslate2DTransform( this.affectedArray, this.offsetArray, { property: transformNames.cssName, delay: this.hiddenArray.length > 0 ? 167 : 0, duration: 367, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("collapseAnimation,StopTM"); }); } }, { // Static Members supportedForProcessing: false, }); var RepositionAnimation = _Base.Class.define(function RepositionAnimation_ctor(target, elementArray, offsetArray) { // Constructor this.elementArray = elementArray; this.offsetArray = offsetArray; }, { // Public Members execute: function () { writeAnimationProfilerMark("repositionAnimation,StartTM"); return animTranslate2DTransform( this.elementArray, this.offsetArray, { property: transformNames.cssName, delay: staggerDelay(0, 33, 1, 250), duration: 367, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }) .then(function () { writeAnimationProfilerMark("repositionAnimation,StopTM"); }); } }, { // Static Members supportedForProcessing: false, }); var AddToListAnimation = _Base.Class.define(function AddToListAnimation_ctor(addedArray, affectedArray, offsetArray) { // Constructor this.addedArray = addedArray; this.affectedArray = affectedArray; this.offsetArray = offsetArray; }, { // Public Members execute: function () { writeAnimationProfilerMark("addToListAnimation,StartTM"); var delay = this.affectedArray.length > 0 ? 240 : 0; var promise1 = _TransitionAnimation.executeAnimation( this.addedArray, [{ keyframe: "WinJS-scale-up", property: transformNames.cssName, delay: delay, duration: 120, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: "scale(0.85)", to: "none" }, { keyframe: "WinJS-opacity-in", property: "opacity", delay: delay, duration: 120, timing: "linear", from: 0, to: 1 }] ); var promise2 = animTranslate2DTransform( this.affectedArray, this.offsetArray, { property: transformNames.cssName, delay: 0, duration: 400, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("addToListAnimation,StopTM"); }); } }, { // Static Members supportedForProcessing: false, }); var DeleteFromListAnimation = _Base.Class.define(function DeleteFromListAnimation_ctor(deletedArray, remainingArray, offsetArray) { // Constructor this.deletedArray = deletedArray; this.remainingArray = remainingArray; this.offsetArray = offsetArray; }, { // Public Members execute: function () { writeAnimationProfilerMark("deleteFromListAnimation,StartTM"); var promise1 = _TransitionAnimation.executeAnimation( this.deletedArray, [{ keyframe: "WinJS-scale-down", property: transformNames.cssName, delay: 0, duration: 120, timing: "cubic-bezier(0.11, 0.5, 0.24, .96)", from: "none", to: "scale(0.85)" }, { keyframe: "WinJS-opacity-out", property: "opacity", delay: 0, duration: 120, timing: "linear", from: 1, to: 0 }]); var promise2 = animTranslate2DTransform( this.remainingArray, this.offsetArray, { property: transformNames.cssName, delay: this.deletedArray.length > 0 ? 60 : 0, duration: 400, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("deleteFromListAnimation,StopTM"); }); } }, { // Static Members supportedForProcessing: false, }); var _UpdateListAnimation = _Base.Class.define(function _UpdateListAnimation_ctor(addedArray, affectedArray, offsetArray, deleted) { // Constructor this.addedArray = addedArray; this.affectedArray = affectedArray; this.offsetArray = offsetArray; var deletedArray = makeArray(deleted); this.deletedArray = deletedArray; this.deletedOffsetArray = collectOffsetArray(deletedArray); }, { // Public Members execute: function () { writeAnimationProfilerMark("_updateListAnimation,StartTM"); makeOffsetsRelative(this.deletedArray, this.deletedOffsetArray); var delay = 0; var promise1 = _TransitionAnimation.executeAnimation( this.deletedArray, [{ keyframe: keyframeCallbackAnimate(this.deletedOffsetArray, "WinJS-scale-down"), property: transformNames.cssName, delay: 0, duration: 120, timing: "cubic-bezier(0.11, 0.5, 0.24, .96)", from: translateCallbackAnimate(this.deletedOffsetArray), to: translateCallbackAnimate(this.deletedOffsetArray, "scale(0.85)") }, { keyframe: "WinJS-opacity-out", property: "opacity", delay: 0, duration: 120, timing: "linear", from: 1, to: 0 }]); if (this.deletedArray.length > 0) { delay += 60; } var promise2 = animTranslate2DTransform( this.affectedArray, this.offsetArray, { property: transformNames.cssName, delay: delay, duration: 400, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }); if (this.affectedArray.length > 0) { delay += 240; } else if (delay) { delay += 60; } var promise3 = _TransitionAnimation.executeAnimation( this.addedArray, [{ keyframe: "WinJS-scale-up", property: transformNames.cssName, delay: delay, duration: 120, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: "scale(0.85)", to: "none" }, { keyframe: "WinJS-opacity-in", property: "opacity", delay: delay, duration: 120, timing: "linear", from: 0, to: 1 }] ); return Promise.join([promise1, promise2, promise3]) .then(function () { writeAnimationProfilerMark("_updateListAnimation,StopTM"); }); } }, { // Static Members supportedForProcessing: false, }); var AddToSearchListAnimation = _Base.Class.define(function AddToSearchListAnimation_ctor(addedArray, affectedArray, offsetArray) { // Constructor this.addedArray = addedArray; this.affectedArray = affectedArray; this.offsetArray = offsetArray; }, { // Public Members execute: function () { writeAnimationProfilerMark("addToSearchListAnimation,StartTM"); var promise1 = _TransitionAnimation.executeAnimation( this.addedArray, { keyframe: "WinJS-opacity-in", property: "opacity", delay: this.affectedArray.length > 0 ? 240 : 0, duration: 117, timing: "linear", from: 0, to: 1 }); var promise2 = animTranslate2DTransform( this.affectedArray, this.offsetArray, { property: transformNames.cssName, delay: 0, duration: 400, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("addToSearchListAnimation,StopTM"); }); } }, { // Static Members supportedForProcessing: false, }); var DeleteFromSearchListAnimation = _Base.Class.define(function DeleteFromSearchListAnimation_ctor(deletedArray, remainingArray, offsetArray) { // Constructor this.deletedArray = deletedArray; this.remainingArray = remainingArray; this.offsetArray = offsetArray; }, { // Public Members execute: function () { writeAnimationProfilerMark("deleteFromSearchListAnimation,StartTM"); var promise1 = _TransitionAnimation.executeAnimation( this.deletedArray, { keyframe: "WinJS-opacity-out", property: "opacity", delay: 0, duration: 93, timing: "linear", from: 1, to: 0 }); var promise2 = animTranslate2DTransform( this.remainingArray, this.offsetArray, { property: transformNames.cssName, delay: this.deletedArray.length > 0 ? 60 : 0, duration: 400, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("deleteFromSearchListAnimation,StopTM"); }); } }, { // Static Members supportedForProcessing: false, }); var PeekAnimation = _Base.Class.define(function PeekAnimation_ctor(target, elementArray, offsetArray) { // Constructor this.elementArray = elementArray; this.offsetArray = offsetArray; }, { // Public Members execute: function () { writeAnimationProfilerMark("peekAnimation,StartTM"); return animTranslate2DTransform( this.elementArray, this.offsetArray, { property: transformNames.cssName, delay: 0, duration: 2000, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }) .then(function () { writeAnimationProfilerMark("peekAnimation,StopTM"); }); } }, { // Static Members supportedForProcessing: false, }); _Base.Namespace._moduleDefine(exports, "WinJS.UI.Animation", { createExpandAnimation: function (revealed, affected) { /// /// /// Creates an expand animation. /// After creating the ExpandAnimation object, /// modify the document to move the elements to their new positions, /// then call the execute method on the ExpandAnimation object. /// /// /// Single element or collection of elements which were revealed. /// /// /// Single element or collection of elements whose positions were /// affected by the expand. /// /// /// ExpandAnimation object whose execute method returns /// a Promise that completes when the animation is complete. /// /// return layoutTransition(ExpandAnimation, revealed, affected); }, createCollapseAnimation: function (hidden, affected) { /// /// /// Creates a collapse animation. /// After creating the CollapseAnimation object, /// modify the document to move the elements to their new positions, /// then call the execute method on the CollapseAnimation object. /// /// /// Single element or collection of elements being removed from view. /// When the animation completes, the application should hide the elements /// or remove them from the document. /// /// /// Single element or collection of elements whose positions were /// affected by the collapse. /// /// /// CollapseAnimation object whose execute method returns /// a Promise that completes when the animation is complete. /// /// return layoutTransition(CollapseAnimation, hidden, affected); }, createRepositionAnimation: function (element) { /// /// /// Creates a reposition animation. /// After creating the RepositionAnimation object, /// modify the document to move the elements to their new positions, /// then call the execute method on the RepositionAnimation object. /// /// /// Single element or collection of elements which were repositioned. /// /// /// RepositionAnimation object whose execute method returns /// a Promise that completes when the animation is complete. /// /// return layoutTransition(RepositionAnimation, null, element); }, fadeIn: function (shown) { /// /// /// Execute a fade-in animation. /// /// /// Single element or collection of elements to fade in. /// At the end of the animation, the opacity of the elements is 1. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("fadeIn,StartTM"); return _TransitionAnimation.executeTransition( shown, { property: "opacity", delay: 0, duration: 250, timing: "linear", from: 0, to: 1 }) .then(function () { writeAnimationProfilerMark("fadeIn,StopTM"); }); }, fadeOut: function (hidden) { /// /// /// Execute a fade-out animation. /// /// /// Single element or collection of elements to fade out. /// At the end of the animation, the opacity of the elements is 0. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("fadeOut,StartTM"); return _TransitionAnimation.executeTransition( hidden, { property: "opacity", delay: 0, duration: 167, timing: "linear", to: 0 }) .then(function () { writeAnimationProfilerMark("fadeOut,StopTM"); }); }, createAddToListAnimation: function (added, affected) { /// /// /// Creates an animation for adding to a list. /// After creating the AddToListAnimation object, /// modify the document to move the elements to their new positions, /// then call the execute method on the AddToListAnimation object. /// /// /// Single element or collection of elements which were added. /// /// /// Single element or collection of elements whose positions were /// affected by the add. /// /// /// AddToListAnimation object whose execute method returns /// a Promise that completes when the animation is complete. /// /// return layoutTransition(AddToListAnimation, added, affected); }, createDeleteFromListAnimation: function (deleted, remaining) { /// /// /// Crestes an animation for deleting from a list. /// After creating the DeleteFromListAnimation object, /// modify the document to reflect the deletion, /// then call the execute method on the DeleteFromListAnimation object. /// /// /// Single element or collection of elements which will be deleted. /// When the animation completes, the application should hide the elements /// or remove them from the document. /// /// /// Single element or collection of elements whose positions were /// affected by the deletion. /// /// /// DeleteFromListAnimation object whose execute method returns /// a Promise that completes when the animation is complete. /// /// return layoutTransition(DeleteFromListAnimation, deleted, remaining); }, _createUpdateListAnimation: function (added, deleted, affected) { return layoutTransition(_UpdateListAnimation, added, affected, deleted); }, createAddToSearchListAnimation: function (added, affected) { /// /// /// Creates an animation for adding to a list of search results. /// This is similar to an AddToListAnimation, but faster. /// After creating the AddToSearchListAnimation object, /// modify the document to move the elements to their new positions, /// then call the execute method on the AddToSearchListAnimation object. /// /// /// Single element or collection of elements which were added. /// /// /// Single element or collection of elements whose positions were /// affected by the add. /// /// /// AddToSearchListAnimation object whose execute method returns /// a Promise that completes when the animation is complete. /// /// return layoutTransition(AddToSearchListAnimation, added, affected); }, createDeleteFromSearchListAnimation: function (deleted, remaining) { /// /// /// Creates an animation for deleting from a list of search results. /// This is similar to an DeleteFromListAnimation, but faster. /// After creating the DeleteFromSearchListAnimation object, /// modify the document to move the elements to their new positions, /// then call the execute method on the DeleteFromSearchListAnimation object. /// /// /// Single element or collection of elements which will be deleted. /// When the animation completes, the application should hide the elements /// or remove them from the document. /// /// /// Single element or collection of elements whose positions were /// affected by the deletion. /// /// /// DeleteFromSearchListAnimation object whose execute method returns /// a Promise that completes when the animation is complete. /// /// return layoutTransition(DeleteFromSearchListAnimation, deleted, remaining); }, showEdgeUI: function (element, offset, options) { /// /// /// Slides an element or elements into position at the edge of the screen. /// This animation is designed for a small object like an appbar. /// /// /// Single element or collection of elements to be slid into position. /// The elements should be at their final positions /// at the time the function is called. /// /// /// Optional offset object or collection of offset objects /// array describing the starting point of the animation. /// If the number of offset objects is less than the length of the /// element parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// /// /// Optional object which can specify the mechanism to use to play the animation. By default css /// animations are used but if { mechanism: "transition" } is provided css transitions will be used. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("showEdgeUI,StartTM"); var offsetArray = new OffsetArray(offset, "WinJS-showEdgeUI", [{ top: "-70px", left: "0px" }]); return _TransitionAnimation[((options && options.mechanism === "transition") ? "executeTransition" : "executeAnimation")]( element, { keyframe: offsetArray.keyframe, property: transformNames.cssName, delay: 0, duration: 367, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: offsetArray.keyframe || translateCallback(offsetArray), to: "none" }) .then(function () { writeAnimationProfilerMark("showEdgeUI,StopTM"); }); }, showPanel: function (element, offset) { /// /// /// Slides an element or elements into position at the edge of the screen. /// This animation is designed for a large object like a keyboard. /// /// /// Single element or collection of elements to be slid into position. /// The elements should be at their final positions /// at the time the function is called. /// /// /// Optional offset object or collection of offset objects /// array describing the starting point of the animation. /// If the number of offset objects is less than the length of the /// element parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// /// /// promise object /// /// writeAnimationProfilerMark("showPanel,StartTM"); var offsetArray = new OffsetArray(offset, "WinJS-showPanel", [{ top: "0px", left: "364px", rtlflip: true }]); return _TransitionAnimation.executeAnimation( element, { keyframe: offsetArray.keyframe, property: transformNames.cssName, delay: 0, duration: 550, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: offsetArray.keyframe || translateCallback(offsetArray), to: "none" }) .then(function () { writeAnimationProfilerMark("showPanel,StopTM"); }); }, hideEdgeUI: function (element, offset, options) { /// /// /// Slides an element or elements at the edge of the screen out of view. /// This animation is designed for a small object like an appbar. /// /// /// Single element or collection of elements to be slid out. /// The elements should be at their onscreen positions /// at the time the function is called. /// /// /// Optional offset object or collection of offset objects /// array describing the ending point of the animation. /// If the number of offset objects is less than the length of the /// element parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// /// /// Optional object which can specify the mechanism to use to play the animation. By default css /// animations are used but if { mechanism: "transition" } is provided css transitions will be used. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("hideEdgeUI,StartTM"); var offsetArray = new OffsetArray(offset, "WinJS-hideEdgeUI", [{ top: "-70px", left: "0px" }]); return _TransitionAnimation[((options && options.mechanism === "transition") ? "executeTransition" : "executeAnimation")]( element, { keyframe: offsetArray.keyframe, property: transformNames.cssName, delay: 0, duration: 367, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: "none", to: offsetArray.keyframe || translateCallback(offsetArray) }) .then(function () { writeAnimationProfilerMark("hideEdgeUI,StopTM"); }); }, hidePanel: function (element, offset) { /// /// /// Slides an element or elements at the edge of the screen out of view. /// This animation is designed for a large object like a keyboard. /// /// /// Single element or collection of elements to be slid out. /// The elements should be at their onscreen positions /// at the time the function is called. /// /// /// Optional offset object or collection of offset objects /// array describing the ending point of the animation. /// If the number of offset objects is less than the length of the /// element parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("hidePanel,StartTM"); var offsetArray = new OffsetArray(offset, "WinJS-hidePanel", [{ top: "0px", left: "364px", rtlflip: true }]); return _TransitionAnimation.executeAnimation( element, { keyframe: offsetArray.keyframe, property: transformNames.cssName, delay: 0, duration: 550, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: "none", to: offsetArray.keyframe || translateCallback(offsetArray) }) .then(function () { writeAnimationProfilerMark("hidePanel,StopTM"); }); }, showPopup: function (element, offset) { /// /// /// Displays an element or elements in the style of a popup. /// /// /// Single element or collection of elements to be shown like a popup. /// /// /// Optional offset object or collection of offset objects /// array describing the starting point of the animation. /// If the number of offset objects is less than the length of the /// element parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("showPopup,StartTM"); var offsetArray = new OffsetArray(offset, "WinJS-showPopup", [{ top: "50px", left: "0px" }]); return _TransitionAnimation.executeAnimation( element, [{ keyframe: "WinJS-opacity-in", property: "opacity", delay: 83, duration: 83, timing: "linear", from: 0, to: 1 }, { keyframe: offsetArray.keyframe, property: transformNames.cssName, delay: 0, duration: 367, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: offsetArray.keyframe || translateCallback(offsetArray), to: "none" }]) .then(function () { writeAnimationProfilerMark("showPopup,StopTM"); }); }, hidePopup: function (element) { /// /// /// Removes a popup from the screen. /// /// /// Single element or collection of elements to be hidden like a popup. /// When the animation completes, the application should hide the elements /// or remove them from the document. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("hidePopup,StartTM"); return _TransitionAnimation.executeAnimation( element, { keyframe: "WinJS-opacity-out", property: "opacity", delay: 0, duration: 83, timing: "linear", from: 1, to: 0 }) .then(function () { writeAnimationProfilerMark("hidePopup,StopTM"); }); }, pointerDown: function (element) { /// /// /// Execute a pointer-down animation. /// Use the pointerUp animation to reverse the effect of this animation. /// /// /// Single element or collection of elements responding to the /// pointer-down event. /// At the end of the animation, the elements' properties have been /// modified to reflect the pointer-down state. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("pointerDown,StartTM"); return _TransitionAnimation.executeTransition( element, { property: transformNames.cssName, delay: 0, duration: 167, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "scale(0.975, 0.975)" }) .then(function () { writeAnimationProfilerMark("pointerDown,StopTM"); }); }, pointerUp: function (element) { /// /// /// Execute a pointer-up animation. /// This reverses the effect of a pointerDown animation. /// /// /// Single element or collection of elements responding to /// the pointer-up event. /// At the end of the animation, the elements' properties have been /// returned to normal. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("pointerUp,StartTM"); return _TransitionAnimation.executeTransition( element, { property: transformNames.cssName, delay: 0, duration: 167, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }) .then(function () { writeAnimationProfilerMark("pointerUp,StopTM"); }); }, dragSourceStart: function (dragSource, affected) { /// /// /// Execute a drag-start animation. /// Use the dragSourceEnd animation to reverse the effects of this animation. /// /// /// Single element or collection of elements being dragged. /// At the end of the animation, the elements' properties have been /// modified to reflect the drag state. /// /// /// Single element or collection of elements to highlight as not /// being dragged. /// At the end of the animation, the elements' properties have been /// modified to reflect the drag state. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("dragSourceStart,StartTM"); var promise1 = _TransitionAnimation.executeTransition( dragSource, [{ property: transformNames.cssName, delay: 0, duration: 240, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "scale(1.05)" }, { property: "opacity", delay: 0, duration: 240, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: 0.65 }]); var promise2 = _TransitionAnimation.executeTransition( affected, { property: transformNames.cssName, delay: 0, duration: 240, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "scale(0.95)" }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("dragSourceStart,StopTM"); }); }, dragSourceEnd: function (dragSource, offset, affected) { /// /// /// Execute a drag-end animation. /// This reverses the effect of the dragSourceStart animation. /// /// /// Single element or collection of elements no longer being dragged. /// At the end of the animation, the elements' properties have been /// returned to normal. /// /// /// Optional offset object or collection of offset objects /// array describing the starting point of the animation. /// If the number of offset objects is less than the length of the /// dragSource parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// /// /// Single element or collection of elements which were highlighted as not /// being dragged. /// At the end of the animation, the elements' properties have been /// returned to normal. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("dragSourceEnd,StartTM"); var offsetArray = new OffsetArray(offset, "WinJS-dragSourceEnd"); var promise1 = _TransitionAnimation.executeTransition( dragSource, [{ property: transformNames.cssName, delay: 0, duration: 500, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" // this removes the scale }, { property: "opacity", delay: 0, duration: 500, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: 1 }]); var promise2 = _TransitionAnimation.executeAnimation( dragSource, { keyframe: offsetArray.keyframe, property: transformNames.cssName, delay: 0, duration: 500, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: offsetArray.keyframe || translateCallback(offsetArray, "scale(1.05) "), to: "none" }); var promise3 = _TransitionAnimation.executeTransition( affected, { property: transformNames.cssName, delay: 0, duration: 500, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }); return Promise.join([promise1, promise2, promise3]) .then(function () { writeAnimationProfilerMark("dragSourceEnd,StopTM"); }); }, enterContent: function (incoming, offset, options) { /// /// /// Execute an enter-content animation. /// /// /// Single element or collection of elements which represent /// the incoming content. /// At the end of the animation, the opacity of the elements is 1. /// /// /// Optional offset object or collection of offset objects /// array describing the starting point of the animation. /// If the number of offset objects is less than the length of the /// incoming parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// /// /// Optional object which can specify the mechanism to use to play the animation. By default css /// animations are used but if { mechanism: "transition" } is provided css transitions will be used. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("enterContent,StartTM"); var animationPromise; var offsetArray = new OffsetArray(offset, "WinJS-enterContent", [{ top: "0px", left: "40px", rtlflip: true }]); if (options && options.mechanism === "transition") { animationPromise = _TransitionAnimation.executeTransition( incoming, [{ property: transformNames.cssName, delay: 0, duration: 550, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: translateCallback(offsetArray), to: "none" }, { property: "opacity", delay: 0, duration: 170, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: 0, to: 1 }]); } else { var promise1 = _TransitionAnimation.executeAnimation( incoming, { keyframe: offsetArray.keyframe, property: transformNames.cssName, delay: 0, duration: 550, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: offsetArray.keyframe || translateCallback(offsetArray), to: "none" }); var promise2 = _TransitionAnimation.executeTransition( incoming, { property: "opacity", delay: 0, duration: 170, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: 0, to: 1 }); animationPromise = Promise.join([promise1, promise2]); } return animationPromise.then(function () { writeAnimationProfilerMark("enterContent,StopTM"); }); }, exitContent: function (outgoing, offset) { /// /// /// Execute an exit-content animation. /// /// /// Single element or collection of elements which represent /// the outgoing content. /// At the end of the animation, the opacity of the elements is 0. /// /// /// Optional offset object or collection of offset objects /// array describing the ending point of the animation. /// If the number of offset objects is less than the length of the /// outgoing parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("exitContent,StartTM"); var offsetArray = new OffsetArray(offset, "WinJS-exit", [{ top: "0px", left: "0px" }]); var promise1 = _TransitionAnimation.executeAnimation( outgoing, offset && { keyframe: offsetArray.keyframe, property: transformNames.cssName, delay: 0, duration: 117, timing: "linear", from: "none", to: offsetArray.keyframe || translateCallback(offsetArray) }); var promise2 = _TransitionAnimation.executeTransition( outgoing, { property: "opacity", delay: 0, duration: 117, timing: "linear", to: 0 }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("exitContent,StopTM"); }); }, dragBetweenEnter: function (target, offset) { /// /// /// Execute an animation which indicates that a dragged object /// can be dropped between other elements. /// Use the dragBetweenLeave animation to reverse the effects of this animation. /// /// /// Single element or collection of elements (usually two) /// that the dragged object can be dropped between. /// At the end of the animation, the elements' properties have been /// modified to reflect the drag-between state. /// /// /// Optional offset object or collection of offset objects /// array describing the ending point of the animation. /// If the number of offset objects is less than the length of the /// element parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("dragBetweenEnter,StartTM"); var offsetArray = new OffsetArray(offset, null, [{ top: "-40px", left: "0px" }, { top: "40px", left: "0px" }]); return _TransitionAnimation.executeTransition( target, { property: transformNames.cssName, delay: 0, duration: 200, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: translateCallback(offsetArray, "scale(0.95) ") }) .then(function () { writeAnimationProfilerMark("dragBetweenEnter,StopTM"); }); }, dragBetweenLeave: function (target) { /// /// /// Execute an animation which indicates that a dragged object /// will no longer be dropped between other elements. /// This reverses the effect of the dragBetweenEnter animation. /// /// /// Single element or collection of elements (usually two) /// that the dragged object no longer will be dropped between. /// At the end of the animation, the elements' properties have been /// set to the dragSourceStart state. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("dragBetweenLeave,StartTM"); return _TransitionAnimation.executeTransition( target, { property: transformNames.cssName, delay: 0, duration: 200, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "scale(0.95)" }) .then(function () { writeAnimationProfilerMark("dragBetweenLeave,StopTM"); }); }, swipeSelect: function (selected, selection) { /// /// /// Slide a swipe-selected object back into position when the /// pointer is released, and show the selection mark. /// /// /// Single element or collection of elements being selected. /// At the end of the animation, the elements' properties have been /// returned to normal. /// /// /// Single element or collection of elements that is the selection mark. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("swipeSelect,StartTM"); var promise1 = _TransitionAnimation.executeTransition( selected, { property: transformNames.cssName, delay: 0, duration: 300, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }); var promise2 = _TransitionAnimation.executeAnimation( selection, { keyframe: "WinJS-opacity-in", property: "opacity", delay: 0, duration: 300, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: 0, to: 1 }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("swipeSelect,StopTM"); }); }, swipeDeselect: function (deselected, selection) { /// /// /// Slide a swipe-deselected object back into position when the /// pointer is released, and hide the selection mark. /// /// /// Single element or collection of elements being deselected. /// At the end of the animation, the elements' properties have been /// returned to normal. /// /// /// Single element or collection of elements that is the selection mark. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("swipeDeselect,StartTM"); var promise1 = _TransitionAnimation.executeTransition( deselected, { property: transformNames.cssName, delay: 0, duration: 300, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" }); var promise2 = _TransitionAnimation.executeAnimation( selection, { keyframe: "WinJS-opacity-out", property: "opacity", delay: 0, duration: 300, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: 1, to: 0 }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("swipeDeselect,StopTM"); }); }, swipeReveal: function (target, offset) { /// /// /// Reveal an object as the result of a swipe, or slide the /// swipe-selected object back into position after the reveal. /// /// /// Single element or collection of elements being selected. /// At the end of the animation, the elements' properties have been /// modified to reflect the specified offset. /// /// /// Optional offset object or collection of offset objects /// array describing the ending point of the animation. /// When moving the object back into position, the offset should be /// { top: "0px", left: "0px" }. /// If the number of offset objects is less than the length of the /// element parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// The default value describes the motion for a reveal. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("swipeReveal,StartTM"); var offsetArray = new OffsetArray(offset, null, [{ top: "25px", left: "0px" }]); return _TransitionAnimation.executeTransition( target, { property: transformNames.cssName, delay: 0, duration: 300, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: translateCallback(offsetArray) }) .then(function () { writeAnimationProfilerMark("swipeReveal,StopTM"); }); }, enterPage: function (element, offset) { /// /// /// Execute an enterPage animation. /// /// /// Single element or collection of elements representing the /// incoming page. /// At the end of the animation, the opacity of the elements is 1. /// /// /// Optional offset object or collection of offset objects /// array describing the starting point of the animation. /// If the number of offset objects is less than the length of the /// element parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("enterPage,StartTM"); var offsetArray = new OffsetArray(offset, "WinJS-enterPage", [{ top: "0px", left: "100px", rtlflip: true }]); var promise1 = _TransitionAnimation.executeAnimation( element, { keyframe: offsetArray.keyframe, property: transformNames.cssName, delay: staggerDelay(0, 83, 1, 333), duration: 1000, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: offsetArray.keyframe || translateCallback(offsetArray), to: "none" }); var promise2 = _TransitionAnimation.executeTransition( element, { property: "opacity", delay: staggerDelay(0, 83, 1, 333), duration: 170, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: 0, to: 1 }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("enterPage,StopTM"); }); }, exitPage: function (outgoing, offset) { /// /// /// Execute an exitPage animation. /// /// /// Single element or collection of elements representing /// the outgoing page. /// At the end of the animation, the opacity of the elements is 0. /// /// /// Optional offset object or collection of offset objects /// array describing the ending point of the animation. /// If the number of offset objects is less than the length of the /// outgoing parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("exitPage,StartTM"); var offsetArray = new OffsetArray(offset, "WinJS-exit", [{ top: "0px", left: "0px" }]); var promise1 = _TransitionAnimation.executeAnimation( outgoing, offset && { keyframe: offsetArray.keyframe, property: transformNames.cssName, delay: 0, duration: 117, timing: "linear", from: "none", to: offsetArray.keyframe || translateCallback(offsetArray) }); var promise2 = _TransitionAnimation.executeTransition( outgoing, { property: "opacity", delay: 0, duration: 117, timing: "linear", to: 0 }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("exitPage,StopTM"); }); }, crossFade: function (incoming, outgoing) { /// /// /// Execute a crossFade animation. /// /// /// Single incoming element or collection of incoming elements. /// At the end of the animation, the opacity of the elements is 1. /// /// /// Single outgoing element or collection of outgoing elements. /// At the end of the animation, the opacity of the elements is 0. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("crossFade,StartTM"); var promise1 = _TransitionAnimation.executeTransition( incoming, { property: "opacity", delay: 0, duration: 167, timing: "linear", to: 1 }); var promise2 = _TransitionAnimation.executeTransition( outgoing, { property: "opacity", delay: 0, duration: 167, timing: "linear", to: 0 }); return Promise.join([promise1, promise2]) .then(function () { writeAnimationProfilerMark("crossFade,StopTM"); }); }, createPeekAnimation: function (element) { /// /// /// Creates a peek animation. /// After creating the PeekAnimation object, /// modify the document to move the elements to their new positions, /// then call the execute method on the PeekAnimation object. /// /// /// Single element or collection of elements to be repositioned for peek. /// /// /// PeekAnimation object whose execute method returns /// a Promise that completes when the animation is complete. /// /// return layoutTransition(PeekAnimation, null, element); }, updateBadge: function (incoming, offset) { /// /// /// Execute an updateBadge animation. /// /// /// Single element or collection of elements representing the /// incoming badge. /// /// /// Optional offset object or collection of offset objects /// array describing the starting point of the animation. /// If the number of offset objects is less than the length of the /// incoming parameter, then the last value is repeated for all /// remaining elements. /// If this parameter is omitted, then a default value is used. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("updateBadge,StartTM"); var offsetArray = new OffsetArray(offset, "WinJS-updateBadge", [{ top: "24px", left: "0px" }]); return _TransitionAnimation.executeAnimation( incoming, [{ keyframe: "WinJS-opacity-in", property: "opacity", delay: 0, duration: 367, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: 0, to: 1 }, { keyframe: offsetArray.keyframe, property: transformNames.cssName, delay: 0, duration: 1333, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", from: offsetArray.keyframe || translateCallback(offsetArray), to: "none" }]) .then(function () { writeAnimationProfilerMark("updateBadge,StopTM"); }); }, turnstileForwardIn: function (incomingElements) { /// /// /// Execute a turnstile forward in animation. /// /// /// Single element or collection of elements to animate. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("turnstileForwardIn,StartTM"); incomingElements = makeArray(incomingElements); var origins = collectTurnstileTransformOrigins(incomingElements); return animRotationTransform( incomingElements, origins, [{ property: transformNames.cssName, delay: staggerDelay(0, 50, 1, 1000), duration: 300, timing: "cubic-bezier(0.01,0.975,0.4775,0.9775)", from: "perspective(600px) rotateY(80deg)", to: "perspective(600px) rotateY(0deg)" }, { property: "opacity", delay: staggerDelay(0, 50, 1, 1000), duration: 300, timing: "cubic-bezier(0, 2, 0, 2)", from: 0, to: 1, }]) .then(function () { writeAnimationProfilerMark("turnstileForwardIn,StopTM"); }); }, turnstileForwardOut: function (outgoingElements) { /// /// /// Execute a turnstile forward out animation. /// /// /// Single element or collection of elements to animate. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("turnstileForwardOut,StartTM"); outgoingElements = makeArray(outgoingElements); var origins = collectTurnstileTransformOrigins(outgoingElements); return animRotationTransform( outgoingElements, origins, [{ property: transformNames.cssName, delay: staggerDelay(0, 50, 1, 1000), duration: 128, timing: "cubic-bezier(0.4925,0.01,0.7675,-0.01)", from: "perspective(600px) rotateY(0deg)", to: "perspective(600px) rotateY(-50deg)", }, { property: "opacity", delay: staggerDelay(0, 50, 1, 1000), duration: 128, timing: "cubic-bezier(1,-0.42,0.995,-0.425)", from: 1, to: 0, }]) .then(function () { writeAnimationProfilerMark("turnstileForwardOut,StopTM"); }); }, turnstileBackwardIn: function (incomingElements) { /// /// /// Execute a turnstile backwards in animation. /// /// /// Single element or collection of elements to animate. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("turnstileBackwardIn,StartTM"); incomingElements = makeArray(incomingElements); var origins = collectTurnstileTransformOrigins(incomingElements); return animRotationTransform( incomingElements, origins, [{ property: transformNames.cssName, delay: staggerDelay(0, 50, 1, 1000), duration: 300, timing: "cubic-bezier(0.01,0.975,0.4775,0.9775)", from: "perspective(600px) rotateY(-50deg)", to: "perspective(600px) rotateY(0deg)" }, { property: "opacity", delay: staggerDelay(0, 50, 1, 1000), duration: 300, timing: "cubic-bezier(0, 2, 0, 2)", from: 0, to: 1, }]) .then(function () { writeAnimationProfilerMark("turnstileBackwardIn,StopTM"); }); }, turnstileBackwardOut: function (outgoingElements) { /// /// /// Execute a turnstile backward out animation. /// /// /// Single element or collection of elements to animate. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("turnstileBackwardOut,StartTM"); outgoingElements = makeArray(outgoingElements); var origins = collectTurnstileTransformOrigins(outgoingElements); return animRotationTransform( outgoingElements, origins, [{ property: transformNames.cssName, delay: staggerDelay(0, 50, 1, 1000), duration: 128, timing: "cubic-bezier(0.4925,0.01,0.7675,-0.01)", from: "perspective(800px) rotateY(0deg)", to: "perspective(800px) rotateY(80deg)", }, { property: "opacity", delay: staggerDelay(0, 50, 1, 1000), duration: 128, timing: "cubic-bezier(1,-0.42,0.995,-0.425)", from: 1, to: 0, }]) .then(function () { writeAnimationProfilerMark("turnstileBackwardOut,StopTM"); }); }, slideDown: function (outgoingElements) { /// /// /// Execute a slide down animation. /// /// /// Single element or collection of elements to animate sliding down. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("slideDown,StartTM"); return animRotationTransform( outgoingElements, { ltr: "", rtl: "" }, [{ property: transformNames.cssName, delay: 0, duration: 250, timing: "cubic-bezier(0.3825,0.0025,0.8775,-0.1075)", from: "translate(0px, 0px)", to: "translate(0px, 200px)" }, { property: "opacity", delay: 0, duration: 250, timing: "cubic-bezier(1,-0.42,0.995,-0.425)", from: 1, to: 0 }]) .then(function () { writeAnimationProfilerMark("slideDown,StopTM"); }); }, slideUp: function (incomingElements) { /// /// /// Execute a slide up animation. /// /// /// Single element or collection of elements to animate sliding up. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("slideUp,StartTM"); return animRotationTransform( incomingElements, { ltr: "", rtl: "" }, [{ property: transformNames.cssName, delay: 0, duration: 350, timing: "cubic-bezier(0.17,0.79,0.215,1.0025)", from: "translate(0px, 200px)", to: "translate(0px, 0px)" }, { property: "opacity", delay: staggerDelay(0, 34, 1, 1000), duration: 350, timing: "cubic-bezier(0, 2, 0, 2)", from: 0, to: 1, }]) .then(function () { writeAnimationProfilerMark("slideUp,StopTM"); }); }, slideRightIn: function (page, firstIncomingElements, secondIncomingElements, thirdIncomingElements) { /// /// /// Execute a slide in from left to right animation. /// /// /// The page containing all elements to slide. /// /// /// First element or collection of elements to animate sliding in. /// /// /// Second element or collection of elements to animate sliding in, which will be offset slightly farther than the first. /// /// /// Third element or collection of elements to animate sliding in, which will be offset slightly farther than the second. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("slideRightIn,StartTM"); return animStaggeredSlide("cubic-bezier(0.17,0.79,0.215,1.0025)", -_Global.innerWidth, 0, true, page, firstIncomingElements, secondIncomingElements, thirdIncomingElements) .then(function () { writeAnimationProfilerMark("slideRightIn,StopTM"); }); }, slideRightOut: function (page, firstOutgoingElements, secondOutgoingElements, thirdOutgoingElements) { /// /// /// Execute a slide out from left to right animation. /// /// /// The page containing all elements to slide. /// /// /// First element or collection of elements to animate sliding out. /// /// /// Second element or collection of elements to animate sliding out, which will be offset slightly farther than the first. /// /// /// Third element or collection of elements to animate sliding out, which will be offset slightly farther than the second. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("slideRightOut,StartTM"); return animStaggeredSlide("cubic-bezier(0.3825,0.0025,0.8775,-0.1075)", 0, _Global.innerWidth, false, page, firstOutgoingElements, secondOutgoingElements, thirdOutgoingElements) .then(function () { writeAnimationProfilerMark("slideRightOut,StopTM"); }); }, slideLeftIn: function (page, firstIncomingElements, secondIncomingElements, thirdIncomingElements) { /// /// /// Execute a slide in from right to left animation. /// /// /// The page containing all elements to slide. /// /// /// First element or collection of elements to animate sliding in. /// /// /// Second element or collection of elements to animate sliding in, which will be offset slightly farther than the first. /// /// /// Third element or collection of elements to animate sliding in, which will be offset slightly farther than the second. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("slideLeftIn,StartTM"); return animStaggeredSlide("cubic-bezier(0.17,0.79,0.215,1.0025)", _Global.innerWidth, 0, true, page, firstIncomingElements, secondIncomingElements, thirdIncomingElements) .then(function () { writeAnimationProfilerMark("slideLeftIn,StopTM"); }); }, slideLeftOut: function (page, firstOutgoingElements, secondOutgoingElements, thirdOutgoingElements) { /// /// /// Execute a slide out from right to left animation. /// /// /// The page containing all elements to slide. /// /// /// First element or collection of elements to animate sliding out. /// /// /// Second element or collection of elements to animate sliding out, which will be offset slightly farther than the first. /// /// /// Third element or collection of elements to animate sliding out, which will be offset slightly farther than the second. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("slideLeftOut,StartTM"); return animStaggeredSlide("cubic-bezier(0.3825,0.0025,0.8775,-0.1075)", 0, -_Global.innerWidth, false, page, firstOutgoingElements, secondOutgoingElements, thirdOutgoingElements) .then(function () { writeAnimationProfilerMark("slideLeftOut,StopTM"); }); }, continuumForwardIn: function (incomingPage, incomingItemRoot, incomingItemContent) { /// /// /// Execute a continuum animation, scaling up the incoming page while scaling, rotating, and translating the incoming item. /// /// /// Single element to be scaled up that is the page root and does not contain the incoming item. /// /// /// Root of the item that will be translated as part of the continuum animation. /// /// /// Content of the item that will be scaled and rotated as part of the continuum animation. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("continuumForwardIn,StartTM"); return Promise.join([ _TransitionAnimation.executeTransition(incomingPage, [{ property: transformNames.cssName, delay: 0, duration: 350, timing: "cubic-bezier(0.33, 0.18, 0.11, 1)", from: "scale(0.5, 0.5)", to: "scale(1.0, 1.0)" }, { property: "opacity", delay: 0, duration: 350, timing: "cubic-bezier(0, 2, 0, 2)", from: 0, to: 1, }]), _TransitionAnimation.executeTransition(incomingItemRoot, [{ property: transformNames.cssName, delay: 0, duration: 350, timing: "cubic-bezier(0.24,1.15,0.11,1.1575)", from: "translate(0px, 225px)", to: "translate(0px, 0px)" }, { property: "opacity", delay: 0, duration: 350, timing: "cubic-bezier(0, 2, 0, 2)", from: 0, to: 1, }]), animRotationTransform(incomingItemContent, { ltr: "0px 50%", rtl: "100% 50%" }, [{ property: transformNames.cssName, delay: 0, duration: 350, timing: "cubic-bezier(0,0.62,0.8225,0.9625)", from: "rotateX(80deg) scale(1.5, 1.5)", to: "rotateX(0deg) scale(1.0, 1.0)" }, { property: "opacity", delay: 0, duration: 350, timing: "cubic-bezier(0, 2, 0, 2)", from: 0, to: 1, }]) ]) .then(function () { writeAnimationProfilerMark("continuumForwardIn,StopTM"); }); }, continuumForwardOut: function (outgoingPage, outgoingItem) { /// /// /// Execute a continuum animation, scaling down the outgoing page while scaling, rotating, and translating the outgoing item. /// /// /// Single element to be scaled down that is the page root and contains the outgoing item. /// /// /// Single element to be scaled, rotated, and translated away from the outgoing page. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("continuumForwardOut,StartTM"); return Promise.join([ _TransitionAnimation.executeTransition(outgoingPage, [{ property: transformNames.cssName, delay: 0, duration: 120, timing: "cubic-bezier(0.3825,0.0025,0.8775,-0.1075)", from: "scale(1.0, 1.0)", to: "scale(1.1, 1.1)" }, { property: "opacity", delay: 0, duration: 120, timing: "cubic-bezier(1,-0.42,0.995,-0.425)", from: 1, to: 0, }]), animRotationTransform(outgoingItem, { ltr: "0px 100%", rtl: "100% 100%" }, [{ property: transformNames.cssName, delay: 0, duration: 152, timing: "cubic-bezier(0.3825,0.0025,0.8775,-0.1075)", from: "rotateX(0deg) scale(1.0, 1.0) translate(0px, 0px)", to: "rotateX(80deg) scale(1.5, 1.5) translate(0px, 150px)" }, { property: "opacity", delay: 0, duration: 152, timing: "cubic-bezier(1,-0.42,0.995,-0.425)", from: 1, to: 0, }]) ]) .then(function () { writeAnimationProfilerMark("continuumForwardOut,StopTM"); }); }, continuumBackwardIn: function (incomingPage, incomingItem) { /// /// /// Execute a continuum animation, scaling down the incoming page while scaling, rotating, and translating the incoming item. /// /// /// Single element to be scaled down that is the page root and contains the incoming item. /// /// /// Single element to be scaled, rotated, and translated into its final position on the page. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("continuumBackwardIn,StartTM"); return Promise.join([ _TransitionAnimation.executeTransition(incomingPage, [{ property: transformNames.cssName, delay: 0, duration: 200, timing: "cubic-bezier(0.33, 0.18, 0.11, 1)", from: "scale(1.25, 1.25)", to: "scale(1.0, 1.0)" }, { property: "opacity", delay: 0, duration: 200, timing: "cubic-bezier(0, 2, 0, 2)", from: 0, to: 1, }]), animRotationTransform(incomingItem, { ltr: "0px 50%", rtl: "100% 50%" }, [{ property: transformNames.cssName, delay: 0, duration: 250, timing: "cubic-bezier(0.2975, 0.7325, 0.4725, 0.99)", from: "rotateX(80deg) translate(0px, -100px)", to: "rotateX(0deg) translate(0px, 0px)" }, { property: "opacity", delay: 0, duration: 250, timing: "cubic-bezier(0, 2, 0, 2)", from: 0, to: 1, }]) ]) .then(function () { writeAnimationProfilerMark("continuumBackwardIn,StopTM"); }); }, continuumBackwardOut: function (outgoingPage) { /// /// /// Execute a continuum animation, scaling down the outgoing page while. /// /// /// Single element to be scaled down that is the page root. /// /// /// Promise object that completes when the animation is complete. /// /// writeAnimationProfilerMark("continuumBackwardOut,StartTM"); return _TransitionAnimation.executeTransition(outgoingPage, [{ property: transformNames.cssName, delay: 0, duration: 167, timing: "cubic-bezier(0.3825,0.0025,0.8775,-0.1075)", from: "scale(1.0, 1.0)", to: "scale(0.5, 0.5)" }, { property: "opacity", delay: 0, duration: 167, timing: "cubic-bezier(1,-0.42,0.995,-0.425)", from: 1, to: 0, }]) .then(function () { writeAnimationProfilerMark("continuumBackwardOut,StopTM"); }); }, createPageNavigationAnimations: function (currentPreferredAnimation, nextPreferredAnimation, movingBackwards) { /// /// /// Creates an exit and entrance animation to play for a page navigation given the current and incoming pages' /// animation preferences and whether the pages are navigating forwards or backwards. /// /// /// A value from WinJS.UI.PageNavigationAnimation describing the animation the current page prefers to use. /// /// /// A value from WinJS.UI.PageNavigationAnimation describing the animation the incoming page prefers to use. /// /// /// Boolean value for whether the navigation is moving backwards. /// /// /// Returns an object containing the exit and entrance animations to play based on the parameters given. /// /// var PageNavigationAnimation = _Constants.PageNavigationAnimation; function emptyAnimationFunction() { return Promise.wrap(); } if (!_BaseUtils.isPhone || currentPreferredAnimation === PageNavigationAnimation.enterPage || nextPreferredAnimation === PageNavigationAnimation.enterPage) { return { exit: emptyAnimationFunction, entrance: exports.enterPage }; } if (!nextPreferredAnimation) { nextPreferredAnimation = PageNavigationAnimation.turnstile; } if ((currentPreferredAnimation === PageNavigationAnimation.slide && movingBackwards) || (nextPreferredAnimation === PageNavigationAnimation.slide && !movingBackwards)) { return { exit: movingBackwards ? exports.slideDown : emptyAnimationFunction, entrance: movingBackwards ? emptyAnimationFunction : exports.slideUp }; } return { exit: exports[nextPreferredAnimation + (movingBackwards ? "Backward" : "Forward") + "Out"], entrance: exports[nextPreferredAnimation + (movingBackwards ? "Backward" : "Forward") + "In"] }; } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Binding/_BindingParser',[ 'exports', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Log', '../Core/_Resources', '../Core/_WriteProfilerMark', '../ControlProcessor/_OptionsLexer', '../ControlProcessor/_OptionsParser' ], function bindingParserInit(exports, _Base, _BaseUtils, _ErrorFromName, _Log, _Resources, _WriteProfilerMark, _OptionsLexer, _OptionsParser) { "use strict"; var strings = { get invalidBinding() { return "Invalid binding:'{0}'. Expected to be ':;'. {1}"; }, get bindingInitializerNotFound() { return "Initializer not found:'{0}'"; }, }; /* See comment for data-win-options attribute grammar for context. Syntactic grammar for the value of the data-win-bind attribute. BindDeclarations: BindDeclaration BindDeclarations ; BindDeclaration BindDeclaration: DestinationPropertyName : SourcePropertyName DestinationPropertyName : SourcePropertyName InitializerName DestinationPropertyName: IdentifierExpression SourcePropertyName: IdentifierExpression InitializerName: IdentifierExpression Value: NumberLiteral StringLiteral AccessExpression: [ Value ] . Identifier AccessExpressions: AccessExpression AccessExpressions AccessExpression IdentifierExpression: Identifier Identifier AccessExpressions */ var imports = _Base.Namespace.defineWithParent(null, null, { lexer: _Base.Namespace._lazy(function () { return _OptionsLexer._optionsLexer; }), tokenType: _Base.Namespace._lazy(function () { return _OptionsLexer._optionsLexer.tokenType; }), }); var requireSupportedForProcessing = _BaseUtils.requireSupportedForProcessing; var local = _Base.Namespace.defineWithParent(null, null, { BindingInterpreter: _Base.Namespace._lazy(function () { return _Base.Class.derive(_OptionsParser.optionsParser._BaseInterpreter, function (tokens, originalSource, context) { this._initialize(tokens, originalSource, context); }, { _error: function (message) { throw new _ErrorFromName("WinJS.Binding.ParseError", _Resources._formatString(strings.invalidBinding, this._originalSource, message)); }, _evaluateInitializerName: function () { if (this._current.type === imports.tokenType.identifier) { var initializer = this._evaluateIdentifierExpression(); if (_Log.log && !initializer) { _Log.log(_Resources._formatString(strings.bindingInitializerNotFound, this._originalSource), "winjs binding", "error"); } return requireSupportedForProcessing(initializer); } return; }, _evaluateValue: function () { switch (this._current.type) { case imports.tokenType.stringLiteral: case imports.tokenType.numberLiteral: var value = this._current.value; this._read(); return value; default: this._unexpectedToken(imports.tokenType.stringLiteral, imports.tokenType.numberLiteral); return; } }, _readBindDeclarations: function () { var bindings = []; while (true) { switch (this._current.type) { case imports.tokenType.identifier: case imports.tokenType.thisKeyword: bindings.push(this._readBindDeclaration()); break; case imports.tokenType.semicolon: this._read(); break; case imports.tokenType.eof: return bindings; default: this._unexpectedToken(imports.tokenType.identifier, imports.tokenType.semicolon, imports.tokenType.eof); return; } } }, _readBindDeclaration: function () { var dest = this._readDestinationPropertyName(); this._read(imports.tokenType.colon); var src = this._readSourcePropertyName(); var initializer = this._evaluateInitializerName(); return { destination: dest, source: src, initializer: initializer, }; }, _readDestinationPropertyName: function () { return this._readIdentifierExpression(); }, _readSourcePropertyName: function () { return this._readIdentifierExpression(); }, run: function () { return this._readBindDeclarations(); } }, { supportedForProcessing: false, }); }), BindingParser: _Base.Namespace._lazy(function () { return _Base.Class.derive(local.BindingInterpreter, function (tokens, originalSource) { this._initialize(tokens, originalSource, {}); }, { _readInitializerName: function () { if (this._current.type === imports.tokenType.identifier) { return this._readIdentifierExpression(); } return; }, _readBindDeclaration: function () { var dest = this._readDestinationPropertyName(); this._read(imports.tokenType.colon); var src = this._readSourcePropertyName(); var initializer = this._readInitializerName(); return { destination: dest, source: src, initializer: initializer, }; }, }, { supportedForProcessing: false, }); }) }); function parser(text, context) { _WriteProfilerMark("WinJS.Binding:bindingParser,StartTM"); var tokens = imports.lexer(text); var interpreter = new local.BindingInterpreter(tokens, text, context || {}); var res = interpreter.run(); _WriteProfilerMark("WinJS.Binding:bindingParser,StopTM"); return res; } function parser2(text) { _WriteProfilerMark("WinJS.Binding:bindingParser,StartTM"); var tokens = imports.lexer(text); var interpreter = new local.BindingParser(tokens, text); var res = interpreter.run(); _WriteProfilerMark("WinJS.Binding:bindingParser,StopTM"); return res; } _Base.Namespace._moduleDefine(exports, "WinJS.Binding", { _bindingParser: parser, _bindingParser2: parser2, }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Binding/_DomWeakRefTable',[ 'exports', '../Core/_Global', '../Core/_WinRT', '../Core/_Base', '../Core/_BaseUtils', '../Scheduler' ], function DOMWeakRefTableInit(exports, _Global, _WinRT, _Base, _BaseUtils, Scheduler) { "use strict"; if (_WinRT.Windows.Foundation.Uri && _WinRT.msSetWeakWinRTProperty && _WinRT.msGetWeakWinRTProperty) { var host = new _WinRT.Windows.Foundation.Uri("about://blank"); _Base.Namespace._moduleDefine(exports, "WinJS.Utilities", { _createWeakRef: function (element, id) { _WinRT.msSetWeakWinRTProperty(host, id, element); return id; }, _getWeakRefElement: function (id) { return _WinRT.msGetWeakWinRTProperty(host, id); } }); return; } // Defaults var SWEEP_PERIOD = 500; var TIMEOUT = 1000; var table = {}; var cleanupToken; var noTimeoutUnderDebugger = true; var fastLoadPath = false; function cleanup() { if (SWEEP_PERIOD === 0) { // If we're using post cleanupToken = 0; // indicate that cleanup has run } var keys = Object.keys(table); var time = Date.now() - TIMEOUT; var i, len; for (i = 0, len = keys.length; i < len; i++) { var id = keys[i]; if (table[id].time < time) { delete table[id]; } } unscheduleCleanupIfNeeded(); } function scheduleCleanupIfNeeded() { if ((_Global.Debug && _Global.Debug.debuggerEnabled && noTimeoutUnderDebugger) || cleanupToken) { return; } if (SWEEP_PERIOD === 0) { Scheduler.schedule(cleanup, Scheduler.Priority.idle, null, "WinJS.Utilities._DOMWeakRefTable.cleanup"); cleanupToken = 1; } else { cleanupToken = _Global.setInterval(cleanup, SWEEP_PERIOD); } } function unscheduleCleanupIfNeeded() { if (_Global.Debug && _Global.Debug.debuggerEnabled && noTimeoutUnderDebugger) { return; } if (SWEEP_PERIOD === 0) { // if we're using post if (!cleanupToken) { // and there isn't already one scheduled if (Object.keys(table).length !== 0) { // and there are items in the table Scheduler.schedule( // schedule another call to cleanup cleanup, Scheduler.Priority.idle, null, "WinJS.Utilities._DOMWeakRefTable.cleanup" ); cleanupToken = 1; // and protect against overscheduling } } } else if (cleanupToken) { if (Object.keys(table).length === 0) { _Global.clearInterval(cleanupToken); cleanupToken = 0; } } } function createWeakRef(element, id) { table[id] = { element: element, time: Date.now() }; scheduleCleanupIfNeeded(); return id; } function getWeakRefElement(id) { if (fastLoadPath) { var entry = table[id]; if (entry) { return entry.element; } else { return _Global.document.getElementById(id); } } else { var element = _Global.document.getElementById(id); if (element) { delete table[id]; unscheduleCleanupIfNeeded(); } else { var entry = table[id]; if (entry) { entry.time = Date.now(); element = entry.element; } } return element; } } _Base.Namespace._moduleDefine(exports, "WinJS.Utilities", { _DOMWeakRefTable_noTimeoutUnderDebugger: { get: function () { return noTimeoutUnderDebugger; }, set: function (value) { noTimeoutUnderDebugger = value; } }, _DOMWeakRefTable_sweepPeriod: { get: function () { return SWEEP_PERIOD; }, set: function (value) { SWEEP_PERIOD = value; } }, _DOMWeakRefTable_timeout: { get: function () { return TIMEOUT; }, set: function (value) { TIMEOUT = value; } }, _DOMWeakRefTable_tableSize: { get: function () { return Object.keys(table).length; } }, _DOMWeakRefTable_fastLoadPath: { get: function () { return fastLoadPath; }, set: function (value) { fastLoadPath = value; } }, _createWeakRef: createWeakRef, _getWeakRefElement: getWeakRefElement }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Binding/_Data',[ 'exports', '../Core/_WinRT', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Log', '../Core/_Resources', '../Promise', '../Scheduler', './_DomWeakRefTable' ], function dataInit(exports, _WinRT, _Base, _BaseUtils, _ErrorFromName, _Log, _Resources, Promise, Scheduler, _DomWeakRefTable) { "use strict"; var strings = { get exceptionFromBindingInitializer() { return "Exception thrown from binding initializer: {0}"; }, get propertyIsUndefined() { return "{0} is undefined"; }, get unsupportedDataTypeForBinding() { return "Unsupported data type"; }, }; var observableMixin = { _listeners: null, _pendingNotifications: null, _notifyId: 0, _getObservable: function () { return this; }, _cancel: function (name) { var v = this._pendingNotifications; var hit = false; if (v) { var k = Object.keys(v); for (var i = k.length - 1; i >= 0; i--) { var entry = v[k[i]]; if (entry.target === name) { if (entry.promise) { entry.promise.cancel(); entry.promise = null; } delete v[k[i]]; hit = true; } } } return hit; }, notify: function (name, newValue, oldValue) { /// /// /// Notifies listeners that a property value was updated. /// /// The name of the property that is being updated. /// The new value for the property. /// The old value for the property. /// A promise that is completed when the notifications are complete. /// var listeners = this._listeners && this._listeners[name]; if (listeners) { var that = this; // Handle the case where we are updating a value that is currently updating // that._cancel(name); // Starting new work, we cache the work description and queue up to do the notifications // that._pendingNotifications = that._pendingNotifications || {}; var x = that._notifyId++; var cap = that._pendingNotifications[x] = { target: name }; var cleanup = function () { delete that._pendingNotifications[x]; }; // Binding guarantees async notification, so we do timeout() // cap.promise = Scheduler.schedulePromiseNormal(null, "WinJS.Binding.observableMixin.notify"). then(function () { // cap.promise is removed after canceled, so we use this as a signal // to indicate that we should abort early // for (var i = 0, l = listeners.length; i < l && cap.promise; i++) { try { listeners[i](newValue, oldValue); } catch (e) { _Log.log && _Log.log(_Resources._formatString(strings.exceptionFromBindingInitializer, e.toString()), "winjs binding", "error"); } } cleanup(); return newValue; }); return cap.promise; } return Promise.as(); }, bind: function (name, action) { /// /// /// Links the specified action to the property specified in the name parameter. /// This function is invoked when the value of the property may have changed. /// It is not guaranteed that the action will be called only when a value has actually changed, /// nor is it guaranteed that the action will be called for every value change. The implementation /// of this function coalesces change notifications, such that multiple updates to a property /// value may result in only a single call to the specified action. /// /// /// The name of the property to which to bind the action. /// /// /// The function to invoke asynchronously when the property may have changed. /// /// /// This object is returned. /// /// this._listeners = this._listeners || {}; var listeners = this._listeners[name] = this._listeners[name] || []; // duplicate detection, multiple binds with the same action should have no effect // var found = false; for (var i = 0, l = listeners.length; i < l; i++) { if (listeners[i] === action) { found = true; break; } } if (!found) { listeners.push(action); // out of band notification, we want to avoid a broadcast to all listeners // so we can't just call notify. // action(unwrap(this[name])); } return this; }, unbind: function (name, action) { /// /// /// Removes one or more listeners from the notification list for a given property. /// /// /// The name of the property to unbind. If this parameter is omitted, all listeners /// for all events are removed. /// /// /// The function to remove from the listener list for the specified property. If this parameter is omitted, all listeners /// are removed for the specific property. /// /// /// This object is returned. /// /// this._listeners = this._listeners || {}; if (name && action) { // this assumes we rarely have more than one // listener, so we optimize to not do a lot of // array manipulation, although it means we // may do some extra GC churn in the other cases... // var listeners = this._listeners[name]; if (listeners) { var nl; for (var i = 0, l = listeners.length; i < l; i++) { if (listeners[i] !== action) { (nl = nl || []).push(listeners[i]); } } this._listeners[name] = nl; } // we allow any pending notification sweep to complete, // which means that "unbind" inside of a notification // will not prevent that notification from occuring. // } else if (name) { this._cancel(name); delete this._listeners[name]; } else { var that = this; if (that._pendingNotifications) { var v = that._pendingNotifications; that._pendingNotifications = {}; Object.keys(v).forEach(function (k) { var n = v[k]; if (n.promise) { n.promise.cancel(); } }); } this._listeners = {}; } return this; } }; var dynamicObservableMixin = { _backingData: null, _initObservable: function (data) { this._backingData = data || {}; }, getProperty: function (name) { /// /// /// Gets a property value by name. /// /// /// The name of property to get. /// /// /// The value of the property as an observable object. /// /// var data = this._backingData[name]; if (_Log.log && data === undefined) { _Log.log(_Resources._formatString(strings.propertyIsUndefined, name), "winjs binding", "warn"); } return as(data); }, setProperty: function (name, value) { /// /// /// Updates a property value and notifies any listeners. /// /// /// The name of the property to update. /// /// /// The new value of the property. /// /// /// This object is returned. /// /// this.updateProperty(name, value); return this; }, addProperty: function (name, value) { /// /// /// Adds a property with change notification to this object, including a ECMAScript5 property definition. /// /// /// The name of the property to add. /// /// /// The value of the property. /// /// /// This object is returned. /// /// // we could walk Object.keys to more deterministically determine this, // however in the normal case this avoids a bunch of string compares // if (!this[name]) { Object.defineProperty(this, name, { get: function () { return this.getProperty(name); }, set: function (value) { this.setProperty(name, value); }, enumerable: true, configurable: true } ); } return this.setProperty(name, value); }, updateProperty: function (name, value) { /// /// /// Updates a property value and notifies any listeners. /// /// /// The name of the property to update. /// /// /// The new value of the property. /// /// /// A promise that completes when the notifications for /// this property change have been processed. If multiple notifications are coalesced, /// the promise may be canceled or the value of the promise may be updated. /// The fulfilled value of the promise is the new value of the property for /// which the notifications have been completed. /// /// var oldValue = this._backingData[name]; var newValue = unwrap(value); if (oldValue !== newValue) { this._backingData[name] = newValue; // This will complete when the listeners are notified, even // if a new value is used. The only time this promise will fail // (cancel) will be if we start notifying and then have to // cancel in the middle of processing it. That's a pretty // subtle contract. // // IE has a bug where readonly properties will not throw, // even in strict mode, when set using a string accessor. // To be consistent across browsers, only notify if the // set succeeded. if (this._backingData[name] === newValue) { return this.notify(name, newValue, oldValue); } } return Promise.as(); }, removeProperty: function (name) { /// /// /// Removes a property value. /// /// /// The name of the property to remove. /// /// /// This object is returned. /// /// var oldValue = this._backingData[name]; var value; // capture "undefined" // in strict mode these may throw try { delete this._backingData[name]; } catch (e) { } try { delete this[name]; } catch (e) { } this.notify(name, value, oldValue); return this; } }; // Merge "obsevable" into "dynamicObservable" // Object.keys(observableMixin).forEach(function (k) { dynamicObservableMixin[k] = observableMixin[k]; }); var bind = function (observable, bindingDescriptor) { /// /// /// Binds to one or more properties on the observable object or or on child values /// of that object. /// /// /// The object to bind to. /// /// /// An object literal containing the binding declarations. Binding declarations take the form: /// { propertyName: (function | bindingDeclaration), ... } /// /// For example, binding to a nested member of an object is declared like this: /// bind(someObject, { address: { street: function(v) { ... } } }); /// /// /// An object that contains at least a "cancel" field, which is /// a function that removes all bindings associated with this bind /// request. /// /// return bindImpl(observable, bindingDescriptor); }; var bindRefId = 0; var createBindRefId = function () { return "bindHandler" + (bindRefId++); }; var createProxy = function (func, bindStateRef) { if (!_WinRT.msGetWeakWinRTProperty) { return func; } var id = createBindRefId(); _DomWeakRefTable._getWeakRefElement(bindStateRef)[id] = func; return function (n, o) { var bindState = _DomWeakRefTable._getWeakRefElement(bindStateRef); if (bindState) { bindState[id](n, o); } }; }; var bindImpl = function (observable, bindingDescriptor, bindStateRef) { observable = as(observable); if (!observable) { return { cancel: function () { }, empty: true }; } var bindState; if (!bindStateRef) { bindStateRef = createBindRefId(); bindState = {}; _DomWeakRefTable._createWeakRef(bindState, bindStateRef); } var complexLast = {}; var simpleLast = null; function cancelSimple() { if (simpleLast) { simpleLast.forEach(function (e) { e.source.unbind(e.prop, e.listener); }); } simpleLast = null; } function cancelComplex(k) { if (complexLast[k]) { complexLast[k].complexBind.cancel(); delete complexLast[k]; } } Object.keys(bindingDescriptor).forEach(function (k) { var listener = bindingDescriptor[k]; if (listener instanceof Function) { // Create a proxy for the listener which indirects weakly through the bind // state, if this is the root object tack the bind state onto the listener // listener = createProxy(listener, bindStateRef); listener.bindState = bindState; simpleLast = simpleLast || []; simpleLast.push({ source: observable, prop: k, listener: listener }); observable.bind(k, listener); } else { var propChanged = function (v) { cancelComplex(k); var complexBind = bindImpl(as(v), listener, bindStateRef); // In the case that we hit an "undefined" in the chain, we prop the change // notification to all listeners, basically saying that x.y.z where "y" // is undefined resolves to undefined. // if (complexBind.empty) { var recursiveNotify = function (root) { Object.keys(root).forEach(function (key) { var item = root[key]; if (item instanceof Function) { item(undefined, undefined); } else { recursiveNotify(item); } }); }; recursiveNotify(listener); } complexLast[k] = { source: v, complexBind: complexBind }; }; // Create a proxy for the listener which indirects weakly through the bind // state, if this is the root object tack the bind state onto the listener // propChanged = createProxy(propChanged, bindStateRef); propChanged.bindState = bindState; simpleLast = simpleLast || []; simpleLast.push({ source: observable, prop: k, listener: propChanged }); observable.bind(k, propChanged); } }); return { cancel: function () { cancelSimple(); Object.keys(complexLast).forEach(function (k) { cancelComplex(k); }); } }; }; var ObservableProxy = _Base.Class.mix(function (data) { this._initObservable(data); Object.defineProperties(this, expandProperties(data)); }, dynamicObservableMixin); var expandProperties = function (shape) { /// /// /// Wraps the specified object so that all its properties /// are instrumented for binding. This is meant to be used in /// conjunction with the binding mixin. /// /// /// The specification for the bindable object. /// /// /// An object with a set of properties all of which are wired for binding. /// /// var props = {}; function addToProps(k) { props[k] = { get: function () { return this.getProperty(k); }, set: function (value) { this.setProperty(k, value); }, enumerable: true, configurable: true // enables delete }; } while (shape && shape !== Object.prototype) { Object.keys(shape).forEach(addToProps); shape = Object.getPrototypeOf(shape); } return props; }; var define = function (data) { /// /// /// Creates a new constructor function that supports observability with /// the specified set of properties. /// /// /// The object to use as the pattern for defining the set of properties, for example: /// var MyPointClass = define({x:0,y:0}); /// /// /// A constructor function with 1 optional argument that is the initial state of /// the properties. /// /// // Common unsupported types, we just coerce to be an empty record // if (!data || typeof (data) !== "object" || (data instanceof Date) || Array.isArray(data)) { if (_BaseUtils.validation) { throw new _ErrorFromName("WinJS.Binding.UnsupportedDataType", _Resources._formatString(strings.unsupportedDataTypeForBinding)); } else { return; } } return _Base.Class.mix( function (init) { /// /// /// Creates a new observable object. /// /// /// The initial values for the properties. /// /// this._initObservable(init || Object.create(data)); }, dynamicObservableMixin, expandProperties(data) ); }; var as = function (data) { /// /// /// Returns an observable object. This may be an observable proxy for the specified object, an existing proxy, or /// the specified object itself if it directly supports observability. /// /// /// Object to provide observability for. /// /// /// The observable object. /// /// if (!data) { return data; } var type = typeof data; if (type === "object" && !(data instanceof Date) && !(Array.isArray(data))) { if (data._getObservable) { return data._getObservable(); } var observable = new ObservableProxy(data); observable.backingData = data; Object.defineProperty( data, "_getObservable", { value: function () { return observable; }, enumerable: false, writable: false } ); return observable; } else { return data; } }; var unwrap = function (data) { /// /// /// Returns the original (non-observable) object is returned if the specified object is an observable proxy, . /// /// /// The object for which to retrieve the original value. /// /// /// If the specified object is an observable proxy, the original object is returned, otherwise the same object is returned. /// /// if (data && data.backingData) { return data.backingData; } else { return data; } }; _Base.Namespace._moduleDefine(exports, "WinJS.Binding", { // must use long form because mixin has "get" and "set" as members, so the define // method thinks it's a property mixin: { value: dynamicObservableMixin, enumerable: false, writable: true, configurable: true }, dynamicObservableMixin: { value: dynamicObservableMixin, enumerable: true, writable: true, configurable: true }, observableMixin: { value: observableMixin, enumerable: true, writable: true, configurable: true }, expandProperties: expandProperties, define: define, as: as, unwrap: unwrap, bind: bind }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Binding/_Declarative',[ 'exports', '../Core/_Global', '../Core/_WinRT', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Log', '../Core/_Resources', '../Core/_WriteProfilerMark', '../Promise', '../Utilities/_ElementUtilities', './_BindingParser', './_Data', './_DomWeakRefTable' ], function declarativeInit(exports, _Global, _WinRT, _Base, _BaseUtils, _ErrorFromName, _Log, _Resources, _WriteProfilerMark, Promise, _ElementUtilities, _BindingParser, _Data, _DomWeakRefTable) { "use strict"; var uid = (Math.random() * 1000) >> 0; // If we have proper weak references then we can move away from using the element's ID property // var optimizeBindingReferences = _WinRT.msSetWeakWinRTProperty && _WinRT.msGetWeakWinRTProperty; var strings = { get attributeBindingSingleProperty() { return "Attribute binding requires a single destination attribute name, often in the form \"this['aria-label']\" or \"width\"."; }, get cannotBindToThis() { return "Can't bind to 'this'."; }, get creatingNewProperty() { return "Creating new property {0}. Full path:{1}"; }, get duplicateBindingDetected() { return "Binding against element with id {0} failed because a duplicate id was detected."; }, get elementNotFound() { return "Element not found:{0}"; }, get errorInitializingBindings() { return "Error initializing bindings: {0}"; }, get propertyDoesNotExist() { return "{0} doesn't exist. Full path:{1}"; }, get idBindingNotSupported() { return "Declarative binding to ID field is not supported. Initializer: {0}"; }, get nestedDOMElementBindingNotSupported() { return "Binding through a property {0} of type HTMLElement is not supported, Full path:{1}."; } }; var markSupportedForProcessing = _BaseUtils.markSupportedForProcessing; var requireSupportedForProcessing = _BaseUtils.requireSupportedForProcessing; function registerAutoDispose(bindable, callback) { var d = bindable._autoDispose; d && d.push(callback); } function autoDispose(bindable) { bindable._autoDispose = (bindable._autoDispose || []).filter(function (callback) { return callback(); }); } function checkBindingToken(element, bindingId) { if (element) { if (element.winBindingToken === bindingId) { return element; } else { _Log.log && _Log.log(_Resources._formatString(strings.duplicateBindingDetected, element.id), "winjs binding", "error"); } } else { return element; } } function setBindingToken(element) { if (element.winBindingToken) { return element.winBindingToken; } var bindingToken = "_win_bind" + (uid++); Object.defineProperty(element, "winBindingToken", { configurable: false, writable: false, enumerable: false, value: bindingToken }); return bindingToken; } function initializerOneBinding(bind, ref, bindingId, source, e, pend, cacheEntry) { var initializer = bind.initializer; if (initializer) { initializer = initializer.winControl || initializer["data-win-control"] || initializer; } if (initializer instanceof Function) { var result = initializer(source, bind.source, e, bind.destination); if (cacheEntry) { if (result && result.cancel) { cacheEntry.bindings.push(function () { result.cancel(); }); } else { // notify the cache that we encountered an uncancellable thing // cacheEntry.nocache = true; } } return result; } else if (initializer && initializer.render) { pend.count++; // notify the cache that we encountered an uncancellable thing // if (cacheEntry) { cacheEntry.nocache = true; } requireSupportedForProcessing(initializer.render).call(initializer, getValue(source, bind.source), e). then(function () { pend.checkComplete(); }); } } function makeBinding(ref, bindingId, pend, bindable, bind, cacheEntry) { var first = true; var bindResult; var canceled = false; autoDispose(bindable); var resolveWeakRef = function () { if (canceled) { return; } var found = checkBindingToken(_DomWeakRefTable._getWeakRefElement(ref), bindingId); if (!found) { _Log.log && _Log.log(_Resources._formatString(strings.elementNotFound, ref), "winjs binding", "info"); if (bindResult) { bindResult.cancel(); } } return found; }; var bindingAction = function (v) { var found = resolveWeakRef(); if (found) { nestedSet(found, bind.destination, v); } if (first) { pend.checkComplete(); first = false; } }; registerAutoDispose(bindable, resolveWeakRef); bindResult = bindWorker(bindable, bind.source, bindingAction); if (bindResult) { var cancel = bindResult.cancel; bindResult.cancel = function () { canceled = true; return cancel.call(bindResult); }; if (cacheEntry) { cacheEntry.bindings.push(function () { bindResult.cancel(); }); } } return bindResult; } function sourceOneBinding(bind, ref, bindingId, source, e, pend, cacheEntry) { var bindable; if (source !== _Global) { source = _Data.as(source); } if (source._getObservable) { bindable = source._getObservable(); } if (bindable) { pend.count++; // declarative binding must use a weak ref to the target element // return makeBinding(ref, bindingId, pend, bindable, bind, cacheEntry); } else { nestedSet(e, bind.destination, getValue(source, bind.source)); } } function filterIdBinding(declBind, bindingStr) { for (var bindIndex = declBind.length - 1; bindIndex >= 0; bindIndex--) { var bind = declBind[bindIndex]; var dest = bind.destination; if (dest.length === 1 && dest[0] === "id") { if (_BaseUtils.validation) { throw new _ErrorFromName("WinJS.Binding.IdBindingNotSupported", _Resources._formatString(strings.idBindingNotSupported, bindingStr)); } _Log.log && _Log.log(_Resources._formatString(strings.idBindingNotSupported, bindingStr), "winjs binding", "error"); declBind.splice(bindIndex, 1); } } return declBind; } function calcBinding(bindingStr, bindingCache) { if (bindingCache) { var declBindCache = bindingCache.expressions[bindingStr]; var declBind; if (!declBindCache) { declBind = filterIdBinding(_BindingParser._bindingParser(bindingStr, _Global), bindingStr); bindingCache.expressions[bindingStr] = declBind; } if (!declBind) { declBind = declBindCache; } return declBind; } else { return filterIdBinding(_BindingParser._bindingParser(bindingStr, _Global), bindingStr); } } function declarativeBindImpl(rootElement, dataContext, skipRoot, bindingCache, defaultInitializer, c) { _WriteProfilerMark("WinJS.Binding:processAll,StartTM"); var pend = { count: 0, checkComplete: function checkComplete() { this.count--; if (this.count === 0) { _WriteProfilerMark("WinJS.Binding:processAll,StopTM"); c(); } } }; var baseElement = (rootElement || _Global.document.body); var selector = "[data-win-bind],[data-win-control]"; var elements = baseElement.querySelectorAll(selector); var neg; if (!skipRoot && baseElement.getAttribute("data-win-bind")) { neg = baseElement; } pend.count++; var source = dataContext || _Global; _DomWeakRefTable._DOMWeakRefTable_fastLoadPath = true; try { var baseElementData = _ElementUtilities.data(baseElement); baseElementData.winBindings = baseElementData.winBindings || []; for (var i = (neg ? -1 : 0), l = elements.length; i < l; i++) { var element = i < 0 ? neg : elements[i]; // If we run into a declarative control container (e.g. Binding.Template) we don't process its // children, but we do give it an opportunity to process them later using this data context. // if (element.winControl && element.winControl.constructor && element.winControl.constructor.isDeclarativeControlContainer) { i += element.querySelectorAll(selector).length; var idcc = element.winControl.constructor.isDeclarativeControlContainer; if (typeof idcc === "function") { idcc = requireSupportedForProcessing(idcc); idcc(element.winControl, function (element) { return declarativeBind(element, dataContext, false, bindingCache, defaultInitializer); }); } } // In order to catch controls above we may have elements which don't have bindings, skip them // if (!element.hasAttribute("data-win-bind")) { continue; } var original = element.getAttribute("data-win-bind"); var declBind = calcBinding(original, bindingCache); if (!declBind.implemented) { for (var bindIndex = 0, bindLen = declBind.length; bindIndex < bindLen; bindIndex++) { var bind = declBind[bindIndex]; bind.initializer = bind.initializer || defaultInitializer; if (bind.initializer) { bind.implementation = initializerOneBinding; } else { bind.implementation = sourceOneBinding; } } declBind.implemented = true; } pend.count++; var bindingId = setBindingToken(element); var ref = optimizeBindingReferences ? bindingId : element.id; if (!ref) { // We use our own counter here, as the IE "uniqueId" is only // global to a document, which means that binding against // unparented DOM elements would get duplicate IDs. // // The elements may not be parented at this point, but they // will be parented by the time the binding action is fired. // element.id = ref = bindingId; } _DomWeakRefTable._createWeakRef(element, ref); var elementData = _ElementUtilities.data(element); elementData.winBindings = null; var cacheEntry; if (bindingCache && bindingCache.elements) { cacheEntry = bindingCache.elements[ref]; if (!cacheEntry) { bindingCache.elements[ref] = cacheEntry = { bindings: [] }; } } for (var bindIndex2 = 0, bindLen2 = declBind.length; bindIndex2 < bindLen2; bindIndex2++) { var bind2 = declBind[bindIndex2]; var cancel2 = bind2.implementation(bind2, ref, bindingId, source, element, pend, cacheEntry); if (cancel2) { elementData.winBindings = elementData.winBindings || []; elementData.winBindings.push(cancel2); baseElementData.winBindings.push(cancel2); } } pend.count--; } } finally { _DomWeakRefTable._DOMWeakRefTable_fastLoadPath = false; } pend.checkComplete(); } function declarativeBind(rootElement, dataContext, skipRoot, bindingCache, defaultInitializer) { /// /// /// Binds values from the specified data context to elements that are descendants of the specified root element /// and have declarative binding attributes (data-win-bind). /// /// /// The element at which to start traversing to find elements to bind to. If this parameter is omitted, the entire document /// is searched. /// /// /// The object to use for default data binding. /// /// /// If true, the elements to be bound skip the specified root element and include only the children. /// /// /// The cached binding data. /// /// /// The binding initializer to use in the case that one is not specified in a binding expression. If not /// provided the behavior is the same as WinJS.Binding.defaultBind. /// /// /// A promise that completes when each item that contains binding declarations has /// been processed and the update has started. /// /// return new Promise(function (c, e, p) { declarativeBindImpl(rootElement, dataContext, skipRoot, bindingCache, defaultInitializer, c, e, p); }).then(null, function (e) { _Log.log && _Log.log(_Resources._formatString(strings.errorInitializingBindings, e && e.message), "winjs binding", "error"); return Promise.wrapError(e); }); } function converter(convert) { /// /// /// Creates a default binding initializer for binding between a source /// property and a destination property with a provided converter function /// that is executed on the value of the source property. /// /// /// The conversion that operates over the result of the source property /// to produce a value that is set to the destination property. /// /// /// The binding initializer. /// /// var userConverter = function (source, sourceProperties, dest, destProperties, initialValue) { var bindingId = setBindingToken(dest); var ref = optimizeBindingReferences ? bindingId : dest.id; if (!ref) { dest.id = ref = bindingId; } _DomWeakRefTable._createWeakRef(dest, ref); var bindable; if (source !== _Global) { source = _Data.as(source); } if (source._getObservable) { bindable = source._getObservable(); } if (bindable) { var counter = 0; var workerResult = bindWorker(_Data.as(source), sourceProperties, function (v) { if (++counter === 1) { if (v === initialValue) { return; } } var found = checkBindingToken(_DomWeakRefTable._getWeakRefElement(ref), bindingId); if (found) { nestedSet(found, destProperties, convert(requireSupportedForProcessing(v))); } else if (workerResult) { _Log.log && _Log.log(_Resources._formatString(strings.elementNotFound, ref), "winjs binding", "info"); workerResult.cancel(); } }); return workerResult; } else { var value = getValue(source, sourceProperties); if (value !== initialValue) { nestedSet(dest, destProperties, convert(value)); } } }; return markSupportedForProcessing(userConverter); } function getValue(obj, path) { if (obj !== _Global) { obj = requireSupportedForProcessing(obj); } if (path) { for (var i = 0, len = path.length; i < len && (obj !== null && obj !== undefined) ; i++) { obj = requireSupportedForProcessing(obj[path[i]]); } } return obj; } function nestedSet(dest, destProperties, v) { requireSupportedForProcessing(v); dest = requireSupportedForProcessing(dest); for (var i = 0, len = (destProperties.length - 1) ; i < len; i++) { dest = requireSupportedForProcessing(dest[destProperties[i]]); if (!dest) { _Log.log && _Log.log(_Resources._formatString(strings.propertyDoesNotExist, destProperties[i], destProperties.join(".")), "winjs binding", "error"); return; } else if (dest instanceof _Global.Node) { _Log.log && _Log.log(_Resources._formatString(strings.nestedDOMElementBindingNotSupported, destProperties[i], destProperties.join(".")), "winjs binding", "error"); return; } } if (destProperties.length === 0) { _Log.log && _Log.log(strings.cannotBindToThis, "winjs binding", "error"); return; } var prop = destProperties[destProperties.length - 1]; if (_Log.log) { if (dest[prop] === undefined) { _Log.log(_Resources._formatString(strings.creatingNewProperty, prop, destProperties.join(".")), "winjs binding", "warn"); } } dest[prop] = v; } function attributeSet(dest, destProperties, v) { dest = requireSupportedForProcessing(dest); if (!destProperties || destProperties.length !== 1 || !destProperties[0]) { _Log.log && _Log.log(strings.attributeBindingSingleProperty, "winjs binding", "error"); return; } dest.setAttribute(destProperties[0], v); } function setAttribute(source, sourceProperties, dest, destProperties, initialValue) { /// /// /// Creates a one-way binding between the source object and /// an attribute on the destination element. /// /// /// The source object. /// /// /// The path on the source object to the source property. /// /// /// The destination object (must be a DOM element). /// /// /// The path on the destination object to the destination property, this must be a single name. /// /// /// The known initial value of the target, if the source value is the same as this initial /// value (using ===) then the target is not set the first time. /// /// /// An object with a cancel method that is used to coalesce bindings. /// /// var bindingId = setBindingToken(dest); var ref = optimizeBindingReferences ? bindingId : dest.id; if (!ref) { dest.id = ref = bindingId; } _DomWeakRefTable._createWeakRef(dest, ref); var bindable; if (source !== _Global) { source = _Data.as(source); } if (source._getObservable) { bindable = source._getObservable(); } if (bindable) { var counter = 0; var workerResult = bindWorker(bindable, sourceProperties, function (v) { if (++counter === 1) { if (v === initialValue) { return; } } var found = checkBindingToken(_DomWeakRefTable._getWeakRefElement(ref), bindingId); if (found) { attributeSet(found, destProperties, requireSupportedForProcessing(v)); } else if (workerResult) { _Log.log && _Log.log(_Resources._formatString(strings.elementNotFound, ref), "winjs binding", "info"); workerResult.cancel(); } }); return workerResult; } else { var value = getValue(source, sourceProperties); if (value !== initialValue) { attributeSet(dest, destProperties, value); } } } function setAttributeOneTime(source, sourceProperties, dest, destProperties) { /// /// /// Sets an attribute on the destination element to the value of the source property /// /// /// The source object. /// /// /// The path on the source object to the source property. /// /// /// The destination object (must be a DOM element). /// /// /// The path on the destination object to the destination property, this must be a single name. /// /// return attributeSet(dest, destProperties, getValue(source, sourceProperties)); } function addClassOneTime(source, sourceProperties, dest) { /// /// /// Adds a class or Array list of classes on the destination element to the value of the source property /// /// /// The source object. /// /// /// The path on the source object to the source property. /// /// /// The destination object (must be a DOM element). /// /// dest = requireSupportedForProcessing(dest); var value = getValue(source, sourceProperties); if (Array.isArray(value)) { value.forEach(function (className) { _ElementUtilities.addClass(dest, className); }); } else if (value) { _ElementUtilities.addClass(dest, value); } } var defaultBindImpl = converter(function defaultBind_passthrough(v) { return v; }); function defaultBind(source, sourceProperties, dest, destProperties, initialValue) { /// /// /// Creates a one-way binding between the source object and /// the destination object. /// /// /// The source object. /// /// /// The path on the source object to the source property. /// /// /// The destination object. /// /// /// The path on the destination object to the destination property. /// /// /// The known initial value of the target, if the source value is the same as this initial /// value (using ===) then the target is not set the first time. /// /// /// An object with a cancel method that is used to coalesce bindings. /// /// return defaultBindImpl(source, sourceProperties, dest, destProperties, initialValue); } function bindWorker(bindable, sourceProperties, func) { if (sourceProperties.length > 1) { var root = {}; var current = root; for (var i = 0, l = sourceProperties.length - 1; i < l; i++) { current = current[sourceProperties[i]] = {}; } current[sourceProperties[sourceProperties.length - 1]] = func; return _Data.bind(bindable, root, true); } else if (sourceProperties.length === 1) { bindable.bind(sourceProperties[0], func, true); return { cancel: function () { bindable.unbind(sourceProperties[0], func); this.cancel = noop; } }; } else { // can't bind to object, so we just push it through // func(bindable); } } function noop() { } function oneTime(source, sourceProperties, dest, destProperties) { /// /// /// Sets the destination property to the value of the source property. /// /// /// The source object. /// /// /// The path on the source object to the source property. /// /// /// The destination object. /// /// /// The path on the destination object to the destination property. /// /// /// An object with a cancel method that is used to coalesce bindings. /// /// nestedSet(dest, destProperties, getValue(source, sourceProperties)); return { cancel: noop }; } function initializer(customInitializer) { /// /// /// Marks a custom initializer function as being compatible with declarative data binding. /// /// /// The custom initializer to be marked as compatible with declarative data binding. /// /// /// The input customInitializer. /// /// return markSupportedForProcessing(customInitializer); } _Base.Namespace._moduleDefine(exports, "WinJS.Binding", { processAll: declarativeBind, oneTime: initializer(oneTime), defaultBind: initializer(defaultBind), converter: converter, initializer: initializer, getValue: getValue, setAttribute: initializer(setAttribute), setAttributeOneTime: initializer(setAttributeOneTime), addClassOneTime: initializer(addClassOneTime), }); }); define('WinJS/Binding',[ './Binding/_BindingParser', './Binding/_Data', './Binding/_Declarative', './Binding/_DomWeakRefTable'], function () { //Wrapper module }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/BindingTemplate/_DataTemplateCompiler',[ 'exports', '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Log', '../Core/_Resources', '../Core/_WriteProfilerMark', '../Binding/_BindingParser', '../Binding/_Declarative', '../ControlProcessor', '../ControlProcessor/_OptionsParser', '../Fragments', '../Promise', '../_Signal', '../Utilities/_Dispose', '../Utilities/_SafeHtml', '../Utilities/_ElementUtilities' ], function templateCompilerInit(exports, _Global, _Base, _BaseUtils, _ErrorFromName, _Log, _Resources, _WriteProfilerMark, _BindingParser, _Declarative, ControlProcessor, _OptionsParser, Fragments, Promise, _Signal, _Dispose, _SafeHtml, _ElementUtilities) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } var strings = { get attributeBindingSingleProperty() { return "Attribute binding requires a single destination attribute name, often in the form \"this['aria-label']\" or \"width\"."; }, get cannotBindToThis() { return "Can't bind to 'this'."; }, get idBindingNotSupported() { return "Declarative binding to ID field is not supported. Initializer: {0}"; }, }; _Base.Namespace._moduleDefine(exports, "WinJS.Binding", { _TemplateCompiler: _Base.Namespace._lazy(function () { var cancelBlocker = Promise._cancelBlocker; // Eagerly bind to stuff that will be needed by the compiler // var init_defaultBind = _Declarative.defaultBind; var init_oneTime = _Declarative.oneTime; var init_setAttribute = _Declarative.setAttribute; var init_setAttributeOneTime = _Declarative.setAttributeOneTime; var init_addClassOneTime = _Declarative.addClassOneTime; var promise_as = Promise.as; var requireSupportedForProcessing = _BaseUtils.requireSupportedForProcessing; var insertAdjacentHTMLUnsafe = _SafeHtml.insertAdjacentHTMLUnsafe; var utilities_data = _ElementUtilities.data; var markDisposable = _Dispose.markDisposable; var ui_processAll = ControlProcessor.processAll; var binding_processAll = _Declarative.processAll; var options_parser = _OptionsParser._optionsParser; var CallExpression = _OptionsParser._CallExpression; var IdentifierExpression = _OptionsParser._IdentifierExpression; var binding_parser = _BindingParser._bindingParser2; var scopedSelect = ControlProcessor.scopedSelect; var writeProfilerMark = _WriteProfilerMark; // Runtime helper functions // function disposeInstance(container, workPromise, renderCompletePromise) { var bindings = _ElementUtilities.data(container).bindTokens; if (bindings) { bindings.forEach(function (binding) { if (binding && binding.cancel) { binding.cancel(); } }); } if (workPromise) { workPromise.cancel(); } if (renderCompletePromise) { renderCompletePromise.cancel(); } } function delayedBindingProcessing(data, defaultInitializer) { return function (element) { return _Declarative.processAll(element, data, false, null, defaultInitializer); }; } function targetSecurityCheck(value) { value = requireSupportedForProcessing(value); return value instanceof _Global.Node ? null : value; } // Compiler formatting functions // var identifierRegEx = /^[A-Za-z]\w*$/; var identifierCharacterRegEx = /[^A-Za-z\w$]/g; var encodeHtmlRegEx = /[&<>'"]/g; var encodeHtmlEscapeMap = { "&": "&", "<": "<", ">": ">", "'": "'", '"': """ }; var formatRegEx = /({{)|(}})|{(\w+)}|({)|(})/g; var semiColonOnlyLineRegEx = /^\s*;\s*$/; var capitalRegEx = /[A-Z]/g; function format(string, parts) { var multiline = string.indexOf("\n") !== -1; var args = arguments; // // This allows you to format a string like: "hello{there}you{0} {{encased in curlies}}" and will // replace the holes {there} and {0} while turning the {{ and }} into single curlies. // // If the replacement is a number then it is inferred to be off of arguments, otherwise it is // a member on parts. // // If the replacement is a multiline string and the hole is indented then the entirety of the // replacement will have the same indentation as the hole. // var result = string.replace(formatRegEx, function (unused, left, right, part, illegalLeft, illegalRight, replacementIndex) { if (illegalLeft || illegalRight) { throw new _ErrorFromName( "Format:MalformedInputString", "Did you forget to escape a: " + (illegalLeft || illegalRight) + " at: " + replacementIndex); } if (left) { return "{"; } if (right) { return "}"; } var result; var index = +part; if (index === +index) { result = args[index + 1]; } else { result = parts[part]; } if (result === undefined) { throw new _ErrorFromName( "Format:MissingPart", "Missing part '" + part + "'" ); } if (multiline) { var pos = replacementIndex; while (pos > 0 && string[--pos] === " ") { /* empty */ } if (pos >= 0 && string[pos] === "\n") { result = indent(replacementIndex - pos - 1, result); } } return result; }); return result; } function indent(numberOfSpaces, multilineStringToBeIndented) { var indent = ""; for (var i = 0; i < numberOfSpaces; i++) { indent += " "; } return multilineStringToBeIndented.split("\n").map(function (line, index) { return index ? indent + line : line; }).join("\n"); } function trim(s) { return s.trim(); } function statements(array) { return array.join(";\n"); } function declarationList(array) { return array.join(", ") || "empty"; } function identifierAccessExpression(parts) { return parts.map(function (part) { // If this can be a member access optimize to that instead of a string lookup // if (part.match(identifierRegEx)) { return "." + part; } if (+part === part) { return format("[{0}]", part); } return format("[{0}]", literal(part)); }).join(""); } function nullableFilteredIdentifierAccessExpression(initial, parts, temporary, filter) { // // generates: "(t = initial) && filter(t = t.p0) && filter(t = t.p1) && t" // // There are a number of contexts in which we will be derefernceing developer provided // identifier access expressions, in those case we don't know whether or not the target // property exists or in fact if anything exists along the path. // // In order to provide 'correct' behavior we dereference conditionally on whether // we have a non-null value. This results in returning undefined unless the entire // path is defined. // // In these cases we also want to filter the result for security purposes. // var parts = parts.map(function (part) { // If this can be a member access optimize to that instead of a string lookup // if (part.match(identifierRegEx)) { return "." + part; } if (+part === part) { part = +part; } return brackets(literal(part)); }).map(function (part) { return format("{filter}({temp} = {temp}{part})", { filter: filter, temp: temporary, part: part }); }); parts.unshift(parens(assignment(temporary, initial))); parts.push(temporary); return parens(parts.join(" && ")); } function literal(instance) { return JSON.stringify(instance); } function newArray(N) { return N ? "new Array(" + (+N) + ")" : "[]"; } function assignment(target, source) { return "" + target + " = " + source; } function parens(expression) { return "(" + expression + ")"; } function brackets(expression) { return "[" + expression + "]"; } function propertyName(name) { if (name.match(identifierRegEx)) { return name; } if (+name === name) { return +name; } return literal(name); } function htmlEscape(str) { str = "" + str; return str.replace(encodeHtmlRegEx, function (m) { return encodeHtmlEscapeMap[m] || " "; }); } function createIdentifier(prefix, count, suffix) { if (suffix) { return new String("" + prefix + count + "_" + suffix); } else { return new String("" + prefix + count); } } function multiline(str) { return str.replace(/\\n/g, "\\n\\\n"); } // Compiler helper functions // function keys(object) { return Object.keys(object); } function values(object) { return Object.keys(object).map(function (key) { return object[key]; }); } function merge(a, b) { return mergeAll([a, b]); } function mergeAll(list) { var o = {}; for (var i = 0, len = list.length; i < len; i++) { var part = list[i]; var keys = Object.keys(part); for (var j = 0, len2 = keys.length; j < len2; j++) { var key = keys[j]; o[key] = part[key]; } } return o; } function globalLookup(parts) { return parts.reduce( function (current, name) { if (current) { return requireSupportedForProcessing(current[name]); } return null; }, _Global ); } function visit(node, key, pre, post) { var children = node.children; if (children) { var keys = Object.keys(children); (key && pre) && pre(node, key, keys.length); for (var i = 0, len = keys.length; i < len; i++) { var childKey = keys[i]; var child = children[childKey]; visit(child, childKey, pre, post); } (key && post) && post(node, key, Object.keys(children).length); } else { (key && pre) && pre(node, key, 0); (key && post) && post(node, key, 0); } } // Compiler helper types // var TreeCSE = _Base.Class.define(function (compiler, name, kind, accessExpression, filter) { var that = this; this.compiler = compiler; this.kind = kind; this.base = new String(name); this.tree = { children: {}, parent: this.base, reference: function () { return that.base; } }; this.accessExpression = accessExpression; this.filter = filter || ""; }, { createPathExpression: function (path, name) { if (path.length) { var that = this; var tail = path.reduce( function (node, part) { node.children = node.children || {}; node.children[part] = node.children[part] || { parent: node }; return node.children[part]; }, this.tree ); tail.name = tail.name || that.compiler.defineInstance( that.kind, name || "", function () { return that.accessExpression( /*l*/tail.parent.name ? tail.parent.name : tail.parent.reference(), /*r*/path.slice(-1)[0], /*root*/tail.parent.parent === that.base, /*filter*/that.filter, /*last*/true ); } ); return tail.name; } else { return this.base; } }, lower: function () { var that = this; var aggregatedName = []; var reference = function (node, name, last) { return that.accessExpression( /*l*/node.parent.name ? node.parent.name : node.parent.reference(), /*r*/name, /*root*/node.parent.parent === that.base, /*filter*/that.filter, /*last*/last ); }; // Ensure that all shared internal nodes have names and that all nodes // know who they reference // visit(this.tree, "", function pre(node, key, childCount) { aggregatedName.push(key); if (childCount > 1) { node.name = node.name || that.compiler.defineInstance( that.kind, aggregatedName.join("_"), reference.bind(null, node, key, true) ); node.reference = function () { return node.name; }; } else if (childCount === 1) { node.reference = reference.bind(null, node, key); } }, function post() { aggregatedName.pop(); } ); }, deadNodeElimination: function () { // Kill all dead nodes from the tree // visit(this.tree, "", null, function post(node, key, childCount) { if (!node.name || node.name.dead) { if (childCount === 0) { if (node.parent && node.parent.children) { delete node.parent.children[key]; } } } }); }, definitions: function () { var nodes = []; // Gather the nodes in a depth first ordering, any node which has a name // needs to have a definition generated // visit(this.tree, "", function pre(node) { if (node.name) { nodes.push(node); } }); return nodes.map(function (n) { return n.name.definition(); }); }, }); var InstanceKind = { "capture": "capture", "temporary": "temporary", "variable": "variable", "data": "data", "global": "global", }; var InstanceKindPrefixes = { "capture": "c", "temporary": "t", "variable": "iv", "data": "d", "global": "g", }; var StaticKind = { "import": "import", "variable": "variable", }; var StaticKindPrefixes = { "import": "i", "variable": "sv", }; var BindingKind = { "tree": "tree", "text": "text", "initializer": "initializer", "template": "template", "error": "error", }; var TextBindingKind = { "attribute": "attribute", "booleanAttribute": "booleanAttribute", "inlineStyle": "inlineStyle", "textContent": "textContent", }; // Constants // var IMPORTS_ARG_NAME = "imports"; var Stage = { initial: 0, analyze: 1, optimze: 2, lower: 3, compile: 4, link: 5, done: 6, }; // Compiler // var TemplateCompiler = _Base.Class.define(function (templateElement, options) { this._stage = Stage.initial; this._staticVariables = {}; this._staticVariablesCount = 0; this._instanceVariables = {}; this._instanceVariablesCount = {}; this._debugBreak = options.debugBreakOnRender; this._defaultInitializer = requireSupportedForProcessing(options.defaultInitializer || init_defaultBind); this._optimizeTextBindings = !options.disableTextBindingOptimization; this._templateElement = templateElement; this._templateContent = _Global.document.createElement(templateElement.tagName); this._extractChild = options.extractChild || false; this._controls = null; this._bindings = null; this._bindTokens = null; this._textBindingPrefix = null; this._textBindingId = 0; this._suffix = []; this._htmlProcessors = []; this._profilerMarkIdentifier = options.profilerMarkIdentifier; this._captureCSE = new TreeCSE(this, "container", InstanceKind.capture, this.generateElementCaptureAccess.bind(this)); this._dataCSE = new TreeCSE(this, "data", InstanceKind.data, this.generateNormalAccess.bind(this), this.importSafe("dataSecurityCheck", requireSupportedForProcessing)); this._globalCSE = new TreeCSE(this, this.importSafe("global", _Global), InstanceKind.global, this.generateNormalAccess.bind(this), this.importSafe("globalSecurityCheck", requireSupportedForProcessing)); // Clone the template content and import it into its own HTML document for further processing Fragments.renderCopy(this._templateElement, this._templateContent); // If we are extracting the first child we only bother compiling the one child if (this._extractChild) { while (this._templateContent.childElementCount > 1) { this._templateContent.removeChild(this._templateContent.lastElementChild); } } }, { addClassOneTimeTextBinding: function (binding) { var that = this; var id = this.createTextBindingHole(binding.elementCapture.element.tagName, "class", ++this._textBindingId); binding.textBindingId = id; binding.kind = BindingKind.text; binding.elementCapture.element.classList.add(id); binding.elementCapture.refCount--; binding.definition = function () { return that.formatCode("{htmlEscape}({value})", { htmlEscape: that._staticVariables.htmlEscape, value: binding.value(), }); }; }, addClassOneTimeTreeBinding: function (binding) { var that = this; binding.pathExpression = this.bindingExpression(binding); binding.value = function () { return binding.pathExpression; }; binding.kind = BindingKind.tree; binding.definition = function () { return that.formatCode("{element}.classList.add({value})", { element: binding.elementCapture, value: binding.value(), }); }; }, analyze: function () { if (this._stage > Stage.analyze) { throw "Illegal: once we have moved past analyze we cannot revist it"; } this._stage = Stage.analyze; // find activatable and bound elements this._controls = this.gatherControls(); this._bindings = this.gatherBindings(); this._children = this.gatherChildren(); // remove attributes which are no longer needed since we will inline bindings and controls this.cleanControlAndBindingAttributes(); if (this.async) { this.createAsyncParts(); } this.nullableIdentifierAccessTemporary = this.defineInstance(InstanceKind.temporary); // snapshot html var html = this._templateContent.innerHTML; this._html = function () { return multiline(literal(html)); }; this._html.text = html; }, bindingExpression: function (binding) { return this._dataCSE.createPathExpression(binding.source, binding.source.join("_")); }, capture: function (element) { var capture = element._capture; if (capture) { capture.refCount++; return capture; } // Find the path to the captured element // var path = [element]; var e = element.parentNode; var name = element.tagName; while (e !== this._templateContent) { name = e.tagName + "_" + name; path.unshift(e); e = e.parentNode; } // Identify which child each path member is so we can form an indexed lookup // for (var i = 0, len = path.length; i < len; i++) { var child = path[i]; path[i] = Array.prototype.indexOf.call(e.children, child); e = child; } // Create the capture and put it on the // capture = this._captureCSE.createPathExpression(path, name.toLowerCase()); capture.element = element; capture.element._capture = capture; capture.refCount = 1; return capture; }, cleanControlAndBindingAttributes: function () { var selector = "[data-win-bind],[data-win-control]"; var elements = this._templateContent.querySelectorAll(selector); for (var i = 0, len = elements.length; i < len; i++) { var element = elements[i]; if (element.isDeclarativeControlContainer) { i += element.querySelectorAll("[data-win-bind],[data-win-control]").length; } element.removeAttribute("data-win-bind"); element.removeAttribute("data-win-control"); element.removeAttribute("data-win-options"); } }, compile: function (bodyTemplate, replacements, supportDelayBindings) { if (this._stage > Stage.compile) { throw "Illegal: once we have moved past compile we cannot revist it"; } this._stage = Stage.compile; var that = this; this._returnedElement = this._extractChild ? "container.firstElementChild" : "container"; var control_processing = this._controls.map(function (control) { var constructionFormatString; if (control.async) { constructionFormatString = "{target}.winControl = {target}.winControl || new {SafeConstructor}({target}, {options}, controlDone)"; } else { constructionFormatString = "{target}.winControl = {target}.winControl || new {SafeConstructor}({target}, {options})"; } var construction = that.formatCode( constructionFormatString, { target: control.elementCapture, SafeConstructor: control.SafeConstructor, options: that.generateOptionsLiteral(control.optionsParsed, control.elementCapture), } ); if (control.isDeclarativeControlContainer && typeof control.isDeclarativeControlContainer.import === "function") { var result = [construction]; result.push(that.formatCode( "{isDeclarativeControlContainer}({target}.winControl, {delayedControlProcessing})", { target: control.elementCapture, isDeclarativeControlContainer: control.isDeclarativeControlContainer, delayedControlProcessing: that._staticVariables.ui_processAll } )); result.push(that.formatCode( "{isDeclarativeControlContainer}({target}.winControl, {delayedBindingProcessing}(data, {templateDefaultInitializer}))", { target: control.elementCapture, isDeclarativeControlContainer: control.isDeclarativeControlContainer, delayedBindingProcessing: that._staticVariables.delayedBindingProcessing, templateDefaultInitializer: that._staticVariables.templateDefaultInitializer || literal(null), } )); return result.join(";\n"); } else { return construction; } }); var all_binding_processing = this._bindings.map(function (binding) { switch (binding.kind) { case BindingKind.template: return that.formatCode( "({nestedTemplates}[{nestedTemplate}] = {template}.render({path}, {dest}))", { nestedTemplates: that._nestedTemplates, nestedTemplate: literal(binding.nestedTemplate), template: binding.template, path: binding.pathExpression, dest: binding.elementCapture, } ); case BindingKind.initializer: var formatString; if (binding.initialValue) { formatString = "({bindTokens}[{bindToken}] = {initializer}(data, {sourceProperties}, {dest}, {destProperties}, {initialValue}))"; } else { formatString = "({bindTokens}[{bindToken}] = {initializer}(data, {sourceProperties}, {dest}, {destProperties}))"; } return that.formatCode( formatString, { bindTokens: that._bindTokens, bindToken: literal(binding.bindToken), initializer: binding.initializer, sourceProperties: literal(binding.source), destProperties: literal(binding.destination), dest: binding.elementCapture, initialValue: binding.initialValue, } ); case BindingKind.tree: return binding.definition(); case BindingKind.text: // do nothing, text bindings are taken care of seperately break; case BindingKind.error: // do nothing, errors are reported and ignored break; default: throw "NYI"; } }); var binding_processing, delayed_binding_processing; if (supportDelayBindings) { binding_processing = all_binding_processing.filter(function (unused, index) { return !that._bindings[index].delayable; }); delayed_binding_processing = all_binding_processing.filter(function (unused, index) { return that._bindings[index].delayable; }); } else { binding_processing = all_binding_processing; delayed_binding_processing = []; } var instances = values(this._instanceVariables); var instanceDefinitions = instances .filter(function (instance) { return instance.kind === InstanceKind.variable; }) .map(function (variable) { return variable.definition(); }); var captures = this._captureCSE.definitions(); var globals = this._globalCSE.definitions(); var data = this._dataCSE.definitions(); var set_msParentSelectorScope = this._children.map(function (child) { return that.formatCodeN("{0}.msParentSelectorScope = true", child); }); var suffix = this._suffix.map(function (statement) { return statement(); }); var renderComplete = ""; if (supportDelayBindings && delayed_binding_processing.length) { renderComplete = that.formatCode( renderItemImplRenderCompleteTemplate, { delayed_binding_processing: statements(delayed_binding_processing) } ); } var result = that.formatCode( bodyTemplate, mergeAll([ this._staticVariables, replacements || {}, { profilerMarkIdentifierStart: literal("WinJS.Binding.Template:render" + this._profilerMarkIdentifier + ",StartTM"), profilerMarkIdentifierStop: literal("WinJS.Binding.Template:render" + this._profilerMarkIdentifier + ",StopTM"), html: this._html(), tagName: literal(this._templateElement.tagName), instance_variable_declarations: declarationList(instances), global_definitions: statements(globals), data_definitions: statements(data), instance_variable_definitions: statements(instanceDefinitions), capture_definitions: statements(captures), set_msParentSelectorScope: statements(set_msParentSelectorScope), debug_break: this.generateDebugBreak(), control_processing: statements(control_processing), control_counter: this._controlCounter, binding_processing: statements(binding_processing), renderComplete: renderComplete, suffix_statements: statements(suffix), nestedTemplates: this._nestedTemplates, returnedElement: this._returnedElement, }, ]) ); return this.prettify(result); }, createAsyncParts: function () { this._nestedTemplates = this._nestedTemplates || this.defineInstance( InstanceKind.variable, "nestedTemplates", function () { return newArray(0); } ); this._controlCounter = this._controlCounter || this.defineInstance( InstanceKind.variable, "controlCounter", function () { return literal(1); } ); }, createTextBindingHole: function (tagName, attribute, id) { if (!this._textBindingPrefix) { var c = ""; while (this._html.text.indexOf("textbinding" + c) !== -1) { c = c || 0; c++; } this._textBindingPrefix = "textbinding" + c; // NOTE: the form of this regex needs to be coordinated with any special cases which // are introduced by the switch below. this._textBindingRegex = new RegExp("(#?" + this._textBindingPrefix + "_\\d+)"); } // Sometimes text bindings need to be of a particular form to suppress warnings from // the host, specifically there is a case with IMG/src attribute where if you assign // a naked textbinding_X to it you get a warning in the console of an unresolved image // instead we prefix it with a # which is enough to suppress that message. // var result = this._textBindingPrefix + "_" + id; if (tagName === "IMG" && attribute === "src") { result = "#" + result; } return result; }, deadCodeElimination: function () { var that = this; // Eliminate all captured elements which are no longer in the tree, this can happen if // these captured elements are children of a node which had a text binding to 'innerText' // or 'textContent' as those kill the subtree. // Object.keys(this._instanceVariables).forEach(function (key) { var iv = that._instanceVariables[key]; if (iv.kind === InstanceKind.capture) { if (!that._templateContent.contains(iv.element)) { iv.dead = true; } if (iv.refCount === 0) { iv.dead = true; } if (iv.dead) { // This dead instance variable returns a blank definition which will then get // cleaned up by prettify. iv.definition = function () { }; iv.name = null; delete that._instanceVariables[key]; } } }); // Eliminate all control activations which target elements which are no longer in the tree // this._controls = this._controls.filter(function (c) { return !c.elementCapture.dead; }); // Eliminate all bindings which target elements which are no longer in the tree // this._bindings = this._bindings.filter(function (b) { return !b.elementCapture.dead; }); // Cleanup the capture CSE tree now that we know dead nodes are marked as such. // this._captureCSE.deadNodeElimination(); }, defineInstance: function (kind, name, definition) { if (this._stage >= Stage.compile) { throw "Illegal: define instance variable after compilation stage has started"; } var variableCount = this._instanceVariablesCount[kind] || 0; var suffix = name ? name.replace(identifierCharacterRegEx, "_") : ""; var identifier = createIdentifier(InstanceKindPrefixes[kind], variableCount, suffix); identifier.definition = function () { return assignment(identifier, definition()); }; identifier.kind = kind; this._instanceVariables[identifier] = identifier; this._instanceVariablesCount[kind] = variableCount + 1; return identifier; }, defineStatic: function (kind, name, definition) { if (this._stage >= Stage.link) { throw "Illegal: define static variable after link stage has started"; } if (name) { var known = this._staticVariables[name]; if (known) { return known; } } var suffix = name ? name.replace(identifierCharacterRegEx, "_") : ""; var identifier = createIdentifier(StaticKindPrefixes[kind], this._staticVariablesCount, suffix); identifier.definition = function () { return assignment(identifier, definition()); }; identifier.kind = kind; this._staticVariables[name || identifier] = identifier; this._staticVariablesCount++; return identifier; }, done: function () { if (this._stage > Stage.done) { throw "Illegal: once we have moved past done we cannot revist it"; } this._stage = Stage.done; }, emitScopedSelect: function (selector, elementCapture) { return this.formatCode( "{scopedSelect}({selector}, {element})", { scopedSelect: this._staticVariables.scopedSelect, selector: literal(selector), element: elementCapture, } ); }, emitOptionsNode: function (node, parts, elementCapture) { var that = this; if (node) { switch (typeof node) { case "object": if (Array.isArray(node)) { parts.push("["); for (var i = 0, len = node.length; i < len; i++) { this.emitOptionsNode(node[i], parts, elementCapture); parts.push(","); } parts.push("]"); } else if (node instanceof CallExpression) { parts.push(node.target === "select" ? this.emitScopedSelect(node.arg0Value, elementCapture) : literal(null)); } else if (node instanceof IdentifierExpression && node.parts[0] instanceof CallExpression) { var call = node.parts[0]; parts.push( nullableFilteredIdentifierAccessExpression( call.target === "select" ? this.emitScopedSelect(call.arg0Value, elementCapture) : literal(null), node.parts.slice(1), this.nullableIdentifierAccessTemporary, this.importSafe("requireSupportedForProcessing", requireSupportedForProcessing) ) ); } else if (node instanceof IdentifierExpression) { parts.push(node.pathExpression); } else { parts.push("{"); Object.keys(node).forEach(function (key) { parts.push(propertyName(key)); parts.push(":"); that.emitOptionsNode(node[key], parts, elementCapture); parts.push(","); }); parts.push("}"); } break; default: parts.push(literal(node)); break; } } else { parts.push(literal(null)); } }, findGlobalIdentifierExpressions: function (obj, results) { results = results || []; var that = this; Object.keys(obj).forEach(function (key) { var prop = obj[key]; if (typeof prop === "object") { if (prop instanceof IdentifierExpression) { if (!(prop.parts[0] instanceof CallExpression)) { results.push(prop); } } else { that.findGlobalIdentifierExpressions(prop, results); } } }); return results; }, formatCodeN: function () { if (this._stage < Stage.compile) { throw "Illegal: format code at before compilation stage has started"; } return format.apply(null, arguments); }, formatCode: function (string, parts) { if (this._stage < Stage.compile) { throw "Illegal: format code at before compilation stage has started"; } return format(string, parts); }, gatherBindings: function () { var bindTokens = -1; var that = this; var nestedTemplates = -1; var bindings = []; var selector = "[data-win-bind],[data-win-control]"; var elements = this._templateContent.querySelectorAll(selector); for (var i = 0, len = elements.length; i < len; i++) { var element = elements[i]; // If we run into a declarative control container (e.g. Binding.Template) we don't // bind its children, but we will give it an opportunity to process later using the // same data context. if (element.isDeclarativeControlContainer) { i += element.querySelectorAll(selector).length; } // Since we had to look for controls as well as bindings in order to skip controls // which are declarative control containers we have to check if this element is bound if (!element.hasAttribute("data-win-bind")) { continue; } var bindingText = element.getAttribute("data-win-bind"); var elementBindings = binding_parser(bindingText, _Global); elementBindings.forEach(function (binding) { if (binding.initializer) { // If an initializer is specified it may be a nested template var initializerName = binding.initializer.join("."); var initializer = globalLookup(binding.initializer); if (initializer.render) { requireSupportedForProcessing(initializer.render); // We have already chceked this to be safe for import binding.template = that.importSafe(initializerName, initializer); binding.pathExpression = that.bindingExpression(binding); binding.nestedTemplate = ++nestedTemplates; binding.kind = BindingKind.template; } else if (initializer.winControl && initializer.winControl.render) { requireSupportedForProcessing(initializer.winControl.render); // We have already checked this to be safe to import binding.template = that.importSafe(initializerName, initializer.winControl); binding.pathExpression = that.bindingExpression(binding); binding.nestedTemplate = ++nestedTemplates; binding.kind = BindingKind.template; } else { // Don't get the path expression here, we will do it if needed in optimize binding.initializer = that.import(initializerName, initializer); binding.bindToken = ++bindTokens; binding.kind = BindingKind.initializer; } } else { // Don't get the path expression here, we will do it if needed in optimize // We have already checked this to be safe to import binding.initializer = that.importSafe("templateDefaultInitializer", that._defaultInitializer); binding.bindToken = ++bindTokens; binding.kind = BindingKind.initializer; } binding.elementCapture = that.capture(element); binding.bindingText = bindingText; }); bindings.push.apply(bindings, elementBindings); } var nestedTemplateCount = nestedTemplates + 1; if (nestedTemplateCount > 0) { this.async = true; this._nestedTemplates = this.defineInstance( InstanceKind.variable, "nestedTemplates", function () { return newArray(nestedTemplateCount); } ); } var bindTokenCount = bindTokens + 1; if (bindTokenCount > 0) { this._bindTokens = this.defineInstance( InstanceKind.variable, "bindTokens", function () { return newArray(bindTokenCount); } ); this._suffix.push(function () { // NOTE: returnedElement is a local in the template which is set to either be the container // or in the extractChild: true case the first child. return that.formatCode( "{utilities_data}(returnedElement).bindTokens = {bindTokens}", { utilities_data: that._staticVariables.utilities_data, bindTokens: that._bindTokens, } ); }); } return bindings; }, gatherChildren: function () { var that = this; return Array.prototype.map.call(this._templateContent.children, function (child) { return that.capture(child); }); }, gatherControls: function () { var that = this; var asyncCount = 0; var controls = []; var selector = "[data-win-control]"; var elements = this._templateContent.querySelectorAll(selector); for (var i = 0, len = elements.length; i < len; i++) { var element = elements[i]; var name = element.getAttribute("data-win-control"); // Control constructors are checked along the entirety of their path to be supported // for processing when they are bound var ControlConstructor = _BaseUtils._getMemberFiltered(name.trim(), _Global, requireSupportedForProcessing); if (!ControlConstructor) { continue; } var optionsText = element.getAttribute("data-win-options") || literal({}); var async = ControlConstructor.length > 2; if (async) { asyncCount++; this.async = true; } var isDeclarativeControlContainer = ControlConstructor.isDeclarativeControlContainer; if (isDeclarativeControlContainer) { if (typeof isDeclarativeControlContainer === "function") { isDeclarativeControlContainer = this.import(name + "_isDeclarativeControlContainer", isDeclarativeControlContainer); } element.isDeclarativeControlContainer = isDeclarativeControlContainer; i += element.querySelectorAll(selector).length; } var control = { elementCapture: this.capture(element), name: name, // We have already checked this for requireSupportedForProcessing SafeConstructor: this.importSafe(name, ControlConstructor), async: async, optionsText: literal(optionsText), optionsParsed: options_parser(optionsText), isDeclarativeControlContainer: isDeclarativeControlContainer, }; controls.push(control); var globalReferences = this.findGlobalIdentifierExpressions(control.optionsParsed); globalReferences.forEach(function (identifierExpression) { identifierExpression.pathExpression = that.globalExpression(identifierExpression.parts); }); } if (asyncCount > 0) { this._controlCounter = this.defineInstance( InstanceKind.variable, "controlCounter", // +1 because we call it once to start in case we have no async controls in the async template function () { return literal(asyncCount + 1); } ); } return controls; }, generateElementCaptureAccess: function (l, r, root) { if (root) { // Clean up the right hand side so we don't end up with "startIndex + 0" var right = ("" + r === "0" ? "" : " + " + r); return this.formatCodeN("{0}.children[startIndex{1}]", l, right); } return this.formatCodeN("{0}.children[{1}]", l, r); }, generateNormalAccess: function (left, right, root, filter, last) { // The 'last' parameter indicates that this path access is the last part of a greater // access expression and therefore does not need to be further assigned to the temp. if (left.indexOf(this.nullableIdentifierAccessTemporary) >= 0) { // If the nullableIdentifierAccessTemporary is on the LHS then the // LHS is already an access expression and does not need to be null-checked again var formatString; if (last) { formatString = "{left} && {filter}({temp}{right})"; } else { formatString = "{left} && ({temp} = {filter}({temp}{right}))"; } return this.formatCode(formatString, { temp: this.nullableIdentifierAccessTemporary, left: left, right: identifierAccessExpression([right]), filter: filter }); } var formatString; if (last) { formatString = "({temp} = {left}) && {filter}({temp}{right})"; } else { formatString = "({temp} = {left}) && ({temp} = {filter}({temp}{right}))"; } return this.formatCode(formatString, { temp: this.nullableIdentifierAccessTemporary, left: left, right: identifierAccessExpression([right]), filter: filter }); }, generateOptionsLiteral: function (optionsParsed, elementCapture) { var parts = []; this.emitOptionsNode(optionsParsed, parts, elementCapture); return parts.join(" "); }, generateDebugBreak: function () { if (this._debugBreak) { var counter = this.defineStatic( StaticKind.variable, "debugCounter", function () { return literal(0); } ); return this.formatCodeN("if (++{0} === 1) {{ debugger; }}", counter); } return ""; }, globalExpression: function (path) { return this._globalCSE.createPathExpression(path, path.join("_")); }, import: function (name, i) { // Used for functions which are gathered from user code (e.g. binding initializers and // control constructors). For these functions we need to assert that they are safe for // use in a declarative context, however since the values are known at compile time we // can do that check once. // return this.importSafe(name, requireSupportedForProcessing(i)); }, importSafe: function (name, i) { // Used for functions and objects which are intrinsic to the template compiler and are safe // for their intended usages and don't need to be marked requireSupportedForProcessing. // var that = this; var identifier = this.defineStatic( StaticKind.import, name, function () { return that.formatCodeN("({0}{1})", IMPORTS_ARG_NAME, identifierAccessExpression([name])); } ); if (identifier.import && identifier.import !== i) { throw "Duplicate import: '" + name + "'"; } identifier.import = i; return identifier; }, importAll: function (imports) { Object.keys(imports).forEach(function (key) { requireSupportedForProcessing(imports[key]); }); return this.importAllSafe(imports); }, importAllSafe: function (imports) { var that = this; var result = Object.keys(imports).reduce( function (o, key) { o[key] = that.importSafe(key, imports[key]); return o; }, {} ); return result; }, link: function (body) { if (this._stage > Stage.link) { throw "Illegal: once we have moved past link we cannot revist it"; } this._stage = Stage.link; var that = this; // Gather the set of imported instances (functions mostly). All of these are runtime values which // are already safety checked for things like requireSupportedForProcessing. // var imports = keys(this._staticVariables) .filter(function (key) { return that._staticVariables[key].kind === StaticKind.import; }) .reduce( function (o, key) { o[key] = that._staticVariables[key].import; return o; }, {} ); var statics = values(this._staticVariables); return new Function(IMPORTS_ARG_NAME, // jshint ignore:line this.formatCode( linkerCodeTemplate, { static_variable_declarations: declarationList(statics), static_variable_definitions: statements(statics.map(function (s) { return s.definition(); })), body: body.trim(), } ) )(imports); }, lower: function () { if (this._stage > Stage.lower) { throw "Illegal: once we have moved past lower we cannot revist it"; } this._stage = Stage.lower; this._captureCSE.lower(); this._dataCSE.lower(); this._globalCSE.lower(); }, markBindingAsError: function (binding) { if (binding) { binding.kind = BindingKind.error; this.markBindingAsError(binding.original); } }, oneTimeTextBinding: function (binding) { var that = this; var result = this.oneTimeTextBindingAnalyze(binding); if (result) { var initialValue; if (binding.original) { initialValue = binding.original.initialValue; } var id = this.createTextBindingHole(binding.elementCapture.element.tagName, result.attribute, ++this._textBindingId); binding.textBindingId = id; binding.kind = BindingKind.text; binding.elementCapture.refCount--; binding.definition = function () { var formatString; if (initialValue) { formatString = "{htmlEscape}({initialValue})"; } else { formatString = "{htmlEscape}({getter})"; } return that.formatCode( formatString, { htmlEscape: that._staticVariables.htmlEscape, getter: binding.value(), initialValue: initialValue, } ); }; switch (result.kind) { case TextBindingKind.attribute: binding.elementCapture.element.setAttribute(result.attribute, id); break; case TextBindingKind.booleanAttribute: // Boolean attributes work differently, the presence of the attribute in any // form means true and its absence means false. This means that we need to // add or remove the whole thing and make the definition of the binding // expression at runtime add it back. // binding.elementCapture.element.setAttribute(result.attribute, id); // Wrap the definition in a ternary expression which yields either the attribute // name or an empty string. // binding.definition = function () { var formatString; if (initialValue) { formatString = "({initialValue} ? {attribute} : \"\")"; } else { formatString = "({value} ? {attribute} : \"\")"; } return that.formatCode( formatString, { value: binding.value(), attribute: literal(result.attribute), initialValue: initialValue } ); }; // Arrange for the attribute in the HTML with the 'id' as a value to be wholy // replaced by the ID. // this._htmlProcessors.push(function (html) { return html.replace(new RegExp(result.attribute + "=\"" + id + "\"", "i"), id); }); break; case TextBindingKind.textContent: binding.elementCapture.element.textContent = id; break; case TextBindingKind.inlineStyle: var element = binding.elementCapture.element; // Inline styles require a little finesseing, their form always needs to be // legal CSS include in the value space. We could attempt to special case // all CSS properties and produce something which is legal in their value // space but instead we wholesale replace the inline CSS with a extension // property temporarially in order to get valid HTML. Later we go replace // that well-known hole with the original inline CSS text as well as any // new properties we are setting as a result of data binding. // if (!element.msReplaceStyle) { element.msReplaceStyle = element.getAttribute("style") || ""; // Ensure that there is always a trailing ';' if (element.msReplaceStyle !== "" && element.msReplaceStyle[element.msReplaceStyle.length - 1] !== ";") { element.msReplaceStyle = element.msReplaceStyle + ";"; } element.setAttribute("style", "msReplaceStyle:'" + id + "'"); var temp = element.getAttribute("style"); this._htmlProcessors.push(function (html) { return html.replace(temp, element.msReplaceStyle); }); } element.msReplaceStyle = element.msReplaceStyle + result.property + ":" + id + ";"; break; default: throw "NYI"; } } }, oneTimeTextBindingAnalyze: function (binding) { var element = binding.elementCapture.element; var elementType = element.tagName; var targetProperty = binding.destination[0]; // Properties which can only be optimized for a given element types // switch (elementType) { case "A": switch (targetProperty) { case "href": return { kind: TextBindingKind.attribute, attribute: targetProperty }; } break; case "IMG": switch (targetProperty) { case "alt": case "src": case "width": case "height": return { kind: TextBindingKind.attribute, attribute: targetProperty }; } break; case "SELECT": switch (targetProperty) { case "disabled": case "multiple": case "required": return { kind: TextBindingKind.booleanAttribute, attribute: targetProperty }; case "size": return { kind: TextBindingKind.attribute, attribute: targetProperty }; } break; case "OPTION": switch (targetProperty) { case "label": case "value": return { kind: TextBindingKind.attribute, attribute: targetProperty }; case "disabled": case "selected": return { kind: TextBindingKind.booleanAttribute, attribute: targetProperty }; } break; case "INPUT": switch (targetProperty) { case "checked": switch (element.type) { case "checkbox": case "radio": return { kind: TextBindingKind.booleanAttribute, attribute: targetProperty }; } break; case "disabled": return { kind: TextBindingKind.booleanAttribute, attribute: targetProperty }; case "max": case "maxLength": case "min": case "step": case "value": return { kind: TextBindingKind.attribute, attribute: targetProperty }; case "size": switch (element.type) { case "text": case "search": case "tel": case "url": case "email": case "password": return { kind: TextBindingKind.attribute, attribute: targetProperty }; } break; case "readOnly": switch (element.type) { case "hidden": case "range": case "color": case "checkbox": case "radio": case "file": case "button": // not supported: break; default: return { kind: TextBindingKind.booleanAttribute, attribute: targetProperty }; } break; } break; case "BUTTON": switch (targetProperty) { case "disabled": return { kind: TextBindingKind.booleanAttribute, attribute: targetProperty }; case "value": return { kind: TextBindingKind.attribute, attribute: targetProperty }; } break; case "TEXTAREA": switch (targetProperty) { case "disabled": case "readOnly": case "required": return { kind: TextBindingKind.booleanAttribute, attribute: targetProperty }; case "cols": case "maxLength": case "placeholder": case "rows": case "wrap": return { kind: TextBindingKind.attribute, attribute: targetProperty }; } break; } // Properties which can be optimized for all element types // switch (targetProperty) { case "className": return { kind: TextBindingKind.attribute, attribute: "class" }; case "dir": case "lang": case "name": case "title": case "tabIndex": return { kind: TextBindingKind.attribute, attribute: targetProperty }; case "style": if (binding.destination.length > 1) { var targetCssProperty = binding.destination[1]; if (targetCssProperty === "cssText") { // We don't support optimizing the cssText property on styles // return; } // If this is a supported css property we will get a string (frequently empty) // from the style object. // var supported = typeof element.style[targetCssProperty] === "string"; if (supported) { // The mapping from css property name to JS property name is regular: // Chrome uses webkit as the JS property name prefix. // IE uses ms as the JS property name prefix. // Firefox uses Moz as the JS property name prefix. // // To calculate the css property name we replace capital letters with // a dash followed by the lowercase version of that letter. // // For Chrome and IE we have to add the leading dash manually since // the JS property name prefix is lowercase. For Firefox the replace // call will take care of this for us since their JS property name // prefix begins with a capital letter. if (targetCssProperty[0] === "m" && targetCssProperty[1] === "s" || targetCssProperty.substring(0, 6) === "webkit") { targetCssProperty = "-" + targetCssProperty; } targetCssProperty = targetCssProperty.replace(capitalRegEx, function (l) { return "-" + l.toLowerCase(); }); return { kind: TextBindingKind.inlineStyle, property: targetCssProperty, attribute: "style" }; } } break; case "innerText": case "textContent": return { kind: TextBindingKind.textContent, attribute: "textContent" }; } }, oneTimeTreeBinding: function (binding) { if (binding.destination.length === 1 && binding.destination[0] === "id") { if (_BaseUtils.validation) { throw new _ErrorFromName("WinJS.Binding.IdBindingNotSupported", _Resources._formatString(strings.idBindingNotSupported, binding.bindingText)); } _Log.log && _Log.log(_Resources._formatString(strings.idBindingNotSupported, binding.bindingText), "winjs binding", "error"); this.markBindingAsError(binding); return; } if (binding.destination.length === 0) { _Log.log && _Log.log(strings.cannotBindToThis, "winjs binding", "error"); this.markBindingAsError(binding); return; } var that = this; var initialValue; binding.pathExpression = this.bindingExpression(binding); binding.value = function () { return binding.pathExpression; }; if (binding.original) { initialValue = binding.pathExpression; binding.original.initialValue = initialValue; } binding.kind = BindingKind.tree; binding.definition = function () { var formatString; if (initialValue) { formatString = "({targetPath} || {{}}){prop} = {initialValue}"; } else { formatString = "({targetPath} || {{}}){prop} = {sourcePath}"; } return that.formatCode( formatString, { targetPath: nullableFilteredIdentifierAccessExpression( binding.elementCapture, binding.destination.slice(0, -1), that.nullableIdentifierAccessTemporary, that.importSafe("targetSecurityCheck", targetSecurityCheck) ), prop: identifierAccessExpression(binding.destination.slice(-1)), sourcePath: binding.value(), initialValue: initialValue, } ); }; }, optimize: function () { if (this._stage > Stage.optimze) { throw "Illegal: once we have moved past link we cannot revist it"; } this._stage = Stage.optimze; // Identify all bindings which can be turned into tree bindings, in some cases this consists // of simply changing their type and providing a definition, in other cases it involves // adding a new tree binding to complement the other binding // for (var i = 0; i < this._bindings.length; i++) { var binding = this._bindings[i]; if (binding.template) { continue; } switch (binding.initializer.import) { case init_defaultBind: // Add a new tree binding for one-time binding and mark the defaultBind as delayable var newBinding = merge(binding, { kind: BindingKind.tree, initializer: this.importSafe("init_oneTime", init_oneTime), original: binding, }); newBinding.elementCapture.refCount++; this.oneTimeTreeBinding(newBinding); this._bindings.splice(i, 0, newBinding); binding.delayable = true; i++; break; case init_setAttribute: // Add a new tree binding for one-time setAttribute and mark the setAttribute as delayable var newBinding = merge(binding, { kind: BindingKind.tree, initializer: this.importSafe("init_setAttributeOneTime", init_setAttributeOneTime), original: binding, }); newBinding.elementCapture.refCount++; this.setAttributeOneTimeTreeBinding(newBinding); this._bindings.splice(i, 0, newBinding); binding.delayable = true; i++; break; case init_oneTime: this.oneTimeTreeBinding(binding); break; case init_setAttributeOneTime: this.setAttributeOneTimeTreeBinding(binding); break; case init_addClassOneTime: this.addClassOneTimeTreeBinding(binding); break; default: if (binding.initializer) { binding.delayable = !!binding.initializer.import.delayable; } break; } } if (this._optimizeTextBindings) { // Identifiy all potential text bindings and generate text replacement expressions // var textBindings = {}; for (var i = 0; i < this._bindings.length; i++) { var binding = this._bindings[i]; if (binding.template) { continue; } if (binding.kind === BindingKind.error) { continue; } switch (binding.initializer.import) { case init_oneTime: this.oneTimeTextBinding(binding); break; case init_setAttributeOneTime: this.setAttributeOneTimeTextBinding(binding); break; case init_addClassOneTime: this.addClassOneTimeTextBinding(binding); break; default: break; } if (binding.textBindingId) { textBindings[binding.textBindingId] = binding; } } if (Object.keys(textBindings).length) { var newHtml = this._templateContent.innerHTML; // Perform any adjustments to the HTML that are needed for things like styles and // boolean attributes // newHtml = this._htmlProcessors.reduce( function (html, replacer) { return replacer(html); }, newHtml ); // All the even indexes are content and all the odds are replacements // // NOTE: this regular expression is var parts = newHtml.split(this._textBindingRegex); for (var i = 1; i < parts.length; i += 2) { var binding = textBindings[parts[i]]; parts[i] = binding.definition; } // Generate the function which will code-gen the HTML replacements. // this._html = function () { var result = parts.map(function (p) { // the strings are the literal parts of the HTML that came directly from the DOM // the functions are the definitions for string replacements return typeof p === "string" ? literal(p) : p(); }).join(" + "); return multiline(result); }; } } }, prettify: function (result) { // remove all lines which contain nothing but a semi-colon var lines = result.split("\n"); return lines.filter(function (line) { return !semiColonOnlyLineRegEx.test(line); }).join("\n"); }, setAttributeOneTimeTextBinding: function (binding) { var that = this; var attribute = binding.destination[0]; var id = this.createTextBindingHole(binding.elementCapture.element.tagName, attribute, ++this._textBindingId); var initialValue; if (binding.original) { initialValue = binding.original.initialValue; } binding.textBindingId = id; binding.kind = BindingKind.text; binding.elementCapture.element.setAttribute(attribute, id); binding.elementCapture.refCount--; binding.definition = function () { var formatString; if (initialValue) { formatString = "{htmlEscape}({initialValue})"; } else { formatString = "{htmlEscape}({value})"; } return that.formatCode( formatString, { htmlEscape: that._staticVariables.htmlEscape, initialValue: initialValue, value: binding.value(), } ); }; }, setAttributeOneTimeTreeBinding: function (binding) { if (binding.destination.length === 1 && binding.destination[0] === "id") { if (_BaseUtils.validation) { throw new _ErrorFromName("WinJS.Binding.IdBindingNotSupported", _Resources._formatString(strings.idBindingNotSupported, binding.bindingText)); } _Log.log && _Log.log(_Resources._formatString(strings.idBindingNotSupported, binding.bindingText), "winjs binding", "error"); this.markBindingAsError(binding); return; } if (binding.destination.length !== 1 || !binding.destination[0]) { _Log.log && _Log.log(strings.attributeBindingSingleProperty, "winjs binding", "error"); this.markBindingAsError(binding); return; } var that = this; var initialValue; binding.pathExpression = this.bindingExpression(binding); binding.value = function () { return binding.pathExpression; }; if (binding.original) { initialValue = this.defineInstance(InstanceKind.variable, "", binding.value); binding.original.initialValue = initialValue; } binding.kind = BindingKind.tree; binding.definition = function () { var formatString; if (initialValue) { formatString = "{element}.setAttribute({attribute}, \"\" + {initialValue})"; } else { formatString = "{element}.setAttribute({attribute}, \"\" + {value})"; } return that.formatCode( formatString, { element: binding.elementCapture, attribute: literal(binding.destination[0]), initialValue: initialValue, value: binding.value(), } ); }; }, }, { _TreeCSE: TreeCSE, compile: function TemplateCompiler_compile(template, templateElement, options) { if (!(templateElement instanceof _Global.HTMLElement)) { throw "Illegal"; } writeProfilerMark("WinJS.Binding.Template:compile" + options.profilerMarkIdentifier + ",StartTM"); var compiler = new TemplateCompiler(templateElement, options); compiler.analyze(); var importAliases = compiler.importAllSafe({ Signal: _Signal, global: _Global, document: _Global.document, cancelBlocker: cancelBlocker, promise_as: promise_as, disposeInstance: disposeInstance, markDisposable: markDisposable, ui_processAll: ui_processAll, binding_processAll: binding_processAll, insertAdjacentHTMLUnsafe: insertAdjacentHTMLUnsafe, promise: Promise, utilities_data: utilities_data, requireSupportedForProcessing: requireSupportedForProcessing, htmlEscape: htmlEscape, scopedSelect: scopedSelect, delayedBindingProcessing: delayedBindingProcessing, writeProfilerMark: writeProfilerMark }); compiler.optimize(); compiler.deadCodeElimination(); compiler.lower(); var codeTemplate; var delayBindings; switch (options.target) { case "render": codeTemplate = compiler.async ? renderImplCodeAsyncTemplate : renderImplCodeTemplate; delayBindings = false; break; case "renderItem": codeTemplate = compiler.async ? renderItemImplCodeAsyncTemplate : renderItemImplCodeTemplate; delayBindings = true; break; } var body = compiler.compile(codeTemplate, importAliases, delayBindings); var render = compiler.link(body); compiler.done(); writeProfilerMark("WinJS.Binding.Template:compile" + options.profilerMarkIdentifier + ",StopTM"); return render; } }); // // Templates // function trimLinesRight(string) { // Replace all empty lines with just a newline // Remove all trailing spaces return string.replace(/^\s*$/gm, "").replace(/^(.*[^\s])( *)$/gm, function (unused, content) { return content; }); } var renderImplMainCodePrefixTemplate = trimLinesRight( "container.classList.add(\"win-template\"); \n" + "var html = {html}; \n" + "{insertAdjacentHTMLUnsafe}(container, \"beforeend\", html); \n" + "returnedElement = {returnedElement}; \n" + " \n" + "// Capture Definitions \n" + "{capture_definitions}; \n" + "{set_msParentSelectorScope}; \n" + " \n" ); var renderImplControlAndBindingProcessing = trimLinesRight( "// Control Processing \n" + "{control_processing}; \n" + " \n" + "// Binding Processing \n" + "{binding_processing}; \n" + " \n" + "var result = {promise_as}(returnedElement); \n" ); var renderImplAsyncControlAndBindingProcessing = trimLinesRight( "var controlSignal = new {Signal}(); \n" + "var controlDone = function () {{ if (--{control_counter} === 0) {{ controlSignal.complete(); }} }}; \n" + "controlDone(); \n" + " \n" + "// Control Processing \n" + "{control_processing}; \n" + " \n" + "var result = controlSignal.promise.then(function () {{ \n" + " // Binding Processing \n" + " {binding_processing}; \n" + " return {promise}.join({nestedTemplates}); \n" + "}}).then(function () {{ \n" + " return returnedElement; \n" + "}}).then(null, function (e) {{ \n" + " if (typeof e === \"object\" && e.name === \"Canceled\") {{ returnedElement.dispose(); }} \n" + " return {promise}.wrapError(e); \n" + "}}); \n" ); var renderImplMainCodeSuffixTemplate = trimLinesRight( "{markDisposable}(returnedElement, function () {{ {disposeInstance}(returnedElement, result); }}); \n" + "{suffix_statements}; \n" ); var renderImplCodeTemplate = trimLinesRight( "function render(data, container) {{ \n" + " {debug_break} \n" + " if (typeof data === \"object\" && typeof data.then === \"function\") {{ \n" + " // async data + a container falls back to interpreted path \n" + " if (container) {{ \n" + " var result = this._renderInterpreted(data, container); \n" + " return result.element.then(function () {{ return result.renderComplete; }}); \n" + " }} \n" + " return {cancelBlocker}(data).then(function(data) {{ return render(data); }}); \n" + " }} \n" + " \n" + " {writeProfilerMark}({profilerMarkIdentifierStart}); \n" + " \n" + " // Declarations \n" + " var {instance_variable_declarations}; \n" + " var returnedElement; \n" + " \n" + " // Global Definitions \n" + " {global_definitions}; \n" + " \n" + " // Data Definitions \n" + " data = (data === {global} ? data : {requireSupportedForProcessing}(data)); \n" + " {data_definitions}; \n" + " \n" + " // Instance Variable Definitions \n" + " {instance_variable_definitions}; \n" + " \n" + " // HTML Processing \n" + " container = container || {document}.createElement({tagName}); \n" + " var startIndex = container.childElementCount; \n" + " " + trim(indent(4, renderImplMainCodePrefixTemplate)) + " \n" + " \n" + " " + trim(indent(4, renderImplControlAndBindingProcessing)) + " \n" + " " + trim(indent(4, renderImplMainCodeSuffixTemplate)) + " \n" + " \n" + " {writeProfilerMark}({profilerMarkIdentifierStop}); \n" + " \n" + " return result; \n" + "}} \n" ); var renderImplCodeAsyncTemplate = trimLinesRight( "function render(data, container) {{ \n" + " {debug_break} \n" + " if (typeof data === \"object\" && typeof data.then === \"function\") {{ \n" + " // async data + a container falls back to interpreted path \n" + " if (container) {{ \n" + " var result = this._renderInterpreted(data, container); \n" + " return result.element.then(function () {{ return result.renderComplete; }}); \n" + " }} \n" + " return {cancelBlocker}(data).then(function(data) {{ return render(data, container); }}); \n" + " }} \n" + " \n" + " {writeProfilerMark}({profilerMarkIdentifierStart}); \n" + " \n" + " // Declarations \n" + " var {instance_variable_declarations}; \n" + " var returnedElement; \n" + " \n" + " // Global Definitions \n" + " {global_definitions}; \n" + " \n" + " // Data Definitions \n" + " data = (data === {global} ? data : {requireSupportedForProcessing}(data)); \n" + " {data_definitions}; \n" + " \n" + " // Instance Variable Definitions \n" + " {instance_variable_definitions}; \n" + " \n" + " // HTML Processing \n" + " container = container || {document}.createElement({tagName}); \n" + " var startIndex = container.childElementCount; \n" + " " + trim(indent(4, renderImplMainCodePrefixTemplate)) + " \n" + " \n" + " " + trim(indent(4, renderImplAsyncControlAndBindingProcessing)) + " \n" + " " + trim(indent(4, renderImplMainCodeSuffixTemplate)) + " \n" + " \n" + " {writeProfilerMark}({profilerMarkIdentifierStop}); \n" + " \n" + " return result; \n" + "}} \n" ); var renderItemImplMainCodeSuffixTemplate = trimLinesRight( "{markDisposable}(returnedElement, function () {{ {disposeInstance}(returnedElement, result, renderComplete); }});\n" + "{suffix_statements}; \n" ); var renderItemImplCodeTemplate = trimLinesRight( "function renderItem(itemPromise) {{ \n" + " {debug_break} \n" + " // Declarations \n" + " var {instance_variable_declarations}; \n" + " var element, renderComplete, data, returnedElement; \n" + " \n" + " element = itemPromise.then(function renderItem(item) {{ \n" + " if (typeof item.data === \"object\" && typeof item.data.then === \"function\") {{ \n" + " return {cancelBlocker}(item.data).then(function (data) {{ return renderItem({{ data: data }}); }});\n" + " }} \n" + " \n" + " {writeProfilerMark}({profilerMarkIdentifierStart}); \n" + " \n" + " // Global Definitions \n" + " {global_definitions}; \n" + " \n" + " // Data Definitions \n" + " data = item.data; \n" + " data = (data === {global} ? data : {requireSupportedForProcessing}(data)); \n" + " {data_definitions}; \n" + " \n" + " // Instance Variable Definitions \n" + " {instance_variable_definitions}; \n" + " \n" + " // HTML Processing \n" + " var container = {document}.createElement({tagName}); \n" + " var startIndex = 0; \n" + " " + trim(indent(8, renderImplMainCodePrefixTemplate)) + " \n" + " \n" + " " + trim(indent(8, renderImplControlAndBindingProcessing)) + " \n" + " " + trim(indent(8, renderItemImplMainCodeSuffixTemplate)) + " \n" + " \n" + " {writeProfilerMark}({profilerMarkIdentifierStop}); \n" + " \n" + " return result; \n" + " }}); \n" + " {renderComplete}; \n" + " return {{ \n" + " element: element, \n" + " renderComplete: renderComplete || element, \n" + " }}; \n" + "}} \n" ); var renderItemImplRenderCompleteTemplate = trimLinesRight( "renderComplete = element.then(function () {{ \n" + " return itemPromise; \n" + "}}).then(function (item) {{ \n" + " return item.ready || item; \n" + "}}).then(function (item) {{ \n" + " {delayed_binding_processing}; \n" + " return element; \n" + "}}); \n" ); var renderItemImplCodeAsyncTemplate = trimLinesRight( "function renderItem(itemPromise) {{ \n" + " {debug_break} \n" + " // Declarations \n" + " var {instance_variable_declarations}; \n" + " var element, renderComplete, data, returnedElement; \n" + " \n" + " element = itemPromise.then(function renderItem(item) {{ \n" + " if (typeof item.data === \"object\" && typeof item.data.then === \"function\") {{ \n" + " return {cancelBlocker}(item.data).then(function (data) {{ return renderItem({{ data: data }}); }});\n" + " }} \n" + " \n" + " {writeProfilerMark}({profilerMarkIdentifierStart}); \n" + " \n" + " // Global Definitions \n" + " {global_definitions}; \n" + " \n" + " // Data Definitions \n" + " data = item.data; \n" + " data = (data === {global} ? data : {requireSupportedForProcessing}(data)); \n" + " {data_definitions}; \n" + " \n" + " // Instance Variable Definitions \n" + " {instance_variable_definitions}; \n" + " \n" + " // HTML Processing \n" + " var container = {document}.createElement({tagName}); \n" + " var startIndex = 0; \n" + " " + trim(indent(8, renderImplMainCodePrefixTemplate)) + " \n" + " \n" + " " + trim(indent(8, renderImplAsyncControlAndBindingProcessing)) + " \n" + " " + trim(indent(8, renderItemImplMainCodeSuffixTemplate)) + " \n" + " \n" + " {writeProfilerMark}({profilerMarkIdentifierStop}); \n" + " \n" + " return result; \n" + " }}); \n" + " {renderComplete}; \n" + " return {{ \n" + " element: element, \n" + " renderComplete: renderComplete || element, \n" + " }}; \n" + "}} \n" ); var linkerCodeTemplate = trimLinesRight( "\"use strict\"; \n" + " \n" + "// statics \n" + "var {static_variable_declarations}; \n" + "{static_variable_definitions}; \n" + " \n" + "// generated template rendering function \n" + "return {body}; \n" ); // // End Templates // return TemplateCompiler; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/BindingTemplate',[ 'exports', './Core/_Global', './Core/_WinRT', './Core/_Base', './Core/_BaseUtils', './Core/_Log', './Core/_WriteProfilerMark', './Binding/_Declarative', './BindingTemplate/_DataTemplateCompiler', './ControlProcessor', './Fragments', './Promise', './Utilities/_Dispose', './Utilities/_ElementUtilities' ], function dataTemplateInit(exports, _Global, _WinRT, _Base, _BaseUtils, _Log, _WriteProfilerMark, _Declarative, _DataTemplateCompiler, ControlProcessor, Fragments, Promise, _Dispose, _ElementUtilities) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } var cancelBlocker = Promise._cancelBlocker; _Base.Namespace._moduleDefine(exports, "WinJS.Binding", { /// /// /// Provides a reusable declarative binding element. /// /// /// Template ///
Place content here
]]>
/// /// /// /// Template: _Base.Namespace._lazy(function () { function interpretedRender(template, dataContext, container) { _WriteProfilerMark("WinJS.Binding:templateRender" + template._profilerMarkIdentifier + ",StartTM"); if (++template._counter === 1 && (template.debugBreakOnRender || Template._debugBreakOnRender)) { debugger; // jshint ignore:line } var workPromise = Promise.wrap(); var d = container || _Global.document.createElement(template.element.tagName); _ElementUtilities.addClass(d, "win-template"); _ElementUtilities.addClass(d, "win-loading"); var that = template; function done() { _ElementUtilities.removeClass(d, "win-loading"); _WriteProfilerMark("WinJS.Binding:templateRender" + template._profilerMarkIdentifier + ",StopTM"); return extractedChild || d; } var initial = d.children.length; var element; var extractedChild; var dispose = function () { var bindings = _ElementUtilities.data(d).winBindings; if (bindings) { bindings.forEach(function (item) { item.cancel(); }); } workPromise.cancel(); }; if (template.extractChild) { element = Fragments.renderCopy(that.href || that.element, _Global.document.createElement(that.element.tagName)).then(function (frag) { var child = frag.firstElementChild; extractedChild = child; _Dispose.markDisposable(child, dispose); d.appendChild(child); return child; }); } else { _Dispose.markDisposable(d, dispose); element = Fragments.renderCopy(that.href || that.element, d); } var renderComplete = element. then(function Template_renderImpl_renderComplete_then() { var work; // If no existing children, we can do the faster path of just calling // on the root element... // if (initial === 0) { work = function (f, a, b, c) { return f(extractedChild || d, a, b, c); }; } else { // We only grab the newly added nodes (always at the end) // and in the common case of only adding a single new element // we avoid the "join" overhead // var all = d.children; if (all.length === initial + 1) { work = function (f, a, b, c) { return f(all[initial], a, b, c); }; } else { // we have to capture the elements first, in case // doing the work affects the children order/content // var elements = []; for (var i = initial, l = all.length; i < l; i++) { elements.push(all[i]); } work = function (f, a, b, c) { var join = []; elements.forEach(function (e) { join.push(f(e, a, b, c)); }); return Promise.join(join); }; } } var child = d.firstElementChild; while (child) { child.msParentSelectorScope = true; child = child.nextElementSibling; } // This allows "0" to mean no timeout (at all) and negative values // mean setImmediate (no setTimeout). Since Promise.timeout uses // zero to mean setImmediate, we have to coerce. // var timeout = that.processTimeout; function complete() { return work(ControlProcessor.processAll). then(function () { return cancelBlocker(dataContext); }). then(function Template_renderImpl_Binding_processAll(data) { return work(_Declarative.processAll, data, !extractedChild && !initial, that.bindingCache); }). then(null, function (e) { if (typeof e === "object" && e.name === "Canceled") { (extractedChild || d).dispose(); } return Promise.wrapError(e); }); } if (timeout) { if (timeout < 0) { timeout = 0; } return Promise.timeout(timeout).then(function () { workPromise = complete(); return workPromise; }); } else { workPromise = complete(); return workPromise; } }).then(done, function (err) { done(); return Promise.wrapError(err); }); return { element: element, renderComplete: renderComplete }; } var Template = _Base.Class.define(function Template_ctor(element, options) { /// /// /// Creates a template that provides a reusable declarative binding element. /// /// /// The DOM element to convert to a template. /// /// /// If this parameter is supplied, the template is loaded from the URI and /// the content of the element parameter is ignored. /// /// this._element = element || _Global.document.createElement("div"); this._element.winControl = this; this._profilerMarkIdentifier = _BaseUtils._getProfilerMarkIdentifier(this._element); _WriteProfilerMark("WinJS.Binding:newTemplate" + this._profilerMarkIdentifier + ",StartTM"); var that = this; this._element.renderItem = function (itemPromise, recycled) { return that._renderItemImpl(itemPromise, recycled); }; options = options || {}; this.href = options.href; this.enableRecycling = !!options.enableRecycling; this.processTimeout = options.processTimeout || 0; this.bindingInitializer = options.bindingInitializer; this.debugBreakOnRender = options.debugBreakOnRender; this.disableOptimizedProcessing = options.disableOptimizedProcessing; this.extractChild = options.extractChild; this._counter = 0; // This will eventually change name and reverse polarity, starting opt-in. // this._compile = !!options._compile; if (!this.href) { this.element.style.display = "none"; } this.bindingCache = { expressions: {} }; _WriteProfilerMark("WinJS.Binding:newTemplate" + this._profilerMarkIdentifier + ",StopTM"); }, { _shouldCompile: { get: function () { // This is the temporary switch to opt-in to compilation, eventually replaced // by default opt-in with an opt-out switch. // var shouldCompile = true; shouldCompile = shouldCompile && !Template._interpretAll; shouldCompile = shouldCompile && !this.disableOptimizedProcessing; if (shouldCompile) { shouldCompile = shouldCompile && this.processTimeout === 0; shouldCompile = shouldCompile && (!this.href || this.href instanceof _Global.HTMLElement); if (!shouldCompile) { _Log.log && _Log.log("Cannot compile templates which use processTimeout or href properties", "winjs binding", "warn"); } } return shouldCompile; } }, /// /// If specified this function is used as the default initializer for any data bindings which do not explicitly specify one. The /// provided function must be marked as supported for processing. /// bindingInitializer: { get: function () { return this._bindingInitializer; }, set: function (value) { this._bindingInitializer = value; this._reset(); } }, /// /// Indicates whether a templates should break in the debugger on first render /// debugBreakOnRender: { get: function () { return this._debugBreakOnRender; }, set: function (value) { this._debugBreakOnRender = !!value; this._reset(); } }, /// /// Set this property to true to resotre classic template processing and data binding and disable template compilation. /// disableOptimizedProcessing: { get: function () { return this._disableOptimizedProcessing; }, set: function (value) { this._disableOptimizedProcessing = !!value; this._reset(); } }, /// element: { get: function () { return this._element; }, }, /// /// Return the first element child of the template instead of a wrapper element hosting all the template content. /// extractChild: { get: function () { return this._extractChild; }, set: function (value) { this._extractChild = !!value; this._reset(); } }, /// /// Number of milliseconds to delay instantiating declarative controls. Zero (0) will result in no delay, any negative number /// will result in a setImmediate delay, any positive number will be treated as the number of milliseconds. /// processTimeout: { get: function () { return this._processTimeout || 0; }, set: function (value) { this._processTimeout = value; this._reset(); } }, render: _BaseUtils.markSupportedForProcessing(function (dataContext, container) { /// /// /// Binds values from the specified data context to elements that are descendents of the specified root element /// and have the declarative binding attributes (data-win-bind). /// /// /// The object to use for default data binding. /// /// /// The element to which to add this rendered template. If this parameter is omitted, a new DIV is created. /// /// /// A promise that is completed after binding has finished. The value is /// either the element specified in the container parameter or the created DIV. /// /// return this._renderImpl(dataContext, container); }), // Hook point for compiled template // _renderImpl: function (dataContext, container) { if (this._shouldCompile) { try { this._renderImpl = this._compileTemplate({ target: "render" }); return this._renderImpl(dataContext, container); } catch (e) { return Promise.wrapError(e); } } var render = interpretedRender(this, dataContext, container); return render.element.then(function () { return render.renderComplete; }); }, _renderInterpreted: function (dataContext, container) { return interpretedRender(this, dataContext, container); }, renderItem: function (item, recycled) { /// /// /// Renders an instance of this template bound to the data contained in item. If /// the recycled parameter is present, and enableRecycling is true, then the template attempts /// to reuse the DOM elements from the recycled parameter. /// /// /// The object that contains the data to bind to. Only item.data is required. /// /// /// A previously-generated template instance. /// /// /// The DOM element. /// /// return this._renderItemImpl(item, recycled); }, // Hook point for compiled template // _renderItemImpl: function (item, recycled) { if (this._shouldCompile) { try { this._renderItemImpl = this._compileTemplate({ target: "renderItem" }); return this._renderItemImpl(item); } catch (e) { return { element: Promise.wrapError(e), renderComplete: Promise.wrapError(e), }; } } var that = this; // we only enable element cache when we are trying // to recycle. Otherwise our element cache would // grow unbounded. // if (this.enableRecycling && !this.bindingCache.elements) { this.bindingCache.elements = {}; } if (this.enableRecycling && recycled && recycled.msOriginalTemplate === this) { // If we are asked to recycle, we cleanup any old work no matter what // var cacheEntry = this.bindingCache.elements[recycled.id]; var okToReuse = true; if (cacheEntry) { cacheEntry.bindings.forEach(function (v) { v(); }); cacheEntry.bindings = []; okToReuse = !cacheEntry.nocache; } // If our cache indicates that we hit a non-cancelable thing, then we are // in an unknown state, so we actually can't recycle the tree. We have // cleaned up what we can, but at this point we need to reset and create // a new tree. // if (okToReuse) { // Element recycling requires that there be no other content in "recycled" other than this // templates' output. // return { element: recycled, renderComplete: item.then(function (item) { return _Declarative.processAll(recycled, item.data, true, that.bindingCache); }), }; } } var render = interpretedRender(this, item.then(function (item) { return item.data; })); render.element = render.element.then(function (e) { e.msOriginalTemplate = that; return e; }); return render; }, _compileTemplate: function (options) { var that = this; var result = _DataTemplateCompiler._TemplateCompiler.compile(this, this.href || this.element, { debugBreakOnRender: this.debugBreakOnRender || Template._debugBreakOnRender, defaultInitializer: this.bindingInitializer || options.defaultInitializer, disableTextBindingOptimization: options.disableTextBindingOptimization || false, target: options.target, extractChild: this.extractChild, profilerMarkIdentifier: this._profilerMarkIdentifier }); var resetOnFragmentChange = options.resetOnFragmentChange || _WinRT.Windows.ApplicationModel.DesignMode.designModeEnabled; if (resetOnFragmentChange) { // For platforms that don't support MutationObserver the shim // currently will never fire. This is OK because only MutationObserver // can monitor DocFragments and this feature is only for // assisting authoring tools. var mo = new _ElementUtilities._MutationObserver(function () { that._reset(); mo.disconnect(); }); mo.observe(_ElementUtilities.data(this.element).docFragment, { childList: true, attributes: true, characterData: true, subtree: true, }); } return result; }, _reset: function () { // Reset the template to being not compiled. In design mode this triggers on a mutation // of the original document fragment. delete this._renderImpl; delete this._renderItemImpl; }, }, { isDeclarativeControlContainer: { value: true, writable: false, configurable: false }, render: { value: function (href, dataContext, container) { /// /// /// Renders a template based on a URI. /// /// /// The URI from which to load the template. /// /// /// The object to use for default data binding. /// /// /// The element to which to add this rendered template. If this parameter is omitted, a new DIV is created. /// /// /// A promise that is completed after binding has finished. The value is /// either the object in the container parameter or the created DIV. /// /// return new Template(null, { href: href }).render(dataContext, container); } } }); return Template; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // WinJS.Binding.ListDataSource // define('WinJS/BindingList/_BindingListDataSource',[ 'exports', '../Core/_WinRT', '../Core/_Base', '../Core/_ErrorFromName', '../Binding/_DomWeakRefTable', '../Promise', '../Scheduler', '../Utilities/_UI' ], function bindingListDataSourceInit(exports, _WinRT, _Base, _ErrorFromName, _DomWeakRefTable, Promise, Scheduler, _UI) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.Binding", { _BindingListDataSource: _Base.Namespace._lazy(function () { var errors = { get noLongerMeaningful() { return Promise.wrapError(new _ErrorFromName(_UI.EditError.noLongerMeaningful)); } }; function findNextKey(list, index) { var len = list.length; while (index < len - 1) { var item = list.getItem(++index); if (item) { return item.key; } } return null; } function findPreviousKey(list, index) { while (index > 0) { var item = list.getItem(--index); if (item) { return item.key; } } return null; } function subscribe(target, handlers) { Object.keys(handlers).forEach(function (handler) { target.addEventListener(handler, handlers[handler]); }); } function unsubscribe(target, handlers) { Object.keys(handlers).forEach(function (handler) { target.removeEventListener(handler, handlers[handler]); }); } var CompletePromise = Promise.wrap().constructor; var NullWrappedItem = _Base.Class.derive(CompletePromise, function () { this._value = null; }, { release: function () { }, retain: function () { return this; } }, { supportedForProcessing: false, } ); var WrappedItem = _Base.Class.derive(CompletePromise, function (listBinding, item) { this._value = item; this._listBinding = listBinding; }, { handle: { get: function () { return this._value.key; } }, index: { get: function () { return this._value.index; } }, release: function () { this._listBinding._release(this._value, this._listBinding._list.indexOfKey(this._value.key)); }, retain: function () { this._listBinding._addRef(this._value, this._listBinding._list.indexOfKey(this._value.key)); return this; } }, { supportedForProcessing: false, } ); var AsyncWrappedItem = _Base.Class.derive(Promise, function (listBinding, item, name) { var that = this; this._item = item; this._listBinding = listBinding; Promise.call(this, function (c) { Scheduler.schedule(function BindingList_async_item() { if (listBinding._released) { that.cancel(); return; } c(item); }, Scheduler.Priority.normal, null, "WinJS.Binding.List." + name); }); }, { handle: { get: function () { return this._item.key; } }, index: { get: function () { return this._item.index; } }, release: function () { this._listBinding._release(this._item, this._listBinding._list.indexOfKey(this._item.key)); }, retain: function () { this._listBinding._addRef(this._item, this._listBinding._list.indexOfKey(this._item.key)); return this; } }, { supportedForProcessing: false, } ); function wrap(listBinding, item) { return item ? new WrappedItem(listBinding, item) : new NullWrappedItem(); } function wrapAsync(listBinding, item, name) { return item ? new AsyncWrappedItem(listBinding, item, name) : new NullWrappedItem(); } function cloneWithIndex(list, item, index) { return item && list._annotateWithIndex(item, index); } var ListBinding = _Base.Class.define(function ListBinding_ctor(dataSource, list, notificationHandler, id) { this._dataSource = dataSource; this._list = list; this._editsCount = 0; this._notificationHandler = notificationHandler; this._pos = -1; this._retained = []; this._retained.length = list.length; this._retainedKeys = {}; this._affectedRange = null; // When in WebContext, weakref utility functions don't work as desired so we capture this // ListBinding object in the handler's closure. This causes the same leak as in 1.0. var fallbackReference = null; if (!_WinRT.msSetWeakWinRTProperty || !_WinRT.msGetWeakWinRTProperty) { fallbackReference = this; } if (notificationHandler) { var handleEvent = function (eventName, eventArg) { var lb = _DomWeakRefTable._getWeakRefElement(id) || fallbackReference; if (lb) { lb["_" + eventName](eventArg); return true; } return false; }; this._handlers = { itemchanged: function handler(event) { if (!handleEvent("itemchanged", event)) { list.removeEventListener("itemchanged", handler); } }, iteminserted: function handler(event) { if (!handleEvent("iteminserted", event)) { list.removeEventListener("iteminserted", handler); } }, itemmoved: function handler(event) { if (!handleEvent("itemmoved", event)) { list.removeEventListener("itemmoved", handler); } }, itemremoved: function handler(event) { if (!handleEvent("itemremoved", event)) { list.removeEventListener("itemremoved", handler); } }, reload: function handler() { if (!handleEvent("reload")) { list.removeEventListener("reload", handler); } } }; subscribe(this._list, this._handlers); } }, { _itemchanged: function (event) { var key = event.detail.key; var index = event.detail.index; this._updateAffectedRange(index, "changed"); var newItem = event.detail.newItem; var oldItem = this._retained[index]; if (oldItem) { var handler = this._notificationHandler; if (oldItem.index !== index) { var oldIndex = oldItem.index; oldItem.index = index; if (handler && handler.indexChanged) { handler.indexChanged(newItem.key, index, oldIndex); } } newItem = cloneWithIndex(this._list, newItem, index); newItem._retainedCount = oldItem._retainedCount; this._retained[index] = newItem; this._retainedKeys[key] = newItem; this._beginEdits(this._list.length); if (handler && handler.changed) { handler.changed( newItem, oldItem ); } this._endEdits(); } else { // Item was not retained, but we still want to batch this change with the other edits to send the affectedRange notification. this._beginEdits(this._list.length); this._endEdits(); } }, _iteminserted: function (event) { var index = event.detail.index; this._updateAffectedRange(index, "inserted"); this._beginEdits(this._list.length - 1); if (index <= this._pos) { this._pos = Math.min(this._pos + 1, this._list.length); } var retained = this._retained; // create a hole for this thing and then immediately make it undefined retained.splice(index, 0, 0); delete retained[index]; if (this._shouldNotify(index) || this._list.length === 1) { var handler = this._notificationHandler; if (handler && handler.inserted) { handler.inserted( wrap(this, cloneWithIndex(this._list, this._list.getItem(index), index)), findPreviousKey(this._list, index), findNextKey(this._list, index) ); } } this._endEdits(); }, _itemmoved: function (event) { var oldIndex = event.detail.oldIndex; var newIndex = event.detail.newIndex; this._updateAffectedRange(oldIndex, "moved"); this._updateAffectedRange(newIndex, "moved"); this._beginEdits(this._list.length); if (oldIndex < this._pos || newIndex <= this._pos) { if (newIndex > this._pos) { this._pos = Math.max(-1, this._pos - 1); } else if (oldIndex > this._pos) { this._pos = Math.min(this._pos + 1, this._list.length); } } var retained = this._retained; var item = retained.splice(oldIndex, 1)[0]; retained.splice(newIndex, 0, item); if (!item) { delete retained[newIndex]; item = cloneWithIndex(this._list, this._list.getItem(newIndex), newIndex); } item._moved = true; this._addRef(item, newIndex); this._endEdits(); }, _itemremoved: function (event) { var key = event.detail.key; var index = event.detail.index; this._updateAffectedRange(index, "removed"); this._beginEdits(this._list.length + 1); if (index < this._pos) { this._pos = Math.max(-1, this._pos - 1); } var retained = this._retained; var retainedKeys = this._retainedKeys; var wasRetained = index in retained; retained.splice(index, 1); delete retainedKeys[key]; var handler = this._notificationHandler; if (wasRetained && handler && handler.removed) { handler.removed(key, false); } this._endEdits(); }, _reload: function () { this._retained = []; this._retainedKeys = {}; var handler = this._notificationHandler; if (handler && handler.reload) { handler.reload(); } }, _addRef: function (item, index) { if (index in this._retained) { this._retained[index]._retainedCount++; } else { this._retained[index] = item; this._retainedKeys[item.key] = item; item._retainedCount = 1; } }, _release: function (item, index) { var retained = this._retained[index]; if (retained) { if (retained._retainedCount === 1) { delete this._retained[index]; delete this._retainedKeys[retained.key]; } else { retained._retainedCount--; } } }, _shouldNotify: function (index) { var retained = this._retained; return index in retained || index + 1 in retained || index - 1 in retained; }, _updateAffectedRange: function ListBinding_updateAffectedRange(index, operation) { // Creates a range of affected indices [start, end). // Definition of _affectedRange.start: All items in the set of data with indices < _affectedRange.start have not been directly modified. // Definition of _affectedRange.end: All items in the set of data with indices >= _affectedRange.end have not been directly modified. if (!this._notificationHandler.affectedRange) { return; } //[newStart, newEnd) var newStart = index; var newEnd = (operation !== "removed") ? index + 1 : index; if (this._affectedRange) { switch (operation) { case "inserted": if (index <= this._affectedRange.end) { ++this._affectedRange.end; } break; case "removed": if (index < this._affectedRange.end) { --this._affectedRange.end; } break; case "moved": case "changed": break; } this._affectedRange.start = Math.min(this._affectedRange.start, newStart); this._affectedRange.end = Math.max(this._affectedRange.end, newEnd); } else { // Handle the initial state this._affectedRange = { start: newStart, end: newEnd }; } }, _notifyAffectedRange: function ListBinding_notifyAffectedRange() { if (this._affectedRange) { if (this._notificationHandler && this._notificationHandler.affectedRange) { this._notificationHandler.affectedRange(this._affectedRange); } // reset range this._affectedRange = null; } }, _notifyCountChanged: function () { var oldCount = this._countAtBeginEdits; var newCount = this._list.length; if (oldCount !== newCount) { var handler = this._notificationHandler; if (handler && handler.countChanged) { handler.countChanged(newCount, oldCount); } } }, _notifyIndicesChanged: function () { var retained = this._retained; for (var i = 0, len = retained.length; i < len; i++) { var item = retained[i]; if (item && item.index !== i) { var newIndex = i; var oldIndex = item.index; item.index = newIndex; var handler = this._notificationHandler; if (handler && handler.indexChanged) { handler.indexChanged(item.key, newIndex, oldIndex); } } } }, _notifyMoved: function () { var retained = this._retained; for (var i = 0, len = retained.length; i < len; i++) { var item = retained[i]; if (item && item._moved) { item._moved = false; this._release(item, i); if (this._shouldNotify(i)) { var handler = this._notificationHandler; if (handler && handler.moved) { handler.moved( wrap(this, item), findPreviousKey(this._list, i), findNextKey(this._list, i) ); } } } } }, _beginEdits: function (length, explicit) { this._editsCount++; var handler = this._notificationHandler; if (this._editsCount === 1 && handler) { if (!explicit) { // Batch all edits between now and the job running. This has the effect // of batching synchronous edits. // this._editsCount++; var that = this; Scheduler.schedule(function BindingList_async_batchedEdits() { that._endEdits(); }, Scheduler.Priority.high, null, "WinJS.Binding.List._endEdits"); } if (handler.beginNotifications) { handler.beginNotifications(); } this._countAtBeginEdits = length; } }, _endEdits: function () { this._editsCount--; var handler = this._notificationHandler; if (this._editsCount === 0 && handler) { this._notifyIndicesChanged(); this._notifyMoved(); this._notifyCountChanged(); // It's important to notify the affectedRange after _notifyCountChanged since we expect developers // may take a dependancy on the count being up to date when they recieve the affected range. this._notifyAffectedRange(); if (handler.endNotifications) { handler.endNotifications(); } } }, jumpToItem: function (item) { var index = this._list.indexOfKey(item.handle); if (index === -1) { return Promise.wrap(null); } this._pos = index; return this.current(); }, current: function () { return this.fromIndex(this._pos); }, previous: function () { this._pos = Math.max(-1, this._pos - 1); return this._fromIndex(this._pos, true, "previous"); }, next: function () { this._pos = Math.min(this._pos + 1, this._list.length); return this._fromIndex(this._pos, true, "next"); }, releaseItem: function (item) { if (item.release) { item.release(); } else { this._release(item, this._list.indexOfKey(item.key)); } }, release: function () { if (this._notificationHandler) { unsubscribe(this._list, this._handlers); } this._notificationHandler = null; this._dataSource._releaseBinding(this); this._released = true; }, first: function () { return this.fromIndex(0); }, last: function () { return this.fromIndex(this._list.length - 1); }, fromKey: function (key) { var retainedKeys = this._retainedKeys; var item; if (key in retainedKeys) { item = retainedKeys[key]; } else { item = cloneWithIndex(this._list, this._list.getItemFromKey(key), this._list.indexOfKey(key)); } return wrap(this, item); }, fromIndex: function (index) { return this._fromIndex(index, false, "fromIndex"); }, _fromIndex: function (index, async, name) { var retained = this._retained; var item; if (index in retained) { item = retained[index]; } else { item = cloneWithIndex(this._list, this._list.getItem(index), index); } return async ? wrapAsync(this, item, name) : wrap(this, item); }, }, { supportedForProcessing: false, }); function insertAtStart(unused, data) { /*jshint validthis: true */ // List ignores the key because its key management is internal this._list.unshift(data); return this.itemFromIndex(0); } function insertBefore(unused, data, nextKey) { /*jshint validthis: true */ // List ignores the key because its key management is internal var index = this._list.indexOfKey(nextKey); if (index === -1) { return errors.noLongerMeaningful; } this._list.splice(index, 0, data); return this.itemFromIndex(index); } function insertAfter(unused, data, previousKey) { /*jshint validthis: true */ // List ignores the key because its key management is internal var index = this._list.indexOfKey(previousKey); if (index === -1) { return errors.noLongerMeaningful; } index += 1; this._list.splice(index, 0, data); return this.itemFromIndex(index); } function insertAtEnd(unused, data) { /*jshint validthis: true */ // List ignores the key because its key management is internal this._list.push(data); return this.itemFromIndex(this._list.length - 1); } function change(key, newData) { /*jshint validthis: true */ var index = this._list.indexOfKey(key); if (index === -1) { return errors.noLongerMeaningful; } this._list.setAt(index, newData); return this.itemFromIndex(index); } function moveToStart(key) { /*jshint validthis: true */ var sourceIndex = this._list.indexOfKey(key); if (sourceIndex === -1) { return errors.noLongerMeaningful; } var targetIndex = 0; this._list.move(sourceIndex, targetIndex); return this.itemFromIndex(targetIndex); } function moveBefore(key, nextKey) { /*jshint validthis: true */ var sourceIndex = this._list.indexOfKey(key); var targetIndex = this._list.indexOfKey(nextKey); if (sourceIndex === -1 || targetIndex === -1) { return errors.noLongerMeaningful; } targetIndex = sourceIndex < targetIndex ? targetIndex - 1 : targetIndex; this._list.move(sourceIndex, targetIndex); return this.itemFromIndex(targetIndex); } function moveAfter(key, previousKey) { /*jshint validthis: true */ var sourceIndex = this._list.indexOfKey(key); var targetIndex = this._list.indexOfKey(previousKey); if (sourceIndex === -1 || targetIndex === -1) { return errors.noLongerMeaningful; } targetIndex = sourceIndex <= targetIndex ? targetIndex : targetIndex + 1; this._list.move(sourceIndex, targetIndex); return this.itemFromIndex(targetIndex); } function moveToEnd(key) { /*jshint validthis: true */ var sourceIndex = this._list.indexOfKey(key); if (sourceIndex === -1) { return errors.noLongerMeaningful; } var targetIndex = this._list.length - 1; this._list.move(sourceIndex, targetIndex); return this.itemFromIndex(targetIndex); } function remove(key) { /*jshint validthis: true */ var index = this._list.indexOfKey(key); if (index === -1) { return errors.noLongerMeaningful; } this._list.splice(index, 1); return Promise.wrap(); } var bindingId = 0; var DataSource = _Base.Class.define(function DataSource_ctor(list) { this._usingWeakRef = _WinRT.msSetWeakWinRTProperty && _WinRT.msGetWeakWinRTProperty; this._bindings = {}; this._list = list; if (list.unshift) { this.insertAtStart = insertAtStart; } if (list.push) { this.insertAtEnd = insertAtEnd; } if (list.setAt) { this.change = change; } if (list.splice) { this.insertAfter = insertAfter; this.insertBefore = insertBefore; this.remove = remove; } if (list.move) { this.moveAfter = moveAfter; this.moveBefore = moveBefore; this.moveToEnd = moveToEnd; this.moveToStart = moveToStart; } }, { _releaseBinding: function (binding) { delete this._bindings[binding._id]; }, addEventListener: function () { // nop, we don't send statusChanged }, removeEventListener: function () { // nop, we don't send statusChanged }, createListBinding: function (notificationHandler) { var id = "ds_" + (++bindingId); var binding = new ListBinding(this, this._list, notificationHandler, id); binding._id = id; if (this._usingWeakRef) { _DomWeakRefTable._createWeakRef(binding, id); this._bindings[id] = id; } else { this._bindings[id] = binding; } return binding; }, getCount: function () { return Promise.wrap(this._list.length); }, itemFromKey: function (key) { // Clone with a dummy index var list = this._list, item = cloneWithIndex(list, list.getItemFromKey(key), -1); // Override the index property with a getter Object.defineProperty(item, "index", { get: function () { return list.indexOfKey(key); }, enumerable: false, configurable: true }); return Promise.wrap(item); }, itemFromIndex: function (index) { return Promise.wrap(cloneWithIndex(this._list, this._list.getItem(index), index)); }, list: { get: function () { return this._list; } }, beginEdits: function () { var length = this._list.length; this._forEachBinding(function (binding) { binding._beginEdits(length, true); }); }, endEdits: function () { this._forEachBinding(function (binding) { binding._endEdits(); }); }, _forEachBinding: function (callback) { if (this._usingWeakRef) { var toBeDeleted = []; Object.keys(this._bindings).forEach(function (id) { var lb = _DomWeakRefTable._getWeakRefElement(id); if (lb) { callback(lb); } else { toBeDeleted.push(id); } }); for (var i = 0, len = toBeDeleted.length; i < len; i++) { delete this._bindings[toBeDeleted[i]]; } } else { var that = this; Object.keys(this._bindings).forEach(function (id) { callback(that._bindings[id]); }); } }, invalidateAll: function () { return Promise.wrap(); }, // // insert* and change are not implemented as I don't understand how they are // used by the controls since it is hard to fathom how they would be able // to make up unique keys. Manual editing of the List is meant to go through // the list itself. // // move* are implemented only if the underlying list supports move(). The // GroupsListProjection for instance does not. // moveAfter: undefined, moveBefore: undefined, moveToEnd: undefined, moveToStart: undefined }, { supportedForProcessing: false, }); return DataSource; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // WinJS.Binding.List // define('WinJS/BindingList',[ 'exports', './Core/_Base', './Core/_BaseUtils', './Core/_ErrorFromName', './Core/_Events', './Core/_Resources', './Binding/_Data', './BindingList/_BindingListDataSource' ], function listInit(exports, _Base, _BaseUtils, _ErrorFromName, _Events, _Resources, _Data, _BindingListDataSource) { "use strict"; var strings = { get sparseArrayNotSupported() { return "Sparse arrays are not supported with proxy: true"; }, get illegalListLength() { return "List length must be assigned a finite positive number"; }, }; function copyargs(args) { return Array.prototype.slice.call(args, 0); } function cloneItem(item) { return { handle: item.handle, key: item.key, data: item.data, groupKey: item.groupKey, groupSize: item.groupSize, firstItemKey: item.firstItemKey, firstItemIndexHint: item.firstItemIndexHint }; } function asNumber(n) { return n === undefined ? undefined : +n; } var createEvent = _Events._createEventProperty; var emptyOptions = {}; // We need a stable sort in order to implement SortedListProjection because we want to be able to // perform insertions in a predictable location s.t. if we were to apply another sorted projection // over the same list (now containing the inserted data) the resulting order would be the same. // function mergeSort(m, sorter) { var length = m.length; if (length <= 1) { return m; } var middle = (length / 2) >>> 0; var left = mergeSort(m.slice(0, middle), sorter); var right = mergeSort(m.slice(middle), sorter); return merge(left, right, sorter); } function merge(left, right, sorter) { var result = []; while (left.length && right.length) { var r = sorter(left[0], right[0]); if (r <= 0) { result.push(left.shift()); } else { result.push(right.shift()); } } if (left.length) { result.push.apply(result, left); } if (right.length) { result.push.apply(result, right); } return result; } // Private namespace used for local lazily init'd classes var ns = _Base.Namespace.defineWithParent(null, null, { ListBase: _Base.Namespace._lazy(function () { var ListBase = _Base.Class.define(null, { _annotateWithIndex: function (item, index) { var result = cloneItem(item); result.index = index; return result; }, /// /// The value identified by the specified key has been replaced with a different value. /// onitemchanged: createEvent("itemchanged"), /// /// A new value has been inserted into the list. /// oniteminserted: createEvent("iteminserted"), /// /// The value identified by the specified key has been moved from one index in the list to another index. /// onitemmoved: createEvent("itemmoved"), /// /// The value identified by the specified key has been mutated. /// onitemmutated: createEvent("itemmutated"), /// /// The value identified by the specified key has been removed from the list. /// onitemremoved: createEvent("itemremoved"), /// /// The list has been refreshed. Any references to items in the list may be incorrect. /// onreload: createEvent("reload"), _notifyItemChanged: function (key, index, oldValue, newValue, oldItem, newItem) { if (this._listeners && this._listeners.itemchanged) { this.dispatchEvent("itemchanged", { key: key, index: index, oldValue: oldValue, newValue: newValue, oldItem: oldItem, newItem: newItem }); } }, _notifyItemInserted: function (key, index, value) { if (this._listeners && this._listeners.iteminserted) { this.dispatchEvent("iteminserted", { key: key, index: index, value: value }); } var len = this.length; if (len !== this._lastNotifyLength) { this.notify("length", len, this._lastNotifyLength); this._lastNotifyLength = len; } }, _notifyItemMoved: function (key, oldIndex, newIndex, value) { if (this._listeners && this._listeners.itemmoved) { this.dispatchEvent("itemmoved", { key: key, oldIndex: oldIndex, newIndex: newIndex, value: value }); } }, _notifyItemMutated: function (key, value, item) { if (this._listeners && this._listeners.itemmutated) { this.dispatchEvent("itemmutated", { key: key, value: value, item: item }); } }, _notifyItemRemoved: function (key, index, value, item) { if (this._listeners && this._listeners.itemremoved) { this.dispatchEvent("itemremoved", { key: key, index: index, value: value, item: item }); } var len = this.length; if (len !== this._lastNotifyLength) { this.notify("length", len, this._lastNotifyLength); this._lastNotifyLength = len; } }, _notifyReload: function () { if (this._listeners && this._listeners.reload) { this.dispatchEvent("reload"); } if (len !== this._lastNotifyLength) { var len = this.length; this.notify("length", len, this._lastNotifyLength); this._lastNotifyLength = len; } }, _normalizeIndex: function (index) { index = asNumber(index); return index < 0 ? this.length + index : index; }, // ABSTRACT: length // Notifications: // // ABSTRACT: notifyMutated: function (index) _notifyMutatedFromKey: function (key) { var item = this.getItemFromKey(key); this._notifyItemMutated(key, item.data, item); }, notifyReload: function () { /// /// /// Forces the list to send a reload notification to any listeners. /// /// this._notifyReload(); }, // NOTE: performance can be improved in a number of the projections by overriding getAt/_getArray/_getFromKey/_getKey // getAt: function (index) { /// /// /// Gets the value at the specified index. /// /// The index of the value to get. /// The value at the specified index. /// index = asNumber(index); var item = this.getItem(index); return item && item.data; }, // returns [ data* ] _getArray: function () { var results = new Array(this.length); for (var i = 0, len = this.length; i < len; i++) { var item = this.getItem(i); if (item) { results[i] = item.data; } } return results; }, // returns data _getFromKey: function (key) { var item = this.getItemFromKey(key); return item && item.data; }, // ABSTRACT: getItem(index) // ABSTRACT: getItemFromKey(key) // returns string _getKey: function (index) { index = asNumber(index); var item = this.getItem(index); return item && item.key; }, // Normal list non-modifiying operations // concat: function () { /// /// /// Returns a new list consisting of a combination of two arrays. /// /// Additional items to add to the end of the list. /// An array containing the concatenation of the list and any other supplied items. /// var a = this._getArray(); return a.concat.apply(a, arguments); }, join: function (separator) { /// /// /// Returns a string consisting of all the elements of a list separated by the specified separator string. /// /// A string used to separate the elements of a list. If this parameter is omitted, the list elements are separated with a comma. /// The elements of a list separated by the specified separator string. /// return this._getArray().join(separator || ","); }, slice: function (begin, end) { /// /// /// Extracts a section of a list and returns a new list. /// /// The index that specifies the beginning of the section. /// The index that specifies the end of the section. /// Returns a section of an array. /// return this._getArray().slice(begin, end); }, indexOf: function (searchElement, fromIndex) { /// /// /// Gets the index of the first occurrence of the specified value in a list. /// /// The value to locate in the list. /// The index at which to begin the search. If fromIndex is omitted, the search starts at index 0. /// Index of the first occurrence of a value in a list or -1 if not found. /// fromIndex = asNumber(fromIndex); fromIndex = Math.max(0, this._normalizeIndex(fromIndex) || 0); for (var i = fromIndex, len = this.length; i < len; i++) { var item = this.getItem(i); if (item && item.data === searchElement) { return i; } } return -1; }, // ABSTRACT: indexOfKey(key) lastIndexOf: function (searchElement, fromIndex) { /// /// /// Gets the index of the last occurrence of the specified value in a list. /// /// The value to locate in the list. /// The index at which to begin the search. If fromIndex is omitted, the search starts at the last index in the list. /// The index of the last occurrence of a value in a list, or -1 if not found. /// fromIndex = asNumber(fromIndex); var length = this.length; fromIndex = Math.min(this._normalizeIndex(fromIndex !== undefined ? fromIndex : length), length - 1); var i; for (i = fromIndex; i >= 0; i--) { var item = this.getItem(i); if (item && item.data === searchElement) { return i; } } return -1; }, // // Normal list projection operations // every: function (callback, thisArg) { /// /// /// Checks whether the specified callback function returns true for all elements in a list. /// /// A function that accepts up to three arguments. This function is called for each element in the list until it returns false or the end of the list is reached. /// An object to which the this keyword can refer in the callback function. If thisArg is omitted, undefined is used. /// True if the callback returns true for all elements in the list. /// return this._getArray().every(callback, thisArg); }, filter: function (callback, thisArg) { /// /// /// Returns the elements of a list that meet the condition specified in a callback function. /// /// A function that accepts up to three arguments. The function is called for each element in the list. /// An object to which the this keyword can refer in the callback function. If thisArg is omitted, undefined is used. /// An array containing the elements that meet the condition specified in the callback function. /// return this._getArray().filter(callback, thisArg); }, forEach: function (callback, thisArg) { /// /// /// Calls the specified callback function for each element in a list. /// /// A function that accepts up to three arguments. The function is called for each element in the list. /// An object to which the this keyword can refer in the callback function. If thisArg is omitted, undefined is used. /// this._getArray().forEach(callback, thisArg); }, map: function (callback, thisArg) { /// /// /// Calls the specified callback function on each element of a list, and returns an array that contains the results. /// /// A function that accepts up to three arguments. The function is called for each element in the list. /// An object to which the this keyword can refer in the callback function. If thisArg is omitted, undefined is used. /// An array containing the result of calling the callback function on each element in the list. /// return this._getArray().map(callback, thisArg); }, some: function (callback, thisArg) { /// /// /// Checks whether the specified callback function returns true for any element of a list. /// /// A function that accepts up to three arguments. The function is called for each element in the list until it returns true, or until the end of the list. /// An object to which the this keyword can refer in the callback function. If thisArg is omitted, undefined is used. /// True if callback returns true for any element in the list. /// return this._getArray().some(callback, thisArg); }, reduce: function (callback, initialValue) { /// /// /// Accumulates a single result by calling the specified callback function for all elements in a list. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. /// /// A function that accepts up to four arguments. The function is called for each element in the list. /// If initialValue is specified, it is used as the value with which to start the accumulation. The first call to the function provides this value as an argument instead of a list value. /// The return value from the last call to the callback function. /// if (arguments.length > 1) { return this._getArray().reduce(callback, initialValue); } return this._getArray().reduce(callback); }, reduceRight: function (callback, initialValue) { /// /// /// Accumulates a single result by calling the specified callback function for all elements in a list, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. /// /// A function that accepts up to four arguments. The function is called for each element in the list. /// If initialValue is specified, it is used as the value with which to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of a list value. /// The return value from last call to callback function. /// if (arguments.length > 1) { return this._getArray().reduceRight(callback, initialValue); } return this._getArray().reduceRight(callback); }, // // Live Projections - if you want the lifetime of the returned projections to // be shorter than that of the list object on which they are based you have // to remember to call .dispose() on them when done. // createFiltered: function (predicate) { /// /// /// Creates a live filtered projection over this list. As the list changes, the filtered projection reacts to those changes and may also change. /// /// A function that accepts a single argument. The createFiltered function calls the callback with each element in the list. If the function returns true, that element will be included in the filtered list. /// Filtered projection over the list. /// return new ns.FilteredListProjection(this, predicate); }, createGrouped: function (groupKey, groupData, groupSorter) { /// /// /// Creates a live grouped projection over this list. As the list changes, the grouped projection reacts to those changes and may also change. The grouped projection sorts all the elements of the list to be in group-contiguous order. The grouped projection also contains a .groups property which is a WinJS.Binding.List representing the groups that were found in the list. /// /// A function that accepts a single argument. The function is called with each element in the list, the function should return a string representing the group containing the element. /// A function that accepts a single argument. The function is called on an element in the list for each group. It should return the value that should be set as the data of the .groups list element for this group. /// A function that accepts two arguments. The function is called with the key of groups found in the list. It must return one of the following numeric values: negative if the first argument is less than the second, zero if the two arguments are equivalent, positive if the first argument is greater than the second. If omitted, the groups are sorted in ascending, ASCII character order. /// A grouped projection over the list. /// return new ns.GroupedSortedListProjection(this, groupKey, groupData, groupSorter); }, createSorted: function (sorter) { /// /// /// Creates a live sorted projection over this list. As the list changes, the sorted projection reacts to those changes and may also change. /// /// A function that accepts two arguments. The function is called with elements in the list. It must return one of the following numeric values: negative if the first argument is less than the second, zero if the two arguments are equivalent, positive if the first argument is greater than the second. /// A sorted projection over the list. /// return new ns.SortedListProjection(this, sorter); }, dataSource: { get: function () { return (this._dataSource = this._dataSource || new _BindingListDataSource._BindingListDataSource(this)); } }, }, { supportedForProcessing: false, }); _Base.Class.mix(ListBase, _Data.observableMixin); _Base.Class.mix(ListBase, _Events.eventMixin); return ListBase; }), ListBaseWithMutators: _Base.Namespace._lazy(function () { return _Base.Class.derive(ns.ListBase, null, { // ABSTRACT: setAt(index, value) // Normal list modifying operations // // returns data from tail of list pop: function () { /// /// /// Removes the last element from a list and returns it. /// /// Last element from the list. /// return this.splice(-1, 1)[0]; }, push: function (value) { /// /// /// Appends new element(s) to a list, and returns the new length of the list. /// /// The element to insert at the end of the list. /// The new length of the list. /// if (arguments.length === 1) { this.splice(this.length, 0, value); return this.length; } else { var args = copyargs(arguments); args.splice(0, 0, this.length, 0); this.splice.apply(this, args); return this.length; } }, // returns data from head of list shift: function () { /// /// /// Removes the first element from a list and returns it. /// /// First element from the list. /// return this.splice(0, 1)[0]; }, unshift: function (value) { /// /// /// Appends new element(s) to a list, and returns the new length of the list. /// /// The element to insert at the start of the list. /// The new length of the list. /// if (arguments.length === 1) { this.splice(0, 0, value); } else { var args = copyargs(arguments); // Wow, this looks weird. Insert 0, 0 at the beginning of splice. args.splice(0, 0, 0, 0); this.splice.apply(this, args); } return this.length; } // ABSTRACT: splice(index, howMany, values...) // ABSTRACT: _spliceFromKey(key, howMany, values...) }, { supportedForProcessing: false, }); }), ListProjection: _Base.Namespace._lazy(function () { return _Base.Class.derive(ns.ListBaseWithMutators, null, { _list: null, _myListeners: null, _addListListener: function (name, func) { var l = { name: name, handler: func.bind(this) }; this._myListeners = this._myListeners || []; this._myListeners.push(l); this._list.addEventListener(name, l.handler); }, // ABSTRACT: _listReload() dispose: function () { /// /// /// Disconnects this WinJS.Binding.List projection from its underlying WinJS.Binding.List. This is important only if they have different lifetimes. /// /// var list = this._list; var listeners = this._myListeners; this._myListeners = []; for (var i = 0, len = listeners.length; i < len; i++) { var l = listeners[i]; list.removeEventListener(l.name, l.handler); } // Set this to an empty list and tell everyone that they need to reload to avoid // consumers null-refing on an empty list. this._list = new exports.List(); this._listReload(); }, getItemFromKey: function (key) { /// /// /// Gets a key/data pair for the specified key. /// /// The key of the value to retrieve. /// An object with .key and .data properties. /// return this._list.getItemFromKey(key); }, move: function (index, newIndex) { /// /// /// Moves the value at index to position newIndex. /// /// The original index of the value. /// The index of the value after the move. /// index = asNumber(index); newIndex = asNumber(newIndex); if (index === newIndex || index < 0 || newIndex < 0 || index >= this.length || newIndex >= this.length) { return; } index = this._list.indexOfKey(this._getKey(index)); newIndex = this._list.indexOfKey(this._getKey(newIndex)); this._list.move(index, newIndex); }, _notifyMutatedFromKey: function (key) { this._list._notifyMutatedFromKey(key); }, splice: function (index, howMany, item) { /// /// /// Removes elements from a list and, if necessary, inserts new elements in their place, returning the deleted elements. /// /// The zero-based location in the list from which to start removing elements. /// The number of elements to remove. /// The elements to insert into the list in place of the deleted elements. /// The deleted elements. /// index = asNumber(index); index = Math.max(0, this._normalizeIndex(index)); var args = copyargs(arguments); if (index === this.length) { // In order to getAt the tail right we just push on to the end of the underlying list args[0] = this._list.length; return this._list.splice.apply(this._list, args); } else { args[0] = this._getKey(index); return this._spliceFromKey.apply(this, args); } }, _setAtKey: function (key, value) { this._list._setAtKey(key, value); }, }, { supportedForProcessing: false, }); }), FilteredListProjection: _Base.Namespace._lazy(function () { return _Base.Class.derive(ns.ListProjection, function (list, filter) { this._list = list; this._addListListener("itemchanged", this._listItemChanged); this._addListListener("iteminserted", this._listItemInserted); this._addListListener("itemmutated", this._listItemMutated); this._addListListener("itemmoved", this._listItemMoved); this._addListListener("itemremoved", this._listItemRemoved); this._addListListener("reload", this._listReload); this._filter = filter; this._initFilteredKeys(); }, { _filter: null, _filteredKeys: null, _initFilteredKeys: function () { var filter = this._filter; var list = this._list; var keys = []; for (var i = 0, len = list.length; i < len; i++) { var item = list.getItem(i); if (item && filter(item.data)) { keys.push(item.key); } } this._filteredKeys = keys; }, _findInsertionPosition: function (key, index) { // find the spot to insert this by identifing the previous element in the list var filter = this._filter; var previousKey; while ((--index) >= 0) { var item = this._list.getItem(index); if (item && filter(item.data)) { previousKey = item.key; break; } } var filteredKeys = this._filteredKeys; var filteredIndex = previousKey ? (filteredKeys.indexOf(previousKey) + 1) : 0; return filteredIndex; }, _listItemChanged: function (event) { var key = event.detail.key; var index = event.detail.index; var oldValue = event.detail.oldValue; var newValue = event.detail.newValue; var oldItem = event.detail.oldItem; var newItem = event.detail.newItem; var filter = this._filter; var oldInFilter = filter(oldValue); var newInFilter = filter(newValue); if (oldInFilter && newInFilter) { var filteredKeys = this._filteredKeys; var filteredIndex = filteredKeys.indexOf(key); this._notifyItemChanged(key, filteredIndex, oldValue, newValue, oldItem, newItem); } else if (oldInFilter && !newInFilter) { this._listItemRemoved({ detail: { key: key, index: index, value: oldValue, item: oldItem } }); } else if (!oldInFilter && newInFilter) { this._listItemInserted({ detail: { key: key, index: index, value: newValue } }); } }, _listItemInserted: function (event) { var key = event.detail.key; var index = event.detail.index; var value = event.detail.value; var filter = this._filter; if (filter(value)) { var filteredIndex = this._findInsertionPosition(key, index); var filteredKeys = this._filteredKeys; filteredKeys.splice(filteredIndex, 0, key); this._notifyItemInserted(key, filteredIndex, value); } }, _listItemMoved: function (event) { var key = event.detail.key; var newIndex = event.detail.newIndex; var value = event.detail.value; var filteredKeys = this._filteredKeys; var oldFilteredIndex = filteredKeys.indexOf(key); if (oldFilteredIndex !== -1) { filteredKeys.splice(oldFilteredIndex, 1); var newFilteredIndex = this._findInsertionPosition(key, newIndex); filteredKeys.splice(newFilteredIndex, 0, key); this._notifyItemMoved(key, oldFilteredIndex, newFilteredIndex, value); } }, _listItemMutated: function (event) { var key = event.detail.key; var value = event.detail.value; var item = event.detail.item; var filter = this._filter; var filteredKeys = this._filteredKeys; var filteredIndex = filteredKeys.indexOf(key); var oldInFilter = filteredIndex !== -1; var newInFilter = filter(value); if (oldInFilter && newInFilter) { this._notifyItemMutated(key, value, item); } else if (oldInFilter && !newInFilter) { filteredKeys.splice(filteredIndex, 1); this._notifyItemRemoved(key, filteredIndex, value, item); } else if (!oldInFilter && newInFilter) { this._listItemInserted({ detail: { key: key, index: this._list.indexOfKey(key), value: value } }); } }, _listItemRemoved: function (event) { var key = event.detail.key; var value = event.detail.value; var item = event.detail.item; var filteredKeys = this._filteredKeys; var filteredIndex = filteredKeys.indexOf(key); if (filteredIndex !== -1) { filteredKeys.splice(filteredIndex, 1); this._notifyItemRemoved(key, filteredIndex, value, item); } }, _listReload: function () { this._initFilteredKeys(); this._notifyReload(); }, /// Returns an integer value one higher than the highest element defined in an list. length: { get: function () { return this._filteredKeys.length; }, set: function (value) { if (typeof value === "number" && value >= 0) { var current = this.length; if (current > value) { this.splice(value, current - value); } } else { throw new _ErrorFromName("WinJS.Binding.List.IllegalLength", strings.illegalListLength); } } }, getItem: function (index) { /// /// /// Returns a key/data pair for the specified index. /// /// The index of the value to retrieve. /// An object with .key and .data properties. /// index = asNumber(index); return this.getItemFromKey(this._filteredKeys[index]); }, indexOfKey: function (key) { /// /// /// Returns the index of the first occurrence of a key in a list. /// /// The key to locate in the list. /// The index of the first occurrence of a key in a list, or -1 if not found. /// return this._filteredKeys.indexOf(key); }, notifyMutated: function (index) { /// /// /// Forces the list to send a itemmutated notification to any listeners for the value at the specified index. /// /// The index of the value that was mutated. /// index = asNumber(index); return this._notifyMutatedFromKey(this._filteredKeys[index]); }, setAt: function (index, value) { /// /// /// Replaces the value at the specified index with a new value. /// /// The index of the value that was replaced. /// The new value. /// index = asNumber(index); this._setAtKey(this._filteredKeys[index], value); }, // returns [ data* ] of removed items _spliceFromKey: function (key, howMany) { // first add in all the new items if we have any, this should serve to push key to the right if (arguments.length > 2) { var args = copyargs(arguments); args[1] = 0; // howMany this._list._spliceFromKey.apply(this._list, args); } // now we can remove anything that needs to be removed, since they are not necessarially contiguous // in the underlying list we remove them one by one. var result = []; if (howMany) { var keysToRemove = []; var filteredKeys = this._filteredKeys; var filteredKeyIndex = filteredKeys.indexOf(key); for (var i = filteredKeyIndex, len = filteredKeys.length; i < len && (i - filteredKeyIndex) < howMany; i++) { var key = filteredKeys[i]; keysToRemove.push(key); } var that = this; keysToRemove.forEach(function (key) { result.push(that._list._spliceFromKey(key, 1)[0]); }); } return result; } }, { supportedForProcessing: false, }); }), SortedListProjection: _Base.Namespace._lazy(function () { return _Base.Class.derive(ns.ListProjection, function (list, sortFunction) { this._list = list; this._addListListener("itemchanged", this._listItemChanged); this._addListListener("iteminserted", this._listItemInserted); this._addListListener("itemmoved", this._listItemMoved); this._addListListener("itemmutated", this._listItemMutated); this._addListListener("itemremoved", this._listItemRemoved); this._addListListener("reload", this._listReload); this._sortFunction = sortFunction; this._initSortedKeys(); }, { _sortFunction: null, _sortedKeys: null, _initSortedKeys: function () { var list = this._list; var keys = []; for (var i = 0, len = list.length; i < len; i++) { var item = list.getItem(i); if (item) { keys[i] = item.key; } } var sorter = this._sortFunction; var sorted = mergeSort(keys, function (l, r) { l = list.getItemFromKey(l).data; r = list.getItemFromKey(r).data; return sorter(l, r); }); this._sortedKeys = sorted; }, _findInsertionPos: function (key, index, value, startingMin, startingMax) { var sorter = this._sortFunction; var sortedKeys = this._sortedKeys; var min = Math.max(0, startingMin || -1); var max = Math.min(sortedKeys.length, startingMax || Number.MAX_VALUE); var mid = min; while (min <= max) { mid = ((min + max) / 2) >>> 0; var sortedKey = sortedKeys[mid]; if (!sortedKey) { break; } var sortedItem = this.getItemFromKey(sortedKey); var r = sorter(sortedItem.data, value); if (r < 0) { min = mid + 1; } else if (r === 0) { return this._findStableInsertionPos(key, index, min, max, mid, value); } else { max = mid - 1; } } return min; }, _findBeginningOfGroup: function (mid, sorter, list, sortedKeys, value) { // we made it to the beginning of the list without finding something // that sorts equal to this value, insert this key at the beginning of // this section of keys. var min = 0; var max = mid; while (min <= max) { mid = ((min + max) / 2) >>> 0; var sortedKey = sortedKeys[mid]; var sortedItem = list.getItemFromKey(sortedKey); var r = sorter(sortedItem.data, value); if (r < 0) { min = mid + 1; } else { max = mid - 1; } } return min; }, _findEndOfGroup: function (mid, sorter, list, sortedKeys, value) { // we made it ot the end of the list without finding something that sorts // equal to this value, insert this key at the end of this section of // keys. var min = mid; var max = sortedKeys.length; while (min <= max) { mid = ((min + max) / 2) >>> 0; var sortedKey = sortedKeys[mid]; if (!sortedKey) { return sortedKeys.length; } var sortedItem = list.getItemFromKey(sortedKey); var r = sorter(sortedItem.data, value); if (r <= 0) { min = mid + 1; } else { max = mid - 1; } } return min; }, _findStableInsertionPos: function (key, index, min, max, mid, value) { var list = this._list; var length = list.length; var sorter = this._sortFunction; var sortedKeys = this._sortedKeys; if (index < (length / 2)) { for (var i = index - 1; i >= 0; i--) { var item = list.getItem(i); if (sorter(item.data, value) === 0) { // we have found the next item to the left, insert this item to // the right of that. if ((length - min) > max) { return sortedKeys.indexOf(item.key, min) + 1; } else { return sortedKeys.lastIndexOf(item.key, max) + 1; } } } return this._findBeginningOfGroup(mid, sorter, list, sortedKeys, value); } else { for (var i = index + 1; i < length; i++) { var item = list.getItem(i); if (sorter(item.data, value) === 0) { // we have found the next item to the right, insert this item // to the left of that. if ((length - min) > max) { return sortedKeys.indexOf(item.key, min); } else { return sortedKeys.lastIndexOf(item.key, max); } } } return this._findEndOfGroup(mid, sorter, list, sortedKeys, value); } }, _listItemChanged: function (event) { var key = event.detail.key; var newValue = event.detail.newValue; var oldValue = event.detail.oldValue; var sortFunction = this._sortFunction; if (sortFunction(oldValue, newValue) === 0) { var sortedIndex = this.indexOfKey(key); this._notifyItemChanged(key, sortedIndex, oldValue, newValue, event.detail.oldItem, event.detail.newItem); } else { this._listItemRemoved({ detail: { key: key, index: event.detail.index, value: event.detail.oldValue, item: event.detail.oldItem } }); this._listItemInserted({ detail: { key: key, index: event.detail.index, value: event.detail.newValue } }); } }, _listItemInserted: function (event, knownMin, knownMax) { var key = event.detail.key; var index = event.detail.index; var value = event.detail.value; var sortedIndex = this._findInsertionPos(key, index, value, knownMin, knownMax); this._sortedKeys.splice(sortedIndex, 0, key); this._notifyItemInserted(key, sortedIndex, value); }, _listItemMoved: function (event, knownMin, knownMax) { var key = event.detail.key; var newIndex = event.detail.newIndex; var value = event.detail.value; var sortedKeys = this._sortedKeys; var oldSortedIndex = sortedKeys.indexOf(key, knownMin); sortedKeys.splice(oldSortedIndex, 1); var newSortedIndex = this._findInsertionPos(key, newIndex, value, knownMin, knownMax); sortedKeys.splice(newSortedIndex, 0, key); if (newSortedIndex !== oldSortedIndex) { // The move in the underlying list resulted in a move in the sorted list // this._notifyItemMoved(key, oldSortedIndex, newSortedIndex, value); } else { // The move in the underlying list resulted in no change in the sorted list // } }, _listItemMutated: function (event) { var key = event.detail.key; var value = event.detail.value; var item = event.detail.item; var index = this._list.indexOfKey(key); var sortedIndex = this._sortedKeys.indexOf(key); this._sortedKeys.splice(sortedIndex, 1); var targetIndex = this._findInsertionPos(key, index, value); this._sortedKeys.splice(sortedIndex, 0, key); if (sortedIndex === targetIndex) { this._notifyItemMutated(key, value, item); return; } this._listItemRemoved({ detail: { key: key, index: index, value: value, item: item } }); this._listItemInserted({ detail: { key: key, index: index, value: value } }); }, _listItemRemoved: function (event, knownMin) { var key = event.detail.key; var value = event.detail.value; var item = event.detail.item; var sortedKeys = this._sortedKeys; var sortedIndex = sortedKeys.indexOf(key, knownMin); sortedKeys.splice(sortedIndex, 1); this._notifyItemRemoved(key, sortedIndex, value, item); }, _listReload: function () { this._initSortedKeys(); this._notifyReload(); }, /// Gets or sets the length of the list. Returns an integer value one higher than the highest element defined in a list. length: { get: function () { return this._sortedKeys.length; }, set: function (value) { if (typeof value === "number" && value >= 0) { var current = this.length; if (current > value) { this.splice(value, current - value); } } else { throw new _ErrorFromName("WinJS.Binding.List.IllegalLength", strings.illegalListLength); } } }, getItem: function (index) { /// /// /// Returns a key/data pair for the specified index. /// /// The index of the value to retrieve. /// An object with .key and .data properties. /// index = asNumber(index); return this.getItemFromKey(this._sortedKeys[index]); }, indexOfKey: function (key) { /// /// /// Returns the index of the first occurrence of a key. /// /// The key to locate in the list. /// The index of the first occurrence of a key in a list, or -1 if not found. /// return this._sortedKeys.indexOf(key); }, notifyMutated: function (index) { /// /// /// Forces the list to send a itemmutated notification to any listeners for the value at the specified index. /// /// The index of the value that was mutated. /// index = asNumber(index); this._notifyMutatedFromKey(this._sortedKeys[index]); }, setAt: function (index, value) { /// /// /// Replaces the value at the specified index with a new value. /// /// The index of the value that was replaced. /// The new value. /// index = asNumber(index); this._setAtKey(this._sortedKeys[index], value); }, // returns [ data* ] of removed items _spliceFromKey: function (key, howMany) { // first add in all the new items if we have any, this should serve to push key to the right if (arguments.length > 2) { var args = copyargs(arguments); args[1] = 0; // howMany this._list._spliceFromKey.apply(this._list, args); } // now we can remove anything that needs to be removed, since they are not necessarially contiguous // in the underlying list we remove them one by one. var result = []; if (howMany) { var keysToRemove = []; var sortedKeys = this._sortedKeys; var sortedKeyIndex = sortedKeys.indexOf(key); for (var i = sortedKeyIndex, len = sortedKeys.length; i < len && (i - sortedKeyIndex) < howMany; i++) { keysToRemove.push(sortedKeys[i]); } var that = this; keysToRemove.forEach(function (key) { result.push(that._list._spliceFromKey(key, 1)[0]); }); } return result; } }, { supportedForProcessing: false, }); }), // This projection sorts the underlying list by group key and within a group // respects the position of the item in the underlying list. It is built on top // of the SortedListProjection and has an intimate contract with // GroupsListProjection. // GroupedSortedListProjection: _Base.Namespace._lazy(function () { return _Base.Class.derive(ns.SortedListProjection, function (list, groupKeyOf, groupDataOf, groupSorter) { this._list = list; this._addListListener("itemchanged", this._listGroupedItemChanged); this._addListListener("iteminserted", this._listGroupedItemInserted); this._addListListener("itemmoved", this._listGroupedItemMoved); this._addListListener("itemmutated", this._listGroupedItemMutated); this._addListListener("itemremoved", this._listGroupedItemRemoved); this._addListListener("reload", this._listReload); this._sortFunction = function (l, r) { l = groupKeyOf(l); r = groupKeyOf(r); if (groupSorter) { return groupSorter(l, r); } else { return l < r ? -1 : l === r ? 0 : 1; } }; this._groupKeyOf = groupKeyOf; this._groupDataOf = groupDataOf; this._initSortedKeys(); this._initGroupedItems(); }, { _groupKeyOf: null, _groupDataOf: null, _groupedItems: null, _initGroupedItems: function () { var groupedItems = {}; var list = this._list; var groupKeyOf = this._groupKeyOf; for (var i = 0, len = list.length; i < len; i++) { var item = cloneItem(list.getItem(i)); item.groupKey = groupKeyOf(item.data); groupedItems[item.key] = item; } this._groupedItems = groupedItems; }, _groupsProjection: null, _listGroupedItemChanged: function (event) { var key = event.detail.key; var oldValue = event.detail.oldValue; var newValue = event.detail.newValue; var groupedItems = this._groupedItems; var oldGroupedItem = groupedItems[key]; var newGroupedItem = cloneItem(oldGroupedItem); newGroupedItem.data = newValue; newGroupedItem.groupKey = this._groupKeyOf(newValue); groupedItems[key] = newGroupedItem; var index; if (oldGroupedItem.groupKey === newGroupedItem.groupKey) { index = this.indexOfKey(key); this._notifyItemChanged(key, index, oldValue, newValue, oldGroupedItem, newGroupedItem); } else { index = event.detail.index; this._listItemChanged({ detail: { key: key, index: index, oldValue: oldValue, newValue: newValue, oldItem: oldGroupedItem, newItem: newGroupedItem } }); } }, _listGroupedItemInserted: function (event) { var key = event.detail.key; var value = event.detail.value; var groupKey = this._groupKeyOf(value); this._groupedItems[key] = { handle: key, key: key, data: value, groupKey: groupKey }; var groupMin, groupMax; if (this._groupsProjection) { var groupItem = this._groupsProjection._groupItems[groupKey]; if (groupItem) { groupMin = groupItem.firstItemIndexHint; groupMax = groupMin + groupItem.groupSize; } } this._listItemInserted(event, groupMin, groupMax); }, _listGroupedItemMoved: function (event) { var groupMin, groupMax; var groupKey = this._groupedItems[event.detail.key].groupKey; if (this._groupsProjection) { var groupItem = this._groupsProjection._groupItems[groupKey]; groupMin = groupItem.firstItemIndexHint; groupMax = groupMin + groupItem.groupSize; } this._listItemMoved(event, groupMin, groupMax); }, _listGroupedItemMutated: function (event) { var key = event.detail.key; var value = event.detail.value; var groupedItems = this._groupedItems; var oldGroupedItem = groupedItems[key]; var groupKey = this._groupKeyOf(value); if (oldGroupedItem.groupKey === groupKey) { this._notifyItemMutated(key, value, oldGroupedItem); } else { var newGroupedItem = cloneItem(oldGroupedItem); newGroupedItem.groupKey = groupKey; groupedItems[key] = newGroupedItem; var index = this._list.indexOfKey(key); this._listItemRemoved({ detail: { key: key, index: index, value: value, item: oldGroupedItem } }); this._listItemInserted({ detail: { key: key, index: index, value: value } }); } }, _listGroupedItemRemoved: function (event) { var key = event.detail.key; var index = event.detail.index; var value = event.detail.value; var groupedItems = this._groupedItems; var groupedItem = groupedItems[key]; delete groupedItems[key]; var groupMin, groupMax; if (this._groupsProjection) { var groupItem = this._groupsProjection._groupItems[groupedItem.groupKey]; groupMin = groupItem.firstItemIndexHint; groupMax = groupMin + groupItem.groupSize; } this._listItemRemoved({ detail: { key: key, index: index, value: value, item: groupedItem } }, groupMin, groupMax); }, // override _listReload _listReload: function () { this._initGroupedItems(); ns.SortedListProjection.prototype._listReload.call(this); }, /// Gets a WinJS.Binding.List, which is a projection of the groups that were identified in this list. groups: { get: function () { if (this._groupsProjection === null) { this._groupsProjection = new ns.GroupsListProjection(this, this._groupKeyOf, this._groupDataOf); } return this._groupsProjection; } }, // We have to implement this because we keep our own set of items so that we can // tag them with groupKey. // getItemFromKey: function (key) { /// /// /// Gets a key/data pair for the specified item key. /// /// The key of the value to retrieve. /// An object with .key and .data properties. /// return this._groupedItems[key]; } }, { supportedForProcessing: false, }); }), // This is really an implementation detail of GroupedSortedListProjection and takes a // dependency on its internals and implementation details. // GroupsListProjection: _Base.Namespace._lazy(function () { return _Base.Class.derive(ns.ListBase, function (list, groupKeyOf, groupDataOf) { this._list = list; this._addListListener("itemchanged", this._listItemChanged); this._addListListener("iteminserted", this._listItemInserted); this._addListListener("itemmoved", this._listItemMoved); // itemmutated is handled by the GroupedSortedListProjection because if the item // changes groups it turns into a remove/insert. this._addListListener("itemremoved", this._listItemRemoved); this._addListListener("reload", this._listReload); this._groupKeyOf = groupKeyOf; this._groupDataOf = groupDataOf; this._initGroupKeysAndItems(); }, { _list: null, _addListListener: function (name, func) { // interestingly, since GroupsListProjection has the same lifetime as the GroupedSortedListProjection // we don't have to worry about cleaning up the cycle here. this._list.addEventListener(name, func.bind(this)); }, _groupDataOf: null, _groupKeyOf: null, _groupOf: function (item) { return this.getItemFromKey(this._groupKeyOf(item.data)); }, _groupKeys: null, _groupItems: null, _initGroupKeysAndItems: function () { var groupDataOf = this._groupDataOf; var list = this._list; var groupItems = {}; var groupKeys = []; var currentGroupKey = null; var currentGroupItem = null; var groupCount; for (var i = 0, len = list.length; i < len; i++) { var item = list.getItem(i); var groupKey = item.groupKey; if (groupKey !== currentGroupKey) { // new group if (currentGroupItem) { currentGroupItem.groupSize = groupCount; } groupCount = 1; currentGroupKey = groupKey; currentGroupItem = { handle: groupKey, key: groupKey, data: groupDataOf(item.data), firstItemKey: item.key, firstItemIndexHint: i }; groupItems[groupKey] = currentGroupItem; groupKeys.push(groupKey); } else { // existing group groupCount++; } } if (currentGroupItem) { currentGroupItem.groupSize = groupCount; } this._groupKeys = groupKeys; this._groupItems = groupItems; }, _listItemChanged: function (event) { // itemchanged is only interesting if the item that changed is the first item // of a group at which point we need to regenerate the group item. // var key = event.detail.key; var index = event.detail.index; var newValue = event.detail.newValue; var list = this._list; var groupKey = list.getItemFromKey(key).groupKey; var groupItems = this._groupItems; var groupItem = groupItems[groupKey]; if (groupItem.firstItemIndexHint === index) { var newGroupItem = cloneItem(groupItem); newGroupItem.data = this._groupDataOf(newValue); newGroupItem.firstItemKey = key; groupItems[groupKey] = newGroupItem; this._notifyItemChanged(groupKey, this._groupKeys.indexOf(groupKey), groupItem.data, newGroupItem.data, groupItem, newGroupItem); } }, _listItemInserted: function (event) { // iteminserted is only interesting if this is a new group, or is the first // item of the group at which point the group data is regenerated. It will // however always result in a +1 to all the following firstItemIndexHints // var key = event.detail.key; var index = event.detail.index; var value = event.detail.value; var list = this._list; var groupKey = list.getItemFromKey(key).groupKey; var groupItems = this._groupItems; var groupKeys = this._groupKeys; var groupItem = groupItems[groupKey]; var groupIndex; var oldGroupItem, newGroupItem; var i, len; if (!groupItem) { // we have a new group, add it for (i = 0, len = groupKeys.length; i < len; i++) { groupItem = groupItems[groupKeys[i]]; if (groupItem.firstItemIndexHint >= index) { break; } } groupIndex = i; groupItem = { handle: groupKey, key: groupKey, data: this._groupDataOf(value), groupSize: 1, firstItemKey: key, firstItemIndexHint: index }; groupKeys.splice(groupIndex, 0, groupKey); groupItems[groupKey] = groupItem; this._notifyItemInserted(groupKey, groupIndex, groupItem.data); } else { oldGroupItem = groupItem; newGroupItem = cloneItem(oldGroupItem); newGroupItem.groupSize++; if (oldGroupItem.firstItemIndexHint === index) { newGroupItem.groupData = this._groupDataOf(value); newGroupItem.firstItemKey = key; newGroupItem.firstItemIndexHint = index; } groupItems[groupKey] = newGroupItem; groupIndex = groupKeys.indexOf(groupKey); this._notifyItemChanged(groupKey, groupIndex, oldGroupItem.data, newGroupItem.data, oldGroupItem, newGroupItem); } // update the firstItemIndexHint on following groups for (i = groupIndex + 1, len = groupKeys.length; i < len; i++) { oldGroupItem = groupItems[groupKeys[i]]; newGroupItem = cloneItem(oldGroupItem); newGroupItem.firstItemIndexHint++; groupItems[newGroupItem.key] = newGroupItem; this._notifyItemChanged(newGroupItem.key, i, oldGroupItem.data, newGroupItem.data, oldGroupItem, newGroupItem); } }, _listItemMoved: function (event) { // itemmoved is not important to grouping unless the move resulted in a new // first item in the group at which point we will regenerate the group data // var key = event.detail.key; var oldIndex = event.detail.oldIndex; var newIndex = event.detail.newIndex; var list = this._list; var groupKey = list.getItemFromKey(key).groupKey; var groupItems = this._groupItems; var groupItem = groupItems[groupKey]; if (groupItem.firstItemIndexHint === newIndex || groupItem.firstItemIndexHint === oldIndex) { // the first item of the group has changed, update it. var item = list.getItem(groupItem.firstItemIndexHint); var newGroupItem = cloneItem(groupItem); newGroupItem.data = this._groupDataOf(item.data); newGroupItem.firstItemKey = item.key; groupItems[groupKey] = newGroupItem; this._notifyItemChanged(groupKey, this._groupKeys.indexOf(groupKey), groupItem.data, newGroupItem.data, groupItem, newGroupItem); } }, _listItemRemoved: function (event) { // itemremoved is only interesting if the group was of size 1 or was the // first item of the group at which point the group data is regenerated. // It will however always result in a -1 to all of the following // firstItemIndexHints. // var index = event.detail.index; var item = event.detail.item; var groupItems = this._groupItems; var groupKeys = this._groupKeys; // since the value is no longer in the list we can't ask for its item and // get the group key from there. var groupKey = item.groupKey; var groupItem = groupItems[groupKey]; var groupIndex = groupKeys.indexOf(groupKey); var oldGroupItem, newGroupItem; if (groupItem.groupSize === 1) { groupKeys.splice(groupIndex, 1); delete groupItems[groupKey]; this._notifyItemRemoved(groupKey, groupIndex, groupItem.data, groupItem); // after removing the group we need to decrement the index because it is used // for modifying subsequent group firstItemIndexHint's groupIndex--; } else { oldGroupItem = groupItem; newGroupItem = cloneItem(oldGroupItem); newGroupItem.groupSize--; if (oldGroupItem.firstItemIndexHint === index) { // find the next group item, it will be at the same index as the old // first group item. var newFirstItem = this._list.getItem(index); newGroupItem.data = this._groupDataOf(newFirstItem.data); newGroupItem.firstItemKey = newFirstItem.key; } groupItems[groupKey] = newGroupItem; this._notifyItemChanged(groupKey, groupIndex, oldGroupItem.data, newGroupItem.data, oldGroupItem, newGroupItem); } for (var i = groupIndex + 1, len = groupKeys.length; i < len; i++) { oldGroupItem = groupItems[groupKeys[i]]; newGroupItem = cloneItem(oldGroupItem); newGroupItem.firstItemIndexHint--; groupItems[newGroupItem.key] = newGroupItem; this._notifyItemChanged(newGroupItem.key, i, oldGroupItem.data, newGroupItem.data, oldGroupItem, newGroupItem); } }, _listReload: function () { this._initGroupKeysAndItems(); this._notifyReload(); }, /// Gets the length of the list. Returns an integer value one higher than the highest element defined in a list. length: { get: function () { return this._groupKeys.length; } }, getItem: function (index) { /// /// /// Gets a key/data pair for the specified index . /// /// The index of the value to retrieve. /// An object with .key and .data properties. /// index = asNumber(index); return this._groupItems[this._groupKeys[index]]; }, getItemFromKey: function (key) { /// /// /// Gets a key/data pair for the specified key. /// /// The key of the value to retrieve. /// An object with .key and .data properties. /// return this._groupItems[key]; }, indexOfKey: function (key) { /// /// /// Returns the index of the first occurrence of a key in a list. /// /// The key to locate in the list. /// The index of the first occurrence of a key in a list, or -1 if not found. /// return this._groupKeys.indexOf(key); } }, { supportedForProcessing: false, }); }), }); _Base.Namespace._moduleDefine(exports, "WinJS.Binding", { List: _Base.Namespace._lazy(function () { return _Base.Class.derive(ns.ListBaseWithMutators, function (list, options) { /// /// /// Creates a WinJS.Binding.List object. /// /// The array containing the elements to initalize the list. /// If options.binding is true, the list will contain the result of calling WinJS.Binding.as() on the element values. If options.proxy is true, the list specified as the first parameter is used as the storage for the WinJS.Binding.List. This option should be used with care because uncoordinated edits to the data storage will result in errors. /// The newly-constructed WinJS.Binding.List instance. /// this._currentKey = 0; this._keys = null; this._keyMap = {}; // options: // - binding: binding.as on items // - proxy: proxy over input data // options = options || emptyOptions; this._proxy = options.proxy; this._binding = options.binding; if (this._proxy) { if (Object.keys(list).length !== list.length) { throw new _ErrorFromName("WinJS.Binding.List.NotSupported", strings.sparseArrayNotSupported); } this._data = list; this._currentKey = list.length; } else if (list) { var keyDataMap = this._keyMap; var pos = 0, i = 0; for (var len = list.length; i < len; i++) { if (i in list) { var item = list[i]; if (this._binding) { item = _Data.as(item); } var key = pos.toString(); pos++; keyDataMap[key] = { handle: key, key: key, data: item }; } } if (pos !== i) { this._initializeKeys(); } this._currentKey = pos; } }, { _currentKey: 0, _keys: null, _keyMap: null, _modifyingData: 0, _initializeKeys: function () { if (this._keys) { return; } var keys = []; if (this._data) { // If this list is a proxy over the data then we will have been lazily initializing // the entries in the list, however the 1:1 mapping between index and key is about // to go away so this is our last chance to pull the items out of the data. // var keyMap = this._keyMap; var data = this._data; for (var i = 0, len = data.length; i < len; i++) { if (i in data) { var key = i.toString(); keys[i] = key; if (!(key in keyMap)) { var item = data[i]; if (this._binding) { item = _Data.as(item); } keyMap[key] = { handle: key, key: key, data: item }; } } } } else { // In the case where List owns the data we will have created the keyMap at initialization // time and can use that to harvest all the keys into the _keys list. // Object.keys(this._keyMap).forEach(function (key) { keys[key >>> 0] = key; }); } this._keys = keys; }, _lazyPopulateEntry: function (index) { if (this._data && index in this._data) { var item = this._data[index]; if (this._binding) { item = _Data.as(item); } var key = index.toString(); var entry = { handle: key, key: key, data: item }; this._keyMap[entry.key] = entry; return entry; } }, _assignKey: function () { return (++this._currentKey).toString(); }, /// Gets or sets the length of the list, which is an integer value one higher than the highest element defined in the list. length: { get: function () { // If we are proxying use the underlying list's length // If we have already allocated keys use that length // If we haven't allocated keys then we can use _currentKey which was set at initialization time // to be length of the input list. if (this._data) { return this._data.length; } else if (this._keys) { return this._keys.length; } else { return this._currentKey; } }, set: function (value) { if (typeof value === "number" && value >= 0) { this._initializeKeys(); var current = this.length; if (current > value) { this.splice(value, current - value); } else { // We don't support setting lengths to longer in order to have sparse behavior value = current; } if (this._data) { this._modifyingData++; try { this._data.length = value; } finally { this._modifyingData--; } } if (this._keys) { this._keys.length = value; } } else { throw new _ErrorFromName("WinJS.Binding.List.IllegalLength", strings.illegalListLength); } } }, getItem: function (index) { /// /// /// Gets a key/data pair for the specified list index. /// /// The index of value to retrieve. /// An object with .key and .data properties. /// var entry; var key; index = asNumber(index); if (this._keys) { key = this._keys[index]; entry = key && this._keyMap[key]; } else { key = index.toString(); entry = this._keyMap[key] || this._lazyPopulateEntry(index); } return entry; }, getItemFromKey: function (key) { /// /// /// Gets a key/data pair for the list item key specified. /// /// The key of the value to retrieve. /// An object with .key and .data properties. /// var entry; // if we have a keys list we know to go through the keyMap, or if we are not // proxying through _data we also know to go through the keyMap. if (this._keys || !this._data) { entry = this._keyMap[key]; } else { entry = this.getItem(key >>> 0); } return entry; }, indexOfKey: function (key) { /// /// /// Gets the index of the first occurrence of a key in a list. /// /// The key to locate in the list. /// The index of the first occurrence of a key in a list, or -1 if not found. /// var index = -1; if (this._keys) { index = this._keys.indexOf(key); } else { var t = key >>> 0; if (t < this._currentKey) { index = t; } } return index; }, move: function (index, newIndex) { /// /// /// Moves the value at index to the specified position. /// /// The original index of the value. /// The index of the value after the move. /// index = asNumber(index); newIndex = asNumber(newIndex); this._initializeKeys(); if (index === newIndex || index < 0 || newIndex < 0 || index >= this.length || newIndex >= this.length) { return; } if (this._data) { this._modifyingData++; try { var item = this._data.splice(index, 1)[0]; this._data.splice(newIndex, 0, item); } finally { this._modifyingData--; } } var key = this._keys.splice(index, 1)[0]; this._keys.splice(newIndex, 0, key); this._notifyItemMoved(key, index, newIndex, this.getItemFromKey(key).data); }, notifyMutated: function (index) { /// /// /// Forces the list to send a itemmutated notification to any listeners for the value at the specified index. /// /// The index of the value that was mutated. /// index = asNumber(index); var key = this._keys ? this._keys[index] : index.toString(); this._notifyMutatedFromKey(key); }, setAt: function (index, newValue) { /// /// /// Replaces the value at the specified index with a new value. /// /// The index of the value that was replaced. /// The new value. /// index = asNumber(index); this._initializeKeys(); var length = this.length; if (index === length) { this.push(newValue); } else if (index < length) { if (this._data) { this._modifyingData++; try { this._data[index] = newValue; } finally { this._modifyingData--; } } if (this._binding) { newValue = _Data.as(newValue); } if (index in this._keys) { var key = this._keys[index]; var oldEntry = this._keyMap[key]; var newEntry = cloneItem(oldEntry); newEntry.data = newValue; this._keyMap[key] = newEntry; this._notifyItemChanged(key, index, oldEntry.data, newValue, oldEntry, newEntry); } } }, _setAtKey: function (key, newValue) { this.setAt(this.indexOfKey(key), newValue); }, // These are the traditional Array mutators, they don't result in projections. In particular // having both sort and sorted is a bit confusing. It may be the case that we want to eliminate // the various array helpers outside of the standard push/pop,shift/unshift,splice,get*,setAt // and then offer up the specific projections: filter, sorted, grouped. Anything else can be // obtained through _getArray(). // reverse: function () { /// /// /// Returns a list with the elements reversed. This method reverses the elements of a list object in place. It does not create a new list object during execution. /// /// The reversed list. /// this._initializeKeys(); if (this._data) { this._modifyingData++; try { this._data.reverse(); } finally { this._modifyingData--; } } this._keys.reverse(); this._notifyReload(); return this; }, sort: function (sortFunction) { /// /// /// Returns a list with the elements sorted. This method sorts the elements of a list object in place. It does not create a new list object during execution. /// /// The function used to determine the order of the elements. If omitted, the elements are sorted in ascending, ASCII character order. /// The sorted list. /// this._initializeKeys(); if (this._data) { this._modifyingData++; try { this._data.sort(sortFunction); } finally { this._modifyingData--; } } var that = this; this._keys.sort(function (l, r) { l = that._keyMap[l]; r = that._keyMap[r]; if (sortFunction) { return sortFunction(l.data, r.data); } l = (l && l.data || "").toString(); r = (l && r.data || "").toString(); return l < r ? -1 : l === r ? 0 : 1; }); this._notifyReload(); return this; }, pop: function () { /// /// /// Removes the last element from a list and returns it. /// /// Last element from the list. /// if (this.length === 0) { return; } this._initializeKeys(); var key = this._keys.pop(); var entry = this._keyMap[key]; var data = entry && entry.data; if (this._data) { this._modifyingData++; try { this._data.pop(); } finally { this._modifyingData--; } } delete this._keyMap[key]; this._notifyItemRemoved(key, this._keys.length, data, entry); return data; }, push: function () { /// /// /// Appends new element(s) to a list, and returns the new length of the list. /// /// The element to insert at the end of the list. /// The new length of the list. /// this._initializeKeys(); var length = arguments.length; for (var i = 0; i < length; i++) { var item = arguments[i]; if (this._binding) { item = _Data.as(item); } var key = this._assignKey(); this._keys.push(key); if (this._data) { this._modifyingData++; try { this._data.push(arguments[i]); } finally { this._modifyingData--; } } this._keyMap[key] = { handle: key, key: key, data: item }; this._notifyItemInserted(key, this._keys.length - 1, item); } return this.length; }, shift: function () { /// /// /// Removes the first element from a list and returns it. /// /// First element from the list. /// if (this.length === 0) { return; } this._initializeKeys(); var key = this._keys.shift(); var entry = this._keyMap[key]; var data = entry && entry.data; if (this._data) { this._modifyingData++; try { this._data.shift(); } finally { this._modifyingData--; } } delete this._keyMap[key]; this._notifyItemRemoved(key, 0, data, entry); return data; }, unshift: function () { /// /// /// Appends new element(s) to a list, and returns the new length of the list. /// /// The element to insert at the start of the list. /// The new length of the list. /// this._initializeKeys(); var length = arguments.length; for (var i = length - 1; i >= 0; i--) { var item = arguments[i]; if (this._binding) { item = _Data.as(item); } var key = this._assignKey(); this._keys.unshift(key); if (this._data) { this._modifyingData++; try { this._data.unshift(arguments[i]); } finally { this._modifyingData--; } } this._keyMap[key] = { handle: key, key: key, data: item }; this._notifyItemInserted(key, 0, item); } return this.length; }, splice: function (index, howMany, item) { /// /// /// Removes elements from a list and, if necessary, inserts new elements in their place, returning the deleted elements. /// /// The zero-based location in the list from which to start removing elements. /// The number of elements to remove. /// The elements to insert into the list in place of the deleted elements. /// The deleted elements. /// index = asNumber(index); this._initializeKeys(); index = Math.max(0, this._normalizeIndex(index)); howMany = Math.max(0, Math.min(howMany || 0, this.length - index)); var result = []; while (howMany) { var key = this._keys[index]; var entry = this._keyMap[key]; var data = entry && entry.data; result.push(data); this._keys.splice(index, 1); if (this._data) { this._modifyingData++; try { this._data.splice(index, 1); } finally { this._modifyingData--; } } delete this._keyMap[key]; this._notifyItemRemoved(key, index, data, entry); --howMany; } if (arguments.length > 2) { for (var i = 2, len = arguments.length; i < len; i++) { var additionalItem = arguments[i]; if (this._binding) { additionalItem = _Data.as(additionalItem); } var pos = Math.min(index + i - 2, this.length); var newKey = this._assignKey(); this._keys.splice(pos, 0, newKey); if (this._data) { this._modifyingData++; try { this._data.splice(pos, 0, arguments[i]); } finally { this._modifyingData--; } } this._keyMap[newKey] = { handle: newKey, key: newKey, data: additionalItem }; this._notifyItemInserted(newKey, pos, additionalItem); } } return result; }, // returns [ data* ] of removed items _spliceFromKey: function (key) { this._initializeKeys(); var args = copyargs(arguments); args[0] = this._keys.indexOf(key); return this.splice.apply(this, args); } }, { supportedForProcessing: false, }); }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Res',[ 'exports', './Core/_Global', './Core/_Base', './Core/_BaseUtils', './Core/_ErrorFromName', './Core/_Resources', './ControlProcessor/_OptionsParser', './Promise' ], function resInit(exports, _Global, _Base, _BaseUtils, _ErrorFromName, _Resources, _OptionsParser, Promise) { "use strict"; var readyComplete = false; var requireSupportedForProcessing = _BaseUtils.requireSupportedForProcessing; function processAllImpl(rootElement, count) { rootElement = rootElement || _Global.document.body; var count = count || 0; if (count < 4) { // Only 3 depth is supported in the innerHTML if (count === 0) { if (rootElement.getAttribute) { // Fragment-loaded root element isn't caught by querySelectorAll var rootElementNode = rootElement.getAttribute('data-win-res'); if (rootElementNode) { var decls = _OptionsParser.optionsParser(rootElementNode); setMembers(rootElement, rootElement, decls, count); } } } var selector = "[data-win-res],[data-win-control]"; var elements = rootElement.querySelectorAll(selector); if (elements.length === 0) { return Promise.as(rootElement); } for (var i = 0, len = elements.length; i < len; i++) { var e = elements[i]; if (e.winControl && e.winControl.constructor && e.winControl.constructor.isDeclarativeControlContainer) { var idcc = e.winControl.constructor.isDeclarativeControlContainer; if (typeof idcc === "function") { idcc = requireSupportedForProcessing(idcc); idcc(e.winControl, processAll); // Skip all children of declarative control container i += e.querySelectorAll(selector).length; } } if (!e.hasAttribute("data-win-res")) { continue; } // Use optionsParser that accept string format // {name="value", name2="value2"} var decls = _OptionsParser.optionsParser(e.getAttribute('data-win-res')); setMembers(e, e, decls, count); } } else if (_BaseUtils.validation) { throw new _ErrorFromName("WinJS.Res.NestingExceeded", "NestingExceeded"); } return Promise.as(rootElement); } function setAttributes(root, descriptor) { var names = Object.keys(descriptor); for (var k = 0, l = names.length ; k < l; k++) { var name = names[k]; var value = descriptor[name]; var data = _Resources.getString(value); if (!data || !data.empty) { root.setAttribute(name, data.value); if ((data.lang !== undefined) && (root.lang !== undefined) && (root.lang !== data.lang)) { root.lang = data.lang; } } else if (_BaseUtils.validation) { notFound(value); } } } function notFound(name) { throw new _ErrorFromName("WinJS.Res.NotFound", _Resources._formatString("NotFound: {0}", name)); } function setMembers(root, target, descriptor, count) { var names = Object.keys(descriptor); target = requireSupportedForProcessing(target); for (var k = 0, l = names.length ; k < l; k++) { var name = names[k]; var value = descriptor[name]; if (typeof value === "string") { var data = _Resources.getString(value); if (!data || !data.empty) { target[name] = data.value; if ((data.lang !== undefined) && (root.lang !== undefined) && (root.lang !== data.lang)) { // When lang property is different, we set the language with selected string's language root.lang = data.lang; } if (name === "innerHTML") { processAllImpl(target, count + 1); } } else if (_BaseUtils.validation) { notFound(value); } } else if (root === target && name === "attributes") { //Exposing setAttribute for attributes that don't have HTML properties, like aria, through a fake 'attributes' property setAttributes(root, value); } else { setMembers(root, target[name], value, count); } } } function processAll(rootElement) { /// /// /// Processes resources tag and replaces strings /// with localized strings. /// /// /// The DOM element at which to start processing. processAll processes the element and its child elements. /// If you don't specify root element, processAll processes the entire document. /// /// if (!readyComplete) { return _BaseUtils.ready().then(function () { readyComplete = true; return processAllImpl(rootElement); }); } else { try { return processAllImpl(rootElement); } catch (e) { return Promise.wrapError(e); } } } _Base.Namespace._moduleDefine(exports, "WinJS.Resources", { processAll: processAll }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Pages/_BasePage',[ 'exports', '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', '../Core/_WriteProfilerMark', '../Promise', '../Utilities/_Control', '../Utilities/_Dispose', '../Utilities/_ElementUtilities' ], function pagesInit(exports, _Global, _Base, _BaseUtils, _WriteProfilerMark, Promise, _Control, _Dispose, _ElementUtilities) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } function abs(uri) { var a = _Global.document.createElement("a"); a.href = uri; return a.href; } var viewMap = {}; function selfhost(uri) { return _Global.document.location.href.toLowerCase() === uri.toLowerCase(); } var _mixin = { dispose: function () { /// /// /// Disposes this Page. /// /// if (this._disposed) { return; } this._disposed = true; _Dispose.disposeSubTree(this.element); this.element = null; }, load: function (uri) { /// /// /// Creates a copy of the DOM elements from the specified URI. In order for this override /// to be used, the page that contains the load override needs to be defined by calling /// WinJS.UI.Pages.define() before WinJS.UI.Pages.render() is called. /// /// /// The URI from which to copy the DOM elements. /// /// /// A promise whose fulfilled value is the set of unparented DOM elements, if asynchronous processing is necessary. If not, returns nothing. /// /// }, init: function (element, options) { /// /// /// Initializes the control before the content of the control is set. /// Use the processed method for any initialization that should be done after the content /// of the control has been set. /// /// /// The DOM element that will contain all the content for the page. /// /// /// The options passed to the constructor of the page. /// /// /// A promise that is fulfilled when initialization is complete, if asynchronous processing is necessary. If not, returns nothing. /// /// }, process: function (element, options) { /// /// /// Processes the unparented DOM elements returned by load. /// /// /// The DOM element that will contain all the content for the page. /// /// /// The options that are to be passed to the constructor of the page. /// /// /// A promise that is fulfilled when processing is complete. /// /// }, processed: function (element, options) { /// /// /// Initializes the control after the content of the control is set. /// /// /// The DOM element that will contain all the content for the page. /// /// /// The options that are to be passed to the constructor of the page. /// /// /// A promise that is fulfilled when initialization is complete, if asynchronous processing is necessary. If not, returns nothing. /// /// }, render: function (element, options, loadResult) { /// /// /// Renders the control, typically by adding the elements specified in the loadResult parameter to the specified element. /// /// /// The DOM element that will contain all the content for the page. /// /// /// The options passed into the constructor of the page. /// /// /// The elements returned from the load method. /// /// /// A promise that is fulfilled when rendering is complete, if asynchronous processing is necessary. If not, returns nothing. /// /// }, ready: function (element, options) { /// /// /// Called after all initialization and rendering is complete. At this /// time the element is ready for use. /// /// /// The DOM element that contains all the content for the page. /// /// /// The options passed into the constructor of the page /// /// }, error: function (err) { /// /// /// Called if any error occurs during the processing of the page. /// /// /// The error that occurred. /// /// /// Nothing if the error was handled, or an error promise if the error was not handled. /// /// return Promise.wrapError(err); } }; function Pages_define(uri, members) { /// /// /// Creates a new page control from the specified URI that contains the specified members. /// Multiple calls to this method for the same URI are allowed, and all members will be /// merged. /// /// /// The URI for the content that defines the page. /// /// /// Additional members that the control will have. /// /// /// A constructor function that creates the page. /// /// var base = get(uri); uri = abs(uri); if (!base) { base = _Base.Class.define( // This needs to follow the WinJS.UI.processAll "async constructor" // pattern to interop nicely in the "Views.Control" use case. // function PageControl_ctor(element, options, complete, parentedPromise) { var that = this; this._disposed = false; this.element = element = element || _Global.document.createElement("div"); _ElementUtilities.addClass(element, "win-disposable"); element.msSourceLocation = uri; this.uri = uri; this.selfhost = selfhost(uri); element.winControl = this; _ElementUtilities.addClass(element, "pagecontrol"); var profilerMarkIdentifier = " uri='" + uri + "'" + _BaseUtils._getProfilerMarkIdentifier(this.element); _WriteProfilerMark("WinJS.UI.Pages:createPage" + profilerMarkIdentifier + ",StartTM"); var load = Promise.wrap(). then(function Pages_load() { return that.load(uri); }); var renderCalled = load.then(function Pages_init(loadResult) { return Promise.join({ loadResult: loadResult, initResult: that.init(element, options) }); }).then(function Pages_render(result) { return that.render(element, options, result.loadResult); }); this.elementReady = renderCalled.then(function () { return element; }); this.renderComplete = renderCalled. then(function Pages_process() { return that.process(element, options); }).then(function Pages_processed() { return that.processed(element, options); }).then(function () { return that; }); var callComplete = function () { complete && complete(that); _WriteProfilerMark("WinJS.UI.Pages:createPage" + profilerMarkIdentifier + ",StopTM"); }; // promises guarantee order, so this will be called prior to ready path below // this.renderComplete.then(callComplete, callComplete); this.renderComplete.then(function () { return parentedPromise; }).then(function Pages_ready() { that.ready(element, options); }).then( null, function Pages_error(err) { return that.error(err); } ); }, _mixin ); base = _Base.Class.mix(base, _Control.DOMEventMixin); viewMap[uri.toLowerCase()] = base; } // Lazily mix in the members, allowing for multiple definitions of "define" to augment // the shared definition of the member. // if (members) { base = _Base.Class.mix(base, members); } base.selfhost = selfhost(uri); return base; } function get(uri) { uri = abs(uri); return viewMap[uri.toLowerCase()]; } function remove(uri) { uri = abs(uri); delete viewMap[uri.toLowerCase()]; } _Base.Namespace._moduleDefine(exports, null, { abs: abs, define: Pages_define, get: get, remove: remove }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Pages',[ 'exports', './Core/_Global', './Core/_Base', './Core/_BaseUtils', './ControlProcessor', './Fragments', './Pages/_BasePage', './Promise', ], function pagesInit(exports, _Global, _Base, _BaseUtils, ControlProcessor, Fragments, _BasePage, Promise) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } var _mixin = { load: function (uri) { /// /// /// Creates a copy of the DOM elements from the specified URI. In order for this override /// to be used, the page that contains the load override needs to be defined by calling /// WinJS.UI.Pages.define() before WinJS.UI.Pages.render() is called. /// /// /// The URI from which to copy the DOM elements. /// /// /// A promise whose fulfilled value is the set of unparented DOM elements, if asynchronous processing is necessary. If not, returns nothing. /// /// if (!this.selfhost) { return Fragments.renderCopy(_BasePage.abs(uri)); } }, process: function (element, options) { /// /// /// Processes the unparented DOM elements returned by load. /// /// /// The DOM element that will contain all the content for the page. /// /// /// The options that are to be passed to the constructor of the page. /// /// /// A promise that is fulfilled when processing is complete. /// /// return ControlProcessor.processAll(element); }, render: function (element, options, loadResult) { /// /// /// Renders the control, typically by adding the elements specified in the loadResult parameter to the specified element. /// /// /// The DOM element that will contain all the content for the page. /// /// /// The options passed into the constructor of the page. /// /// /// The elements returned from the load method. /// /// /// A promise that is fulfilled when rendering is complete, if asynchronous processing is necessary. If not, returns nothing. /// /// if (!this.selfhost) { element.appendChild(loadResult); } return element; } }; function Pages_define(uri, members) { /// /// /// Creates a new page control from the specified URI that contains the specified members. /// Multiple calls to this method for the same URI are allowed, and all members will be /// merged. /// /// /// The URI for the content that defines the page. /// /// /// Additional members that the control will have. /// /// /// A constructor function that creates the page. /// /// var Page = _BasePage.get(uri); if (!Page) { Page = _BasePage.define(uri, _mixin); } if (members) { Page = _Base.Class.mix(Page, members); } if (Page.selfhost) { _BaseUtils.ready(function () { render(_BasePage.abs(uri), _Global.document.body); }, true); } return Page; } function get(uri) { /// /// /// Gets an already-defined page control for the specified URI, or creates a new one. /// /// /// The URI for the content that defines the page. /// /// /// A constructor function that creates the page. /// /// var ctor = _BasePage.get(uri); if (!ctor) { ctor = Pages_define(uri); } return ctor; } function _remove(uri) { Fragments.clearCache(_BasePage.abs(uri)); _BasePage.remove(uri); } function render(uri, element, options, parentedPromise) { /// /// /// Creates a page control from the specified URI inside /// the specified element with the specified options. /// /// /// The URI for the content that defines the page. /// /// /// The element to populate with the page. /// /// /// The options for configuring the page. /// /// /// A promise that is fulfilled when the specified element is parented to the final document. /// /// /// A promise that is fulfilled when the page is done rendering /// /// var Ctor = get(uri); var control = new Ctor(element, options, null, parentedPromise); return control.renderComplete; } _Base.Namespace._moduleDefine(exports, "WinJS.UI.Pages", { define: Pages_define, get: get, _remove: _remove, render: render }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/HtmlControl',[ 'exports', '../Core/_Global', '../Core/_Base', '../Pages' ], function htmlControlInit(exports, _Global, _Base, Pages) { "use strict"; // not supported in WebWorker if (!_Global.document) { return; } _Base.Namespace._moduleDefine(exports, "WinJS.UI", { /// /// /// Enables you to include an HTML page dynamically. /// /// /// HtmlControl /// /// /// ]]> /// /// HtmlControl: _Base.Class.define(function HtmlControl_ctor(element, options, complete) { /// /// /// Initializes a new instance of HtmlControl to define a new page control. /// /// /// The element that hosts the HtmlControl. /// /// /// The options for configuring the page. The uri option is required in order to specify the source /// document for the content of the page. /// /// Pages.render(options.uri, element, options). then(complete, function () { complete(); }); }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Virtualized Data Source define('WinJS/VirtualizedDataSource/_VirtualizedDataSourceImpl',[ 'exports', '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Events', '../Core/_Log', '../Core/_Resources', '../Core/_WriteProfilerMark', '../Promise', '../Scheduler', '../_Signal', '../Utilities/_UI' ], function listDataSourceInit(exports, _Global, _Base, _BaseUtils, _ErrorFromName, _Events, _Log, _Resources, _WriteProfilerMark, Promise, Scheduler, _Signal, _UI) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { VirtualizedDataSource: _Base.Namespace._lazy(function () { var MAX_BEGINREFRESH_COUNT = 100; var uniqueID = 1; var DataSourceStatus = _UI.DataSourceStatus, CountResult = _UI.CountResult, FetchError = _UI.FetchError, EditError = _UI.EditError; // Private statics var strings = { get listDataAdapterIsInvalid() { return "Invalid argument: listDataAdapter must be an object or an array."; }, get indexIsInvalid() { return "Invalid argument: index must be a non-negative integer."; }, get keyIsInvalid() { return "Invalid argument: key must be a string."; }, get invalidItemReturned() { return "Error: data adapter returned item that is not an object."; }, get invalidKeyReturned() { return "Error: data adapter returned item with undefined or null key."; }, get invalidIndexReturned() { return "Error: data adapter should return undefined, null or a non-negative integer for the index."; }, get invalidCountReturned() { return "Error: data adapter should return undefined, null, CountResult.unknown, or a non-negative integer for the count."; }, get invalidRequestedCountReturned() { return "Error: data adapter should return CountResult.unknown, CountResult.failure, or a non-negative integer for the count."; }, get refreshCycleIdentified() { return "refresh cycle found, likely data inconsistency"; }, }; var statusChangedEvent = "statuschanged"; function _baseDataSourceConstructor(listDataAdapter, options) { /// /// /// Initializes the VirtualizedDataSource base class of a custom data source. /// /// /// An object that implements IListDataAdapter and supplies data to the VirtualizedDataSource. /// /// /// An object that contains properties that specify additonal options for the VirtualizedDataSource: /// /// cacheSize /// A Number that specifies minimum number of unrequested items to cache in case they are requested. /// /// The options parameter is optional. /// /// // Private members /*jshint validthis: true */ var listDataNotificationHandler, cacheSize, status, statusPending, statusChangePosted, bindingMap, nextListBindingID, nextHandle, nextListenerID, getCountPromise, resultsProcessed, beginEditsCalled, editsInProgress, firstEditInProgress, editQueue, editsQueued, synchronousEdit, waitForRefresh, dataNotificationsInProgress, countDelta, indexUpdateDeferred, nextTempKey, currentRefreshID, fetchesPosted, nextFetchID, fetchesInProgress, fetchCompleteCallbacks, startMarker, endMarker, knownCount, slotsStart, slotListEnd, slotsEnd, handleMap, keyMap, indexMap, releasedSlots, lastSlotReleased, reduceReleasedSlotCountPosted, refreshRequested, refreshInProgress, refreshSignal, refreshFetchesInProgress, refreshItemsFetched, refreshCount, refreshStart, refreshEnd, keyFetchIDs, refreshKeyMap, refreshIndexMap, deletedKeys, synchronousProgress, reentrantContinue, synchronousRefresh, reentrantRefresh; var beginRefreshCount = 0, refreshHistory = new Array(100), refreshHistoryPos = -1; var itemsFromKey, itemsFromIndex, itemsFromStart, itemsFromEnd, itemsFromDescription; if (listDataAdapter.itemsFromKey) { itemsFromKey = function (fetchID, key, countBefore, countAfter, hints) { var perfID = "fetchItemsFromKey id=" + fetchID + " key=" + key + " countBefore=" + countBefore + " countAfter=" + countAfter; profilerMarkStart(perfID); refreshHistory[++refreshHistoryPos % refreshHistory.length] = { kind: "itemsFromKey", key: key, countBefore: countBefore, countAfter: countAfter }; var result = listDataAdapter.itemsFromKey(key, countBefore, countAfter, hints); profilerMarkEnd(perfID); return result; }; } if (listDataAdapter.itemsFromIndex) { itemsFromIndex = function (fetchID, index, countBefore, countAfter) { var perfID = "fetchItemsFromIndex id=" + fetchID + " index=" + index + " countBefore=" + countBefore + " countAfter=" + countAfter; profilerMarkStart(perfID); refreshHistory[++refreshHistoryPos % refreshHistory.length] = { kind: "itemsFromIndex", index: index, countBefore: countBefore, countAfter: countAfter }; var result = listDataAdapter.itemsFromIndex(index, countBefore, countAfter); profilerMarkEnd(perfID); return result; }; } if (listDataAdapter.itemsFromStart) { itemsFromStart = function (fetchID, count) { var perfID = "fetchItemsFromStart id=" + fetchID + " count=" + count; profilerMarkStart(perfID); refreshHistory[++refreshHistoryPos % refreshHistory.length] = { kind: "itemsFromStart", count: count }; var result = listDataAdapter.itemsFromStart(count); profilerMarkEnd(perfID); return result; }; } if (listDataAdapter.itemsFromEnd) { itemsFromEnd = function (fetchID, count) { var perfID = "fetchItemsFromEnd id=" + fetchID + " count=" + count; profilerMarkStart(perfID); refreshHistory[++refreshHistoryPos % refreshHistory.length] = { kind: "itemsFromEnd", count: count }; var result = listDataAdapter.itemsFromEnd(count); profilerMarkEnd(perfID); return result; }; } if (listDataAdapter.itemsFromDescription) { itemsFromDescription = function (fetchID, description, countBefore, countAfter) { var perfID = "fetchItemsFromDescription id=" + fetchID + " desc=" + description + " countBefore=" + countBefore + " countAfter=" + countAfter; profilerMarkStart(perfID); refreshHistory[++refreshHistoryPos % refreshHistory.length] = { kind: "itemsFromDescription", description: description, countBefore: countBefore, countAfter: countAfter }; var result = listDataAdapter.itemsFromDescription(description, countBefore, countAfter); profilerMarkEnd(perfID); return result; }; } var dataSourceID = ++uniqueID; function profilerMarkStart(text) { var message = "WinJS.UI.VirtualizedDataSource:" + dataSourceID + ":" + text + ",StartTM"; _WriteProfilerMark(message); _Log.log && _Log.log(message, "winjs vds", "perf"); } function profilerMarkEnd(text) { var message = "WinJS.UI.VirtualizedDataSource:" + dataSourceID + ":" + text + ",StopTM"; _WriteProfilerMark(message); _Log.log && _Log.log(message, "winjs vds", "perf"); } function isNonNegativeNumber(n) { return (typeof n === "number") && n >= 0; } function isNonNegativeInteger(n) { return isNonNegativeNumber(n) && n === Math.floor(n); } function validateIndexReturned(index) { // Ensure that index is always undefined or a non-negative integer if (index === null) { index = undefined; } else if (index !== undefined && !isNonNegativeInteger(index)) { throw new _ErrorFromName("WinJS.UI.ListDataSource.InvalidIndexReturned", strings.invalidIndexReturned); } return index; } function validateCountReturned(count) { // Ensure that count is always undefined or a non-negative integer if (count === null) { count = undefined; } else if (count !== undefined && !isNonNegativeInteger(count) && count !== CountResult.unknown) { throw new _ErrorFromName("WinJS.UI.ListDataSource.InvalidCountReturned", strings.invalidCountReturned); } return count; } // Slot List function createSlot() { var handle = (nextHandle++).toString(), slotNew = { handle: handle, item: null, itemNew: null, fetchListeners: null, cursorCount: 0, bindingMap: null }; // Deliberately not initialized: // - directFetchListeners handleMap[handle] = slotNew; return slotNew; } function createPrimarySlot() { return createSlot(); } function insertSlot(slot, slotNext) { slot.prev = slotNext.prev; slot.next = slotNext; slot.prev.next = slot; slotNext.prev = slot; } function removeSlot(slot) { if (slot.lastInSequence) { delete slot.lastInSequence; slot.prev.lastInSequence = true; } if (slot.firstInSequence) { delete slot.firstInSequence; slot.next.firstInSequence = true; } slot.prev.next = slot.next; slot.next.prev = slot.prev; } function sequenceStart(slot) { while (!slot.firstInSequence) { slot = slot.prev; } return slot; } function sequenceEnd(slot) { while (!slot.lastInSequence) { slot = slot.next; } return slot; } // Does a little careful surgery to the slot sequence from slotFirst to slotLast before slotNext function moveSequenceBefore(slotNext, slotFirst, slotLast) { slotFirst.prev.next = slotLast.next; slotLast.next.prev = slotFirst.prev; slotFirst.prev = slotNext.prev; slotLast.next = slotNext; slotFirst.prev.next = slotFirst; slotNext.prev = slotLast; return true; } // Does a little careful surgery to the slot sequence from slotFirst to slotLast after slotPrev function moveSequenceAfter(slotPrev, slotFirst, slotLast) { slotFirst.prev.next = slotLast.next; slotLast.next.prev = slotFirst.prev; slotFirst.prev = slotPrev; slotLast.next = slotPrev.next; slotPrev.next = slotFirst; slotLast.next.prev = slotLast; return true; } function mergeSequences(slotPrev) { delete slotPrev.lastInSequence; delete slotPrev.next.firstInSequence; } function splitSequence(slotPrev) { var slotNext = slotPrev.next; slotPrev.lastInSequence = true; slotNext.firstInSequence = true; if (slotNext === slotListEnd) { // Clear slotListEnd's index, as that's now unknown changeSlotIndex(slotListEnd, undefined); } } // Inserts a slot in the middle of a sequence or between sequences. If the latter, mergeWithPrev and mergeWithNext // parameters specify whether to merge the slot with the previous sequence, or next, or neither. function insertAndMergeSlot(slot, slotNext, mergeWithPrev, mergeWithNext) { insertSlot(slot, slotNext); var slotPrev = slot.prev; if (slotPrev.lastInSequence) { if (mergeWithPrev) { delete slotPrev.lastInSequence; } else { slot.firstInSequence = true; } if (mergeWithNext) { delete slotNext.firstInSequence; } else { slot.lastInSequence = true; } } } // Keys and Indices function setSlotKey(slot, key) { slot.key = key; // Add the slot to the keyMap, so it is possible to quickly find the slot given its key keyMap[slot.key] = slot; } function setSlotIndex(slot, index, indexMapForSlot) { // Tolerate NaN, so clients can pass (undefined - 1) or (undefined + 1) if (+index === index) { slot.index = index; // Add the slot to the indexMap, so it is possible to quickly find the slot given its index indexMapForSlot[index] = slot; if (!indexUpdateDeferred) { // See if any sequences should be merged if (slot.firstInSequence && slot.prev && slot.prev.index === index - 1) { mergeSequences(slot.prev); } if (slot.lastInSequence && slot.next && slot.next.index === index + 1) { mergeSequences(slot); } } } } // Creates a new slot and adds it to the slot list before slotNext function createAndAddSlot(slotNext, indexMapForSlot) { var slotNew = (indexMapForSlot === indexMap ? createPrimarySlot() : createSlot()); insertSlot(slotNew, slotNext); return slotNew; } function createSlotSequence(slotNext, index, indexMapForSlot) { var slotNew = createAndAddSlot(slotNext, indexMapForSlot); slotNew.firstInSequence = true; slotNew.lastInSequence = true; setSlotIndex(slotNew, index, indexMapForSlot); return slotNew; } function createPrimarySlotSequence(slotNext, index) { return createSlotSequence(slotNext, index, indexMap); } function addSlotBefore(slotNext, indexMapForSlot) { var slotNew = createAndAddSlot(slotNext, indexMapForSlot); delete slotNext.firstInSequence; // See if we've bumped into the previous sequence if (slotNew.prev.index === slotNew.index - 1) { delete slotNew.prev.lastInSequence; } else { slotNew.firstInSequence = true; } setSlotIndex(slotNew, slotNext.index - 1, indexMapForSlot); return slotNew; } function addSlotAfter(slotPrev, indexMapForSlot) { var slotNew = createAndAddSlot(slotPrev.next, indexMapForSlot); delete slotPrev.lastInSequence; // See if we've bumped into the next sequence if (slotNew.next.index === slotNew.index + 1) { delete slotNew.next.firstInSequence; } else { slotNew.lastInSequence = true; } setSlotIndex(slotNew, slotPrev.index + 1, indexMapForSlot); return slotNew; } function reinsertSlot(slot, slotNext, mergeWithPrev, mergeWithNext) { insertAndMergeSlot(slot, slotNext, mergeWithPrev, mergeWithNext); keyMap[slot.key] = slot; if (slot.index !== undefined) { indexMap[slot.index] = slot; } } function removeSlotPermanently(slot) { removeSlot(slot); if (slot.key) { delete keyMap[slot.key]; } if (slot.index !== undefined && indexMap[slot.index] === slot) { delete indexMap[slot.index]; } var bindingMap = slot.bindingMap; for (var listBindingID in bindingMap) { var handle = bindingMap[listBindingID].handle; if (handle && handleMap[handle] === slot) { delete handleMap[handle]; } } // Invalidating the slot's handle marks it as deleted if (handleMap[slot.handle] === slot) { delete handleMap[slot.handle]; } } function slotPermanentlyRemoved(slot) { return !handleMap[slot.handle]; } function successorFromIndex(index, indexMapForSlot, listStart, listEnd, skipPreviousIndex) { // Try the previous index var slotNext = (skipPreviousIndex ? null : indexMapForSlot[index - 1]); if (slotNext && (slotNext.next !== listEnd || listEnd.firstInSequence)) { // We want the successor slotNext = slotNext.next; } else { // Try the next index slotNext = indexMapForSlot[index + 1]; if (!slotNext) { // Resort to a linear search slotNext = listStart.next; var lastSequenceStart; while (true) { if (slotNext.firstInSequence) { lastSequenceStart = slotNext; } if (!(index >= slotNext.index) || slotNext === listEnd) { break; } slotNext = slotNext.next; } if (slotNext === listEnd && !listEnd.firstInSequence) { // Return the last insertion point between sequences, or undefined if none slotNext = (lastSequenceStart && lastSequenceStart.index === undefined ? lastSequenceStart : undefined); } } } return slotNext; } // Slot Items function isPlaceholder(slot) { return !slot.item && !slot.itemNew && slot !== slotListEnd; } function defineHandleProperty(item, handle) { Object.defineProperty(item, "handle", { value: handle, writable: false, enumerable: false, configurable: true }); } function defineCommonItemProperties(item, slot, handle) { defineHandleProperty(item, handle); Object.defineProperty(item, "index", { get: function () { while (slot.slotMergedWith) { slot = slot.slotMergedWith; } return slot.index; }, enumerable: false, configurable: true }); } function validateData(data) { if (data === undefined) { return data; } else { // Convert the data object to JSON to enforce the constraints we want. For example, we don't want // functions, arrays with extra properties, DOM objects, cyclic or acyclic graphs, or undefined values. var dataValidated = JSON.stringify(data); if (dataValidated === undefined) { throw new _ErrorFromName("WinJS.UI.ListDataSource.ObjectIsNotValidJson", strings.objectIsNotValidJson); } return dataValidated; } } function itemSignature(item) { return ( listDataAdapter.itemSignature ? listDataAdapter.itemSignature(item.data) : validateData(item.data) ); } function prepareSlotItem(slot) { var item = slot.itemNew; slot.itemNew = null; if (item) { item = Object.create(item); defineCommonItemProperties(item, slot, slot.handle); if (!listDataAdapter.compareByIdentity) { // Store the item signature or a stringified copy of the data for comparison later slot.signature = itemSignature(item); } } slot.item = item; delete slot.indexRequested; delete slot.keyRequested; } // Slot Caching function slotRetained(slot) { return slot.bindingMap || slot.cursorCount > 0; } function slotRequested(slot) { return slotRetained(slot) || slot.fetchListeners || slot.directFetchListeners; } function slotLive(slot) { return slotRequested(slot) || (!slot.firstInSequence && slotRetained(slot.prev)) || (!slot.lastInSequence && slotRetained(slot.next)) || (!itemsFromIndex && ( (!slot.firstInSequence && slot.prev !== slotsStart && !(slot.prev.item || slot.prev.itemNew)) | (!slot.lastInSequence && slot.next !== slotListEnd && !(slot.next.item || slot.next.itemNew)) )); } function deleteUnnecessarySlot(slot) { splitSequence(slot); removeSlotPermanently(slot); } function reduceReleasedSlotCount() { // Must not release slots while edits are queued, as undo queue might refer to them if (!editsQueued) { // If lastSlotReleased is no longer valid, use the end of the list instead if (!lastSlotReleased || slotPermanentlyRemoved(lastSlotReleased)) { lastSlotReleased = slotListEnd.prev; } // Now use the simple heuristic of walking outwards in both directions from lastSlotReleased until the // desired cache size is reached, then removing everything else. var slotPrev = lastSlotReleased.prev, slotNext = lastSlotReleased.next, releasedSlotsFound = 0; var considerDeletingSlot = function (slotToDelete) { if (slotToDelete !== slotListEnd && !slotLive(slotToDelete)) { if (releasedSlotsFound <= cacheSize) { releasedSlotsFound++; } else { deleteUnnecessarySlot(slotToDelete); } } }; while (slotPrev || slotNext) { if (slotPrev) { var slotPrevToDelete = slotPrev; slotPrev = slotPrevToDelete.prev; if (slotPrevToDelete !== slotsStart) { considerDeletingSlot(slotPrevToDelete); } } if (slotNext) { var slotNextToDelete = slotNext; slotNext = slotNextToDelete.next; if (slotNextToDelete !== slotsEnd) { considerDeletingSlot(slotNextToDelete); } } } // Reset the count to zero, so this method is only called periodically releasedSlots = 0; } } function releaseSlotIfUnrequested(slot) { if (!slotRequested(slot)) { releasedSlots++; // Must not release slots while edits are queued, as undo queue might refer to them. If a refresh is in // progress, retain all slots, just in case the user re-requests some of them before the refresh completes. if (!editsQueued && !refreshInProgress) { // Track which slot was released most recently lastSlotReleased = slot; // See if the number of released slots has exceeded the cache size. In practice there will be more // live slots than retained slots, so this is just a heuristic to periodically shrink the cache. if (releasedSlots > cacheSize && !reduceReleasedSlotCountPosted) { reduceReleasedSlotCountPosted = true; Scheduler.schedule(function VDS_async_releaseSlotIfUnrequested() { reduceReleasedSlotCountPosted = false; reduceReleasedSlotCount(); }, Scheduler.Priority.idle, null, "WinJS.UI.VirtualizedDataSource.releaseSlotIfUnrequested"); } } } } // Notifications function forEachBindingRecord(callback) { for (var listBindingID in bindingMap) { callback(bindingMap[listBindingID]); } } function forEachBindingRecordOfSlot(slot, callback) { for (var listBindingID in slot.bindingMap) { callback(slot.bindingMap[listBindingID].bindingRecord, listBindingID); } } function handlerToNotify(bindingRecord) { if (!bindingRecord.notificationsSent) { bindingRecord.notificationsSent = true; if (bindingRecord.notificationHandler.beginNotifications) { bindingRecord.notificationHandler.beginNotifications(); } } return bindingRecord.notificationHandler; } function finishNotifications() { if (!editsInProgress && !dataNotificationsInProgress) { forEachBindingRecord(function (bindingRecord) { if (bindingRecord.notificationsSent) { bindingRecord.notificationsSent = false; if (bindingRecord.notificationHandler.endNotifications) { bindingRecord.notificationHandler.endNotifications(); } } }); } } function handleForBinding(slot, listBindingID) { var bindingMap = slot.bindingMap; if (bindingMap) { var slotBinding = bindingMap[listBindingID]; if (slotBinding) { var handle = slotBinding.handle; if (handle) { return handle; } } } return slot.handle; } function itemForBinding(item, handle) { if (item && item.handle !== handle) { item = Object.create(item); defineHandleProperty(item, handle); } return item; } function changeCount(count) { var oldCount = knownCount; knownCount = count; forEachBindingRecord(function (bindingRecord) { if (bindingRecord.notificationHandler && bindingRecord.notificationHandler.countChanged) { handlerToNotify(bindingRecord).countChanged(knownCount, oldCount); } }); } function sendIndexChangedNotifications(slot, indexOld) { forEachBindingRecordOfSlot(slot, function (bindingRecord, listBindingID) { if (bindingRecord.notificationHandler.indexChanged) { handlerToNotify(bindingRecord).indexChanged(handleForBinding(slot, listBindingID), slot.index, indexOld); } }); } function changeSlotIndex(slot, index) { var indexOld = slot.index; if (indexOld !== undefined && indexMap[indexOld] === slot) { // Remove the slot's old index from the indexMap delete indexMap[indexOld]; } // Tolerate NaN, so clients can pass (undefined - 1) or (undefined + 1) if (+index === index) { setSlotIndex(slot, index, indexMap); } else if (+indexOld === indexOld) { delete slot.index; } else { // If neither the new index or the old index is defined then there was no index changed. return; } sendIndexChangedNotifications(slot, indexOld); } function insertionNotificationRecipients(slot, slotPrev, slotNext, mergeWithPrev, mergeWithNext) { var bindingMapRecipients = {}; // Start with the intersection of the bindings for the two adjacent slots if ((mergeWithPrev || !slotPrev.lastInSequence) && (mergeWithNext || !slotNext.firstInSequence)) { if (slotPrev === slotsStart) { if (slotNext === slotListEnd) { // Special case: if the list was empty, broadcast the insertion to all ListBindings with // notification handlers. for (var listBindingID in bindingMap) { bindingMapRecipients[listBindingID] = bindingMap[listBindingID]; } } else { // Include every binding on the next slot for (var listBindingID in slotNext.bindingMap) { bindingMapRecipients[listBindingID] = bindingMap[listBindingID]; } } } else if (slotNext === slotListEnd || slotNext.bindingMap) { for (var listBindingID in slotPrev.bindingMap) { if (slotNext === slotListEnd || slotNext.bindingMap[listBindingID]) { bindingMapRecipients[listBindingID] = bindingMap[listBindingID]; } } } } // Use the union of that result with the bindings for the slot being inserted for (var listBindingID in slot.bindingMap) { bindingMapRecipients[listBindingID] = bindingMap[listBindingID]; } return bindingMapRecipients; } function sendInsertedNotification(slot) { var slotPrev = slot.prev, slotNext = slot.next, bindingMapRecipients = insertionNotificationRecipients(slot, slotPrev, slotNext), listBindingID; for (listBindingID in bindingMapRecipients) { var bindingRecord = bindingMapRecipients[listBindingID]; if (bindingRecord.notificationHandler) { handlerToNotify(bindingRecord).inserted(bindingRecord.itemPromiseFromKnownSlot(slot), slotPrev.lastInSequence || slotPrev === slotsStart ? null : handleForBinding(slotPrev, listBindingID), slotNext.firstInSequence || slotNext === slotListEnd ? null : handleForBinding(slotNext, listBindingID) ); } } } function changeSlot(slot) { var itemOld = slot.item; prepareSlotItem(slot); forEachBindingRecordOfSlot(slot, function (bindingRecord, listBindingID) { var handle = handleForBinding(slot, listBindingID); handlerToNotify(bindingRecord).changed(itemForBinding(slot.item, handle), itemForBinding(itemOld, handle)); }); } function moveSlot(slot, slotMoveBefore, mergeWithPrev, mergeWithNext, skipNotifications) { var slotMoveAfter = slotMoveBefore.prev, listBindingID; // If the slot is being moved before or after itself, adjust slotMoveAfter or slotMoveBefore accordingly. If // nothing is going to change in the slot list, don't send a notification. if (slotMoveBefore === slot) { if (!slot.firstInSequence || !mergeWithPrev) { return; } slotMoveBefore = slot.next; } else if (slotMoveAfter === slot) { if (!slot.lastInSequence || !mergeWithNext) { return; } slotMoveAfter = slot.prev; } if (!skipNotifications) { // Determine which bindings to notify var bindingMapRecipients = insertionNotificationRecipients(slot, slotMoveAfter, slotMoveBefore, mergeWithPrev, mergeWithNext); // Send the notification before the move for (listBindingID in bindingMapRecipients) { var bindingRecord = bindingMapRecipients[listBindingID]; handlerToNotify(bindingRecord).moved(bindingRecord.itemPromiseFromKnownSlot(slot), ((slotMoveAfter.lastInSequence || slotMoveAfter === slot.prev) && !mergeWithPrev) || slotMoveAfter === slotsStart ? null : handleForBinding(slotMoveAfter, listBindingID), ((slotMoveBefore.firstInSequence || slotMoveBefore === slot.next) && !mergeWithNext) || slotMoveBefore === slotListEnd ? null : handleForBinding(slotMoveBefore, listBindingID) ); } // If a ListBinding cursor is at the slot that's moving, adjust the cursor forEachBindingRecord(function (bindingRecord) { bindingRecord.adjustCurrentSlot(slot); }); } removeSlot(slot); insertAndMergeSlot(slot, slotMoveBefore, mergeWithPrev, mergeWithNext); } function deleteSlot(slot, mirage) { completeFetchPromises(slot, true); forEachBindingRecordOfSlot(slot, function (bindingRecord, listBindingID) { handlerToNotify(bindingRecord).removed(handleForBinding(slot, listBindingID), mirage); }); // If a ListBinding cursor is at the slot that's being removed, adjust the cursor forEachBindingRecord(function (bindingRecord) { bindingRecord.adjustCurrentSlot(slot); }); removeSlotPermanently(slot); } function deleteMirageSequence(slot) { // Remove the slots in order while (!slot.firstInSequence) { slot = slot.prev; } var last; do { last = slot.lastInSequence; var slotNext = slot.next; deleteSlot(slot, true); slot = slotNext; } while (!last); } // Deferred Index Updates // Returns the index of the slot taking into account any outstanding index updates function adjustedIndex(slot) { var undefinedIndex; if (!slot) { return undefinedIndex; } var delta = 0; while (!slot.firstInSequence) { delta++; slot = slot.prev; } return ( typeof slot.indexNew === "number" ? slot.indexNew + delta : typeof slot.index === "number" ? slot.index + delta : undefinedIndex ); } // Updates the new index of the first slot in each sequence after the given slot function updateNewIndicesAfterSlot(slot, indexDelta) { // Adjust all the indexNews after this slot for (slot = slot.next; slot; slot = slot.next) { if (slot.firstInSequence) { var indexNew = (slot.indexNew !== undefined ? slot.indexNew : slot.index); if (indexNew !== undefined) { slot.indexNew = indexNew + indexDelta; } } } // Adjust the overall count countDelta += indexDelta; indexUpdateDeferred = true; // Increment currentRefreshID so any outstanding fetches don't cause trouble. If a refresh is in progress, // restart it (which will also increment currentRefreshID). if (refreshInProgress) { beginRefresh(); } else { currentRefreshID++; } } // Updates the new index of the given slot if necessary, and all subsequent new indices function updateNewIndices(slot, indexDelta) { // If this slot is at the start of a sequence, transfer the indexNew if (slot.firstInSequence) { var indexNew; if (indexDelta < 0) { // The given slot is about to be removed indexNew = slot.indexNew; if (indexNew !== undefined) { delete slot.indexNew; } else { indexNew = slot.index; } if (!slot.lastInSequence) { // Update the next slot now slot = slot.next; if (indexNew !== undefined) { slot.indexNew = indexNew; } } } else { // The given slot was just inserted if (!slot.lastInSequence) { var slotNext = slot.next; indexNew = slotNext.indexNew; if (indexNew !== undefined) { delete slotNext.indexNew; } else { indexNew = slotNext.index; } if (indexNew !== undefined) { slot.indexNew = indexNew; } } } } updateNewIndicesAfterSlot(slot, indexDelta); } // Updates the new index of the first slot in each sequence after the given new index function updateNewIndicesFromIndex(index, indexDelta) { for (var slot = slotsStart; slot !== slotListEnd; slot = slot.next) { var indexNew = slot.indexNew; if (indexNew !== undefined && index <= indexNew) { updateNewIndicesAfterSlot(slot, indexDelta); break; } } } // Adjust the indices of all slots to be consistent with any indexNew properties, and strip off the indexNews function updateIndices() { var slot, slotFirstInSequence, indexNew; for (slot = slotsStart; ; slot = slot.next) { if (slot.firstInSequence) { slotFirstInSequence = slot; if (slot.indexNew !== undefined) { indexNew = slot.indexNew; delete slot.indexNew; if (isNaN(indexNew)) { break; } } else { indexNew = slot.index; } // See if this sequence should be merged with the previous one if (slot !== slotsStart && slot.prev.index === indexNew - 1) { mergeSequences(slot.prev); } } if (slot.lastInSequence) { var index = indexNew; for (var slotUpdate = slotFirstInSequence; slotUpdate !== slot.next; slotUpdate = slotUpdate.next) { if (index !== slotUpdate.index) { changeSlotIndex(slotUpdate, index); } if (+index === index) { index++; } } } if (slot === slotListEnd) { break; } } // Clear any indices on slots that were moved adjacent to slots without indices for (; slot !== slotsEnd; slot = slot.next) { if (slot.index !== undefined && slot !== slotListEnd) { changeSlotIndex(slot, undefined); } } indexUpdateDeferred = false; if (countDelta && +knownCount === knownCount) { if (getCountPromise) { getCountPromise.reset(); } else { changeCount(knownCount + countDelta); } countDelta = 0; } } // Fetch Promises function createFetchPromise(slot, listenersProperty, listenerID, listBindingID, onComplete) { if (slot.item) { return new Promise(function (complete) { if (onComplete) { onComplete(complete, slot.item); } else { complete(slot.item); } }); } else { var listener = { listBindingID: listBindingID, retained: false }; if (!slot[listenersProperty]) { slot[listenersProperty] = {}; } slot[listenersProperty][listenerID] = listener; listener.promise = new Promise(function (complete, error) { listener.complete = (onComplete ? function (item) { onComplete(complete, item); } : complete); listener.error = error; }, function () { // By now the slot might have been merged with another while (slot.slotMergedWith) { slot = slot.slotMergedWith; } var fetchListeners = slot[listenersProperty]; if (fetchListeners) { delete fetchListeners[listenerID]; // See if there are any other listeners if (Object.keys(fetchListeners).length > 0) { return; } delete slot[listenersProperty]; } releaseSlotIfUnrequested(slot); }); return listener.promise; } } function completePromises(item, listeners) { for (var listenerID in listeners) { listeners[listenerID].complete(item); } } function completeFetchPromises(slot, completeSynchronously) { var fetchListeners = slot.fetchListeners, directFetchListeners = slot.directFetchListeners; if (fetchListeners || directFetchListeners) { prepareSlotItem(slot); // By default, complete asynchronously to minimize reentrancy var item = slot.item; var completeOrQueuePromises = function (listeners) { if (completeSynchronously) { completePromises(item, listeners); } else { fetchCompleteCallbacks.push(function () { completePromises(item, listeners); }); } }; if (directFetchListeners) { slot.directFetchListeners = null; completeOrQueuePromises(directFetchListeners); } if (fetchListeners) { slot.fetchListeners = null; completeOrQueuePromises(fetchListeners); } releaseSlotIfUnrequested(slot); } } function callFetchCompleteCallbacks() { var callbacks = fetchCompleteCallbacks; // Clear fetchCompleteCallbacks first to avoid reentrancy problems fetchCompleteCallbacks = []; for (var i = 0, len = callbacks.length; i < len; i++) { callbacks[i](); } } function returnDirectFetchError(slot, error) { var directFetchListeners = slot.directFetchListeners; if (directFetchListeners) { slot.directFetchListeners = null; for (var listenerID in directFetchListeners) { directFetchListeners[listenerID].error(error); } releaseSlotIfUnrequested(slot); } } // Item Requests function requestSlot(slot) { // Ensure that there's a slot on either side of each requested item if (slot.firstInSequence) { addSlotBefore(slot, indexMap); } if (slot.lastInSequence) { addSlotAfter(slot, indexMap); } // If the item has already been fetched, prepare it now to be returned to the client if (slot.itemNew) { prepareSlotItem(slot); } // Start a new fetch if necessary postFetch(); return slot; } function requestSlotBefore(slotNext) { // First, see if the previous slot already exists if (!slotNext.firstInSequence) { var slotPrev = slotNext.prev; // Next, see if the item is known to not exist return (slotPrev === slotsStart ? null : requestSlot(slotPrev)); } return requestSlot(addSlotBefore(slotNext, indexMap)); } function requestSlotAfter(slotPrev) { // First, see if the next slot already exists if (!slotPrev.lastInSequence) { var slotNext = slotPrev.next; // Next, see if the item is known to not exist return (slotNext === slotListEnd ? null : requestSlot(slotNext)); } return requestSlot(addSlotAfter(slotPrev, indexMap)); } function itemDirectlyFromSlot(slot) { // Return a complete promise for a non-existent slot return ( slot ? createFetchPromise(slot, "directFetchListeners", (nextListenerID++).toString()) : Promise.wrap(null) ); } function validateKey(key) { if (typeof key !== "string" || !key) { throw new _ErrorFromName("WinJS.UI.ListDataSource.KeyIsInvalid", strings.keyIsInvalid); } } function createSlotForKey(key) { var slot = createPrimarySlotSequence(slotsEnd); setSlotKey(slot, key); slot.keyRequested = true; return slot; } function slotFromKey(key, hints) { validateKey(key); var slot = keyMap[key]; if (!slot) { slot = createSlotForKey(key); slot.hints = hints; } return requestSlot(slot); } function slotFromIndex(index) { if (typeof index !== "number" || index < 0) { throw new _ErrorFromName("WinJS.UI.ListDataSource.IndexIsInvalid", strings.indexIsInvalid); } if (slotListEnd.index <= index) { return null; } var slot = indexMap[index]; if (!slot) { var slotNext = successorFromIndex(index, indexMap, slotsStart, slotListEnd); if (!slotNext) { // The complete list has been observed, and this index isn't a part of it; a refresh may be necessary return null; } if (slotNext === slotListEnd && index >= slotListEnd) { // Clear slotListEnd's index, as that's now unknown changeSlotIndex(slotListEnd, undefined); } // Create a new slot and start a request for it if (slotNext.prev.index === index - 1) { slot = addSlotAfter(slotNext.prev, indexMap); } else if (slotNext.index === index + 1) { slot = addSlotBefore(slotNext, indexMap); } else { slot = createPrimarySlotSequence(slotNext, index); } } if (!slot.item) { slot.indexRequested = true; } return requestSlot(slot); } function slotFromDescription(description) { // Create a new slot and start a request for it var slot = createPrimarySlotSequence(slotsEnd); slot.description = description; return requestSlot(slot); } // Status var that = this; function setStatus(statusNew) { statusPending = statusNew; if (status !== statusPending) { var dispatch = function () { statusChangePosted = false; if (status !== statusPending) { status = statusPending; that.dispatchEvent(statusChangedEvent, status); } }; if (statusPending === DataSourceStatus.failure) { dispatch(); } else if (!statusChangePosted) { statusChangePosted = true; // Delay the event to filter out rapid changes _Global.setTimeout(dispatch, 40); } } } // Slot Fetching function slotFetchInProgress(slot) { var fetchID = slot.fetchID; return fetchID && fetchesInProgress[fetchID]; } function setFetchID(slot, fetchID) { slot.fetchID = fetchID; } function newFetchID() { var fetchID = nextFetchID; nextFetchID++; fetchesInProgress[fetchID] = true; return fetchID; } function setFetchIDs(slot, countBefore, countAfter) { var fetchID = newFetchID(); setFetchID(slot, fetchID); var slotBefore = slot; while (!slotBefore.firstInSequence && countBefore > 0) { slotBefore = slotBefore.prev; countBefore--; setFetchID(slotBefore, fetchID); } var slotAfter = slot; while (!slotAfter.lastInSequence && countAfter > 0) { slotAfter = slotAfter.next; countAfter--; setFetchID(slotAfter, fetchID); } return fetchID; } // Adds markers on behalf of the data adapter if their presence can be deduced function addMarkers(fetchResult) { var items = fetchResult.items, offset = fetchResult.offset, totalCount = fetchResult.totalCount, absoluteIndex = fetchResult.absoluteIndex, atStart = fetchResult.atStart, atEnd = fetchResult.atEnd; if (isNonNegativeNumber(absoluteIndex)) { if (isNonNegativeNumber(totalCount)) { var itemsLength = items.length; if (absoluteIndex - offset + itemsLength === totalCount) { atEnd = true; } } if (offset === absoluteIndex) { atStart = true; } } if (atStart) { items.unshift(startMarker); fetchResult.offset++; } if (atEnd) { items.push(endMarker); } } function resultsValid(slot, refreshID, fetchID) { // This fetch has completed, whatever it has returned delete fetchesInProgress[fetchID]; if (refreshID !== currentRefreshID || slotPermanentlyRemoved(slot)) { // This information is out of date, or the slot has since been discarded postFetch(); return false; } return true; } function fetchItems(slot, fetchID, promiseItems, index) { var refreshID = currentRefreshID; promiseItems.then(function (fetchResult) { if (fetchResult.items && fetchResult.items.length) { var perfID = "itemsFetched id=" + fetchID + " count=" + fetchResult.items.length; profilerMarkStart(perfID); if (resultsValid(slot, refreshID, fetchID)) { if (+index === index) { fetchResult.absoluteIndex = index; } addMarkers(fetchResult); processResults(slot, fetchResult.items, fetchResult.offset, fetchResult.totalCount, fetchResult.absoluteIndex); } profilerMarkEnd(perfID); } else { return Promise.wrapError(new _ErrorFromName(FetchError.doesNotExist)); } }).then(null, function (error) { if (resultsValid(slot, refreshID, fetchID)) { processErrorResult(slot, error); } }); } function fetchItemsForIndex(indexRequested, slot, fetchID, promiseItems) { var refreshID = currentRefreshID; promiseItems.then(function (fetchResult) { if (fetchResult.items && fetchResult.items.length) { var perfID = "itemsFetched id=" + fetchID + " count=" + fetchResult.items.length; profilerMarkStart(perfID); if (resultsValid(slot, refreshID, fetchID)) { fetchResult.absoluteIndex = indexRequested; addMarkers(fetchResult); processResultsForIndex(indexRequested, slot, fetchResult.items, fetchResult.offset, fetchResult.totalCount, fetchResult.absoluteIndex); } profilerMarkEnd(perfID); } else { return Promise.wrapError(new _ErrorFromName(FetchError.doesNotExist)); } }).then(null, function () { if (resultsValid(slot, refreshID, fetchID)) { processErrorResultForIndex(indexRequested, slot, refreshID); } }); } function fetchItemsFromStart(slot, count) { var fetchID = setFetchIDs(slot, 0, count - 1); if (itemsFromStart) { fetchItems(slot, fetchID, itemsFromStart(fetchID, count), 0); } else { fetchItems(slot, fetchID, itemsFromIndex(fetchID, 0, 0, count - 1), 0); } } function fetchItemsFromEnd(slot, count) { var fetchID = setFetchIDs(slot, count - 1, 0); fetchItems(slot, fetchID, itemsFromEnd(fetchID, count)); } function fetchItemsFromKey(slot, countBefore, countAfter) { var fetchID = setFetchIDs(slot, countBefore, countAfter); fetchItems(slot, fetchID, itemsFromKey(fetchID, slot.key, countBefore, countAfter, slot.hints)); } function fetchItemsFromIndex(slot, countBefore, countAfter) { var index = slot.index; // Don't ask for items with negative indices if (countBefore > index) { countBefore = index; } if (itemsFromIndex) { var fetchID = setFetchIDs(slot, countBefore, countAfter); fetchItems(slot, fetchID, itemsFromIndex(fetchID, index, countBefore, countAfter), index); } else { // If the slot key is known, we just need to request the surrounding items if (slot.key) { fetchItemsFromKey(slot, countBefore, countAfter); } else { // Search for the slot with the closest index that has a known key (using the start of the list as a // last resort). var slotClosest = slotsStart, closestDelta = index + 1, slotSearch, delta; // First search backwards for (slotSearch = slot.prev; slotSearch !== slotsStart; slotSearch = slotSearch.prev) { if (slotSearch.index !== undefined && slotSearch.key) { delta = index - slotSearch.index; if (closestDelta > delta) { closestDelta = delta; slotClosest = slotSearch; } break; } } // Then search forwards for (slotSearch = slot.next; slotSearch !== slotListEnd; slotSearch = slotSearch.next) { if (slotSearch.index !== undefined && slotSearch.key) { delta = slotSearch.index - index; if (closestDelta > delta) { closestDelta = delta; slotClosest = slotSearch; } break; } } if (slotClosest === slotsStart) { var fetchID = setFetchIDs(slot, 0, index + 1); fetchItemsForIndex(0, slot, fetchID, itemsFromStart(fetchID, index + 1)); } else { var fetchBefore = Math.max(slotClosest.index - index, 0); var fetchAfter = Math.max(index - slotClosest.index, 0); var fetchID = setFetchIDs(slotClosest, fetchBefore, fetchAfter); fetchItemsForIndex(slotClosest.index, slot, fetchID, itemsFromKey(fetchID, slotClosest.key, fetchBefore, fetchAfter, slot.hints )); } } } } function fetchItemsFromDescription(slot, countBefore, countAfter) { var fetchID = setFetchIDs(slot, countBefore, countAfter); fetchItems(slot, fetchID, itemsFromDescription(fetchID, slot.description, countBefore, countAfter)); } function fetchItemsForAllSlots() { if (!refreshInProgress) { var slotFirstPlaceholder, placeholderCount, fetchInProgress = false, fetchesInProgress = false, slotRequestedByKey, requestedKeyOffset, slotRequestedByDescription, requestedDescriptionOffset, slotRequestedByIndex, requestedIndexOffset; for (var slot = slotsStart.next; slot !== slotsEnd;) { var slotNext = slot.next; if (slot !== slotListEnd && isPlaceholder(slot)) { fetchesInProgress = true; if (!slotFirstPlaceholder) { slotFirstPlaceholder = slot; placeholderCount = 1; } else { placeholderCount++; } if (slotFetchInProgress(slot)) { fetchInProgress = true; } if (slot.keyRequested && !slotRequestedByKey) { slotRequestedByKey = slot; requestedKeyOffset = placeholderCount - 1; } if (slot.description !== undefined && !slotRequestedByDescription) { slotRequestedByDescription = slot; requestedDescriptionOffset = placeholderCount - 1; } if (slot.indexRequested && !slotRequestedByIndex) { slotRequestedByIndex = slot; requestedIndexOffset = placeholderCount - 1; } if (slot.lastInSequence || slotNext === slotsEnd || !isPlaceholder(slotNext)) { if (fetchInProgress) { fetchInProgress = false; } else { resultsProcessed = false; // Start a new fetch for this placeholder sequence // Prefer fetches in terms of a known item if (!slotFirstPlaceholder.firstInSequence && slotFirstPlaceholder.prev.key && itemsFromKey) { fetchItemsFromKey(slotFirstPlaceholder.prev, 0, placeholderCount); } else if (!slot.lastInSequence && slotNext.key && itemsFromKey) { fetchItemsFromKey(slotNext, placeholderCount, 0); } else if (slotFirstPlaceholder.prev === slotsStart && !slotFirstPlaceholder.firstInSequence && (itemsFromStart || itemsFromIndex)) { fetchItemsFromStart(slotFirstPlaceholder, placeholderCount); } else if (slotNext === slotListEnd && !slot.lastInSequence && itemsFromEnd) { fetchItemsFromEnd(slot, placeholderCount); } else if (slotRequestedByKey) { fetchItemsFromKey(slotRequestedByKey, requestedKeyOffset, placeholderCount - 1 - requestedKeyOffset); } else if (slotRequestedByDescription) { fetchItemsFromDescription(slotRequestedByDescription, requestedDescriptionOffset, placeholderCount - 1 - requestedDescriptionOffset); } else if (slotRequestedByIndex) { fetchItemsFromIndex(slotRequestedByIndex, requestedIndexOffset, placeholderCount - 1 - requestedIndexOffset); } else if (typeof slotFirstPlaceholder.index === "number") { fetchItemsFromIndex(slotFirstPlaceholder, placeholderCount - 1, 0); } else { // There is no way to fetch anything in this sequence deleteMirageSequence(slotFirstPlaceholder); } if (resultsProcessed) { // A re-entrant fetch might have altered the slots list - start again postFetch(); return; } if (refreshInProgress) { // A re-entrant fetch might also have caused a refresh return; } } slotFirstPlaceholder = slotRequestedByIndex = slotRequestedByKey = null; } } slot = slotNext; } setStatus(fetchesInProgress ? DataSourceStatus.waiting : DataSourceStatus.ready); } } function postFetch() { if (!fetchesPosted) { fetchesPosted = true; Scheduler.schedule(function VDS_async_postFetch() { fetchesPosted = false; fetchItemsForAllSlots(); // A mirage sequence might have been removed finishNotifications(); }, Scheduler.Priority.max, null, "WinJS.UI.ListDataSource._fetch"); } } // Fetch Result Processing function itemChanged(slot) { var itemNew = slot.itemNew; if (!itemNew) { return false; } var item = slot.item; for (var property in item) { switch (property) { case "data": // This is handled below break; default: if (item[property] !== itemNew[property]) { return true; } break; } } return ( listDataAdapter.compareByIdentity ? item.data !== itemNew.data : slot.signature !== itemSignature(itemNew) ); } function changeSlotIfNecessary(slot) { if (!slotRequested(slot)) { // There's no need for any notifications, just delete the old item slot.item = null; } else if (itemChanged(slot)) { changeSlot(slot); } else { slot.itemNew = null; } } function updateSlotItem(slot) { if (slot.item) { changeSlotIfNecessary(slot); } else { completeFetchPromises(slot); } } function updateSlot(slot, item) { if (!slot.key) { setSlotKey(slot, item.key); } slot.itemNew = item; updateSlotItem(slot); } function sendMirageNotifications(slot, slotToDiscard, listBindingIDsToDelete) { var bindingMap = slotToDiscard.bindingMap; if (bindingMap) { for (var listBindingID in listBindingIDsToDelete) { if (bindingMap[listBindingID]) { var fetchListeners = slotToDiscard.fetchListeners; for (var listenerID in fetchListeners) { var listener = fetchListeners[listenerID]; if (listener.listBindingID === listBindingID && listener.retained) { delete fetchListeners[listenerID]; listener.complete(null); } } var bindingRecord = bindingMap[listBindingID].bindingRecord; handlerToNotify(bindingRecord).removed(handleForBinding(slotToDiscard, listBindingID), true, handleForBinding(slot, listBindingID)); // A re-entrant call to release from the removed handler might have cleared slotToDiscard.bindingMap if (slotToDiscard.bindingMap) { delete slotToDiscard.bindingMap[listBindingID]; } } } } } function mergeSlots(slot, slotToDiscard) { // This shouldn't be called on a slot that has a pending change notification // Only one of the two slots should have a key // If slotToDiscard is about to acquire an index, send the notifications now; in rare cases, multiple // indexChanged notifications will be sent for a given item during a refresh, but that's fine. if (slot.index !== slotToDiscard.index) { // If slotToDiscard has a defined index, that should have been transferred already var indexOld = slotToDiscard.index; slotToDiscard.index = slot.index; sendIndexChangedNotifications(slotToDiscard, indexOld); } slotToDiscard.slotMergedWith = slot; // Transfer the slotBindings from slotToDiscard to slot var bindingMap = slotToDiscard.bindingMap; for (var listBindingID in bindingMap) { if (!slot.bindingMap) { slot.bindingMap = {}; } var slotBinding = bindingMap[listBindingID]; if (!slotBinding.handle) { slotBinding.handle = slotToDiscard.handle; } handleMap[slotBinding.handle] = slot; slot.bindingMap[listBindingID] = slotBinding; } // Update any ListBinding cursors pointing to slotToDiscard forEachBindingRecord(function (bindingRecord) { bindingRecord.adjustCurrentSlot(slotToDiscard, slot); }); // See if the item needs to be transferred from slotToDiscard to slot var item = slotToDiscard.itemNew || slotToDiscard.item; if (item) { item = Object.create(item); defineCommonItemProperties(item, slot, slot.handle); updateSlot(slot, item); } // Transfer the fetch listeners from slotToDiscard to slot, or complete them if item is known if (slot.item) { if (slotToDiscard.directFetchListeners) { fetchCompleteCallbacks.push(function () { completePromises(slot.item, slotToDiscard.directFetchListeners); }); } if (slotToDiscard.fetchListeners) { fetchCompleteCallbacks.push(function () { completePromises(slot.item, slotToDiscard.fetchListeners); }); } } else { var listenerID; for (listenerID in slotToDiscard.directFetchListeners) { if (!slot.directFetchListeners) { slot.directFetchListeners = {}; } slot.directFetchListeners[listenerID] = slotToDiscard.directFetchListeners[listenerID]; } for (listenerID in slotToDiscard.fetchListeners) { if (!slot.fetchListeners) { slot.fetchListeners = {}; } slot.fetchListeners[listenerID] = slotToDiscard.fetchListeners[listenerID]; } } // This might be the first time this slot's item can be prepared if (slot.itemNew) { completeFetchPromises(slot); } // Give slotToDiscard an unused handle so it appears to be permanently removed slotToDiscard.handle = (nextHandle++).toString(); splitSequence(slotToDiscard); removeSlotPermanently(slotToDiscard); } function mergeSlotsAndItem(slot, slotToDiscard, item) { if (slotToDiscard && slotToDiscard.key) { if (!item) { item = slotToDiscard.itemNew || slotToDiscard.item; } // Free up the key for the promoted slot delete slotToDiscard.key; delete keyMap[item.key]; slotToDiscard.itemNew = null; slotToDiscard.item = null; } if (item) { updateSlot(slot, item); } if (slotToDiscard) { mergeSlots(slot, slotToDiscard); } } function slotFromResult(result) { if (typeof result !== "object") { throw new _ErrorFromName("WinJS.UI.ListDataSource.InvalidItemReturned", strings.invalidItemReturned); } else if (result === startMarker) { return slotsStart; } else if (result === endMarker) { return slotListEnd; } else if (!result.key) { throw new _ErrorFromName("WinJS.UI.ListDataSource.InvalidKeyReturned", strings.invalidKeyReturned); } else { if (_BaseUtils.validation) { validateKey(result.key); } return keyMap[result.key]; } } function matchSlot(slot, result) { // First see if there is an existing slot that needs to be merged var slotExisting = slotFromResult(result); if (slotExisting === slot) { slotExisting = null; } if (slotExisting) { sendMirageNotifications(slot, slotExisting, slot.bindingMap); } mergeSlotsAndItem(slot, slotExisting, result); } function promoteSlot(slot, item, index, insertionPoint) { if (item && slot.key && slot.key !== item.key) { // A contradiction has been found beginRefresh(); return false; } // The slot with the key "wins"; slots without bindings can be merged without any change in observable behavior var slotWithIndex = indexMap[index]; if (slotWithIndex) { if (slotWithIndex === slot) { slotWithIndex = null; } else if (slotWithIndex.key && (slot.key || (item && slotWithIndex.key !== item.key))) { // A contradiction has been found beginRefresh(); return false; } else if (!slot.key && slotWithIndex.bindingMap) { return false; } } var slotWithKey; if (item) { slotWithKey = keyMap[item.key]; if (slotWithKey === slot) { slotWithKey = null; } else if (slotWithKey && slotWithKey.bindingMap) { return false; } } if (slotWithIndex) { sendMirageNotifications(slot, slotWithIndex, slot.bindingMap); // Transfer the index to the promoted slot delete indexMap[index]; changeSlotIndex(slot, index); // See if this sequence should be merged with its neighbors if (slot.prev.index === index - 1) { mergeSequences(slot.prev); } if (slot.next.index === index + 1) { mergeSequences(slot); } insertionPoint.slotNext = slotWithIndex.slotNext; if (!item) { item = slotWithIndex.itemNew || slotWithIndex.item; if (item) { slotWithKey = keyMap[item.key]; } } } else { changeSlotIndex(slot, index); } if (slotWithKey && slotWithIndex !== slotWithKey) { sendMirageNotifications(slot, slotWithKey, slot.bindingMap); } mergeSlotsAndItem(slot, slotWithKey, item); // Do this after mergeSlotsAndItem, since its call to updateSlot might send changed notifications, and those // wouldn't make sense to clients that never saw the old item. if (slotWithIndex && slotWithIndex !== slotWithKey) { mergeSlots(slot, slotWithIndex); } return true; } function mergeAdjacentSlot(slotExisting, slot, listBindingIDsToDelete) { if (slot.key && slotExisting.key && slot.key !== slotExisting.key) { // A contradiction has been found beginRefresh(); return false; } for (var listBindingID in slotExisting.bindingMap) { listBindingIDsToDelete[listBindingID] = true; } sendMirageNotifications(slotExisting, slot, listBindingIDsToDelete); mergeSlotsAndItem(slotExisting, slot); return true; } function mergeSlotsBefore(slot, slotExisting) { var listBindingIDsToDelete = {}; while (slot) { var slotPrev = (slot.firstInSequence ? null : slot.prev); if (!slotExisting.firstInSequence && slotExisting.prev === slotsStart) { deleteSlot(slot, true); } else { if (slotExisting.firstInSequence) { slotExisting = addSlotBefore(slotExisting, indexMap); } else { slotExisting = slotExisting.prev; } if (!mergeAdjacentSlot(slotExisting, slot, listBindingIDsToDelete)) { return; } } slot = slotPrev; } } function mergeSlotsAfter(slot, slotExisting) { var listBindingIDsToDelete = {}; while (slot) { var slotNext = (slot.lastInSequence ? null : slot.next); if (!slotExisting.lastInSequence && slotExisting.next === slotListEnd) { deleteSlot(slot, true); } else { if (slotExisting.lastInSequence) { slotExisting = addSlotAfter(slotExisting, indexMap); } else { slotExisting = slotExisting.next; } if (!mergeAdjacentSlot(slotExisting, slot, listBindingIDsToDelete)) { return; } } slot = slotNext; } } function mergeSequencePairs(sequencePairsToMerge) { for (var i = 0; i < sequencePairsToMerge.length; i++) { var sequencePairToMerge = sequencePairsToMerge[i]; mergeSlotsBefore(sequencePairToMerge.slotBeforeSequence, sequencePairToMerge.slotFirstInSequence); mergeSlotsAfter(sequencePairToMerge.slotAfterSequence, sequencePairToMerge.slotLastInSequence); } } // Removes any placeholders with indices that exceed the given upper bound on the count function removeMirageIndices(countMax, indexFirstKnown) { var placeholdersAtEnd = 0; function removePlaceholdersAfterSlot(slotRemoveAfter) { for (var slot2 = slotListEnd.prev; !(slot2.index < countMax) && slot2 !== slotRemoveAfter;) { var slotPrev2 = slot2.prev; if (slot2.index !== undefined) { deleteSlot(slot2, true); } slot2 = slotPrev2; } placeholdersAtEnd = 0; } for (var slot = slotListEnd.prev; !(slot.index < countMax) || placeholdersAtEnd > 0;) { var slotPrev = slot.prev; if (slot === slotsStart) { removePlaceholdersAfterSlot(slotsStart); break; } else if (slot.key) { if (slot.index >= countMax) { beginRefresh(); return false; } else if (slot.index >= indexFirstKnown) { removePlaceholdersAfterSlot(slot); } else { if (itemsFromKey) { fetchItemsFromKey(slot, 0, placeholdersAtEnd); } else { fetchItemsFromIndex(slot, 0, placeholdersAtEnd); } // Wait until the fetch has completed before doing anything return false; } } else if (slot.indexRequested || slot.firstInSequence) { removePlaceholdersAfterSlot(slotPrev); } else { placeholdersAtEnd++; } slot = slotPrev; } return true; } // Merges the results of a fetch into the slot list data structure, and determines if any notifications need to be // synthesized. function processResults(slot, results, offset, count, index) { var perfId = "WinJS.UI.ListDataSource.processResults"; profilerMarkStart(perfId); index = validateIndexReturned(index); count = validateCountReturned(count); // If there are edits queued, we need to wait until the slots get back in sync with the data if (editsQueued) { profilerMarkEnd(perfId); return; } if (indexUpdateDeferred) { updateIndices(); } // If the count has changed, and the end of the list has been reached, that's a contradiction if ((isNonNegativeNumber(count) || count === CountResult.unknown) && count !== knownCount && !slotListEnd.firstInSequence) { beginRefresh(); profilerMarkEnd(perfId); return; } resultsProcessed = true; (function () { var i, j, resultsCount = results.length, slotExisting, slotBefore; // If an index wasn't passed in, see if the indices of these items can be determined if (typeof index !== "number") { for (i = 0; i < resultsCount; i++) { slotExisting = slotFromResult(results[i]); if (slotExisting && slotExisting.index !== undefined) { index = slotExisting.index + offset - i; break; } } } // See if these results include the end of the list if (typeof index === "number" && results[resultsCount - 1] === endMarker) { // If the count wasn't known, it is now count = index - offset + resultsCount - 1; } else if (isNonNegativeNumber(count) && (index === undefined || index === null)) { // If the index wasn't known, it is now index = count - (resultsCount - 1) + offset; } // If the count is known, remove any mirage placeholders at the end if (isNonNegativeNumber(count) && !removeMirageIndices(count, index - offset)) { // "Forget" the count - a subsequent fetch or refresh will update the count and list end count = undefined; } // Find any existing slots that correspond to the results, and check for contradictions var offsetMap = new Array(resultsCount); for (i = 0; i < resultsCount; i++) { var slotBestMatch = null; slotExisting = slotFromResult(results[i]); if (slotExisting) { // See if this item is currently adjacent to a different item, or has a different index if ((i > 0 && !slotExisting.firstInSequence && slotExisting.prev.key && slotExisting.prev.key !== results[i - 1].key) || (typeof index === "number" && slotExisting.index !== undefined && slotExisting.index !== index - offset + i)) { // A contradiction has been found, so we can't proceed further beginRefresh(); return; } if (slotExisting === slotsStart || slotExisting === slotListEnd || slotExisting.bindingMap) { // First choice is a slot with the given key and at least one binding (or an end of the list) slotBestMatch = slotExisting; } } if (typeof index === "number") { slotExisting = indexMap[index - offset + i]; if (slotExisting) { if (slotExisting.key && slotExisting.key !== results[i].key) { // A contradiction has been found, so we can't proceed further beginRefresh(); return; } if (!slotBestMatch && slotExisting.bindingMap) { // Second choice is a slot with the given index and at least one binding slotBestMatch = slotExisting; } } } if (i === offset) { if ((slot.key && slot.key !== results[i].key) || (typeof slot.index === "number" && typeof index === "number" && slot.index !== index)) { // A contradiction has been found, so we can't proceed further beginRefresh(); return; } if (!slotBestMatch) { // Third choice is the slot that was passed in slotBestMatch = slot; } } offsetMap[i] = slotBestMatch; } // Update items with known indices (and at least one binding) first, as they will not be merged with // anything. for (i = 0; i < resultsCount; i++) { slotExisting = offsetMap[i]; if (slotExisting && slotExisting.index !== undefined && slotExisting !== slotsStart && slotExisting !== slotListEnd) { matchSlot(slotExisting, results[i]); } } var sequencePairsToMerge = []; // Now process the sequences without indices var firstSequence = true; var slotBeforeSequence; var slotAfterSequence; for (i = 0; i < resultsCount; i++) { slotExisting = offsetMap[i]; if (slotExisting && slotExisting !== slotListEnd) { var iLast = i; if (slotExisting.index === undefined) { var insertionPoint = {}; promoteSlot(slotExisting, results[i], index - offset + i, insertionPoint); // Find the extents of the sequence of slots that we can use var slotFirstInSequence = slotExisting, slotLastInSequence = slotExisting, result; for (j = i - 1; !slotFirstInSequence.firstInSequence; j--) { // Keep going until we hit the start marker or a slot that we can't use or promote (it's ok // if j leaves the results range). result = results[j]; if (result === startMarker) { break; } // Avoid assigning negative indices to slots var index2 = index - offset + j; if (index2 < 0) { break; } if (promoteSlot(slotFirstInSequence.prev, result, index2, insertionPoint)) { slotFirstInSequence = slotFirstInSequence.prev; if (j >= 0) { offsetMap[j] = slotFirstInSequence; } } else { break; } } for (j = i + 1; !slotLastInSequence.lastInSequence; j++) { // Keep going until we hit the end marker or a slot that we can't use or promote (it's ok // if j leaves the results range). // If slotListEnd is in this sequence, it should not be separated from any predecessor // slots, but they may need to be promoted. result = results[j]; if ((result === endMarker || j === count) && slotLastInSequence.next !== slotListEnd) { break; } if (slotLastInSequence.next === slotListEnd || promoteSlot(slotLastInSequence.next, result, index - offset + j, insertionPoint)) { slotLastInSequence = slotLastInSequence.next; if (j < resultsCount) { offsetMap[j] = slotLastInSequence; } iLast = j; if (slotLastInSequence === slotListEnd) { break; } } else { break; } } slotBeforeSequence = (slotFirstInSequence.firstInSequence ? null : slotFirstInSequence.prev); slotAfterSequence = (slotLastInSequence.lastInSequence ? null : slotLastInSequence.next); if (slotBeforeSequence) { splitSequence(slotBeforeSequence); } if (slotAfterSequence) { splitSequence(slotLastInSequence); } // Move the sequence if necessary if (typeof index === "number") { if (slotLastInSequence === slotListEnd) { // Instead of moving the list end, move the sequence before out of the way if (slotBeforeSequence) { moveSequenceAfter(slotListEnd, sequenceStart(slotBeforeSequence), slotBeforeSequence); } } else { var slotInsertBefore = insertionPoint.slotNext; if (!slotInsertBefore) { slotInsertBefore = successorFromIndex(slotLastInSequence.index, indexMap, slotsStart, slotListEnd, true); } moveSequenceBefore(slotInsertBefore, slotFirstInSequence, slotLastInSequence); } if (slotFirstInSequence.prev.index === slotFirstInSequence.index - 1) { mergeSequences(slotFirstInSequence.prev); } if (slotLastInSequence.next.index === slotLastInSequence.index + 1) { mergeSequences(slotLastInSequence); } } else if (!firstSequence) { slotBefore = offsetMap[i - 1]; if (slotBefore) { if (slotFirstInSequence.prev !== slotBefore) { if (slotLastInSequence === slotListEnd) { // Instead of moving the list end, move the sequence before out of the way and // the predecessor sequence into place. if (slotBeforeSequence) { moveSequenceAfter(slotListEnd, sequenceStart(slotBeforeSequence), slotBeforeSequence); } moveSequenceBefore(slotFirstInSequence, sequenceStart(slotBefore), slotBefore); } else { moveSequenceAfter(slotBefore, slotFirstInSequence, slotLastInSequence); } } mergeSequences(slotBefore); } } firstSequence = false; if (refreshRequested) { return; } sequencePairsToMerge.push({ slotBeforeSequence: slotBeforeSequence, slotFirstInSequence: slotFirstInSequence, slotLastInSequence: slotLastInSequence, slotAfterSequence: slotAfterSequence }); } // See if the fetched slot needs to be merged if (i === offset && slotExisting !== slot && !slotPermanentlyRemoved(slot)) { slotBeforeSequence = (slot.firstInSequence ? null : slot.prev); slotAfterSequence = (slot.lastInSequence ? null : slot.next); sendMirageNotifications(slotExisting, slot, slotExisting.bindingMap); mergeSlots(slotExisting, slot); sequencePairsToMerge.push({ slotBeforeSequence: slotBeforeSequence, slotFirstInSequence: slotExisting, slotLastInSequence: slotExisting, slotAfterSequence: slotAfterSequence }); } // Skip past all the other items in the sequence we just processed i = iLast; } } // If the count is known, set the index of the list end (wait until now because promoteSlot can sometimes // delete it; do this before mergeSequencePairs so the list end can have slots inserted immediately before // it). if (isNonNegativeNumber(count) && slotListEnd.index !== count) { changeSlotIndex(slotListEnd, count); } // Now that all the sequences have been moved, merge any colliding slots mergeSequencePairs(sequencePairsToMerge); // Match or cache any leftover items for (i = 0; i < resultsCount; i++) { // Find the first matched item slotExisting = offsetMap[i]; if (slotExisting) { for (j = i - 1; j >= 0; j--) { var slotAfter = offsetMap[j + 1]; matchSlot(offsetMap[j] = (slotAfter.firstInSequence ? addSlotBefore(offsetMap[j + 1], indexMap) : slotAfter.prev), results[j]); } for (j = i + 1; j < resultsCount; j++) { slotBefore = offsetMap[j - 1]; slotExisting = offsetMap[j]; if (!slotExisting) { matchSlot(offsetMap[j] = (slotBefore.lastInSequence ? addSlotAfter(slotBefore, indexMap) : slotBefore.next), results[j]); } else if (slotExisting.firstInSequence) { // Adding the cached items may result in some sequences merging if (slotExisting.prev !== slotBefore) { moveSequenceAfter(slotBefore, slotExisting, sequenceEnd(slotExisting)); } mergeSequences(slotBefore); } } break; } } // The description is no longer required delete slot.description; })(); if (!refreshRequested) { // If the count changed, but that's the only thing, just send the notification if (count !== undefined && count !== knownCount) { changeCount(count); } // See if there are more requests we can now fulfill postFetch(); } finishNotifications(); // Finally complete any promises for newly obtained items callFetchCompleteCallbacks(); profilerMarkEnd(perfId); } function processErrorResult(slot, error) { switch (error.name) { case FetchError.noResponse: setStatus(DataSourceStatus.failure); returnDirectFetchError(slot, error); break; case FetchError.doesNotExist: // Don't return an error, just complete with null (when the slot is deleted) if (slot.indexRequested) { // We now have an upper bound on the count removeMirageIndices(slot.index); } else if (slot.keyRequested || slot.description) { // This item, and any items in the same sequence, count as mirages, since they might never have // existed. deleteMirageSequence(slot); } finishNotifications(); // It's likely that the client requested this item because something has changed since the client's // latest observations of the data. Begin a refresh just in case. beginRefresh(); break; } } function processResultsForIndex(indexRequested, slot, results, offset, count, index) { index = validateIndexReturned(index); count = validateCountReturned(count); var indexFirst = indexRequested - offset; var resultsCount = results.length; if (slot.index >= indexFirst && slot.index < indexFirst + resultsCount) { // The item is in this batch of results - process them all processResults(slot, results, slot.index - indexFirst, count, slot.index); } else if ((offset === resultsCount - 1 && indexRequested < slot.index) || (isNonNegativeNumber(count) && count <= slot.index)) { // The requested index does not exist processErrorResult(slot, new _ErrorFromName(FetchError.doesNotExist)); } else { // We didn't get all the results we requested - pick up where they left off if (slot.index < indexFirst) { var fetchID = setFetchIDs(slot, 0, indexFirst - slot.index); fetchItemsForIndex(indexFirst, slot, fetchID, itemsFromKey( fetchID, results[0].key, indexFirst - slot.index, 0 )); } else { var indexLast = indexFirst + resultsCount - 1; var fetchID = setFetchIDs(slot, slot.index - indexLast, 0); fetchItemsForIndex(indexLast, slot, fetchID, itemsFromKey( fetchID, results[resultsCount - 1].key, 0, slot.index - indexLast )); } } } function processErrorResultForIndex(indexRequested, slot, error) { // If the request was for an index other than the initial one, and the result was doesNotExist, this doesn't switch (error.name) { case FetchError.doesNotExist: if (indexRequested === slotsStart.index) { // The request was for the start of the list, so the item must not exist, and we now have an upper // bound of zero for the count. removeMirageIndices(0); processErrorResult(slot, error); // No need to check return value of removeMirageIndices, since processErrorResult is going to start // a refresh anyway. } else { // Something has changed, but this index might still exist, so request a refresh beginRefresh(); } break; default: processErrorResult(slot, error); break; } } // Refresh function identifyRefreshCycle() { // find refresh cycles, find the first beginRefresh in the refreshHistory and see whether it // matches the next beginRefresh, if so then move the data source into an error state and stop // refreshing. var start = 0; for (; start < refreshHistory.length; start++) { if (refreshHistory[start].kind === "beginRefresh") { break; } } var end = start; for (; end < refreshHistory.length; end++) { if (refreshHistory[end].kind === "beginRefresh") { break; } } if (end > start && (end + (end - start) < refreshHistory.length)) { var match = true; var length = end - start; for (var i = 0; i < length; i++) { if (refreshHistory[start + i].kind !== refreshHistory[end + i].kind) { match = false; break; } } if (match) { if (_Log.log) { _Log.log(strings.refreshCycleIdentified, "winjs vds", "error"); for (var i = start; i < end; i++) { _Log.log("" + (i - start) + ": " + JSON.stringify(refreshHistory[i]), "winjs vds", "error"); } } } return match; } } function resetRefreshState() { if (++beginRefreshCount > MAX_BEGINREFRESH_COUNT) { if (identifyRefreshCycle()) { setStatus(DataSourceStatus.failure); return; } } refreshHistory[++refreshHistoryPos % refreshHistory.length] = { kind: "beginRefresh" }; // Give the start sentinel an index so we can always use predecessor + 1 refreshStart = { firstInSequence: true, lastInSequence: true, index: -1 }; refreshEnd = { firstInSequence: true, lastInSequence: true }; refreshStart.next = refreshEnd; refreshEnd.prev = refreshStart; refreshItemsFetched = false; refreshCount = undefined; keyFetchIDs = {}; refreshKeyMap = {}; refreshIndexMap = {}; refreshIndexMap[-1] = refreshStart; deletedKeys = {}; } function beginRefresh() { if (refreshRequested) { // There's already a refresh that has yet to start return; } refreshRequested = true; setStatus(DataSourceStatus.waiting); if (waitForRefresh) { waitForRefresh = false; // The edit queue has been paused until the next refresh - resume it now applyNextEdit(); return; } if (editsQueued) { // The refresh will be started once the edit queue empties out return; } var refreshID = ++currentRefreshID; refreshInProgress = true; refreshFetchesInProgress = 0; // Batch calls to beginRefresh Scheduler.schedule(function VDS_async_beginRefresh() { if (currentRefreshID !== refreshID) { return; } refreshRequested = false; resetRefreshState(); // Remove all slots that aren't live, so we don't waste time fetching them for (var slot = slotsStart.next; slot !== slotsEnd;) { var slotNext = slot.next; if (!slotLive(slot) && slot !== slotListEnd) { deleteUnnecessarySlot(slot); } slot = slotNext; } startRefreshFetches(); }, Scheduler.Priority.high, null, "WinJS.VirtualizedDataSource.beginRefresh"); } function requestRefresh() { refreshSignal = refreshSignal || new _Signal(); beginRefresh(); return refreshSignal.promise; } function resultsValidForRefresh(refreshID, fetchID) { // This fetch has completed, whatever it has returned delete fetchesInProgress[fetchID]; if (refreshID !== currentRefreshID) { // This information is out of date. Ignore it. return false; } refreshFetchesInProgress--; return true; } function fetchItemsForRefresh(key, fromStart, fetchID, promiseItems, index) { var refreshID = currentRefreshID; refreshFetchesInProgress++; promiseItems.then(function (fetchResult) { if (fetchResult.items && fetchResult.items.length) { var perfID = "itemsFetched id=" + fetchID + " count=" + fetchResult.items.length; profilerMarkStart(perfID); if (resultsValidForRefresh(refreshID, fetchID)) { addMarkers(fetchResult); processRefreshResults(key, fetchResult.items, fetchResult.offset, fetchResult.totalCount, (typeof index === "number" ? index : fetchResult.absoluteIndex)); } profilerMarkEnd(perfID); } else { return Promise.wrapError(new _ErrorFromName(FetchError.doesNotExist)); } }).then(null, function (error) { if (resultsValidForRefresh(refreshID, fetchID)) { processRefreshErrorResult(key, fromStart, error); } }); } function refreshRange(slot, fetchID, countBefore, countAfter) { if (itemsFromKey) { // Keys are the preferred identifiers when the item might have moved fetchItemsForRefresh(slot.key, false, fetchID, itemsFromKey(fetchID, slot.key, countBefore, countAfter, slot.hints)); } else { // Request additional items to try to locate items that have moved var searchDelta = 10, index = slot.index; if (refreshIndexMap[index] && refreshIndexMap[index].firstInSequence) { // Ensure at least one element is observed before this one fetchItemsForRefresh(slot.key, false, fetchID, itemsFromIndex(fetchID, index - 1, Math.min(countBefore + searchDelta, index) - 1, countAfter + 1 + searchDelta), index - 1); } else if (refreshIndexMap[index] && refreshIndexMap[index].lastInSequence) { // Ask for the next index we need directly fetchItemsForRefresh(slot.key, false, fetchID, itemsFromIndex(fetchID, index + 1, Math.min(countBefore + searchDelta, index) + 1, countAfter - 1 + searchDelta), index + 1); } else { fetchItemsForRefresh(slot.key, false, fetchID, itemsFromIndex(fetchID, index, Math.min(countBefore + searchDelta, index), countAfter + searchDelta), index); } } } function refreshFirstItem(fetchID) { if (itemsFromStart) { fetchItemsForRefresh(null, true, fetchID, itemsFromStart(fetchID, 1), 0); } else if (itemsFromIndex) { fetchItemsForRefresh(null, true, fetchID, itemsFromIndex(fetchID, 0, 0, 0), 0); } } function keyFetchInProgress(key) { return fetchesInProgress[keyFetchIDs[key]]; } function refreshRanges(slotFirst, allRanges) { // Fetch a few extra items each time, to catch insertions without requiring an extra fetch var refreshFetchExtra = 3; var refreshID = currentRefreshID; var slotFetchFirst, slotRefreshFirst, fetchCount = 0, fetchID; // Walk through the slot list looking for keys we haven't fetched or attempted to fetch yet. Rely on the // heuristic that items that were close together before the refresh are likely to remain so after, so batched // fetches will locate most of the previously fetched items. for (var slot = slotFirst; slot !== slotsEnd; slot = slot.next) { if (!slotFetchFirst && slot.key && !deletedKeys[slot.key] && !keyFetchInProgress(slot.key)) { var slotRefresh = refreshKeyMap[slot.key]; // Keep attempting to fetch an item until at least one item on either side of it has been observed, so // we can determine its position relative to others. if (!slotRefresh || slotRefresh.firstInSequence || slotRefresh.lastInSequence) { slotFetchFirst = slot; slotRefreshFirst = slotRefresh; fetchID = newFetchID(); } } if (!slotFetchFirst) { // Also attempt to fetch placeholders for requests for specific keys, just in case those items no // longer exist. if (slot.key && isPlaceholder(slot) && !deletedKeys[slot.key]) { // Fulfill each "itemFromKey" request if (!refreshKeyMap[slot.key]) { // Fetch at least one item before and after, just to verify item's position in list fetchID = newFetchID(); fetchItemsForRefresh(slot.key, false, fetchID, itemsFromKey(fetchID, slot.key, 1, 1, slot.hints)); } } } else { var keyAlreadyFetched = keyFetchInProgress(slot.key); if (!deletedKeys[slot.key] && !refreshKeyMap[slot.key] && !keyAlreadyFetched) { if (slot.key) { keyFetchIDs[slot.key] = fetchID; } fetchCount++; } if (slot.lastInSequence || slot.next === slotListEnd || keyAlreadyFetched) { refreshRange(slotFetchFirst, fetchID, (!slotRefreshFirst || slotRefreshFirst.firstInSequence ? refreshFetchExtra : 0), fetchCount - 1 + refreshFetchExtra); if (!allRanges) { break; } slotFetchFirst = null; fetchCount = 0; } } } if (refreshFetchesInProgress === 0 && !refreshItemsFetched && currentRefreshID === refreshID) { // If nothing was successfully fetched, try fetching the first item, to detect an empty list refreshFirstItem(newFetchID()); } } function startRefreshFetches() { var refreshID = currentRefreshID; do { synchronousProgress = false; reentrantContinue = true; refreshRanges(slotsStart.next, true); reentrantContinue = false; } while (refreshFetchesInProgress === 0 && synchronousProgress && currentRefreshID === refreshID && refreshInProgress); if (refreshFetchesInProgress === 0 && currentRefreshID === refreshID) { concludeRefresh(); } } function continueRefresh(key) { var refreshID = currentRefreshID; // If the key is absent, then the attempt to fetch the first item just completed, and there is nothing else to // fetch. if (key) { var slotContinue = keyMap[key]; if (!slotContinue) { // In a rare case, the slot might have been deleted; just start scanning from the beginning again slotContinue = slotsStart.next; } do { synchronousRefresh = false; reentrantRefresh = true; refreshRanges(slotContinue, false); reentrantRefresh = false; } while (synchronousRefresh && currentRefreshID === refreshID && refreshInProgress); } if (reentrantContinue) { synchronousProgress = true; } else { if (refreshFetchesInProgress === 0 && currentRefreshID === refreshID) { // Walk through the entire list one more time, in case any edits were made during the refresh startRefreshFetches(); } } } function slotRefreshFromResult(result) { if (typeof result !== "object" || !result) { throw new _ErrorFromName("WinJS.UI.ListDataSource.InvalidItemReturned", strings.invalidItemReturned); } else if (result === startMarker) { return refreshStart; } else if (result === endMarker) { return refreshEnd; } else if (!result.key) { throw new _ErrorFromName("WinJS.UI.ListDataSource.InvalidKeyReturned", strings.invalidKeyReturned); } else { return refreshKeyMap[result.key]; } } function processRefreshSlotIndex(slot, expectedIndex) { while (slot.index === undefined) { setSlotIndex(slot, expectedIndex, refreshIndexMap); if (slot.firstInSequence) { return true; } slot = slot.prev; expectedIndex--; } if (slot.index !== expectedIndex) { // Something has changed since the refresh began; start again beginRefresh(); return false; } return true; } function setRefreshSlotResult(slotRefresh, result) { slotRefresh.key = result.key; refreshKeyMap[slotRefresh.key] = slotRefresh; slotRefresh.item = result; } // Returns the slot after the last insertion point between sequences function lastRefreshInsertionPoint() { var slotNext = refreshEnd; while (!slotNext.firstInSequence) { slotNext = slotNext.prev; if (slotNext === refreshStart) { return null; } } return slotNext; } function processRefreshResults(key, results, offset, count, index) { index = validateIndexReturned(index); count = validateCountReturned(count); var keyPresent = false; refreshItemsFetched = true; var indexFirst = index - offset, result = results[0]; if (result.key === key) { keyPresent = true; } var slot = slotRefreshFromResult(result); if (!slot) { if (refreshIndexMap[indexFirst]) { // Something has changed since the refresh began; start again beginRefresh(); return; } // See if these results should be appended to an existing sequence var slotPrev; if (index !== undefined && (slotPrev = refreshIndexMap[indexFirst - 1])) { if (!slotPrev.lastInSequence) { // Something has changed since the refresh began; start again beginRefresh(); return; } slot = addSlotAfter(slotPrev, refreshIndexMap); } else { // Create a new sequence var slotSuccessor = ( +indexFirst === indexFirst ? successorFromIndex(indexFirst, refreshIndexMap, refreshStart, refreshEnd) : lastRefreshInsertionPoint(refreshStart, refreshEnd) ); if (!slotSuccessor) { // Something has changed since the refresh began; start again beginRefresh(); return; } slot = createSlotSequence(slotSuccessor, indexFirst, refreshIndexMap); } setRefreshSlotResult(slot, results[0]); } else { if (+indexFirst === indexFirst) { if (!processRefreshSlotIndex(slot, indexFirst)) { return; } } } var resultsCount = results.length; for (var i = 1; i < resultsCount; i++) { result = results[i]; if (result.key === key) { keyPresent = true; } var slotNext = slotRefreshFromResult(result); if (!slotNext) { if (!slot.lastInSequence) { // Something has changed since the refresh began; start again beginRefresh(); return; } slotNext = addSlotAfter(slot, refreshIndexMap); setRefreshSlotResult(slotNext, result); } else { if (slot.index !== undefined && !processRefreshSlotIndex(slotNext, slot.index + 1)) { return; } // If the slots aren't adjacent, see if it's possible to reorder sequences to make them so if (slotNext !== slot.next) { if (!slot.lastInSequence || !slotNext.firstInSequence) { // Something has changed since the refresh began; start again beginRefresh(); return; } var slotLast = sequenceEnd(slotNext); if (slotLast !== refreshEnd) { moveSequenceAfter(slot, slotNext, slotLast); } else { var slotFirst = sequenceStart(slot); if (slotFirst !== refreshStart) { moveSequenceBefore(slotNext, slotFirst, slot); } else { // Something has changed since the refresh began; start again beginRefresh(); return; } } mergeSequences(slot); } else if (slot.lastInSequence) { mergeSequences(slot); } } slot = slotNext; } if (!keyPresent) { deletedKeys[key] = true; } // If the count wasn't provided, see if it can be determined from the end of the list. if (!isNonNegativeNumber(count) && !refreshEnd.firstInSequence) { var indexLast = refreshEnd.prev.index; if (indexLast !== undefined) { count = indexLast + 1; } } if (isNonNegativeNumber(count) || count === CountResult.unknown) { if (isNonNegativeNumber(refreshCount)) { if (count !== refreshCount) { // Something has changed since the refresh began; start again beginRefresh(); return; } } else { refreshCount = count; } if (isNonNegativeNumber(refreshCount) && !refreshIndexMap[refreshCount]) { setSlotIndex(refreshEnd, refreshCount, refreshIndexMap); } } if (reentrantRefresh) { synchronousRefresh = true; } else { continueRefresh(key); } } function processRefreshErrorResult(key, fromStart, error) { switch (error.name) { case FetchError.noResponse: setStatus(DataSourceStatus.failure); break; case FetchError.doesNotExist: if (fromStart) { // The attempt to fetch the first item failed, so the list must be empty setSlotIndex(refreshEnd, 0, refreshIndexMap); refreshCount = 0; concludeRefresh(); } else { deletedKeys[key] = true; if (reentrantRefresh) { synchronousRefresh = true; } else { continueRefresh(key); } } break; } } function slotFromSlotRefresh(slotRefresh) { if (slotRefresh === refreshStart) { return slotsStart; } else if (slotRefresh === refreshEnd) { return slotListEnd; } else { return keyMap[slotRefresh.key]; } } function slotRefreshFromSlot(slot) { if (slot === slotsStart) { return refreshStart; } else if (slot === slotListEnd) { return refreshEnd; } else { return refreshKeyMap[slot.key]; } } function mergeSequencesForRefresh(slotPrev) { mergeSequences(slotPrev); // Mark the merge point, so we can distinguish insertions from unrequested items slotPrev.next.mergedForRefresh = true; } function copyRefreshSlotData(slotRefresh, slot) { setSlotKey(slot, slotRefresh.key); slot.itemNew = slotRefresh.item; } function addNewSlotFromRefresh(slotRefresh, slotNext, insertAfter) { var slotNew = createPrimarySlot(); copyRefreshSlotData(slotRefresh, slotNew); insertAndMergeSlot(slotNew, slotNext, insertAfter, !insertAfter); var index = slotRefresh.index; if (+index !== index) { index = (insertAfter ? slotNew.prev.index + 1 : slotNext.next.index - 1); } setSlotIndex(slotNew, index, indexMap); return slotNew; } function matchSlotForRefresh(slotExisting, slot, slotRefresh) { if (slotExisting) { sendMirageNotifications(slotExisting, slot, slotExisting.bindingMap); mergeSlotsAndItem(slotExisting, slot, slotRefresh.item); } else { copyRefreshSlotData(slotRefresh, slot); // If the index was requested, complete the promises now, as the index might be about to change if (slot.indexRequested) { updateSlotItem(slot); } } } function updateSlotForRefresh(slotExisting, slot, slotRefresh) { if (!slot.key) { if (slotExisting) { // Record the relationship between the slot to discard and its neighbors slotRefresh.mergeWithPrev = !slot.firstInSequence; slotRefresh.mergeWithNext = !slot.lastInSequence; } else { slotRefresh.stationary = true; } matchSlotForRefresh(slotExisting, slot, slotRefresh); return true; } else { return false; } } function indexForRefresh(slot) { var indexNew; if (slot.indexRequested) { indexNew = slot.index; } else { var slotRefresh = slotRefreshFromSlot(slot); if (slotRefresh) { indexNew = slotRefresh.index; } } return indexNew; } function concludeRefresh() { beginRefreshCount = 0; refreshHistory = new Array(100); refreshHistoryPos = -1; indexUpdateDeferred = true; keyFetchIDs = {}; var i, j, slot, slotPrev, slotNext, slotBefore, slotAfter, slotRefresh, slotExisting, slotFirstInSequence, sequenceCountOld, sequencesOld = [], sequenceOld, sequenceOldPrev, sequenceOldBestMatch, sequenceCountNew, sequencesNew = [], sequenceNew, index, offset; // Assign a sequence number to each refresh slot sequenceCountNew = 0; for (slotRefresh = refreshStart; slotRefresh; slotRefresh = slotRefresh.next) { slotRefresh.sequenceNumber = sequenceCountNew; if (slotRefresh.firstInSequence) { slotFirstInSequence = slotRefresh; } if (slotRefresh.lastInSequence) { sequencesNew[sequenceCountNew] = { first: slotFirstInSequence, last: slotRefresh, matchingItems: 0 }; sequenceCountNew++; } } // Remove unnecessary information from main slot list, and update the items lastSlotReleased = null; releasedSlots = 0; for (slot = slotsStart.next; slot !== slotsEnd;) { slotRefresh = refreshKeyMap[slot.key]; slotNext = slot.next; if (slot !== slotListEnd) { if (!slotLive(slot)) { // Some more items might have been released since the refresh started. Strip them away from the // main slot list, as they'll just get in the way from now on. Since we're discarding these, but // don't know if they're actually going away, split the sequence as our starting assumption must be // that the items on either side are in separate sequences. deleteUnnecessarySlot(slot); } else if (slot.key && !slotRefresh) { // Remove items that have been deleted (or moved far away) and send removed notifications deleteSlot(slot, false); } else if (refreshCount === 0 || (slot.indexRequested && slot.index >= refreshCount)) { // Remove items that can't exist in the list and send mirage removed notifications deleteSlot(slot, true); } else if (slot.item || slot.keyRequested) { // Store the new item; this value will be compared with that stored in slot.item later slot.itemNew = slotRefresh.item; } else { // Clear keys and items that have never been observed by client if (slot.key) { if (!slot.keyRequested) { delete keyMap[slot.key]; delete slot.key; } slot.itemNew = null; } } } slot = slotNext; } // Placeholders generated by itemsAtIndex should not move. Match these to items now if possible, or merge them // with existing items if necessary. for (slot = slotsStart.next; slot !== slotListEnd;) { slotNext = slot.next; if (slot.indexRequested) { slotRefresh = refreshIndexMap[slot.index]; if (slotRefresh) { matchSlotForRefresh(slotFromSlotRefresh(slotRefresh), slot, slotRefresh); } } slot = slotNext; } // Match old sequences to new sequences var bestMatch, bestMatchCount, previousBestMatch = 0, newSequenceCounts = [], slotIndexRequested, sequenceIndexEnd, sequenceOldEnd; sequenceCountOld = 0; for (slot = slotsStart; slot !== slotsEnd; slot = slot.next) { if (slot.firstInSequence) { slotFirstInSequence = slot; slotIndexRequested = null; for (i = 0; i < sequenceCountNew; i++) { newSequenceCounts[i] = 0; } } if (slot.indexRequested) { slotIndexRequested = slot; } slotRefresh = slotRefreshFromSlot(slot); if (slotRefresh) { newSequenceCounts[slotRefresh.sequenceNumber]++; } if (slot.lastInSequence) { // Determine which new sequence is the best match for this old one. Use a simple greedy algorithm to // ensure the relative ordering of matched sequences is the same; out-of-order sequences will require // move notifications. bestMatchCount = 0; for (i = previousBestMatch; i < sequenceCountNew; i++) { if (bestMatchCount < newSequenceCounts[i]) { bestMatchCount = newSequenceCounts[i]; bestMatch = i; } } sequenceOld = { first: slotFirstInSequence, last: slot, sequenceNew: (bestMatchCount > 0 ? sequencesNew[bestMatch] : undefined), matchingItems: bestMatchCount }; if (slotIndexRequested) { sequenceOld.indexRequested = true; sequenceOld.stationarySlot = slotIndexRequested; } sequencesOld[sequenceCountOld] = sequenceOld; if (slot === slotListEnd) { sequenceIndexEnd = sequenceCountOld; sequenceOldEnd = sequenceOld; } sequenceCountOld++; if (sequencesNew[bestMatch].first.index !== undefined) { previousBestMatch = bestMatch; } } } // Special case: split the old start into a separate sequence if the new start isn't its best match if (sequencesOld[0].sequenceNew !== sequencesNew[0]) { splitSequence(slotsStart); sequencesOld[0].first = slotsStart.next; sequencesOld.unshift({ first: slotsStart, last: slotsStart, sequenceNew: sequencesNew[0], matchingItems: 1 }); sequenceIndexEnd++; sequenceCountOld++; } var listEndObserved = !slotListEnd.firstInSequence; // Special case: split the old end into a separate sequence if the new end isn't its best match if (sequenceOldEnd.sequenceNew !== sequencesNew[sequenceCountNew - 1]) { splitSequence(slotListEnd.prev); sequenceOldEnd.last = slotListEnd.prev; sequenceIndexEnd++; sequencesOld.splice(sequenceIndexEnd, 0, { first: slotListEnd, last: slotListEnd, sequenceNew: sequencesNew[sequenceCountNew - 1], matchingItems: 1 }); sequenceCountOld++; sequenceOldEnd = sequencesOld[sequenceIndexEnd]; } // Map new sequences to old sequences for (i = 0; i < sequenceCountOld; i++) { sequenceNew = sequencesOld[i].sequenceNew; if (sequenceNew && sequenceNew.matchingItems < sequencesOld[i].matchingItems) { sequenceNew.matchingItems = sequencesOld[i].matchingItems; sequenceNew.sequenceOld = sequencesOld[i]; } } // The old end must always be the best match for the new end (if the new end is also the new start, they will // be merged below). sequencesNew[sequenceCountNew - 1].sequenceOld = sequenceOldEnd; sequenceOldEnd.stationarySlot = slotListEnd; // The old start must always be the best match for the new start sequencesNew[0].sequenceOld = sequencesOld[0]; sequencesOld[0].stationarySlot = slotsStart; // Merge additional indexed old sequences when possible // First do a forward pass for (i = 0; i <= sequenceIndexEnd; i++) { sequenceOld = sequencesOld[i]; if (sequenceOld.sequenceNew && (sequenceOldBestMatch = sequenceOld.sequenceNew.sequenceOld) === sequenceOldPrev && sequenceOldPrev.last !== slotListEnd) { mergeSequencesForRefresh(sequenceOldBestMatch.last); sequenceOldBestMatch.last = sequenceOld.last; delete sequencesOld[i]; } else { sequenceOldPrev = sequenceOld; } } // Now do a reverse pass sequenceOldPrev = null; for (i = sequenceIndexEnd; i >= 0; i--) { sequenceOld = sequencesOld[i]; // From this point onwards, some members of sequencesOld may be undefined if (sequenceOld) { if (sequenceOld.sequenceNew && (sequenceOldBestMatch = sequenceOld.sequenceNew.sequenceOld) === sequenceOldPrev && sequenceOld.last !== slotListEnd) { mergeSequencesForRefresh(sequenceOld.last); sequenceOldBestMatch.first = sequenceOld.first; delete sequencesOld[i]; } else { sequenceOldPrev = sequenceOld; } } } // Since we may have forced the list end into a separate sequence, the mergedForRefresh flag may be incorrect if (listEndObserved) { delete slotListEnd.mergedForRefresh; } var sequencePairsToMerge = []; // Find unchanged sequences without indices that can be merged with existing sequences without move // notifications. for (i = sequenceIndexEnd + 1; i < sequenceCountOld; i++) { sequenceOld = sequencesOld[i]; if (sequenceOld && (!sequenceOld.sequenceNew || sequenceOld.sequenceNew.sequenceOld !== sequenceOld)) { // If the order of the known items in the sequence is unchanged, then the sequence probably has not // moved, but we now know where it belongs relative to at least one other sequence. var orderPreserved = true, slotRefreshFirst = null, slotRefreshLast = null, sequenceLength = 0; slotRefresh = slotRefreshFromSlot(sequenceOld.first); if (slotRefresh) { slotRefreshFirst = slotRefreshLast = slotRefresh; sequenceLength = 1; } for (slot = sequenceOld.first; slot !== sequenceOld.last; slot = slot.next) { var slotRefreshNext = slotRefreshFromSlot(slot.next); if (slotRefresh && slotRefreshNext && (slotRefresh.lastInSequence || slotRefresh.next !== slotRefreshNext)) { orderPreserved = false; break; } if (slotRefresh && !slotRefreshFirst) { slotRefreshFirst = slotRefreshLast = slotRefresh; } if (slotRefreshNext && slotRefreshFirst) { slotRefreshLast = slotRefreshNext; sequenceLength++; } slotRefresh = slotRefreshNext; } // If the stationary sequence has indices, verify that there is enough space for this sequence - if // not, then something somewhere has moved after all. if (orderPreserved && slotRefreshFirst && slotRefreshFirst.index !== undefined) { var indexBefore; if (!slotRefreshFirst.firstInSequence) { slotBefore = slotFromSlotRefresh(slotRefreshFirst.prev); if (slotBefore) { indexBefore = slotBefore.index; } } var indexAfter; if (!slotRefreshLast.lastInSequence) { slotAfter = slotFromSlotRefresh(slotRefreshLast.next); if (slotAfter) { indexAfter = slotAfter.index; } } if ((!slotAfter || slotAfter.lastInSequence || slotAfter.mergedForRefresh) && (indexBefore === undefined || indexAfter === undefined || indexAfter - indexBefore - 1 >= sequenceLength)) { sequenceOld.locationJustDetermined = true; // Mark the individual refresh slots as not requiring move notifications for (slotRefresh = slotRefreshFirst; ; slotRefresh = slotRefresh.next) { slotRefresh.locationJustDetermined = true; if (slotRefresh === slotRefreshLast) { break; } } // Store any adjacent placeholders so they can be merged once the moves and insertions have // been processed. var slotFirstInSequence = slotFromSlotRefresh(slotRefreshFirst), slotLastInSequence = slotFromSlotRefresh(slotRefreshLast); sequencePairsToMerge.push({ slotBeforeSequence: (slotFirstInSequence.firstInSequence ? null : slotFirstInSequence.prev), slotFirstInSequence: slotFirstInSequence, slotLastInSequence: slotLastInSequence, slotAfterSequence: (slotLastInSequence.lastInSequence ? null : slotLastInSequence.next) }); } } } } // Remove placeholders in old sequences that don't map to new sequences (and don't contain requests for a // specific index or key), as they no longer have meaning. for (i = 0; i < sequenceCountOld; i++) { sequenceOld = sequencesOld[i]; if (sequenceOld && !sequenceOld.indexRequested && !sequenceOld.locationJustDetermined && (!sequenceOld.sequenceNew || sequenceOld.sequenceNew.sequenceOld !== sequenceOld)) { sequenceOld.sequenceNew = null; slot = sequenceOld.first; var sequenceEndReached; do { sequenceEndReached = (slot === sequenceOld.last); slotNext = slot.next; if (slot !== slotsStart && slot !== slotListEnd && slot !== slotsEnd && !slot.item && !slot.keyRequested) { deleteSlot(slot, true); if (sequenceOld.first === slot) { if (sequenceOld.last === slot) { delete sequencesOld[i]; break; } else { sequenceOld.first = slot.next; } } else if (sequenceOld.last === slot) { sequenceOld.last = slot.prev; } } slot = slotNext; } while (!sequenceEndReached); } } // Locate boundaries of new items in new sequences for (i = 0; i < sequenceCountNew; i++) { sequenceNew = sequencesNew[i]; for (slotRefresh = sequenceNew.first; !slotFromSlotRefresh(slotRefresh) && !slotRefresh.lastInSequence; slotRefresh = slotRefresh.next) { /*@empty*/ } if (slotRefresh.lastInSequence && !slotFromSlotRefresh(slotRefresh)) { sequenceNew.firstInner = sequenceNew.lastInner = null; } else { sequenceNew.firstInner = slotRefresh; for (slotRefresh = sequenceNew.last; !slotFromSlotRefresh(slotRefresh) ; slotRefresh = slotRefresh.prev) { /*@empty*/ } sequenceNew.lastInner = slotRefresh; } } // Determine which items to move for (i = 0; i < sequenceCountNew; i++) { sequenceNew = sequencesNew[i]; if (sequenceNew && sequenceNew.firstInner) { sequenceOld = sequenceNew.sequenceOld; if (sequenceOld) { // Number the slots in each new sequence with their offset in the corresponding old sequence (or // undefined if in a different old sequence). var ordinal = 0; for (slot = sequenceOld.first; true; slot = slot.next, ordinal++) { slotRefresh = slotRefreshFromSlot(slot); if (slotRefresh && slotRefresh.sequenceNumber === sequenceNew.firstInner.sequenceNumber) { slotRefresh.ordinal = ordinal; } if (slot.lastInSequence) { break; } } // Determine longest subsequence of items that are in the same order before and after var piles = []; for (slotRefresh = sequenceNew.firstInner; true; slotRefresh = slotRefresh.next) { ordinal = slotRefresh.ordinal; if (ordinal !== undefined) { var searchFirst = 0, searchLast = piles.length - 1; while (searchFirst <= searchLast) { var searchMidpoint = Math.floor(0.5 * (searchFirst + searchLast)); if (piles[searchMidpoint].ordinal < ordinal) { searchFirst = searchMidpoint + 1; } else { searchLast = searchMidpoint - 1; } } piles[searchFirst] = slotRefresh; if (searchFirst > 0) { slotRefresh.predecessor = piles[searchFirst - 1]; } } if (slotRefresh === sequenceNew.lastInner) { break; } } // The items in the longest ordered subsequence don't move; everything else does var stationaryItems = [], stationaryItemCount = piles.length; slotRefresh = piles[stationaryItemCount - 1]; for (j = stationaryItemCount; j--;) { slotRefresh.stationary = true; stationaryItems[j] = slotRefresh; slotRefresh = slotRefresh.predecessor; } sequenceOld.stationarySlot = slotFromSlotRefresh(stationaryItems[0]); // Try to match new items before the first stationary item to placeholders slotRefresh = stationaryItems[0]; slot = slotFromSlotRefresh(slotRefresh); slotPrev = slot.prev; var sequenceBoundaryReached = slot.firstInSequence; while (!slotRefresh.firstInSequence) { slotRefresh = slotRefresh.prev; slotExisting = slotFromSlotRefresh(slotRefresh); if (!slotExisting || slotRefresh.locationJustDetermined) { // Find the next placeholder walking backwards while (!sequenceBoundaryReached && slotPrev !== slotsStart) { slot = slotPrev; slotPrev = slot.prev; sequenceBoundaryReached = slot.firstInSequence; if (updateSlotForRefresh(slotExisting, slot, slotRefresh)) { break; } } } } // Try to match new items between stationary items to placeholders for (j = 0; j < stationaryItemCount - 1; j++) { slotRefresh = stationaryItems[j]; slot = slotFromSlotRefresh(slotRefresh); var slotRefreshStop = stationaryItems[j + 1], slotRefreshMergePoint = null, slotStop = slotFromSlotRefresh(slotRefreshStop), slotExisting; // Find all the new items slotNext = slot.next; for (slotRefresh = slotRefresh.next; slotRefresh !== slotRefreshStop && !slotRefreshMergePoint && slot !== slotStop; slotRefresh = slotRefresh.next) { slotExisting = slotFromSlotRefresh(slotRefresh); if (!slotExisting || slotRefresh.locationJustDetermined) { // Find the next placeholder while (slotNext !== slotStop) { // If a merge point is reached, match the remainder of the placeholders by walking backwards if (slotNext.mergedForRefresh) { slotRefreshMergePoint = slotRefresh.prev; break; } slot = slotNext; slotNext = slot.next; if (updateSlotForRefresh(slotExisting, slot, slotRefresh)) { break; } } } } // Walk backwards to the first merge point if necessary if (slotRefreshMergePoint) { slotPrev = slotStop.prev; for (slotRefresh = slotRefreshStop.prev; slotRefresh !== slotRefreshMergePoint && slotStop !== slot; slotRefresh = slotRefresh.prev) { slotExisting = slotFromSlotRefresh(slotRefresh); if (!slotExisting || slotRefresh.locationJustDetermined) { // Find the next placeholder walking backwards while (slotPrev !== slot) { slotStop = slotPrev; slotPrev = slotStop.prev; if (updateSlotForRefresh(slotExisting, slotStop, slotRefresh)) { break; } } } } } // Delete remaining placeholders, sending notifications while (slotNext !== slotStop) { slot = slotNext; slotNext = slot.next; if (slot !== slotsStart && isPlaceholder(slot) && !slot.keyRequested) { // This might occur due to two sequences - requested by different clients - being // merged. However, since only sequences with indices are merged, if this placehholder // is no longer necessary, it means an item actually was removed, so this doesn't count // as a mirage. deleteSlot(slot); } } } // Try to match new items after the last stationary item to placeholders slotRefresh = stationaryItems[stationaryItemCount - 1]; slot = slotFromSlotRefresh(slotRefresh); slotNext = slot.next; sequenceBoundaryReached = slot.lastInSequence; while (!slotRefresh.lastInSequence) { slotRefresh = slotRefresh.next; slotExisting = slotFromSlotRefresh(slotRefresh); if (!slotExisting || slotRefresh.locationJustDetermined) { // Find the next placeholder while (!sequenceBoundaryReached && slotNext !== slotListEnd) { slot = slotNext; slotNext = slot.next; sequenceBoundaryReached = slot.lastInSequence; if (updateSlotForRefresh(slotExisting, slot, slotRefresh)) { break; } } } } } } } // Move items and send notifications for (i = 0; i < sequenceCountNew; i++) { sequenceNew = sequencesNew[i]; if (sequenceNew.firstInner) { slotPrev = null; for (slotRefresh = sequenceNew.firstInner; true; slotRefresh = slotRefresh.next) { slot = slotFromSlotRefresh(slotRefresh); if (slot) { if (!slotRefresh.stationary) { var slotMoveBefore, mergeWithPrev = false, mergeWithNext = false; if (slotPrev) { slotMoveBefore = slotPrev.next; mergeWithPrev = true; } else { // The first item will be inserted before the first stationary item, so find that now var slotRefreshStationary; for (slotRefreshStationary = sequenceNew.firstInner; !slotRefreshStationary.stationary && slotRefreshStationary !== sequenceNew.lastInner; slotRefreshStationary = slotRefreshStationary.next) { /*@empty*/ } if (!slotRefreshStationary.stationary) { // There are no stationary items, as all the items are moving from another old // sequence. index = slotRefresh.index; // Find the best place to insert the new sequence if (index === 0) { // Index 0 is a special case slotMoveBefore = slotsStart.next; mergeWithPrev = true; } else if (index === undefined) { slotMoveBefore = slotsEnd; } else { // Use a linear search; unlike successorFromIndex, prefer the last insertion // point between sequences over the precise index slotMoveBefore = slotsStart.next; var lastSequenceStart = null; while (true) { if (slotMoveBefore.firstInSequence) { lastSequenceStart = slotMoveBefore; } if ((index < slotMoveBefore.index && lastSequenceStart) || slotMoveBefore === slotListEnd) { break; } slotMoveBefore = slotMoveBefore.next; } if (!slotMoveBefore.firstInSequence && lastSequenceStart) { slotMoveBefore = lastSequenceStart; } } } else { slotMoveBefore = slotFromSlotRefresh(slotRefreshStationary); mergeWithNext = true; } } // Preserve merge boundaries if (slot.mergedForRefresh) { delete slot.mergedForRefresh; if (!slot.lastInSequence) { slot.next.mergedForRefresh = true; } } mergeWithPrev = mergeWithPrev || slotRefresh.mergeWithPrev; mergeWithNext = mergeWithNext || slotRefresh.mergeWithNext; var skipNotifications = slotRefresh.locationJustDetermined; moveSlot(slot, slotMoveBefore, mergeWithPrev, mergeWithNext, skipNotifications); if (skipNotifications && mergeWithNext) { // Since this item was moved without a notification, this is an implicit merge of // sequences. Mark the item's successor as mergedForRefresh. slotMoveBefore.mergedForRefresh = true; } } slotPrev = slot; } if (slotRefresh === sequenceNew.lastInner) { break; } } } } // Insert new items (with new indices) and send notifications for (i = 0; i < sequenceCountNew; i++) { sequenceNew = sequencesNew[i]; if (sequenceNew.firstInner) { slotPrev = null; for (slotRefresh = sequenceNew.firstInner; true; slotRefresh = slotRefresh.next) { slot = slotFromSlotRefresh(slotRefresh); if (!slot) { var slotInsertBefore; if (slotPrev) { slotInsertBefore = slotPrev.next; } else { // The first item will be inserted *before* the first old item, so find that now var slotRefreshOld; for (slotRefreshOld = sequenceNew.firstInner; !slotFromSlotRefresh(slotRefreshOld) ; slotRefreshOld = slotRefreshOld.next) { /*@empty*/ } slotInsertBefore = slotFromSlotRefresh(slotRefreshOld); } // Create a new slot for the item slot = addNewSlotFromRefresh(slotRefresh, slotInsertBefore, !!slotPrev); var slotRefreshNext = slotRefreshFromSlot(slotInsertBefore); if (!slotInsertBefore.mergedForRefresh && (!slotRefreshNext || !slotRefreshNext.locationJustDetermined)) { prepareSlotItem(slot); // Send the notification after the insertion sendInsertedNotification(slot); } } slotPrev = slot; if (slotRefresh === sequenceNew.lastInner) { break; } } } } // Rebuild the indexMap from scratch, so it is possible to detect colliding indices indexMap = []; // Send indexChanged and changed notifications var indexFirst = -1; for (slot = slotsStart, offset = 0; slot !== slotsEnd; offset++) { var slotNext = slot.next; if (slot.firstInSequence) { slotFirstInSequence = slot; offset = 0; } if (indexFirst === undefined) { var indexNew = indexForRefresh(slot); if (indexNew !== undefined) { indexFirst = indexNew - offset; } } // See if the next slot would cause a contradiction, in which case split the sequences if (indexFirst !== undefined && !slot.lastInSequence) { var indexNewNext = indexForRefresh(slot.next); if (indexNewNext !== undefined && indexNewNext !== indexFirst + offset + 1) { splitSequence(slot); // 'Move' the items in-place individually, so move notifications are sent. In rare cases, this // will result in multiple move notifications being sent for a given item, but that's fine. var first = true; for (var slotMove = slot.next, lastInSequence = false; !lastInSequence && slotMove !== slotListEnd;) { var slotMoveNext = slotMove.next; lastInSequence = slotMove.lastInSequence; moveSlot(slotMove, slotMoveNext, !first, false); first = false; slotMove = slotMoveNext; } } } if (slot.lastInSequence) { index = indexFirst; for (var slotUpdate = slotFirstInSequence; slotUpdate !== slotNext;) { var slotUpdateNext = slotUpdate.next; if (index >= refreshCount && slotUpdate !== slotListEnd) { deleteSlot(slotUpdate, true); } else { var slotWithIndex = indexMap[index]; if (index !== slotUpdate.index) { delete indexMap[index]; changeSlotIndex(slotUpdate, index); } else if (+index === index && indexMap[index] !== slotUpdate) { indexMap[index] = slotUpdate; } if (slotUpdate.itemNew) { updateSlotItem(slotUpdate); } if (slotWithIndex) { // Two slots' indices have collided - merge them if (slotUpdate.key) { sendMirageNotifications(slotUpdate, slotWithIndex, slotUpdate.bindingMap); mergeSlots(slotUpdate, slotWithIndex); if (+index === index) { indexMap[index] = slotUpdate; } } else { sendMirageNotifications(slotWithIndex, slotUpdate, slotWithIndex.bindingMap); mergeSlots(slotWithIndex, slotUpdate); if (+index === index) { indexMap[index] = slotWithIndex; } } } if (+index === index) { index++; } } slotUpdate = slotUpdateNext; } indexFirst = undefined; } slot = slotNext; } // See if any sequences need to be moved and/or merged var indexMax = -2, listEndReached; for (slot = slotsStart, offset = 0; slot !== slotsEnd; offset++) { var slotNext = slot.next; if (slot.firstInSequence) { slotFirstInSequence = slot; offset = 0; } // Clean up during this pass delete slot.mergedForRefresh; if (slot.lastInSequence) { // Move sequence if necessary if (slotFirstInSequence.index === undefined) { slotBefore = slotFirstInSequence.prev; var slotRefreshBefore; if (slotBefore && (slotRefreshBefore = slotRefreshFromSlot(slotBefore)) && !slotRefreshBefore.lastInSequence && (slotRefresh = slotRefreshFromSlot(slot)) && slotRefresh.prev === slotRefreshBefore) { moveSequenceAfter(slotBefore, slotFirstInSequence, slot); mergeSequences(slotBefore); } else if (slot !== slotListEnd && !listEndReached) { moveSequenceBefore(slotsEnd, slotFirstInSequence, slot); } } else { if (indexMax < slot.index && !listEndReached) { indexMax = slot.index; } else { // Find the correct insertion point for (slotAfter = slotsStart.next; slotAfter.index < slot.index; slotAfter = slotAfter.next) { /*@empty*/ } // Move the items individually, so move notifications are sent for (var slotMove = slotFirstInSequence; slotMove !== slotNext;) { var slotMoveNext = slotMove.next; slotRefresh = slotRefreshFromSlot(slotMove); moveSlot(slotMove, slotAfter, slotAfter.prev.index === slotMove.index - 1, slotAfter.index === slotMove.index + 1, slotRefresh && slotRefresh.locationJustDetermined); slotMove = slotMoveNext; } } // Store slotBefore here since the sequence might have just been moved slotBefore = slotFirstInSequence.prev; // See if this sequence should be merged with the previous one if (slotBefore && slotBefore.index === slotFirstInSequence.index - 1) { mergeSequences(slotBefore); } } } if (slot === slotListEnd) { listEndReached = true; } slot = slotNext; } indexUpdateDeferred = false; // Now that all the sequences have been moved, merge any colliding slots mergeSequencePairs(sequencePairsToMerge); // Send countChanged notification if (refreshCount !== undefined && refreshCount !== knownCount) { changeCount(refreshCount); } finishNotifications(); // Before discarding the refresh slot list, see if any fetch requests can be completed by pretending each range // of refresh slots is an incoming array of results. var fetchResults = []; for (i = 0; i < sequenceCountNew; i++) { sequenceNew = sequencesNew[i]; var results = []; slot = null; offset = 0; var slotOffset; for (slotRefresh = sequenceNew.first; true; slotRefresh = slotRefresh.next, offset++) { if (slotRefresh === refreshStart) { results.push(startMarker); } else if (slotRefresh === refreshEnd) { results.push(endMarker); } else { results.push(slotRefresh.item); if (!slot) { slot = slotFromSlotRefresh(slotRefresh); slotOffset = offset; } } if (slotRefresh.lastInSequence) { break; } } if (slot) { fetchResults.push({ slot: slot, results: results, offset: slotOffset }); } } resetRefreshState(); refreshInProgress = false; // Complete any promises for newly obtained items callFetchCompleteCallbacks(); // Now process the 'extra' results from the refresh list for (i = 0; i < fetchResults.length; i++) { var fetchResult = fetchResults[i]; processResults(fetchResult.slot, fetchResult.results, fetchResult.offset, knownCount, fetchResult.slot.index); } if (refreshSignal) { var signal = refreshSignal; refreshSignal = null; signal.complete(); } // Finally, kick-start fetches for any remaining placeholders postFetch(); } // Edit Queue // Queues an edit and immediately "optimistically" apply it to the slots list, sending re-entrant notifications function queueEdit(applyEdit, editType, complete, error, keyUpdate, updateSlots, undo) { var editQueueTail = editQueue.prev, edit = { prev: editQueueTail, next: editQueue, applyEdit: applyEdit, editType: editType, complete: complete, error: error, keyUpdate: keyUpdate }; editQueueTail.next = edit; editQueue.prev = edit; editsQueued = true; // If there's a refresh in progress, abandon it, but request that a new one be started once the edits complete if (refreshRequested || refreshInProgress) { currentRefreshID++; refreshInProgress = false; refreshRequested = true; } if (editQueue.next === edit) { // Attempt the edit immediately, in case it completes synchronously applyNextEdit(); } // If the edit succeeded or is still pending, apply it to the slots (in the latter case, "optimistically") if (!edit.failed) { updateSlots(); // Supply the undo function now edit.undo = undo; } if (!editsInProgress) { completeEdits(); } } function dequeueEdit() { firstEditInProgress = false; var editNext = editQueue.next.next; editQueue.next = editNext; editNext.prev = editQueue; } // Undo all queued edits, starting with the most recent function discardEditQueue() { while (editQueue.prev !== editQueue) { var editLast = editQueue.prev; if (editLast.error) { editLast.error(new _ErrorFromName(EditError.canceled)); } // Edits that haven't been applied to the slots yet don't need to be undone if (editLast.undo && !refreshRequested) { editLast.undo(); } editQueue.prev = editLast.prev; } editQueue.next = editQueue; editsInProgress = false; completeEdits(); } var EditType = { insert: "insert", change: "change", move: "move", remove: "remove" }; function attemptEdit(edit) { if (firstEditInProgress) { return; } var reentrant = true; function continueEdits() { if (!waitForRefresh) { if (reentrant) { synchronousEdit = true; } else { applyNextEdit(); } } } var keyUpdate = edit.keyUpdate; function onEditComplete(item) { if (item) { var slot; if (keyUpdate && keyUpdate.key !== item.key) { var keyNew = item.key; if (!edit.undo) { // If the edit is in the process of being queued, we can use the correct key when we update the // slots, so there's no need for a later update. keyUpdate.key = keyNew; } else { slot = keyUpdate.slot; if (slot) { var keyOld = slot.key; if (keyOld) { delete keyMap[keyOld]; } setSlotKey(slot, keyNew); slot.itemNew = item; if (slot.item) { changeSlot(slot); finishNotifications(); } else { completeFetchPromises(slot); } } } } else if (edit.editType === EditType.change) { slot.itemNew = item; if (!reentrant) { changeSlotIfNecessary(slot); } } } dequeueEdit(); if (edit.complete) { edit.complete(item); } continueEdits(); } function onEditError(error) { switch (error.Name) { case EditError.noResponse: // Report the failure to the client, but do not dequeue the edit setStatus(DataSourceStatus.failure); waitForRefresh = true; firstEditInProgress = false; // Don't report the error, as the edit will be attempted again on the next refresh return; case EditError.notPermitted: break; case EditError.noLongerMeaningful: // Something has changed, so request a refresh beginRefresh(); break; default: break; } // Discard all remaining edits, rather than try to determine which subsequent ones depend on this one edit.failed = true; dequeueEdit(); discardEditQueue(); if (edit.error) { edit.error(error); } continueEdits(); } if (listDataAdapter.beginEdits && !beginEditsCalled) { beginEditsCalled = true; listDataAdapter.beginEdits(); } // Call the applyEdit function for the given edit, passing in our own wrapper of the error handler that the // client passed in. firstEditInProgress = true; edit.applyEdit().then(onEditComplete, onEditError); reentrant = false; } function applyNextEdit() { // See if there are any outstanding edits, and try to process as many as possible synchronously while (editQueue.next !== editQueue) { synchronousEdit = false; attemptEdit(editQueue.next); if (!synchronousEdit) { return; } } // The queue emptied out synchronously (or was empty to begin with) concludeEdits(); } function completeEdits() { updateIndices(); finishNotifications(); callFetchCompleteCallbacks(); if (editQueue.next === editQueue) { concludeEdits(); } } // Once the edit queue has emptied, update state appropriately and resume normal operation function concludeEdits() { editsQueued = false; if (listDataAdapter.endEdits && beginEditsCalled && !editsInProgress) { beginEditsCalled = false; listDataAdapter.endEdits(); } // See if there's a refresh that needs to begin if (refreshRequested) { refreshRequested = false; beginRefresh(); } else { // Otherwise, see if anything needs to be fetched postFetch(); } } // Editing Operations function getSlotForEdit(key) { validateKey(key); return keyMap[key] || createSlotForKey(key); } function insertNewSlot(key, itemNew, slotInsertBefore, mergeWithPrev, mergeWithNext) { // Create a new slot, but don't worry about its index, as indices will be updated during endEdits var slot = createPrimarySlot(); insertAndMergeSlot(slot, slotInsertBefore, mergeWithPrev, mergeWithNext); if (key) { setSlotKey(slot, key); } slot.itemNew = itemNew; updateNewIndices(slot, 1); // If this isn't part of a batch of changes, set the slot index now so renderers can see it if (!editsInProgress && !dataNotificationsInProgress) { if (!slot.firstInSequence && typeof slot.prev.index === "number") { setSlotIndex(slot, slot.prev.index + 1, indexMap); } else if (!slot.lastInSequence && typeof slot.next.index === "number") { setSlotIndex(slot, slot.next.index - 1, indexMap); } } prepareSlotItem(slot); // Send the notification after the insertion sendInsertedNotification(slot); return slot; } function insertItem(key, data, slotInsertBefore, append, applyEdit) { var keyUpdate = { key: key }; return new Promise(function (complete, error) { queueEdit( applyEdit, EditType.insert, complete, error, keyUpdate, // updateSlots function () { if (slotInsertBefore) { var itemNew = { key: keyUpdate.key, data: data }; keyUpdate.slot = insertNewSlot(keyUpdate.key, itemNew, slotInsertBefore, append, !append); } }, // undo function () { var slot = keyUpdate.slot; if (slot) { updateNewIndices(slot, -1); deleteSlot(slot, false); } } ); }); } function moveItem(slot, slotMoveBefore, append, applyEdit) { return new Promise(function (complete, error) { var mergeAdjacent, slotNext, firstInSequence, lastInSequence; queueEdit( applyEdit, EditType.move, complete, error, // keyUpdate null, // updateSlots function () { slotNext = slot.next; firstInSequence = slot.firstInSequence; lastInSequence = slot.lastInSequence; var slotPrev = slot.prev; mergeAdjacent = (typeof slot.index !== "number" && (firstInSequence || !slotPrev.item) && (lastInSequence || !slotNext.item)); updateNewIndices(slot, -1); moveSlot(slot, slotMoveBefore, append, !append); updateNewIndices(slot, 1); if (mergeAdjacent) { splitSequence(slotPrev); if (!firstInSequence) { mergeSlotsBefore(slotPrev, slot); } if (!lastInSequence) { mergeSlotsAfter(slotNext, slot); } } }, // undo function () { if (!mergeAdjacent) { updateNewIndices(slot, -1); moveSlot(slot, slotNext, !firstInSequence, !lastInSequence); updateNewIndices(slot, 1); } else { beginRefresh(); } } ); }); } function ListDataNotificationHandler() { /// /// /// An implementation of IListDataNotificationHandler that is passed to the /// IListDataAdapter.setNotificationHandler method. /// /// this.invalidateAll = function () { /// /// /// Notifies the VirtualizedDataSource that some data has changed, without specifying which data. It might /// be impractical for some data sources to call this method for any or all changes, so this call is optional. /// But if a given data adapter never calls it, the application should periodically call /// invalidateAll on the VirtualizedDataSource to refresh the data. /// /// /// A Promise that completes when the data has been completely refreshed and all change notifications have /// been sent. /// /// if (knownCount === 0) { this.reload(); return Promise.wrap(); } return requestRefresh(); }; this.reload = function () { /// /// /// Notifies the list data source that the list data has changed so much that it is better /// to reload the data from scratch. /// /// // Cancel all promises if (getCountPromise) { getCountPromise.cancel(); } if (refreshSignal) { refreshSignal.cancel(); } for (var slot = slotsStart.next; slot !== slotsEnd; slot = slot.next) { var fetchListeners = slot.fetchListeners; for (var listenerID in fetchListeners) { fetchListeners[listenerID].promise.cancel(); } var directFetchListeners = slot.directFetchListeners; for (var listenerID in directFetchListeners) { directFetchListeners[listenerID].promise.cancel(); } } resetState(); forEachBindingRecord(function (bindingRecord) { if (bindingRecord.notificationHandler) { bindingRecord.notificationHandler.reload(); } }); }; this.beginNotifications = function () { /// /// /// Indicates the start of a notification batch. /// Call it before a sequence of other notification calls to minimize the number of countChanged and /// indexChanged notifications sent to the client of the VirtualizedDataSource. You must pair it with a call /// to endNotifications, and pairs can't be nested. /// /// dataNotificationsInProgress = true; }; function completeNotification() { if (!dataNotificationsInProgress) { updateIndices(); finishNotifications(); callFetchCompleteCallbacks(); } } this.inserted = function (newItem, previousKey, nextKey, index) { /// /// /// Raises a notification that an item was inserted. /// /// /// The inserted item. It must have a key and a data property (it must implement the IItem interface). /// /// /// The key of the item before the insertion point, or null if the item was inserted at the start of the /// list. It can be null if you specified nextKey. /// /// /// The key of the item after the insertion point, or null if the item was inserted at the end of the list. /// It can be null if you specified previousKey. /// /// /// The index of the inserted item. /// /// if (editsQueued) { // We can't change the slots out from under any queued edits beginRefresh(); } else { var key = newItem.key, slotPrev = keyMap[previousKey], slotNext = keyMap[nextKey]; var havePreviousKey = typeof previousKey === "string", haveNextKey = typeof nextKey === "string"; // Only one of previousKey, nextKey needs to be passed in // if (havePreviousKey) { if (slotNext && !slotNext.firstInSequence) { slotPrev = slotNext.prev; } } else if (haveNextKey) { if (slotPrev && !slotPrev.lastInSequence) { slotNext = slotPrev.next; } } // If the VDS believes the list is empty but the data adapter believes the item has // a adjacent item start a refresh. // if ((havePreviousKey || haveNextKey) && !(slotPrev || slotNext) && (slotsStart.next === slotListEnd)) { beginRefresh(); return; } // If this key is known, something has changed, start a refresh. // if (keyMap[key]) { beginRefresh(); return; } // If the slots aren't adjacent or are thought to be distinct sequences by the // VDS something has changed so start a refresh. // if (slotPrev && slotNext) { if (slotPrev.next !== slotNext || slotPrev.lastInSequence || slotNext.firstInSequence) { beginRefresh(); return; } } // If one of the adjacent keys or indicies has only just been requested - rare, // and easier to deal with in a refresh. // if ((slotPrev && (slotPrev.keyRequested || slotPrev.indexRequested)) || (slotNext && (slotNext.keyRequested || slotNext.indexRequested))) { beginRefresh(); return; } if (slotPrev || slotNext) { insertNewSlot(key, newItem, (slotNext ? slotNext : slotPrev.next), !!slotPrev, !!slotNext); } else if (slotsStart.next === slotListEnd) { insertNewSlot(key, newItem, slotsStart.next, true, true); } else if (index !== undefined) { updateNewIndicesFromIndex(index, 1); } else { // We could not find a previous or next slot and an index was not provided, start a refresh // beginRefresh(); return; } completeNotification(); } }; this.changed = function (item) { /// /// /// Raises a notification that an item changed. /// /// /// An IItem that represents the item that changed. /// /// if (editsQueued) { // We can't change the slots out from under any queued edits beginRefresh(); } else { var key = item.key, slot = keyMap[key]; if (slot) { if (slot.keyRequested) { // The key has only just been requested - rare, and easier to deal with in a refresh beginRefresh(); } else { slot.itemNew = item; if (slot.item) { changeSlot(slot); completeNotification(); } } } } }; this.moved = function (item, previousKey, nextKey, oldIndex, newIndex) { /// /// /// Raises a notfication that an item was moved. /// /// /// The item that was moved. /// /// /// The key of the item before the insertion point, or null if the item was moved to the beginning of the list. /// It can be null if you specified nextKey. /// /// /// The key of the item after the insertion point, or null if the item was moved to the end of the list. /// It can be null if you specified previousKey. /// /// /// The index of the item before it was moved. /// /// /// The index of the item after it was moved. /// /// if (editsQueued) { // We can't change the slots out from under any queued edits beginRefresh(); } else { var key = item.key, slot = keyMap[key], slotPrev = keyMap[previousKey], slotNext = keyMap[nextKey]; if ((slot && slot.keyRequested) || (slotPrev && slotPrev.keyRequested) || (slotNext && slotNext.keyRequested)) { // One of the keys has only just been requested - rare, and easier to deal with in a refresh beginRefresh(); } else if (slot) { if (slotPrev && slotNext && (slotPrev.next !== slotNext || slotPrev.lastInSequence || slotNext.firstInSequence)) { // Something has changed, start a refresh beginRefresh(); } else if (!slotPrev && !slotNext) { // If we can't tell where the item moved to, treat this like a removal updateNewIndices(slot, -1); deleteSlot(slot, false); if (oldIndex !== undefined) { if (oldIndex < newIndex) { newIndex--; } updateNewIndicesFromIndex(newIndex, 1); } completeNotification(); } else { updateNewIndices(slot, -1); moveSlot(slot, (slotNext ? slotNext : slotPrev.next), !!slotPrev, !!slotNext); updateNewIndices(slot, 1); completeNotification(); } } else if (slotPrev || slotNext) { // If previousKey or nextKey is known, but key isn't, treat this like an insertion. if (oldIndex !== undefined) { updateNewIndicesFromIndex(oldIndex, -1); if (oldIndex < newIndex) { newIndex--; } } this.inserted(item, previousKey, nextKey, newIndex); } else if (oldIndex !== undefined) { updateNewIndicesFromIndex(oldIndex, -1); if (oldIndex < newIndex) { newIndex--; } updateNewIndicesFromIndex(newIndex, 1); completeNotification(); } } }; this.removed = function (key, index) { /// /// /// Raises a notification that an item was removed. /// /// /// The key of the item that was removed. /// /// /// The index of the item that was removed. /// /// if (editsQueued) { // We can't change the slots out from under any queued edits beginRefresh(); } else { var slot; if (typeof key === "string") { slot = keyMap[key]; } else { slot = indexMap[index]; } if (slot) { if (slot.keyRequested) { // The key has only just been requested - rare, and easier to deal with in a refresh beginRefresh(); } else { updateNewIndices(slot, -1); deleteSlot(slot, false); completeNotification(); } } else if (index !== undefined) { updateNewIndicesFromIndex(index, -1); completeNotification(); } } }; this.endNotifications = function () { /// /// /// Concludes a sequence of notifications that began with a call to beginNotifications. /// /// dataNotificationsInProgress = false; completeNotification(); }; } // ListDataNotificationHandler function resetState() { setStatus(DataSourceStatus.ready); // Track count promises getCountPromise = null; // Track whether listDataAdapter.endEdits needs to be called beginEditsCalled = false; // Track whether finishNotifications should be called after each edit editsInProgress = false; // Track whether the first queued edit should be attempted firstEditInProgress = false; // Queue of edis that have yet to be completed editQueue = {}; editQueue.next = editQueue; editQueue.prev = editQueue; // Track whether there are currently edits queued editsQueued = false; // If an edit has returned noResponse, the edit queue will be reapplied when the next refresh is requested waitForRefresh = false; // Change to count while multiple edits are taking place countDelta = 0; // True while the indices are temporarily in a bad state due to multiple edits indexUpdateDeferred = false; // Next temporary key to use nextTempKey = 0; // Set of fetches for which results have not yet arrived fetchesInProgress = {}; // Queue of complete callbacks for fetches fetchCompleteCallbacks = []; // Tracks the count returned explicitly or implicitly by the data adapter knownCount = CountResult.unknown; // Sentinel objects for list of slots // Give the start sentinel an index so we can always use predecessor + 1. slotsStart = { firstInSequence: true, lastInSequence: true, index: -1 }; slotListEnd = { firstInSequence: true, lastInSequence: true }; slotsEnd = { firstInSequence: true, lastInSequence: true }; slotsStart.next = slotListEnd; slotListEnd.prev = slotsStart; slotListEnd.next = slotsEnd; slotsEnd.prev = slotListEnd; // Map of request IDs to slots handleMap = {}; // Map of keys to slots keyMap = {}; // Map of indices to slots indexMap = {}; indexMap[-1] = slotsStart; // Count of slots that have been released but not deleted releasedSlots = 0; lastSlotReleased = null; // At most one call to reduce the number of refresh slots should be posted at any given time reduceReleasedSlotCountPosted = false; // Multiple refresh requests are coalesced refreshRequested = false; // Requests do not cause fetches while a refresh is in progress refreshInProgress = false; // Refresh requests yield the same promise until a refresh completes refreshSignal = null; } // Construction // Process creation parameters if (!listDataAdapter) { throw new _ErrorFromName("WinJS.UI.ListDataSource.ListDataAdapterIsInvalid", strings.listDataAdapterIsInvalid); } // Minimum number of released slots to retain cacheSize = (listDataAdapter.compareByIdentity ? 0 : 200); if (options) { if (typeof options.cacheSize === "number") { cacheSize = options.cacheSize; } } // Cached listDataNotificationHandler initially undefined if (listDataAdapter.setNotificationHandler) { listDataNotificationHandler = new ListDataNotificationHandler(); listDataAdapter.setNotificationHandler(listDataNotificationHandler); } // Current status status = DataSourceStatus.ready; // Track whether a change to the status has been posted already statusChangePosted = false; // Map of bindingIDs to binding records bindingMap = {}; // ID to assign to the next ListBinding, incremented each time one is created nextListBindingID = 0; // ID assigned to a slot, incremented each time one is created - start with 1 so "if (handle)" tests are valid nextHandle = 1; // ID assigned to a fetch listener, incremented each time one is created nextListenerID = 0; // ID of the refresh in progress, incremented each time a new refresh is started currentRefreshID = 0; // Track whether fetchItemsForAllSlots has been posted already fetchesPosted = false; // ID of a fetch, incremented each time a new fetch is initiated - start with 1 so "if (fetchID)" tests are valid nextFetchID = 1; // Sentinel objects for results arrays startMarker = {}; endMarker = {}; resetState(); // Public methods this.createListBinding = function (notificationHandler) { /// /// /// Creates an IListBinding object that allows a client to read from the list and receive notifications for /// changes that affect those portions of the list that the client already read. /// /// /// An object that implements the IListNotificationHandler interface. If you omit this parameter, /// change notifications won't be available. /// /// /// An object that implements the IListBinding interface. /// /// var listBindingID = (nextListBindingID++).toString(), slotCurrent = null, released = false; function retainSlotForCursor(slot) { if (slot) { slot.cursorCount++; } } function releaseSlotForCursor(slot) { if (slot) { if (--slot.cursorCount === 0) { releaseSlotIfUnrequested(slot); } } } function moveCursor(slot) { // Retain the new slot first just in case it's the same slot retainSlotForCursor(slot); releaseSlotForCursor(slotCurrent); slotCurrent = slot; } function adjustCurrentSlot(slot, slotNew) { if (slot === slotCurrent) { if (!slotNew) { slotNew = ( !slotCurrent || slotCurrent.lastInSequence || slotCurrent.next === slotListEnd ? null : slotCurrent.next ); } moveCursor(slotNew); } } function releaseSlotFromListBinding(slot) { var bindingMap = slot.bindingMap, bindingHandle = bindingMap[listBindingID].handle; delete slot.bindingMap[listBindingID]; // See if there are any listBindings left in the map var releaseBindingMap = true, releaseHandle = true; for (var listBindingID2 in bindingMap) { releaseBindingMap = false; if (bindingHandle && bindingMap[listBindingID2].handle === bindingHandle) { releaseHandle = false; break; } } if (bindingHandle && releaseHandle) { delete handleMap[bindingHandle]; } if (releaseBindingMap) { slot.bindingMap = null; releaseSlotIfUnrequested(slot); } } function retainItem(slot, listenerID) { if (!slot.bindingMap) { slot.bindingMap = {}; } var slotBinding = slot.bindingMap[listBindingID]; if (slotBinding) { slotBinding.count++; } else { slot.bindingMap[listBindingID] = { bindingRecord: bindingMap[listBindingID], count: 1 }; } if (slot.fetchListeners) { var listener = slot.fetchListeners[listenerID]; if (listener) { listener.retained = true; } } } function releaseItem(handle) { var slot = handleMap[handle]; if (slot) { var slotBinding = slot.bindingMap[listBindingID]; if (--slotBinding.count === 0) { var fetchListeners = slot.fetchListeners; for (var listenerID in fetchListeners) { var listener = fetchListeners[listenerID]; if (listener.listBindingID === listBindingID) { listener.retained = false; } } releaseSlotFromListBinding(slot); } } } function itemPromiseFromKnownSlot(slot) { var handle = handleForBinding(slot, listBindingID), listenerID = (nextListenerID++).toString(); var itemPromise = createFetchPromise(slot, "fetchListeners", listenerID, listBindingID, function (complete, item) { complete(itemForBinding(item, handle)); } ); defineCommonItemProperties(itemPromise, slot, handle); // Only implement retain and release methods if a notification handler has been supplied if (notificationHandler) { itemPromise.retain = function () { listBinding._retainItem(slot, listenerID); return itemPromise; }; itemPromise.release = function () { listBinding._releaseItem(handle); }; } return itemPromise; } bindingMap[listBindingID] = { notificationHandler: notificationHandler, notificationsSent: false, adjustCurrentSlot: adjustCurrentSlot, itemPromiseFromKnownSlot: itemPromiseFromKnownSlot, }; function itemPromiseFromSlot(slot) { var itemPromise; if (!released && slot) { itemPromise = itemPromiseFromKnownSlot(slot); } else { // Return a complete promise for a non-existent slot if (released) { itemPromise = new Promise(function () { }); itemPromise.cancel(); } else { itemPromise = Promise.wrap(null); } defineHandleProperty(itemPromise, null); // Only implement retain and release methods if a notification handler has been supplied if (notificationHandler) { itemPromise.retain = function () { return itemPromise; }; itemPromise.release = function () { }; } } moveCursor(slot); return itemPromise; } /// /// /// An interface that enables a client to read from the list and receive notifications for changes that affect /// those portions of the list that the client already read. IListBinding can also enumerate through lists /// that can change at any time. /// /// var listBinding = { _retainItem: function (slot, listenerID) { retainItem(slot, listenerID); }, _releaseItem: function (handle) { releaseItem(handle); }, jumpToItem: function (item) { /// /// /// Makes the specified item the current item. /// /// /// The IItem or IItemPromise to make the current item. /// /// /// An object that implements the IItemPromise interface and serves as a promise for the specified item. If /// the specified item is not in the list, the promise completes with a value of null. /// /// return itemPromiseFromSlot(item ? handleMap[item.handle] : null); }, current: function () { /// /// /// Retrieves the current item. /// /// /// An object that implements the IItemPromise interface and serves as a promise for the current item. /// If the cursor has moved past the start or end of the list, the promise completes with a value /// of null. If the current item has been deleted or moved, the promise returns an error. /// /// return itemPromiseFromSlot(slotCurrent); }, previous: function () { /// /// /// Retrieves the item before the current item and makes it the current item. /// /// /// An object that implements the IItemPromise interface and serves as a promise for the previous item. /// If the cursor moves past the start of the list, the promise completes with a value of null. /// /// return itemPromiseFromSlot(slotCurrent ? requestSlotBefore(slotCurrent) : null); }, next: function () { /// /// /// Retrieves the item after the current item and makes it the current item. /// /// /// An object that implements the IItemPromise interface and serves as a promise for the next item. If /// the cursor moves past the end of the list, the promise completes with a value of null. /// /// return itemPromiseFromSlot(slotCurrent ? requestSlotAfter(slotCurrent) : null); }, releaseItem: function (item) { /// /// /// Creates a request to stop change notfications for the specified item. The item is released only when the /// number of release calls matches the number of IItemPromise.retain calls. The number of release calls cannot /// exceed the number of retain calls. This method is present only if you passed an IListNotificationHandler /// to IListDataSource.createListBinding when it created this IListBinding. /// /// /// The IItem or IItemPromise to release. /// /// this._releaseItem(item.handle); }, release: function () { /// /// /// Releases resources, stops notifications, and cancels outstanding promises /// for all tracked items that this IListBinding returned. /// /// released = true; releaseSlotForCursor(slotCurrent); slotCurrent = null; for (var slot = slotsStart.next; slot !== slotsEnd;) { var slotNext = slot.next; var fetchListeners = slot.fetchListeners; for (var listenerID in fetchListeners) { var listener = fetchListeners[listenerID]; if (listener.listBindingID === listBindingID) { listener.promise.cancel(); delete fetchListeners[listenerID]; } } if (slot.bindingMap && slot.bindingMap[listBindingID]) { releaseSlotFromListBinding(slot); } slot = slotNext; } delete bindingMap[listBindingID]; } }; // Only implement each navigation method if the data adapter implements certain methods if (itemsFromStart || itemsFromIndex) { listBinding.first = function () { /// /// /// Retrieves the first item in the list and makes it the current item. /// /// /// An IItemPromise that serves as a promise for the requested item. /// If the list is empty, the Promise completes with a value of null. /// /// return itemPromiseFromSlot(requestSlotAfter(slotsStart)); }; } if (itemsFromEnd) { listBinding.last = function () { /// /// /// Retrieves the last item in the list and makes it the current item. /// /// /// An IItemPromise that serves as a promise for the requested item. /// If the list is empty, the Promise completes with a value of null. /// /// return itemPromiseFromSlot(requestSlotBefore(slotListEnd)); }; } if (itemsFromKey) { listBinding.fromKey = function (key, hints) { /// /// /// Retrieves the item with the specified key and makes it the current item. /// /// /// The key of the requested item. It must be a non-empty string. /// /// /// Domain-specific hints to the IListDataAdapter /// about the location of the item to improve retrieval time. /// /// /// An IItemPromise that serves as a promise for the requested item. /// If the list doesn't contain an item with the specified key, the Promise completes with a value of null. /// /// return itemPromiseFromSlot(slotFromKey(key, hints)); }; } if (itemsFromIndex || (itemsFromStart && itemsFromKey)) { listBinding.fromIndex = function (index) { /// /// /// Retrieves the item with the specified index and makes it the current item. /// /// /// A value greater than or equal to 0 that is the index of the item to retrieve. /// /// /// An IItemPromise that serves as a promise for the requested item. /// If the list doesn't contain an item with the specified index, the IItemPromise completes with a value of null. /// /// return itemPromiseFromSlot(slotFromIndex(index)); }; } if (itemsFromDescription) { listBinding.fromDescription = function (description) { /// /// /// Retrieves the item with the specified description and makes it the current item. /// /// /// The domain-specific description of the requested item, to be interpreted by the list data adapter. /// /// /// A Promise for the requested item. If the list doesn't contain an item with the specified description, /// the IItemPromise completes with a value of null. /// /// return itemPromiseFromSlot(slotFromDescription(description)); }; } return listBinding; }; this.invalidateAll = function () { /// /// /// Makes the data source refresh its cached items by re-requesting them from the data adapter. /// The data source generates notifications if the data has changed. /// /// /// A Promise that completes when the data has been completely refreshed and all change notifications have been /// sent. /// /// return requestRefresh(); }; // Create a helper which issues new promises for the result of the input promise // but have their cancelations ref-counted so that any given consumer canceling // their promise doesn't result in the incoming promise being canceled unless // all consumers are no longer interested in the result. // var countedCancelation = function (incomingPromise, dataSource) { var signal = new _Signal(); incomingPromise.then( function (v) { signal.complete(v); }, function (e) { signal.error(e); } ); var resultPromise = signal.promise.then(null, function (e) { if (e.name === "WinJS.UI.VirtualizedDataSource.resetCount") { getCountPromise = null; return incomingPromise = dataSource.getCount(); } return Promise.wrapError(e); }); var count = 0; var currentGetCountPromise = { get: function () { count++; return new Promise( function (c, e) { resultPromise.then(c, e); }, function () { if (--count === 0) { // when the count reaches zero cancel the incoming promise signal.promise.cancel(); incomingPromise.cancel(); if (currentGetCountPromise === getCountPromise) { getCountPromise = null; } } } ); }, reset: function () { signal.error(new _ErrorFromName("WinJS.UI.VirtualizedDataSource.resetCount")); }, cancel: function () { // if explicitly asked to cancel the incoming promise signal.promise.cancel(); incomingPromise.cancel(); if (currentGetCountPromise === getCountPromise) { getCountPromise = null; } } }; return currentGetCountPromise; }; this.getCount = function () { /// /// /// Retrieves the number of items in the data source. /// /// if (listDataAdapter.getCount) { // Always do a fetch, even if there is a cached result // var that = this; return Promise.wrap().then(function () { if (editsInProgress || editsQueued) { return knownCount; } var requestPromise; if (!getCountPromise) { var relatedGetCountPromise; // Make a request for the count // requestPromise = listDataAdapter.getCount(); var synchronous; requestPromise.then( function () { if (getCountPromise === relatedGetCountPromise) { getCountPromise = null; } synchronous = true; }, function () { if (getCountPromise === relatedGetCountPromise) { getCountPromise = null; } synchronous = true; } ); // Every time we make a new request for the count we can consider the // countDelta to be invalidated // countDelta = 0; // Wrap the result in a cancelation counter which will block cancelation // of the outstanding promise unless all consumers cancel. // if (!synchronous) { relatedGetCountPromise = getCountPromise = countedCancelation(requestPromise, that); } } return getCountPromise ? getCountPromise.get() : requestPromise; }).then(function (count) { if (!isNonNegativeInteger(count) && count !== undefined) { throw new _ErrorFromName("WinJS.UI.ListDataSource.InvalidRequestedCountReturned", strings.invalidRequestedCountReturned); } if (count !== knownCount) { if (knownCount === CountResult.unknown) { knownCount = count; } else { changeCount(count); finishNotifications(); } } if (count === 0) { if (slotsStart.next !== slotListEnd || slotListEnd.next !== slotsEnd) { // A contradiction has been found beginRefresh(); } else if (slotsStart.lastInSequence) { // Now we know the list is empty mergeSequences(slotsStart); slotListEnd.index = 0; } } return count; }).then(null, function (error) { if (error.name === _UI.CountError.noResponse) { // Report the failure, but still report last known count setStatus(DataSourceStatus.failure); return knownCount; } return Promise.wrapError(error); }); } else { // If the data adapter doesn't support the count method, return the VirtualizedDataSource's // reckoning of the count. return Promise.wrap(knownCount); } }; if (itemsFromKey) { this.itemFromKey = function (key, hints) { /// /// /// Retrieves the item with the specified key. /// /// /// The key of the requested item. It must be a non-empty string. /// /// /// Domain-specific hints to IListDataAdapter about the location of the item /// to improve the retrieval time. /// /// /// A Promise for the requested item. If the list doesn't contain an item with the specified key, /// the Promise completes with a value of null. /// /// return itemDirectlyFromSlot(slotFromKey(key, hints)); }; } if (itemsFromIndex || (itemsFromStart && itemsFromKey)) { this.itemFromIndex = function (index) { /// /// /// Retrieves the item at the specified index. /// /// /// A value greater than or equal to zero that is the index of the requested item. /// /// /// A Promise for the requested item. If the list doesn't contain an item with the specified index, /// the Promise completes with a value of null. /// /// return itemDirectlyFromSlot(slotFromIndex(index)); }; } if (itemsFromDescription) { this.itemFromDescription = function (description) { /// /// /// Retrieves the item with the specified description. /// /// /// Domain-specific info that describes the item to retrieve, to be interpreted by the IListDataAdapter, /// /// /// A Promise for the requested item. If the list doesn't contain an item with the specified description, /// the Promise completes with a value of null. /// /// return itemDirectlyFromSlot(slotFromDescription(description)); }; } this.beginEdits = function () { /// /// /// Notifies the data source that a sequence of edits is about to begin. The data source calls /// IListNotificationHandler.beginNotifications and endNotifications each one time for a sequence of edits. /// /// editsInProgress = true; }; // Only implement each editing method if the data adapter implements the corresponding ListDataAdapter method if (listDataAdapter.insertAtStart) { this.insertAtStart = function (key, data) { /// /// /// Adds an item to the beginning of the data source. /// /// /// The key of the item to insert, if known; otherwise, null. /// /// /// The data for the item to add. /// /// /// A Promise that contains the IItem that was added or an EditError if an error occurred. /// /// // Add item to start of list, only notify if the first item was requested return insertItem( key, data, // slotInsertBefore, append (slotsStart.lastInSequence ? null : slotsStart.next), true, // applyEdit function () { return listDataAdapter.insertAtStart(key, data); } ); }; } if (listDataAdapter.insertBefore) { this.insertBefore = function (key, data, nextKey) { /// /// /// Inserts an item before another item. /// /// /// The key of the item to insert, if known; otherwise, null. /// /// /// The data for the item to insert. /// /// /// The key of an item in the data source. The new data is inserted before this item. /// /// /// A Promise that contains the IItem that was added or an EditError if an error occurred. /// /// var slotNext = getSlotForEdit(nextKey); // Add item before given item and send notification return insertItem( key, data, // slotInsertBefore, append slotNext, false, // applyEdit function () { return listDataAdapter.insertBefore(key, data, nextKey, adjustedIndex(slotNext)); } ); }; } if (listDataAdapter.insertAfter) { this.insertAfter = function (key, data, previousKey) { /// /// /// Inserts an item after another item. /// /// /// The key of the item to insert, if known; otherwise, null. /// /// /// The data for the item to insert. /// /// /// The key for an item in the data source. The new item is added after this item. /// /// /// A Promise that contains the IItem that was added or an EditError if an error occurred. /// /// var slotPrev = getSlotForEdit(previousKey); // Add item after given item and send notification return insertItem( key, data, // slotInsertBefore, append (slotPrev ? slotPrev.next : null), true, // applyEdit function () { return listDataAdapter.insertAfter(key, data, previousKey, adjustedIndex(slotPrev)); } ); }; } if (listDataAdapter.insertAtEnd) { this.insertAtEnd = function (key, data) { /// /// /// Adds an item to the end of the data source. /// /// /// The key of the item to insert, if known; otherwise, null. /// /// /// The data for the item to insert. /// /// /// A Promise that contains the IItem that was added or an EditError if an error occurred. /// /// // Add item to end of list, only notify if the last item was requested return insertItem( key, data, // slotInsertBefore, append (slotListEnd.firstInSequence ? null : slotListEnd), false, // applyEdit function () { return listDataAdapter.insertAtEnd(key, data); } ); }; } if (listDataAdapter.change) { this.change = function (key, newData) { /// /// /// Overwrites the data of the specified item. /// /// /// The key for the item to replace. /// /// /// The new data for the item. /// /// /// A Promise that contains the IItem that was updated or an EditError if an error occurred. /// /// var slot = getSlotForEdit(key); return new Promise(function (complete, error) { var itemOld; queueEdit( // applyEdit function () { return listDataAdapter.change(key, newData, adjustedIndex(slot)); }, EditType.change, complete, error, // keyUpdate null, // updateSlots function () { itemOld = slot.item; slot.itemNew = { key: key, data: newData }; if (itemOld) { changeSlot(slot); } else { completeFetchPromises(slot); } }, // undo function () { if (itemOld) { slot.itemNew = itemOld; changeSlot(slot); } else { beginRefresh(); } } ); }); }; } if (listDataAdapter.moveToStart) { this.moveToStart = function (key) { /// /// /// Moves the specified item to the beginning of the data source. /// /// /// The key of the item to move. /// /// /// A Promise that contains the IItem that was moved or an EditError if an error occurred. /// /// var slot = getSlotForEdit(key); return moveItem( slot, // slotMoveBefore, append slotsStart.next, true, // applyEdit function () { return listDataAdapter.moveToStart(key, adjustedIndex(slot)); } ); }; } if (listDataAdapter.moveBefore) { this.moveBefore = function (key, nextKey) { /// /// /// Moves the specified item before another item. /// /// /// The key of the item to move. /// /// /// The key of another item in the data source. The item specified by the key parameter /// is moved to a position immediately before this item. /// /// /// A Promise that contains the IItem that was moved or an EditError if an error occurred. /// /// var slot = getSlotForEdit(key), slotNext = getSlotForEdit(nextKey); return moveItem( slot, // slotMoveBefore, append slotNext, false, // applyEdit function () { return listDataAdapter.moveBefore(key, nextKey, adjustedIndex(slot), adjustedIndex(slotNext)); } ); }; } if (listDataAdapter.moveAfter) { this.moveAfter = function (key, previousKey) { /// /// /// Moves an item after another item. /// /// /// The key of the item to move. /// /// /// The key of another item in the data source. The item specified by the key parameter will /// is moved to a position immediately after this item. /// /// /// A Promise that contains the IItem that was moved or an EditError if an error occurred. /// /// var slot = getSlotForEdit(key), slotPrev = getSlotForEdit(previousKey); return moveItem( slot, // slotMoveBefore, append slotPrev.next, true, // applyEdit function () { return listDataAdapter.moveAfter(key, previousKey, adjustedIndex(slot), adjustedIndex(slotPrev)); } ); }; } if (listDataAdapter.moveToEnd) { this.moveToEnd = function (key) { /// /// /// Moves an item to the end of the data source. /// /// /// The key of the item to move. /// /// /// A Promise that contains the IItem that was moved or an EditError if an error occurred. /// /// var slot = getSlotForEdit(key); return moveItem( slot, // slotMoveBefore, append slotListEnd, false, // applyEdit function () { return listDataAdapter.moveToEnd(key, adjustedIndex(slot)); } ); }; } if (listDataAdapter.remove) { this.remove = function (key) { /// /// /// Removes an item from the data source. /// /// /// The key of the item to remove. /// /// /// A Promise that contains nothing if the operation was successful or an EditError if an error occurred. /// /// validateKey(key); var slot = keyMap[key]; return new Promise(function (complete, error) { var slotNext, firstInSequence, lastInSequence; queueEdit( // applyEdit function () { return listDataAdapter.remove(key, adjustedIndex(slot)); }, EditType.remove, complete, error, // keyUpdate null, // updateSlots function () { if (slot) { slotNext = slot.next; firstInSequence = slot.firstInSequence; lastInSequence = slot.lastInSequence; updateNewIndices(slot, -1); deleteSlot(slot, false); } }, // undo function () { if (slot) { reinsertSlot(slot, slotNext, !firstInSequence, !lastInSequence); updateNewIndices(slot, 1); sendInsertedNotification(slot); } } ); }); }; } this.endEdits = function () { /// /// /// Notifies the data source that a sequence of edits has ended. The data source will call /// IListNotificationHandler.beginNotifications and endNotifications once each for a sequence of edits. /// /// editsInProgress = false; completeEdits(); }; } // _baseDataSourceConstructor var VDS = _Base.Class.define(function () { /// /// /// Use as a base class when defining a custom data source. Do not instantiate directly. /// /// /// Raised when the status of the VirtualizedDataSource changes between ready, waiting, and failure states. /// /// }, { _baseDataSourceConstructor: _baseDataSourceConstructor, _isVirtualizedDataSource: true }, { // Static Members supportedForProcessing: false }); _Base.Class.mix(VDS, _Events.eventMixin); return VDS; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Group Data Source define('WinJS/VirtualizedDataSource/_GroupDataSource',[ 'exports', '../Core/_Base', '../Core/_ErrorFromName', '../Promise', '../Scheduler', '../Utilities/_UI', './_VirtualizedDataSourceImpl' ], function groupDataSourceInit(exports, _Base, _ErrorFromName, Promise, Scheduler, _UI, VirtualizedDataSource) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _GroupDataSource: _Base.Namespace._lazy(function () { // Private statics function errorDoesNotExist() { return new _ErrorFromName(_UI.FetchError.doesNotExist); } var batchSizeDefault = 101; function groupReady(group) { return group && group.firstReached && group.lastReached; } var ListNotificationHandler = _Base.Class.define(function ListNotificationHandler_ctor(groupDataAdapter) { // Constructor this._groupDataAdapter = groupDataAdapter; }, { // Public methods beginNotifications: function () { }, // itemAvailable: not implemented inserted: function (itemPromise, previousHandle, nextHandle) { this._groupDataAdapter._inserted(itemPromise, previousHandle, nextHandle); }, changed: function (newItem, oldItem) { this._groupDataAdapter._changed(newItem, oldItem); }, moved: function (itemPromise, previousHandle, nextHandle) { this._groupDataAdapter._moved(itemPromise, previousHandle, nextHandle); }, removed: function (handle, mirage) { this._groupDataAdapter._removed(handle, mirage); }, countChanged: function (newCount, oldCount) { if (newCount === 0 && oldCount !== 0) { this._groupDataAdapter.invalidateGroups(); } }, indexChanged: function (handle, newIndex, oldIndex) { this._groupDataAdapter._indexChanged(handle, newIndex, oldIndex); }, endNotifications: function () { this._groupDataAdapter._endNotifications(); }, reload: function () { this._groupDataAdapter._reload(); } }, { supportedForProcessing: false, }); var GroupDataAdapter = _Base.Class.define(function GroupDataAdapater_ctor(listDataSource, groupKey, groupData, options) { // Constructor this._listBinding = listDataSource.createListBinding(new ListNotificationHandler(this)); this._groupKey = groupKey; this._groupData = groupData; // _initializeState clears the count, so call this before processing the groupCountEstimate option this._initializeState(); this._batchSize = batchSizeDefault; this._count = null; if (options) { if (typeof options.groupCountEstimate === "number") { this._count = (options.groupCountEstimate < 0 ? null : Math.max(options.groupCountEstimate, 1)); } if (typeof options.batchSize === "number") { this._batchSize = options.batchSize + 1; } } if (this._listBinding.last) { this.itemsFromEnd = function (count) { var that = this; return this._fetchItems( // getGroup function () { return that._lastGroup; }, // mayExist function (failed) { if (failed) { return false; } var count = that._count; if (+count !== count) { return true; } if (count > 0) { return true; } }, // fetchInitialBatch function () { that._fetchBatch(that._listBinding.last(), that._batchSize - 1, 0); }, count - 1, 0 ); }; } }, { // Public members setNotificationHandler: function (notificationHandler) { this._listDataNotificationHandler = notificationHandler; }, // The ListDataSource should always compare these items by identity; in rare cases, it will do some unnecessary // rerendering, but at least fetching will not stringify items we already know to be valid and that we know // have not changed. compareByIdentity: true, // itemsFromStart: not implemented // itemsFromEnd: implemented in constructor itemsFromKey: function (key, countBefore, countAfter, hints) { var that = this; return this._fetchItems( // getGroup function () { return that._keyMap[key]; }, // mayExist function () { var lastGroup = that._lastGroup; if (!lastGroup) { return true; } if (+lastGroup.index !== lastGroup.index) { return true; } }, // fetchInitialBatch function () { hints = hints || {}; var itemPromise = ( typeof hints.groupMemberKey === "string" && that._listBinding.fromKey ? that._listBinding.fromKey(hints.groupMemberKey) : typeof hints.groupMemberIndex === "number" && that._listBinding.fromIndex ? that._listBinding.fromIndex(hints.groupMemberIndex) : hints.groupMemberDescription !== undefined && that._listBinding.fromDescription ? that._listBinding.fromDescription(hints.groupMemberDescription) : that._listBinding.first() ); var fetchBefore = Math.floor(0.5 * (that._batchSize - 1)); that._fetchBatch(itemPromise, fetchBefore, that._batchSize - 1 - fetchBefore); }, countBefore, countAfter ); }, itemsFromIndex: function (index, countBefore, countAfter) { var that = this; return this._fetchItems( // getGroup function () { return that._indexMap[index]; }, // mayExist function () { var lastGroup = that._lastGroup; if (!lastGroup) { return true; } if (+lastGroup.index !== lastGroup.index) { return true; } if (index <= lastGroup.index) { return true; } }, // fetchInitialBatch function () { that._fetchNextIndex(); }, countBefore, countAfter ); }, // itemsFromDescription: not implemented getCount: function () { if (this._lastGroup && typeof this._lastGroup.index === "number") { return Promise.wrap(this._count); } else { // Even if there's a current estimate for _count, consider this call to be a request to determine the true // count. var that = this; var countPromise = new Promise(function (complete) { var fetch = { initialBatch: function () { that._fetchNextIndex(); }, getGroup: function () { return null; }, countBefore: 0, countAfter: 0, complete: function (failed) { if (failed) { that._count = 0; } var count = that._count; if (typeof count === "number") { complete(count); return true; } else { return false; } } }; that._fetchQueue.push(fetch); if (!that._itemBatch) { that._continueFetch(fetch); } }); return (typeof this._count === "number" ? Promise.wrap(this._count) : countPromise); } }, invalidateGroups: function () { this._beginRefresh(); this._initializeState(); }, // Editing methods not implemented // Private members _initializeState: function () { this._count = null; this._indexMax = null; this._keyMap = {}; this._indexMap = {}; this._lastGroup = null; this._handleMap = {}; this._fetchQueue = []; this._itemBatch = null; this._itemsToFetch = 0; this._indicesChanged = false; }, _releaseItem: function (item) { delete this._handleMap[item.handle]; this._listBinding.releaseItem(item); }, _processBatch: function () { var previousItem = null, previousGroup = null, firstItemInGroup = null, itemsSinceStart = 0, failed = true; for (var i = 0; i < this._batchSize; i++) { var item = this._itemBatch[i], groupKey = (item ? this._groupKey(item) : null); if (item) { failed = false; } if (previousGroup && groupKey !== null && groupKey === previousGroup.key) { // This item is in the same group as the last item. The only thing to do is advance the group's // lastItem if this is definitely the last item that has been processed for the group. itemsSinceStart++; if (previousGroup.lastItem === previousItem) { if (previousGroup.lastItem.handle !== previousGroup.firstItem.handle) { this._releaseItem(previousGroup.lastItem); } previousGroup.lastItem = item; this._handleMap[item.handle] = previousGroup; previousGroup.size++; } else if (previousGroup.firstItem === item) { if (previousGroup.firstItem.handle !== previousGroup.lastItem.handle) { this._releaseItem(previousGroup.firstItem); } previousGroup.firstItem = firstItemInGroup; this._handleMap[firstItemInGroup.handle] = previousGroup; previousGroup.size += itemsSinceStart; } } else { var index = null; if (previousGroup) { previousGroup.lastReached = true; if (typeof previousGroup.index === "number") { index = previousGroup.index + 1; } } if (item) { // See if the current group has already been processed var group = this._keyMap[groupKey]; if (!group) { group = { key: groupKey, data: this._groupData(item), firstItem: item, lastItem: item, size: 1 }; this._keyMap[group.key] = group; this._handleMap[item.handle] = group; } if (i > 0) { group.firstReached = true; if (!previousGroup) { index = 0; } } if (typeof group.index !== "number" && typeof index === "number") { // Set the indices of as many groups as possible for (var group2 = group; group2; group2 = this._nextGroup(group2)) { group2.index = index; this._indexMap[index] = group2; index++; } this._indexMax = index; if (typeof this._count === "number" && !this._lastGroup && this._count <= this._indexMax) { this._count = this._indexMax + 1; } } firstItemInGroup = item; itemsSinceStart = 0; previousGroup = group; } else { if (previousGroup) { this._lastGroup = previousGroup; if (typeof previousGroup.index === "number") { this._count = (previousGroup.index + 1); } // Force a client refresh (which should be fast) to ensure that a countChanged notification is // sent. this._listDataNotificationHandler.invalidateAll(); previousGroup = null; } } } previousItem = item; } // See how many fetches have now completed var fetch; for (fetch = this._fetchQueue[0]; fetch && fetch.complete(failed) ; fetch = this._fetchQueue[0]) { this._fetchQueue.splice(0, 1); } // Continue work on the next fetch, if any if (fetch) { var that = this; // Avoid reentering _processBatch Scheduler.schedule(function GroupDataSource_async_processBatch() { that._continueFetch(fetch); }, Scheduler.Priority.normal, null, "WinJS.UI._GroupDataSource._continueFetch"); } else { this._itemBatch = null; } }, _processPromise: function (itemPromise, batchIndex) { itemPromise.retain(); this._itemBatch[batchIndex] = itemPromise; var that = this; itemPromise.then(function (item) { that._itemBatch[batchIndex] = item; if (--that._itemsToFetch === 0) { that._processBatch(); } }); }, _fetchBatch: function (itemPromise, countBefore) { this._itemBatch = new Array(this._batchSize); this._itemsToFetch = this._batchSize; this._processPromise(itemPromise, countBefore); var batchIndex; this._listBinding.jumpToItem(itemPromise); for (batchIndex = countBefore - 1; batchIndex >= 0; batchIndex--) { this._processPromise(this._listBinding.previous(), batchIndex); } this._listBinding.jumpToItem(itemPromise); for (batchIndex = countBefore + 1; batchIndex < this._batchSize; batchIndex++) { this._processPromise(this._listBinding.next(), batchIndex); } }, _fetchAdjacent: function (item, after) { // Batches overlap by one so group boundaries always fall within at least one batch this._fetchBatch( (this._listBinding.fromKey ? this._listBinding.fromKey(item.key) : this._listBinding.fromIndex(item.index)), (after ? 0 : this._batchSize - 1), (after ? this._batchSize - 1 : 0) ); }, _fetchNextIndex: function () { var groupHighestIndex = this._indexMap[this._indexMax - 1]; if (groupHighestIndex) { // We've already fetched some of the first items, so continue where we left off this._fetchAdjacent(groupHighestIndex.lastItem, true); } else { // Fetch one non-existent item before the list so _processBatch knows the start was reached this._fetchBatch(this._listBinding.first(), 1, this._batchSize - 2); } }, _continueFetch: function (fetch) { if (fetch.initialBatch) { fetch.initialBatch(); fetch.initialBatch = null; } else { var group = fetch.getGroup(); if (group) { var groupPrev, groupNext; if (!group.firstReached) { this._fetchAdjacent(group.firstItem, false); } else if (!group.lastReached) { this._fetchAdjacent(group.lastItem, true); } else if (fetch.countBefore > 0 && group.index !== 0 && !groupReady(groupPrev = this._previousGroup(group))) { this._fetchAdjacent((groupPrev && groupPrev.lastReached ? groupPrev.firstItem : group.firstItem), false); } else { groupNext = this._nextGroup(group); this._fetchAdjacent((groupNext && groupNext.firstReached ? groupNext.lastItem : group.lastItem), true); } } else { // Assume we're searching for a key, index or the count by brute force this._fetchNextIndex(); } } }, _fetchComplete: function (group, countBefore, countAfter, firstRequest, complete) { if (groupReady(group)) { // Check if the minimal requirements for the request are met var groupPrev = this._previousGroup(group); if (firstRequest || groupReady(groupPrev) || group.index === 0 || countBefore === 0) { var groupNext = this._nextGroup(group); if (firstRequest || groupReady(groupNext) || this._lastGroup === group || countAfter === 0) { // Time to return the fetch results // Find the first available group to return (don't return more than asked for) var countAvailableBefore = 0, groupFirst = group; while (countAvailableBefore < countBefore) { groupPrev = this._previousGroup(groupFirst); if (!groupReady(groupPrev)) { break; } groupFirst = groupPrev; countAvailableBefore++; } // Find the last available group to return var countAvailableAfter = 0, groupLast = group; while (countAvailableAfter < countAfter) { groupNext = this._nextGroup(groupLast); if (!groupReady(groupNext)) { break; } groupLast = groupNext; countAvailableAfter++; } // Now create the items to return var len = countAvailableBefore + 1 + countAvailableAfter, items = new Array(len); for (var i = 0; i < len; i++) { var item = { key: groupFirst.key, data: groupFirst.data, firstItemKey: groupFirst.firstItem.key, groupSize: groupFirst.size }; var firstItemIndex = groupFirst.firstItem.index; if (typeof firstItemIndex === "number") { item.firstItemIndexHint = firstItemIndex; } items[i] = item; groupFirst = this._nextGroup(groupFirst); } var result = { items: items, offset: countAvailableBefore }; result.totalCount = ( typeof this._count === "number" ? this._count : _UI.CountResult.unknown ); if (typeof group.index === "number") { result.absoluteIndex = group.index; } if (groupLast === this._lastGroup) { result.atEnd = true; } complete(result); return true; } } } return false; }, _fetchItems: function (getGroup, mayExist, fetchInitialBatch, countBefore, countAfter) { var that = this; return new Promise(function (complete, error) { var group = getGroup(), firstRequest = !group, failureCount = 0; function fetchComplete(failed) { var group2 = getGroup(); if (group2) { return that._fetchComplete(group2, countBefore, countAfter, firstRequest, complete, error); } else if (firstRequest && !mayExist(failed)) { error(errorDoesNotExist()); return true; } else if (failureCount > 2) { error(errorDoesNotExist()); return true; } else { // only consider consecutive failures if (failed) { failureCount++; } else { failureCount = 0; } // _continueFetch will switch to a brute force search return false; } } if (!fetchComplete()) { var fetch = { initialBatch: firstRequest ? fetchInitialBatch : null, getGroup: getGroup, countBefore: countBefore, countAfter: countAfter, complete: fetchComplete }; that._fetchQueue.push(fetch); if (!that._itemBatch) { that._continueFetch(fetch); } } }); }, _previousGroup: function (group) { if (group && group.firstReached) { this._listBinding.jumpToItem(group.firstItem); return this._handleMap[this._listBinding.previous().handle]; } else { return null; } }, _nextGroup: function (group) { if (group && group.lastReached) { this._listBinding.jumpToItem(group.lastItem); return this._handleMap[this._listBinding.next().handle]; } else { return null; } }, _invalidateIndices: function (group) { this._count = null; this._lastGroup = null; if (typeof group.index === "number") { this._indexMax = (group.index > 0 ? group.index : null); } // Delete the indices of this and all subsequent groups for (var group2 = group; group2 && typeof group2.index === "number"; group2 = this._nextGroup(group2)) { delete this._indexMap[group2.index]; group2.index = null; } }, _releaseGroup: function (group) { this._invalidateIndices(group); delete this._keyMap[group.key]; if (this._lastGroup === group) { this._lastGroup = null; } if (group.firstItem !== group.lastItem) { this._releaseItem(group.firstItem); } this._releaseItem(group.lastItem); }, _beginRefresh: function () { // Abandon all current fetches this._fetchQueue = []; if (this._itemBatch) { for (var i = 0; i < this._batchSize; i++) { var item = this._itemBatch[i]; if (item) { if (item.cancel) { item.cancel(); } this._listBinding.releaseItem(item); } } this._itemBatch = null; } this._itemsToFetch = 0; this._listDataNotificationHandler.invalidateAll(); }, _processInsertion: function (item, previousHandle, nextHandle) { var groupPrev = this._handleMap[previousHandle], groupNext = this._handleMap[nextHandle], groupKey = null; if (groupPrev) { // If an item in a different group from groupPrev is being inserted after it, no need to discard groupPrev if (!groupPrev.lastReached || previousHandle !== groupPrev.lastItem.handle || (groupKey = this._groupKey(item)) === groupPrev.key) { this._releaseGroup(groupPrev); } else if (this._lastGroup === groupPrev) { this._lastGroup = null; this._count = null; } this._beginRefresh(); } if (groupNext && groupNext !== groupPrev) { this._invalidateIndices(groupNext); // If an item in a different group from groupNext is being inserted before it, no need to discard groupNext if (!groupNext.firstReached || nextHandle !== groupNext.firstItem.handle || (groupKey !== null ? groupKey : this._groupKey(item)) === groupNext.key) { this._releaseGroup(groupNext); } this._beginRefresh(); } }, _processRemoval: function (handle) { var group = this._handleMap[handle]; if (group && (handle === group.firstItem.handle || handle === group.lastItem.handle)) { this._releaseGroup(group); this._beginRefresh(); } else if (this._itemBatch) { for (var i = 0; i < this._batchSize; i++) { var item = this._itemBatch[i]; if (item && item.handle === handle) { this._beginRefresh(); break; } } } }, _inserted: function (itemPromise, previousHandle, nextHandle) { var that = this; itemPromise.then(function (item) { that._processInsertion(item, previousHandle, nextHandle); }); }, _changed: function (newItem, oldItem) { // A change to the first item could affect the group item var group = this._handleMap[newItem.handle]; if (group && newItem.handle === group.firstItem.handle) { this._releaseGroup(group); this._beginRefresh(); } // If the item is now in a different group, treat this as a move if (this._groupKey(newItem) !== this._groupKey(oldItem)) { this._listBinding.jumpToItem(newItem); var previousHandle = this._listBinding.previous().handle; this._listBinding.jumpToItem(newItem); var nextHandle = this._listBinding.next().handle; this._processRemoval(newItem.handle); this._processInsertion(newItem, previousHandle, nextHandle); } }, _moved: function (itemPromise, previousHandle, nextHandle) { this._processRemoval(itemPromise.handle); var that = this; itemPromise.then(function (item) { that._processInsertion(item, previousHandle, nextHandle); }); }, _removed: function (handle, mirage) { // Mirage removals will just result in null items, which can be ignored if (!mirage) { this._processRemoval(handle); } }, _indexChanged: function (handle, newIndex, oldIndex) { if (typeof oldIndex === "number") { this._indicesChanged = true; } }, _endNotifications: function () { if (this._indicesChanged) { this._indicesChanged = false; // Update the group sizes for (var key in this._keyMap) { var group = this._keyMap[key]; if (group.firstReached && group.lastReached) { var newSize = group.lastItem.index + 1 - group.firstItem.index; if (!isNaN(newSize)) { group.size = newSize; } } } // Invalidate the client, since some firstItemIndexHint properties have probably changed this._beginRefresh(); } }, _reload: function () { this._initializeState(); this._listDataNotificationHandler.reload(); } }, { supportedForProcessing: false, }); return _Base.Class.derive(VirtualizedDataSource.VirtualizedDataSource, function (listDataSource, groupKey, groupData, options) { var groupDataAdapter = new GroupDataAdapter(listDataSource, groupKey, groupData, options); this._baseDataSourceConstructor(groupDataAdapter); this.extensions = { invalidateGroups: function () { groupDataAdapter.invalidateGroups(); } }; }, { /* empty */ }, { supportedForProcessing: false, }); }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Grouped Item Data Source define('WinJS/VirtualizedDataSource/_GroupedItemDataSource',[ '../Core/_Base', './_GroupDataSource' ], function groupedItemDataSourceInit(_Base, _GroupDataSource) { "use strict"; _Base.Namespace.define("WinJS.UI", { computeDataSourceGroups: function (listDataSource, groupKey, groupData, options) { /// /// /// Returns a data source that adds group information to the items of another data source. The "groups" property /// of this data source evaluates to yet another data source that enumerates the groups themselves. /// /// /// The data source for the individual items to group. /// /// /// A callback function that takes an item in the list as an argument. The function is called /// for each item in the list and returns the group key for the item as a string. /// /// /// A callback function that takes an item in the IListDataSource as an argument. /// The function is called on one item in each group and returns /// an object that represents the header of that group. /// /// /// An object that can contain properties that specify additional options: /// /// groupCountEstimate: /// A Number value that is the initial estimate for the number of groups. If you specify -1, /// this function returns no result is until the actual number of groups /// has been determined. /// /// batchSize: /// A Number greater than 0 that specifies the number of items to fetch during each processing pass when /// searching for groups. (In addition to the number specified, one item from the previous batch /// is always included.) /// /// /// An IListDataSource that contains the items in the original data source and provides additional /// group info in a "groups" property. The "groups" property returns another /// IListDataSource that enumerates the different groups in the list. /// /// var groupedItemDataSource = Object.create(listDataSource); function createGroupedItem(item) { if (item) { var groupedItem = Object.create(item); groupedItem.groupKey = groupKey(item); if (groupData) { groupedItem.groupData = groupData(item); } return groupedItem; } else { return null; } } function createGroupedItemPromise(itemPromise) { var groupedItemPromise = Object.create(itemPromise); groupedItemPromise.then = function (onComplete, onError, onCancel) { return itemPromise.then(function (item) { return onComplete(createGroupedItem(item)); }, onError, onCancel); }; return groupedItemPromise; } groupedItemDataSource.createListBinding = function (notificationHandler) { var groupedNotificationHandler; if (notificationHandler) { groupedNotificationHandler = Object.create(notificationHandler); groupedNotificationHandler.inserted = function (itemPromise, previousHandle, nextHandle) { return notificationHandler.inserted(createGroupedItemPromise(itemPromise), previousHandle, nextHandle); }; groupedNotificationHandler.changed = function (newItem, oldItem) { return notificationHandler.changed(createGroupedItem(newItem), createGroupedItem(oldItem)); }; groupedNotificationHandler.moved = function (itemPromise, previousHandle, nextHandle) { return notificationHandler.moved(createGroupedItemPromise(itemPromise), previousHandle, nextHandle); }; } else { groupedNotificationHandler = null; } var listBinding = listDataSource.createListBinding(groupedNotificationHandler), groupedItemListBinding = Object.create(listBinding); var listBindingMethods = [ "first", "last", "fromDescription", "jumpToItem", "current" ]; for (var i = 0, len = listBindingMethods.length; i < len; i++) { (function (listBindingMethod) { if (listBinding[listBindingMethod]) { groupedItemListBinding[listBindingMethod] = function () { return createGroupedItemPromise(listBinding[listBindingMethod].apply(listBinding, arguments)); }; } })(listBindingMethods[i]); } // The following methods should be fast if (listBinding.fromKey) { groupedItemListBinding.fromKey = function (key) { return createGroupedItemPromise(listBinding.fromKey(key)); }; } if (listBinding.fromIndex) { groupedItemListBinding.fromIndex = function (index) { return createGroupedItemPromise(listBinding.fromIndex(index)); }; } groupedItemListBinding.prev = function () { return createGroupedItemPromise(listBinding.prev()); }; groupedItemListBinding.next = function () { return createGroupedItemPromise(listBinding.next()); }; return groupedItemListBinding; }; var listDataSourceMethods = [ "itemFromKey", "itemFromIndex", "itemFromDescription", "insertAtStart", "insertBefore", "insertAfter", "insertAtEnd", "change", "moveToStart", "moveBefore", "moveAfter", "moveToEnd" // remove does not return an itemPromise ]; for (var i = 0, len = listDataSourceMethods.length; i < len; i++) { (function (listDataSourceMethod) { if (listDataSource[listDataSourceMethod]) { groupedItemDataSource[listDataSourceMethod] = function () { return createGroupedItemPromise(listDataSource[listDataSourceMethod].apply(listDataSource, arguments)); }; } })(listDataSourceMethods[i]); } ["addEventListener", "removeEventListener", "dispatchEvent"].forEach(function (methodName) { if (listDataSource[methodName]) { groupedItemDataSource[methodName] = function () { return listDataSource[methodName].apply(listDataSource, arguments); }; } }); var groupDataSource = null; Object.defineProperty(groupedItemDataSource, "groups", { get: function () { if (!groupDataSource) { groupDataSource = new _GroupDataSource._GroupDataSource(listDataSource, groupKey, groupData, options); } return groupDataSource; }, enumerable: true, configurable: true }); return groupedItemDataSource; } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Storage Item Data Source define('WinJS/VirtualizedDataSource/_StorageDataSource',[ 'exports', '../Core/_WinRT', '../Core/_Global', '../Core/_Base', '../Core/_ErrorFromName', '../Core/_WriteProfilerMark', '../Animations', '../Promise', '../Utilities/_UI', './_VirtualizedDataSourceImpl' ], function storageDataSourceInit(exports, _WinRT, _Global, _Base, _ErrorFromName, _WriteProfilerMark, Animations, Promise, _UI, VirtualizedDataSource) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { StorageDataSource: _Base.Namespace._lazy(function () { var StorageDataAdapter = _Base.Class.define(function StorageDataAdapter_ctor(query, options) { // Constructor _WriteProfilerMark("WinJS.UI.StorageDataSource:constructor,StartTM"); var mode = _WinRT.Windows.Storage.FileProperties.ThumbnailMode.singleItem, size = 256, flags = _WinRT.Windows.Storage.FileProperties.ThumbnailOptions.useCurrentScale, delayLoad = true, library; if (query === "Pictures") { mode = _WinRT.Windows.Storage.FileProperties.ThumbnailMode.picturesView; library = _WinRT.Windows.Storage.KnownFolders.picturesLibrary; size = 190; } else if (query === "Music") { mode = _WinRT.Windows.Storage.FileProperties.ThumbnailMode.musicView; library = _WinRT.Windows.Storage.KnownFolders.musicLibrary; size = 256; } else if (query === "Documents") { mode = _WinRT.Windows.Storage.FileProperties.ThumbnailMode.documentsView; library = _WinRT.Windows.Storage.KnownFolders.documentsLibrary; size = 40; } else if (query === "Videos") { mode = _WinRT.Windows.Storage.FileProperties.ThumbnailMode.videosView; library = _WinRT.Windows.Storage.KnownFolders.videosLibrary; size = 190; } if (!library) { this._query = query; } else { var queryOptions = new _WinRT.Windows.Storage.Search.QueryOptions(); queryOptions.folderDepth = _WinRT.Windows.Storage.Search.FolderDepth.deep; queryOptions.indexerOption = _WinRT.Windows.Storage.Search.IndexerOption.useIndexerWhenAvailable; this._query = library.createFileQueryWithOptions(queryOptions); } if (options) { if (typeof options.mode === "number") { mode = options.mode; } if (typeof options.requestedThumbnailSize === "number") { size = Math.max(1, Math.min(options.requestedThumbnailSize, 1024)); } else { switch (mode) { case _WinRT.Windows.Storage.FileProperties.ThumbnailMode.picturesView: case _WinRT.Windows.Storage.FileProperties.ThumbnailMode.videosView: size = 190; break; case _WinRT.Windows.Storage.FileProperties.ThumbnailMode.documentsView: case _WinRT.Windows.Storage.FileProperties.ThumbnailMode.listView: size = 40; break; case _WinRT.Windows.Storage.FileProperties.ThumbnailMode.musicView: case _WinRT.Windows.Storage.FileProperties.ThumbnailMode.singleItem: size = 256; break; } } if (typeof options.thumbnailOptions === "number") { flags = options.thumbnailOptions; } if (typeof options.waitForFileLoad === "boolean") { delayLoad = !options.waitForFileLoad; } } this._loader = new _WinRT.Windows.Storage.BulkAccess.FileInformationFactory(this._query, mode, size, flags, delayLoad); this.compareByIdentity = false; this.firstDataRequest = true; _WriteProfilerMark("WinJS.UI.StorageDataSource:constructor,StopTM"); }, { // Public members setNotificationHandler: function (notificationHandler) { this._notificationHandler = notificationHandler; this._query.addEventListener("contentschanged", function () { notificationHandler.invalidateAll(); }); this._query.addEventListener("optionschanged", function () { notificationHandler.invalidateAll(); }); }, itemsFromEnd: function (count) { var that = this; _WriteProfilerMark("WinJS.UI.StorageDataSource:itemsFromEnd,info"); return this.getCount().then(function (totalCount) { if (totalCount === 0) { return Promise.wrapError(new _ErrorFromName(_UI.FetchError.doesNotExist)); } // Intentionally passing countAfter = 1 to go one over the end so that itemsFromIndex will // report the vector size since its known. return that.itemsFromIndex(totalCount - 1, Math.min(totalCount - 1, count - 1), 1); }); }, itemsFromIndex: function (index, countBefore, countAfter) { // don't allow more than 64 items to be retrieved at once if (countBefore + countAfter > 64) { countBefore = Math.min(countBefore, 32); countAfter = 64 - (countBefore + 1); } var first = (index - countBefore), count = (countBefore + 1 + countAfter); var that = this; // Fetch a minimum of 32 items on the first request for smoothness. Otherwise // listview displays 2 items first and then the rest of the page. if (that.firstDataRequest) { that.firstDataRequest = false; count = Math.max(count, 32); } function listener(ev) { that._notificationHandler.changed(that._item(ev.target)); } var perfId = "WinJS.UI.StorageDataSource:itemsFromIndex(" + first + "-" + (first + count - 1) + ")"; _WriteProfilerMark(perfId + ",StartTM"); return this._loader.getItemsAsync(first, count).then(function (itemsVector) { var vectorSize = itemsVector.size; if (vectorSize <= countBefore) { return Promise.wrapError(new _ErrorFromName(_UI.FetchError.doesNotExist)); } var items = new Array(vectorSize); var localItemsVector = new Array(vectorSize); itemsVector.getMany(0, localItemsVector); for (var i = 0; i < vectorSize; i++) { items[i] = that._item(localItemsVector[i]); localItemsVector[i].addEventListener("propertiesupdated", listener); } var result = { items: items, offset: countBefore, absoluteIndex: index }; // set the totalCount only when we know it (when we retrieived fewer items than were asked for) if (vectorSize < count) { result.totalCount = first + vectorSize; } _WriteProfilerMark(perfId + ",StopTM"); return result; }); }, itemsFromDescription: function (description, countBefore, countAfter) { var that = this; _WriteProfilerMark("WinJS.UI.StorageDataSource:itemsFromDescription,info"); return this._query.findStartIndexAsync(description).then(function (index) { return that.itemsFromIndex(index, countBefore, countAfter); }); }, getCount: function () { _WriteProfilerMark("WinJS.UI.StorageDataSource:getCount,info"); return this._query.getItemCountAsync(); }, itemSignature: function (item) { return item.folderRelativeId; }, // compareByIdentity: set in constructor // itemsFromStart: not implemented // itemsFromKey: not implemented // insertAtStart: not implemented // insertBefore: not implemented // insertAfter: not implemented // insertAtEnd: not implemented // change: not implemented // moveToStart: not implemented // moveBefore: not implemented // moveAfter: not implemented // moveToEnd: not implemented // remove: not implemented // Private members _item: function (item) { return { key: item.path || item.folderRelativeId, data: item }; } }, { supportedForProcessing: false, }); return _Base.Class.derive(VirtualizedDataSource.VirtualizedDataSource, function (query, options) { /// /// /// Creates a data source that enumerates an IStorageQueryResultBase. /// /// /// The object to enumerate. It must support IStorageQueryResultBase. /// /// /// An object that specifies options for the data source. This parameter is optional. It can contain these properties: /// /// mode: /// A Windows.Storage.FileProperties.ThumbnailMode - a value that specifies whether to request /// thumbnails and the type of thumbnails to request. /// /// requestedThumbnailSize: /// A Number that specifies the size of the thumbnails. /// /// thumbnailOptions: /// A Windows.Storage.FileProperties.ThumbnailOptions value that specifies additional options for the thumbnails. /// /// waitForFileLoad: /// If you set this to true, the data source returns items only after it loads their properties and thumbnails. /// /// /// this._baseDataSourceConstructor(new StorageDataAdapter(query, options)); }, { /* empty */ }, { loadThumbnail: function (item, image) { /// /// /// Returns a promise for an image element that completes when the full quality thumbnail of the provided item is drawn to the /// image element. /// /// /// The item to retrieve a thumbnail for. /// /// /// The image element to use. If not provided, a new image element is created. /// /// var thumbnailUpdateHandler, thumbnailPromise, shouldRespondToThumbnailUpdate = false; return new Promise(function (complete) { // Load a thumbnail if it exists. The promise completes when a full quality thumbnail is visible. var tagSupplied = (image ? true : false); var processThumbnail = function (thumbnail) { if (thumbnail) { var url = _Global.URL.createObjectURL(thumbnail, {oneTimeOnly: true}); // If this is the first version of the thumbnail we're loading, fade it in. if (!thumbnailPromise) { thumbnailPromise = item.loadImage(url, image).then(function (image) { // Wrapping the fadeIn call in a promise for the image returned by loadImage allows us to // pipe the result of loadImage to further chained promises. This is necessary because the // image element provided to loadThumbnail is optional, and loadImage will create an image // element if none is provided. return item.isOnScreen().then(function (visible) { var imagePromise; if (visible && tagSupplied) { imagePromise = Animations.fadeIn(image).then(function () { return image; }); } else { image.style.opacity = 1; imagePromise = Promise.wrap(image); } return imagePromise; }); }); } else { // Otherwise, replace the existing version without animation. thumbnailPromise = thumbnailPromise.then(function (image) { return item.loadImage(url, image); }); } // If we have the full resolution thumbnail, we can cancel further updates and complete the promise // when current work is complete. if ((thumbnail.type !== _WinRT.Windows.Storage.FileProperties.ThumbnailType.icon) && !thumbnail.returnedSmallerCachedSize) { _WriteProfilerMark("WinJS.UI.StorageDataSource:loadThumbnail complete,info"); item.data.removeEventListener("thumbnailupdated", thumbnailUpdateHandler); shouldRespondToThumbnailUpdate = false; thumbnailPromise = thumbnailPromise.then(function (image) { thumbnailUpdateHandler = null; thumbnailPromise = null; complete(image); }); } } }; thumbnailUpdateHandler = function (e) { // Ensure that a zombie update handler does not get invoked. if (shouldRespondToThumbnailUpdate) { processThumbnail(e.target.thumbnail); } }; item.data.addEventListener("thumbnailupdated", thumbnailUpdateHandler); shouldRespondToThumbnailUpdate = true; // If we already have a thumbnail we should render it now. processThumbnail(item.data.thumbnail); }, function () { item.data.removeEventListener("thumbnailupdated", thumbnailUpdateHandler); shouldRespondToThumbnailUpdate = false; thumbnailUpdateHandler = null; if (thumbnailPromise) { thumbnailPromise.cancel(); thumbnailPromise = null; } }); }, supportedForProcessing: false, }); }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/VirtualizedDataSource',[ './VirtualizedDataSource/_VirtualizedDataSourceImpl', './VirtualizedDataSource/_GroupDataSource', './VirtualizedDataSource/_GroupedItemDataSource', './VirtualizedDataSource/_StorageDataSource' ], function () { //wrapper module }); define('require-style',{load: function(id){throw new Error("Dynamic load not allowed: " + id);}}); define('require-style!less/desktop/styles-intrinsic',[],function(){}); define('require-style!less/desktop/colors-intrinsic',[],function(){}); define('require-style!less/phone/styles-intrinsic',[],function(){}); define('require-style!less/phone/colors-intrinsic',[],function(){}); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/IntrinsicControls',[ '../Utilities/_Hoverable', 'require-style!less/desktop/styles-intrinsic', 'require-style!less/desktop/colors-intrinsic', 'require-style!less/phone/styles-intrinsic', 'require-style!less/phone/colors-intrinsic' ], function (_Hoverable) { }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ItemContainer/_Constants',[ 'exports', '../../Core/_Base' ], function constantsInit(exports, _Base) { "use strict"; var members = {}; members._listViewClass = "win-listview"; members._listViewSupportsCrossSlideClass = "win-listview-supports-cross-slide"; members._viewportClass = "win-viewport"; members._rtlListViewClass = "win-rtl"; members._horizontalClass = "win-horizontal"; members._verticalClass = "win-vertical"; members._scrollableClass = "win-surface"; members._itemsContainerClass = "win-itemscontainer"; members._padderClass = "win-itemscontainer-padder"; members._proxyClass = "_win-proxy"; members._itemClass = "win-item"; members._itemBoxClass = "win-itembox"; members._itemsBlockClass = "win-itemsblock"; members._containerClass = "win-container"; members._backdropClass = "win-backdrop"; members._footprintClass = "win-footprint"; members._groupsClass = "win-groups"; members._selectedClass = "win-selected"; members._swipeableClass = "win-swipeable"; members._swipeClass = "win-swipe"; members._selectionBorderClass = "win-selectionborder"; members._selectionBackgroundClass = "win-selectionbackground"; members._selectionCheckmarkClass = "win-selectioncheckmark"; members._selectionCheckmarkBackgroundClass = "win-selectioncheckmarkbackground"; members._pressedClass = "win-pressed"; members._headerClass = "win-groupheader"; members._headerContainerClass = "win-groupheadercontainer"; members._groupLeaderClass = "win-groupleader"; members._progressClass = "win-progress"; members._selectionHintClass = "win-selectionhint"; members._revealedClass = "win-revealed"; members._itemFocusClass = "win-focused"; members._itemFocusOutlineClass = "win-focusedoutline"; members._zoomingXClass = "win-zooming-x"; members._zoomingYClass = "win-zooming-y"; members._listLayoutClass = "win-listlayout"; members._gridLayoutClass = "win-gridlayout"; members._headerPositionTopClass = "win-headerpositiontop"; members._headerPositionLeftClass = "win-headerpositionleft"; members._structuralNodesClass = "win-structuralnodes"; members._uniformGridLayoutClass = "win-uniformgridlayout"; members._uniformListLayoutClass = "win-uniformlistlayout"; members._cellSpanningGridLayoutClass = "win-cellspanninggridlayout"; members._laidOutClass = "win-laidout"; members._nonDraggableClass = "win-nondraggable"; members._nonSelectableClass = "win-nonselectable"; members._nonSwipeableClass = "win-nonswipeable"; members._dragOverClass = "win-dragover"; members._dragSourceClass = "win-dragsource"; members._clipClass = "win-clip"; members._selectionModeClass = "win-selectionmode"; members._noCSSGrid = "win-nocssgrid"; members._INVALID_INDEX = -1; members._UNINITIALIZED = -1; members._LEFT_MSPOINTER_BUTTON = 0; members._RIGHT_MSPOINTER_BUTTON = 2; members._TAP_END_THRESHOLD = 10; members._DEFAULT_PAGES_TO_LOAD = 5; members._DEFAULT_PAGE_LOAD_THRESHOLD = 2; members._MIN_AUTOSCROLL_RATE = 150; members._MAX_AUTOSCROLL_RATE = 1500; members._AUTOSCROLL_THRESHOLD = 100; members._AUTOSCROLL_DELAY = 50; members._DEFERRED_ACTION = 250; members._DEFERRED_SCROLL_END = 250; // For horizontal layouts members._VERTICAL_SWIPE_SELECTION_THRESHOLD = 39; members._VERTICAL_SWIPE_SPEED_BUMP_START = 0; members._VERTICAL_SWIPE_SPEED_BUMP_END = 127; members._VERTICAL_SWIPE_SELF_REVEAL_GESTURE = 15; // For vertical layouts members._HORIZONTAL_SWIPE_SELECTION_THRESHOLD = 27; members._HORIZONTAL_SWIPE_SPEED_BUMP_START = 0; members._HORIZONTAL_SWIPE_SPEED_BUMP_END = 150; members._HORIZONTAL_SWIPE_SELF_REVEAL_GESTURE = 23; members._SELECTION_CHECKMARK = "\uE081"; members._LISTVIEW_PROGRESS_DELAY = 2000; var ScrollToPriority = { uninitialized: 0, low: 1, // used by layoutSite.invalidateLayout, forceLayout, _processReload, _update and _onMSElementResize - operations that preserve the scroll position medium: 2, // used by dataSource change, layout change and etc - operations that reset the scroll position to 0 high: 3 // used by indexOfFirstVisible, ensureVisible, scrollPosition - operations in which the developer explicitly sets the scroll position }; var ViewChange = { rebuild: 0, remeasure: 1, relayout: 2, realize: 3 }; members._ScrollToPriority = ScrollToPriority; members._ViewChange = ViewChange; _Base.Namespace._moduleDefine(exports, "WinJS.UI", members); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ItemContainer/_ItemEventsHandler',[ 'exports', '../../Core/_Global', '../../Core/_WinRT', '../../Core/_Base', '../../Core/_BaseUtils', '../../Core/_WriteProfilerMark', '../../Animations', '../../Animations/_TransitionAnimation', '../../Promise', '../../Utilities/_ElementUtilities', '../../Utilities/_UI', './_Constants' ], function itemEventsHandlerInit(exports, _Global, _WinRT, _Base, _BaseUtils, _WriteProfilerMark, Animations, _TransitionAnimation, Promise, _ElementUtilities, _UI, _Constants) { "use strict"; var transformNames = _BaseUtils._browserStyleEquivalents["transform"]; var MAX_TILT_ROTATION = 0.15; var MAX_TILT_SHRINK = 0.025; var uniqueID = _ElementUtilities._uniqueID; var MSManipulationEventStates = _ElementUtilities._MSManipulationEvent; function unitVector3d(v) { var mag = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); return { x: v.x / mag, y: v.y / mag, z: v.z / mag }; } // Returns a CSS rotation matrix which rotates by *angle* radians over *axis*. // *axis* is an object of the form: { x: number, y: number, z: number } function rotationTransform3d(angle, axis) { var u = unitVector3d(axis); var cos = Math.cos(angle); var sin = Math.sin(angle); var matrix = [ cos + u.x * u.x * (1 - cos), u.x * u.y * (1 - cos) - u.z * sin, u.x * u.z * (1 - cos) + u.y * sin, 0, u.y * u.x * (1 - cos) + u.z * sin, cos + u.y * u.y * (1 - cos), u.y * u.z * (1 - cos) - u.x * sin, 0, u.z * u.x * (1 - cos) - u.y * sin, u.z * u.y * (1 - cos) + u.x * sin, cos + u.z * u.z * (1 - cos), 0, 0, 0, 0, 1 ]; // Scientific notation in transform values breaks the CSS3 values spec. matrix = matrix.map(function (value) { return value.toFixed(8); }); return "matrix3d(" + matrix.join(",") + ")"; } // Returns a CSS transformation to rotate and shrink an element when it is // pressed. The closer the click is to the center of the item, the more it // shrinks and the less it rotates. // *elementRect* should be of the form returned by getBoundingClientRect. All // of the parameters must be relative to the same coordinate system. // This function was translated from the Splash implementation. function tiltTransform(clickX, clickY, elementRect) { // x and y range from 0.0 thru 1.0 inclusive with the origin being at the top left. var x = _ElementUtilities._clamp((clickX - elementRect.left) / elementRect.width, 0, 1); var y = _ElementUtilities._clamp((clickY - elementRect.top) / elementRect.height, 0, 1); // Axis is perpendicular to the line drawn between the click position and the center of the item. // We set z to a small value so that even if x and y turn out to be 0, we still have an axis. var axis = { x: y - 0.5, y: -(x - 0.5), z: 0.0001 }; // The angle of the rotation is larger when the click is farther away from the center. var magnitude = Math.abs(x - 0.5) + Math.abs(y - 0.5); // an approximation var angle = magnitude * MAX_TILT_ROTATION; // The distance the control is pushed into z-space is larger when the click is closer to the center. var scale = 1 - (1 - magnitude) * MAX_TILT_SHRINK; var transform = "perspective(800px) scale(" + scale + ", " + scale + ") " + rotationTransform3d(angle, axis); return transform; } _Base.Namespace._moduleDefine(exports, "WinJS.UI", { // Expose these to the unit tests _rotationTransform3d: rotationTransform3d, _tiltTransform: tiltTransform, _ItemEventsHandler: _Base.Namespace._lazy(function () { var PT_TOUCH = _ElementUtilities._MSPointerEvent.MSPOINTER_TYPE_TOUCH || "touch"; function getElementWithClass(parent, className) { return parent.querySelector("." + className); } function createNodeWithClass(className, skipAriaHidden) { var element = _Global.document.createElement("div"); element.className = className; if (!skipAriaHidden) { element.setAttribute("aria-hidden", true); } return element; } var ItemEventsHandler = _Base.Class.define(function ItemEventsHandler_ctor(site) { this._site = site; this._work = []; this._animations = {}; this._selectionHintTracker = {}; this._swipeClassTracker = {}; // The gesture recognizer is used for SRG, which is not supported on Phone if (!_BaseUtils.isPhone && this._selectionAllowed()) { var that = this; _Global.setTimeout(function () { if (!that._gestureRecognizer && !site.isZombie()) { that._gestureRecognizer = that._createGestureRecognizer(); } }, 500); } }, { dispose: function () { if (this._disposed) { return; } this._disposed = true; this._gestureRecognizer = null; _ElementUtilities._removeEventListener(_Global, "pointerup", this._resetPointerDownStateBound); _ElementUtilities._removeEventListener(_Global, "pointercancel", this._resetPointerDownStateBound); }, onMSManipulationStateChanged: function ItemEventsHandler_onMSManipulationStateChanged(eventObject) { var state = eventObject.currentState; // We're not necessarily guaranteed to get onMSPointerDown before we get a selection event from cross slide, // so if we hit a select state with no pressed item box recorded, we need to set up the pressed info before // processing the selection. if (state === MSManipulationEventStates.MS_MANIPULATION_STATE_PRESELECT && !this._site.pressedItemBox) { var currentPressedIndex = this._site.indexForItemElement(eventObject.target); this._site.pressedEntity = { type: _UI.ObjectType.item, index: currentPressedIndex }; if (this._site.pressedEntity.index !== _Constants._INVALID_INDEX) { this._site.pressedItemBox = this._site.itemBoxAtIndex(this._site.pressedEntity.index); this._site.pressedContainer = this._site.containerAtIndex(this._site.pressedEntity.index); this._site.animatedElement = _BaseUtils.isPhone ? this._site.pressedItemBox : this._site.pressedContainer; this._site.pressedHeader = null; var allowed = this._site.verifySelectionAllowed(this._site.pressedEntity); this._canSelect = allowed.canSelect; this._canTapSelect = allowed.canTapSelect; this._swipeBehaviorSelectionChanged = false; this._selectionHint = null; if (this._canSelect) { this._addSelectionHint(); } } } if (this._canSelect && (state === MSManipulationEventStates.MS_MANIPULATION_STATE_PRESELECT || state === MSManipulationEventStates.MS_MANIPULATION_STATE_COMMITTED || state === MSManipulationEventStates.MS_MANIPULATION_STATE_CANCELLED || state === MSManipulationEventStates.MS_MANIPULATION_STATE_SELECTING || state === MSManipulationEventStates.MS_MANIPULATION_STATE_DRAGGING)) { this._dispatchSwipeBehavior(state); } if (state === MSManipulationEventStates.MS_MANIPULATION_STATE_COMMITTED || state === MSManipulationEventStates.MS_MANIPULATION_STATE_CANCELLED || state === MSManipulationEventStates.MS_MANIPULATION_STATE_STOPPED) { this.resetPointerDownState(); } }, onPointerDown: function ItemEventsHandler_onPointerDown(eventObject) { _WriteProfilerMark("WinJS.UI._ItemEventsHandler:MSPointerDown,StartTM"); var site = this._site, touchInput = (eventObject.pointerType === PT_TOUCH), leftButton, rightButton; site.pressedElement = eventObject.target; if (_WinRT.Windows.UI.Input.PointerPoint) { // xButton is true when you've x-clicked with a mouse or pen. Otherwise it is false. var currentPoint = this._getCurrentPoint(eventObject); var pointProps = currentPoint.properties; if (!(touchInput || pointProps.isInverted || pointProps.isEraser || pointProps.isMiddleButtonPressed)) { rightButton = pointProps.isRightButtonPressed; leftButton = !rightButton && pointProps.isLeftButtonPressed; } else { leftButton = rightButton = false; } } else { // xButton is true when you've x-clicked with a mouse. Otherwise it is false. leftButton = (eventObject.button === _Constants._LEFT_MSPOINTER_BUTTON); rightButton = (eventObject.button === _Constants._RIGHT_MSPOINTER_BUTTON); } this._DragStartBound = this._DragStartBound || this.onDragStart.bind(this); this._PointerEnterBound = this._PointerEnterBound || this.onPointerEnter.bind(this); this._PointerLeaveBound = this._PointerLeaveBound || this.onPointerLeave.bind(this); this._swipeBehaviorState = MSManipulationEventStates.MS_MANIPULATION_STATE_STOPPED; var swipeEnabled = site.swipeBehavior === _UI.SwipeBehavior.select, isInteractive = this._isInteractive(eventObject.target), currentPressedIndex = site.indexForItemElement(eventObject.target), currentPressedHeaderIndex = site.indexForHeaderElement(eventObject.target), mustSetCapture = !isInteractive && currentPressedIndex !== _Constants._INVALID_INDEX; if ((touchInput || leftButton || (this._selectionAllowed() && swipeEnabled && rightButton)) && this._site.pressedEntity.index === _Constants._INVALID_INDEX && !isInteractive) { if (currentPressedHeaderIndex === _Constants._INVALID_INDEX) { this._site.pressedEntity = { type: _UI.ObjectType.item, index: currentPressedIndex }; } else { this._site.pressedEntity = { type: _UI.ObjectType.groupHeader, index: currentPressedHeaderIndex }; } if (this._site.pressedEntity.index !== _Constants._INVALID_INDEX) { this._site.pressedPosition = _ElementUtilities._getCursorPos(eventObject); var allowed = site.verifySelectionAllowed(this._site.pressedEntity); this._canSelect = allowed.canSelect; this._canTapSelect = allowed.canTapSelect; this._swipeBehaviorSelectionChanged = false; this._selectionHint = null; if (this._site.pressedEntity.type !== _UI.ObjectType.groupHeader) { this._site.pressedItemBox = site.itemBoxAtIndex(this._site.pressedEntity.index); this._site.pressedContainer = site.containerAtIndex(this._site.pressedEntity.index); this._site.animatedElement = _BaseUtils.isPhone ? this._site.pressedItemBox : this._site.pressedContainer; this._site.pressedHeader = null; this._togglePressed(true, false, eventObject); this._site.pressedContainer.addEventListener('dragstart', this._DragStartBound); if (!touchInput) { // This only works for non touch input because on touch input we set capture which immediately fires the MSPointerOut. _ElementUtilities._addEventListener(this._site.pressedContainer, 'pointerenter', this._PointerEnterBound, false); _ElementUtilities._addEventListener(this._site.pressedContainer, 'pointerleave', this._PointerLeaveBound, false); } } else { this._site.pressedHeader = this._site.headerFromElement(eventObject.target); // Interactions with the headers on phone show an animation if (_BaseUtils.isPhone) { this._site.animatedElement = this._site.pressedHeader; this._togglePressed(true, false, eventObject); } else { this._site.pressedItemBox = null; this._site.pressedContainer = null; this._site.animatedElement = null; } } if (!this._resetPointerDownStateBound) { this._resetPointerDownStateBound = this._resetPointerDownStateForPointerId.bind(this); } if (!touchInput) { _ElementUtilities._addEventListener(_Global, "pointerup", this._resetPointerDownStateBound, false); _ElementUtilities._addEventListener(_Global, "pointercancel", this._resetPointerDownStateBound, false); } // The gesture recognizer is used for SRG, which is not supported on Phone if (this._canSelect && !_BaseUtils.isPhone) { if (!this._gestureRecognizer) { this._gestureRecognizer = this._createGestureRecognizer(); } this._addSelectionHint(); } this._pointerId = eventObject.pointerId; this._pointerRightButton = rightButton; this._pointerTriggeredSRG = false; if (this._gestureRecognizer && touchInput) { try { this._gestureRecognizer.addPointer(this._pointerId); } catch (e) { this._gestureRecognizer.stop(); } } } } if (mustSetCapture) { if (touchInput) { try { // Move pointer capture to avoid hover visual on second finger _ElementUtilities._setPointerCapture(site.canvasProxy, eventObject.pointerId); } catch (e) { _WriteProfilerMark("WinJS.UI._ItemEventsHandler:MSPointerDown,StopTM"); return; } } } // Once the shift selection pivot is set, it remains the same until the user // performs a left- or right-click without holding the shift key down. if (this._site.pressedEntity.type !== _UI.ObjectType.groupHeader && this._selectionAllowed() && this._multiSelection() && // Multi selection enabled this._site.pressedEntity.index !== _Constants._INVALID_INDEX && // A valid item was clicked site.selection._getFocused().index !== _Constants._INVALID_INDEX && site.selection._pivot === _Constants._INVALID_INDEX) { site.selection._pivot = site.selection._getFocused().index; } _WriteProfilerMark("WinJS.UI._ItemEventsHandler:MSPointerDown,StopTM"); }, onPointerEnter: function ItemEventsHandler_onPointerEnter(eventObject) { if (this._site.pressedContainer && this._pointerId === eventObject.pointerId) { this._togglePressed(true, false, eventObject); } }, onPointerLeave: function ItemEventsHandler_onPointerLeave(eventObject) { if (this._site.pressedContainer && this._pointerId === eventObject.pointerId) { this._togglePressed(false, true /* synchronous */, eventObject); } }, onDragStart: function ItemEventsHandler_onDragStart() { this._resetPressedContainer(); }, _resetPressedContainer: function ItemEventsHandler_resetPressedContainer() { if ((this._site.pressedContainer || this._site.pressedHeader) && this._site.animatedElement) { this._togglePressed(false); if (this._site.pressedContainer) { this._site.pressedContainer.removeEventListener('dragstart', this._DragStartBound); _ElementUtilities._removeEventListener(this._site.pressedContainer, 'pointerenter', this._PointerEnterBound, false); _ElementUtilities._removeEventListener(this._site.pressedContainer, 'pointerleave', this._PointerLeaveBound, false); } } }, onClick: function ItemEventsHandler_onClick(eventObject) { if (!this._skipClick) { // Handle the UIA invoke action on an item. this._skipClick is false which tells us that we received a click // event without an associated MSPointerUp event. This means that the click event was triggered thru UIA // rather than thru the GUI. var entity = { type: _UI.ObjectType.item, index: this._site.indexForItemElement(eventObject.target) }; if (entity.index === _Constants._INVALID_INDEX) { entity.index = this._site.indexForHeaderElement(eventObject.target); if (entity.index !== _Constants._INVALID_INDEX) { entity.type = _UI.ObjectType.groupHeader; } } if (entity.index !== _Constants._INVALID_INDEX && (_ElementUtilities.hasClass(eventObject.target, this._site.accessibleItemClass) || _ElementUtilities.hasClass(eventObject.target, _Constants._headerClass))) { var allowed = this._site.verifySelectionAllowed(entity); if (allowed.canTapSelect) { this.handleTap(entity); } this._site.fireInvokeEvent(entity, eventObject.target); } } }, onPointerUp: function ItemEventsHandler_onPointerUp(eventObject) { _WriteProfilerMark("WinJS.UI._ItemEventsHandler:MSPointerUp,StartTM"); var site = this._site; this._skipClick = true; var that = this; var swipeEnabled = this._site.swipeBehavior === _UI.SwipeBehavior.select; _BaseUtils._yieldForEvents(function () { that._skipClick = false; }); try { // Release the pointer capture to allow in air touch pointers to be reused for multiple interactions _ElementUtilities._releasePointerCapture(site.canvasProxy, eventObject.pointerId); } catch (e) { // This can throw if SeZo had capture or if the pointer was not already captured } var touchInput = (eventObject.pointerType === PT_TOUCH), releasedElement = this._releasedElement(eventObject), releasedIndex = site.indexForItemElement(releasedElement), releasedHeaderIndex = releasedElement && _ElementUtilities.hasClass(releasedElement, _Constants._headerContainerClass) ? site.indexForHeaderElement(site.pressedHeader) : site.indexForHeaderElement(releasedElement); if (this._pointerId === eventObject.pointerId) { var releasedEntity; if (releasedHeaderIndex === _Constants._INVALID_INDEX) { releasedEntity = { type: _UI.ObjectType.item, index: releasedIndex }; } else { releasedEntity = { type: _UI.ObjectType.groupHeader, index: releasedHeaderIndex }; } this._resetPressedContainer(); if (this._site.pressedEntity.type !== _UI.ObjectType.groupHeader && releasedEntity.type !== _UI.ObjectType.groupHeader && this._site.pressedContainer && this._site.pressedEntity.index === releasedEntity.index) { if (!eventObject.shiftKey) { // Reset the shift selection pivot when the user clicks w/o pressing shift site.selection._pivot = _Constants._INVALID_INDEX; } if (eventObject.shiftKey) { // Shift selection should work when shift or shift+ctrl are depressed for both left- and right-click if (this._selectionAllowed() && this._multiSelection() && site.selection._pivot !== _Constants._INVALID_INDEX) { var firstIndex = Math.min(this._site.pressedEntity.index, site.selection._pivot), lastIndex = Math.max(this._site.pressedEntity.index, site.selection._pivot), additive = (this._pointerRightButton || eventObject.ctrlKey || site.tapBehavior === _UI.TapBehavior.toggleSelect); site.selectRange(firstIndex, lastIndex, additive); } } else if (eventObject.ctrlKey || (this._selectionAllowed() && swipeEnabled && this._pointerRightButton)) { // Swipe emulation this.handleSwipeBehavior(this._site.pressedEntity.index); } } if ((this._site.pressedHeader || this._site.pressedContainer) && this._swipeBehaviorState !== MSManipulationEventStates.MS_MANIPULATION_STATE_COMMITTED) { var upPosition = _ElementUtilities._getCursorPos(eventObject); var isTap = Math.abs(upPosition.left - this._site.pressedPosition.left) <= _Constants._TAP_END_THRESHOLD && Math.abs(upPosition.top - this._site.pressedPosition.top) <= _Constants._TAP_END_THRESHOLD; this._endSelfRevealGesture(); this._clearItem(this._site.pressedEntity, this._isSelected(this._site.pressedEntity.index)); // We do not care whether or not the pressed and released indices are equivalent when the user is using touch. The only time they won't be is if the user // tapped the edge of an item and the pressed animation shrank the item such that the user's finger was no longer over it. In this case, the item should // be considered tapped. // However, if the user is using touch then we must perform an extra check. Sometimes we receive MSPointerUp events when the user intended to pan or swipe. // This extra check ensures that these intended pans/swipes aren't treated as taps. if (!this._pointerRightButton && !this._pointerTriggeredSRG && !eventObject.ctrlKey && !eventObject.shiftKey && ((touchInput && isTap) || (!touchInput && this._site.pressedEntity.index === releasedEntity.index && this._site.pressedEntity.type === releasedEntity.type))) { if (releasedEntity.type === _UI.ObjectType.groupHeader) { this._site.pressedHeader = site.headerAtIndex(releasedEntity.index); this._site.pressedItemBox = null; this._site.pressedContainer = null; } else { this._site.pressedItemBox = site.itemBoxAtIndex(releasedEntity.index); this._site.pressedContainer = site.containerAtIndex(releasedEntity.index); this._site.pressedHeader = null; } if (this._canTapSelect) { this.handleTap(this._site.pressedEntity); } this._site.fireInvokeEvent(this._site.pressedEntity, this._site.pressedItemBox || this._site.pressedHeader); } } if (this._site.pressedEntity.index !== _Constants._INVALID_INDEX) { site.changeFocus(this._site.pressedEntity, true, false, true); } this.resetPointerDownState(); } _WriteProfilerMark("WinJS.UI._ItemEventsHandler:MSPointerUp,StopTM"); }, onPointerCancel: function ItemEventsHandler_onPointerCancel(eventObject) { if (this._pointerId === eventObject.pointerId && this._swipeBehaviorState !== MSManipulationEventStates.MS_MANIPULATION_STATE_PRESELECT) { _WriteProfilerMark("WinJS.UI._ItemEventsHandler:MSPointerCancel,info"); this.resetPointerDownState(); } }, onLostPointerCapture: function ItemEventsHandler_onLostPointerCapture(eventObject) { if (this._pointerId === eventObject.pointerId && this._swipeBehaviorState !== MSManipulationEventStates.MS_MANIPULATION_STATE_PRESELECT) { _WriteProfilerMark("WinJS.UI._ItemEventsHandler:MSLostPointerCapture,info"); this.resetPointerDownState(); } }, // In order for the control to play nicely with other UI controls such as the app bar, it calls preventDefault on // contextmenu events. It does this only when selection is enabled, the event occurred on or within an item, and // the event did not occur on an interactive element. onContextMenu: function ItemEventsHandler_onContextMenu(eventObject) { var containerElement = this._site.containerFromElement(eventObject.target); if (this._selectionAllowed() && containerElement && !this._isInteractive(eventObject.target)) { eventObject.preventDefault(); } }, onMSHoldVisual: function ItemEventsHandler_onMSHoldVisual(eventObject) { if (!this._isInteractive(eventObject.target)) { eventObject.preventDefault(); } }, onDataChanged: function ItemEventsHandler_onDataChanged() { this.resetPointerDownState(); }, handleSwipeBehavior: function ItemEventsHandler_handleSwipeBehavior(itemIndex) { if (this._selectionAllowed(itemIndex)) { this._toggleItemSelection(itemIndex); } }, handleTap: function ItemEventsHandler_handleTap(entity) { if (entity.type === _UI.ObjectType.groupHeader) { return; } var site = this._site, selection = site.selection; if (this._selectionAllowed(entity.index) && this._selectOnTap()) { if (site.tapBehavior === _UI.TapBehavior.toggleSelect) { this._toggleItemSelection(entity.index); } else { // site.tapBehavior === _UI.TapBehavior.directSelect so ensure only itemIndex is selected if (site.selectionMode === _UI.SelectionMode.multi || !selection._isIncluded(entity.index)) { selection.set(entity.index); } } } }, // In single selection mode, in addition to itemIndex's selection state being toggled, // all other items will become deselected _toggleItemSelection: function ItemEventsHandler_toggleItemSelection(itemIndex) { var site = this._site, selection = site.selection, selected = selection._isIncluded(itemIndex); if (site.selectionMode === _UI.SelectionMode.single) { if (!selected) { selection.set(itemIndex); } else { selection.clear(); } } else { if (!selected) { selection.add(itemIndex); } else { selection.remove(itemIndex); } } }, _getCurrentPoint: function ItemEventsHandler_getCurrentPoint(eventObject) { return _WinRT.Windows.UI.Input.PointerPoint.getCurrentPoint(eventObject.pointerId); }, _containedInElementWithClass: function ItemEventsHandler_containedInElementWithClass(element, className) { if (element.parentNode) { var matches = element.parentNode.querySelectorAll("." + className + ", ." + className + " *"); for (var i = 0, len = matches.length; i < len; i++) { if (matches[i] === element) { return true; } } } return false; }, _isSelected: function ItemEventsHandler_isSelected(index) { return (!this._swipeBehaviorSelectionChanged && this._site.selection._isIncluded(index)) || (this._swipeBehaviorSelectionChanged && this.swipeBehaviorSelected); }, _isInteractive: function ItemEventsHandler_isInteractive(element) { return this._containedInElementWithClass(element, "win-interactive"); }, _togglePressed: function ItemEventsHandler_togglePressed(add, synchronous, eventObject) { var that = this; var isHeader = this._site.pressedEntity.type === _UI.ObjectType.groupHeader; this._site.animatedDownPromise && this._site.animatedDownPromise.cancel(); if (_BaseUtils.isPhone && !isHeader && _ElementUtilities.hasClass(this._site.pressedItemBox, _Constants._nonSelectableClass)) { return; } if (!this._staticMode(isHeader)) { if (add) { if (!_ElementUtilities.hasClass(this._site.animatedElement, _Constants._pressedClass)) { _WriteProfilerMark("WinJS.UI._ItemEventsHandler:applyPressedUI,info"); _ElementUtilities.addClass(this._site.animatedElement, _Constants._pressedClass); if (eventObject && _BaseUtils.isPhone) { var boundingElement = isHeader ? that._site.pressedHeader : that._site.pressedContainer; var transform = tiltTransform(eventObject.clientX, eventObject.clientY, boundingElement.getBoundingClientRect()); // Timeout prevents item from looking like it was pressed down during swipes and pans this._site.animatedDownPromise = Promise.timeout(50).then(function () { applyDownVisual(transform); }); } else { // Shrink by 97.5% unless that is larger than 7px in either direction. In that case we cap the // scale so that it is no larger than 7px in either direction. We keep the scale uniform in both x // and y directions. Note that this scale cap only works if getItemPosition returns synchronously // which it does for the built in layouts. var scale = 0.975; var maxPixelsToShrink = 7; this._site.getItemPosition(this._site.pressedEntity).then(function (pos) { if (pos.contentWidth > 0) { scale = Math.max(scale, (1 - (maxPixelsToShrink / pos.contentWidth))); } if (pos.contentHeight > 0) { scale = Math.max(scale, (1 - (maxPixelsToShrink / pos.contentHeight))); } }, function () { // Swallow errors in case data source changes }); applyDownVisual("scale(" + scale + "," + scale + ")"); } } } else { if (_ElementUtilities.hasClass(this._site.animatedElement, _Constants._pressedClass)) { var element = this._site.animatedElement; var expectingStyle = this._site.animatedElementScaleTransform; if (synchronous) { applyUpVisual(element, expectingStyle); } else { // Force removal of the _pressedClass to be asynchronous so that users will see at // least one frame of the shrunken item when doing a quick tap. // // setImmediate is used rather than requestAnimationFrame to ensure that the item // doesn't get stuck down for too long -- apps are told to put long running invoke // code behind a setImmediate and togglePressed's async code needs to run first. _BaseUtils._setImmediate(function () { if (_ElementUtilities.hasClass(element, _Constants._pressedClass)) { applyUpVisual(element, expectingStyle); } }); } } } } function applyDownVisual(transform) { if (that._site.animatedElement.style[transformNames.scriptName] === "") { that._site.animatedElement.style[transformNames.scriptName] = transform; that._site.animatedElementScaleTransform = that._site.animatedElement.style[transformNames.scriptName]; } else { that._site.animatedElementScaleTransform = ""; } } function applyUpVisual(element, expectingStyle) { _WriteProfilerMark("WinJS.UI._ItemEventsHandler:removePressedUI,info"); _ElementUtilities.removeClass(element, _Constants._pressedClass); if (_BaseUtils.isPhone) { if (that._containsTransform(element, expectingStyle)) { _TransitionAnimation.executeTransition(element, { property: transformNames.cssName, delay: 0, duration: 500, timing: "cubic-bezier(0.7025,0,0.9225,-0.115)", to: element.style[transformNames.scriptName].replace(expectingStyle, "") }); } } else { that._removeTransform(element, expectingStyle); } } }, _containsTransform: function ItemEventsHandler_containsTransform(element, transform) { return transform && element.style[transformNames.scriptName].indexOf(transform) !== -1; }, _removeTransform: function ItemEventsHandler_removeTransform(element, transform) { if (this._containsTransform(element, transform)) { element.style[transformNames.scriptName] = element.style[transformNames.scriptName].replace(transform, ""); } }, _endSwipeBehavior: function ItemEventsHandler_endSwipeBehavior() { if (!(this._swipeBehaviorState === MSManipulationEventStates.MS_MANIPULATION_STATE_PRESELECT || this._swipeBehaviorState === MSManipulationEventStates.MS_MANIPULATION_STATE_SELECTING || this._swipeBehaviorState === MSManipulationEventStates.MS_MANIPULATION_STATE_DRAGGING || this._swipeBehaviorState === MSManipulationEventStates.MS_MANIPULATION_STATE_COMMITTED || this._swipeBehaviorState === MSManipulationEventStates.MS_MANIPULATION_STATE_CANCELLED)) { return; } if (this._site.pressedEntity.type === _UI.ObjectType.groupHeader) { return; } this._flushUIBatches(); var selectionHint = this._selectionHint; this._selectionHint = null; if (this._site.pressedItemBox) { var pressedIndex = this._site.pressedEntity.index, selected = this._site.selection._isIncluded(pressedIndex); if (selected) { var elementsToShowHide = _ElementUtilities._getElementsByClasses(this._site.pressedItemBox, [_Constants._selectionCheckmarkClass, _Constants._selectionCheckmarkBackgroundClass]); for (var i = 0; i < elementsToShowHide.length; i++) { elementsToShowHide[i].style.opacity = 1; } } this._clearItem(this._site.pressedEntity, selected); if (selectionHint) { this._removeSelectionHint(selectionHint); } delete this._animations[pressedIndex]; } }, _createGestureRecognizer: function ItemEventsHandler_createGestureRecognizer() { var rootElement = this._site.eventHandlerRoot; var recognizer = _ElementUtilities._createGestureRecognizer(); recognizer.target = rootElement; var that = this; rootElement.addEventListener("MSGestureHold", function (eventObject) { if (that._site.pressedEntity.index !== -1 && eventObject.detail === _ElementUtilities._MSGestureEvent.MSGESTURE_FLAG_BEGIN) { that._startSelfRevealGesture(); } }); return recognizer; }, _dispatchSwipeBehavior: function ItemEventsHandler_dispatchSwipeBehavior(manipulationState) { if (this._site.pressedEntity.type === _UI.ObjectType.groupHeader || this._site.swipeBehavior !== _UI.SwipeBehavior.select) { return; } this._site.selection._pivot = _Constants._INVALID_INDEX; if (this._site.pressedItemBox) { var pressedIndex = this._site.pressedEntity.index; if (this._swipeBehaviorState !== manipulationState) { if (manipulationState === MSManipulationEventStates.MS_MANIPULATION_STATE_DRAGGING && this._canSelect) { this._animateSelectionChange(this._site.selection._isIncluded(pressedIndex)); this._removeSelectionHint(this._selectionHint); } else if (manipulationState === MSManipulationEventStates.MS_MANIPULATION_STATE_PRESELECT) { _WriteProfilerMark("WinJS.UI._ItemEventsHandler:crossSlidingStarted,info"); var site = this._site, pressedElement = site.itemAtIndex(pressedIndex), selected = site.selection._isIncluded(pressedIndex); if (this._selfRevealGesture) { this._selfRevealGesture.finishAnimation(); this._selfRevealGesture = null; } else if (this._canSelect) { this._prepareItem(this._site.pressedEntity, pressedElement, selected); } if (this._swipeBehaviorState !== MSManipulationEventStates.MS_MANIPULATION_STATE_SELECTING) { if (this._site.animatedElement && _ElementUtilities.hasClass(this._site.animatedElement, _Constants._pressedClass)) { this._site.animatedDownPromise && this._site.animatedDownPromise.cancel(); _ElementUtilities.removeClass(this._site.animatedElement, _Constants._pressedClass); this._removeTransform(this._site.animatedElement, this._site.animatedElementScaleTransform); } this._showSelectionHintCheckmark(); } else { this._animateSelectionChange(this._site.selection._isIncluded(pressedIndex)); } } else if (manipulationState === MSManipulationEventStates.MS_MANIPULATION_STATE_COMMITTED) { _WriteProfilerMark("WinJS.UI._ItemEventsHandler:crossSlidingCompleted,info"); var site = this._site, selection = site.selection, swipeBehaviorSelectionChanged = this._swipeBehaviorSelectionChanged, swipeBehaviorSelected = this.swipeBehaviorSelected; if (this._swipeBehaviorState === MSManipulationEventStates.MS_MANIPULATION_STATE_SELECTING && swipeBehaviorSelectionChanged) { if (this._selectionAllowed() && site.swipeBehavior === _UI.SwipeBehavior.select) { if (site.selectionMode === _UI.SelectionMode.single) { if (swipeBehaviorSelected) { selection.set(pressedIndex); } else if (selection._isIncluded(pressedIndex)) { selection.remove(pressedIndex); } } else { if (swipeBehaviorSelected) { selection.add(pressedIndex); } else if (selection._isIncluded(pressedIndex)) { selection.remove(pressedIndex); } } } } // snap back and remove addional elements this._endSwipeBehavior(); } else if (manipulationState === MSManipulationEventStates.MS_MANIPULATION_STATE_SELECTING && this._canSelect) { this._animateSelectionChange(!this._site.selection._isIncluded(pressedIndex)); } else if (this._swipeBehaviorState === MSManipulationEventStates.MS_MANIPULATION_STATE_SELECTING && this._canSelect) { this._animateSelectionChange(this._site.selection._isIncluded(pressedIndex), (manipulationState === MSManipulationEventStates.MS_MANIPULATION_STATE_CANCELLED)); } } } this._swipeBehaviorState = manipulationState; }, _resetPointerDownStateForPointerId: function ItemEventsHandler_resetPointerDownState(eventObject) { if (this._pointerId === eventObject.pointerId) { this.resetPointerDownState(); } }, resetPointerDownState: function ItemEventsHandler_resetPointerDownState() { if (this._gestureRecognizer) { this._endSelfRevealGesture(); this._endSwipeBehavior(); } this._site.pressedElement = null; _ElementUtilities._removeEventListener(_Global, "pointerup", this._resetPointerDownStateBound); _ElementUtilities._removeEventListener(_Global, "pointercancel", this._resetPointerDownStateBound); this._resetPressedContainer(); this._site.pressedContainer = null; this._site.animatedElement = null; this._site.pressedHeader = null; this._site.pressedItemBox = null; this._removeSelectionHint(this._selectionHint); this._selectionHint = null; this._site.pressedEntity = { type: _UI.ObjectType.item, index: _Constants._INVALID_INDEX }; this._pointerId = null; }, // Play the self-reveal gesture (SRG) animation which jiggles the item to reveal the selection hint behind it. // This function is overridden by internal teams to add a tooltip on SRG start - treat this function as a public API for the sake of function name/parameter changes. _startSelfRevealGesture: function ItemEventsHandler_startSelfRevealGesture() { if (this._canSelect && this._site.swipeBehavior === _UI.SwipeBehavior.select) { _WriteProfilerMark("WinJS.UI._ItemEventsHandler:playSelfRevealGesture,info"); var that = this; var site = this._site, index = this._site.pressedEntity.index, itemBox = site.itemBoxAtIndex(index), selected = site.selection._isIncluded(index), finished = false; var swipeReveal = function () { var top, left; if (site.horizontal) { top = _Constants._VERTICAL_SWIPE_SELF_REVEAL_GESTURE + "px"; left = "0px"; } else { top = "0px"; left = (site.rtl() ? "" : "-") + _Constants._HORIZONTAL_SWIPE_SELF_REVEAL_GESTURE + "px"; } return Animations.swipeReveal(itemBox, { top: top, left: left }); }; var swipeHide = function () { return finished ? Promise.wrap() : Animations.swipeReveal(itemBox, { top: "0px", left: "0px" }); }; var cleanUp = function (selectionHint) { if (!site.isZombie()) { if (selectionHint) { that._removeSelectionHint(selectionHint); } that._clearItem(site.pressedEntity, site.selection._isIncluded(index)); } }; // Immediately begins the last phase of the SRG animation which animates the item back to its original location var finishAnimation = function () { that._selfRevealGesture._promise.cancel(); finished = true; var selectionHint = that._selectionHint; that._selectionHint = null; return swipeHide().then(function () { itemBox.style[transformNames.scriptName] = ""; cleanUp(selectionHint); }); }; this._prepareItem(this._site.pressedEntity, itemBox, selected); this._showSelectionHintCheckmark(); this._pointerTriggeredSRG = true; this._selfRevealGesture = { finishAnimation: finishAnimation, _promise: swipeReveal(). then(swipeHide). then(function () { if (!finished) { that._hideSelectionHintCheckmark(); cleanUp(); that._selfRevealGesture = null; } }) }; } }, // This function is overridden by internal teams to remove a tooltip on SRG completion - treat this function as a public API for the sake of function name/parameter changes _endSelfRevealGesture: function ItemEventsHandler_endSelfRevealGesture() { if (this._selfRevealGesture) { this._selfRevealGesture.finishAnimation(); this._selfRevealGesture = null; } }, _prepareItem: function ItemEventsHandler_prepareItem(pressedEntity, pressedElement, selected) { if (pressedEntity.type === _UI.ObjectType.groupHeader) { return; } var that = this, site = this._site, pressedIndex = pressedEntity.index; function addSwipeClass(container) { if (!that._swipeClassTracker[uniqueID(container)]) { _ElementUtilities.addClass(container, _Constants._swipeClass); that._swipeClassTracker[uniqueID(container)] = 1; } else { that._swipeClassTracker[uniqueID(container)]++; } } if (!selected) { (this._animations[pressedIndex] || Promise.wrap()).then(function () { if (!site.isZombie() && pressedEntity.type !== _UI.ObjectType.groupHeader && site.pressedEntity.index !== -1) { pressedIndex = site.pressedEntity.index; var pressedElement = site.itemAtIndex(pressedIndex), itemBox = site.itemBoxAtIndex(pressedIndex), container = site.containerAtIndex(pressedIndex); addSwipeClass(container); if (!_ElementUtilities._isSelectionRendered(itemBox)) { ItemEventsHandler.renderSelection(itemBox, pressedElement, true, container); _ElementUtilities.removeClass(itemBox, _Constants._selectedClass); _ElementUtilities.removeClass(container, _Constants._selectedClass); var nodes = itemBox.querySelectorAll(_ElementUtilities._selectionPartsSelector); for (var i = 0, len = nodes.length; i < len; i++) { nodes[i].style.opacity = 0; } } } }); } else { var container = site.containerAtIndex(pressedIndex); addSwipeClass(container); } }, _clearItem: function ItemEventsHandler_clearItem(pressedEntity, selected) { if (pressedEntity.type !== _UI.ObjectType.item) { return; } var that = this, site = this._site, container = site.containerAtIndex(pressedEntity.index), itemBox = site.itemBoxAtIndex(pressedEntity.index), element = site.itemAtIndex(pressedEntity.index); function removeSwipeClass(container) { var refCount = --that._swipeClassTracker[uniqueID(container)]; if (!refCount) { delete that._swipeClassTracker[uniqueID(container)]; _ElementUtilities.removeClass(container, _Constants._swipeClass); return true; } return false; } function removeSwipeFromItemsBlock(container) { var itemsBlock = container.parentNode; if (itemsBlock && _ElementUtilities.hasClass(itemsBlock, _Constants._itemsBlockClass)) { removeSwipeClass(itemsBlock); } } if (container && itemBox && element) { var doneSwiping = removeSwipeClass(container); removeSwipeFromItemsBlock(container); if (doneSwiping) { ItemEventsHandler.renderSelection(itemBox, element, selected, true, container); } } }, _animateSelectionChange: function ItemEventsHandler_animateSelectionChange(select, includeCheckmark) { var that = this, pressedContainer = this._site.pressedContainer, pressedItemBox = this._site.pressedItemBox; function toggleClasses() { var classOperation = select ? "addClass" : "removeClass"; _ElementUtilities[classOperation](pressedItemBox, _Constants._selectedClass); _ElementUtilities[classOperation](pressedContainer, _Constants._selectedClass); if (that._selectionHint) { var hintCheckMark = getElementWithClass(that._selectionHint, _Constants._selectionHintClass); if (hintCheckMark) { _ElementUtilities[classOperation](hintCheckMark, _Constants._revealedClass); } } } this._swipeBehaviorSelectionChanged = true; this.swipeBehaviorSelected = select; var elementsToShowHide = _ElementUtilities._getElementsByClasses(this._site.pressedItemBox, [_Constants._selectionBorderClass, _Constants._selectionBackgroundClass]); if (!select || includeCheckmark) { elementsToShowHide = elementsToShowHide.concat(_ElementUtilities._getElementsByClasses(this._site.pressedItemBox, [_Constants._selectionCheckmarkBackgroundClass, _Constants._selectionCheckmarkClass])); } _WriteProfilerMark("WinJS.UI._ItemEventsHandler:" + (select ? "hitSelectThreshold" : "hitUnselectThreshold") + ",info"); this._applyUIInBatches(function () { _WriteProfilerMark("WinJS.UI._ItemEventsHandler:" + (select ? "apply" : "remove") + "SelectionVisual,info"); var opacity = (select ? 1 : 0); for (var i = 0; i < elementsToShowHide.length; i++) { elementsToShowHide[i].style.opacity = opacity; } toggleClasses(); }); }, _showSelectionHintCheckmark: function ItemEventsHandler_showSelectionHintCheckmark() { if (this._selectionHint) { var hintCheckMark = getElementWithClass(this._selectionHint, _Constants._selectionHintClass); if (hintCheckMark) { hintCheckMark.style.display = 'block'; } } }, _hideSelectionHintCheckmark: function ItemEventsHandler_hideSelectionHintCheckmark() { if (this._selectionHint) { var hintCheckMark = getElementWithClass(this._selectionHint, _Constants._selectionHintClass); if (hintCheckMark) { hintCheckMark.style.display = 'none'; } } }, _addSelectionHint: function ItemEventsHandler_addSelectionHint() { if (this._site.pressedEntity.type === _UI.ObjectType.groupHeader) { return; } var selectionHint, site = this._site; if (site.customFootprintParent) { selectionHint = this._selectionHint = _Global.document.createElement("div"); selectionHint.className = _Constants._containerClass; var that = this; site.getItemPosition(this._site.pressedEntity).then(function (pos) { if (!site.isZombie() && that._selectionHint && that._selectionHint === selectionHint) { var style = selectionHint.style; var cssText = ";position:absolute;" + (site.rtl() ? "right:" : "left:") + pos.left + "px;top:" + pos.top + "px;width:" + pos.contentWidth + "px;height:" + pos.contentHeight + "px"; style.cssText += cssText; site.customFootprintParent.insertBefore(that._selectionHint, that._site.pressedItemBox); } }, function () { // Swallow errors in case data source changes }); } else { selectionHint = this._selectionHint = this._site.pressedContainer; } if (!this._selectionHintTracker[uniqueID(selectionHint)]) { _ElementUtilities.addClass(selectionHint, _Constants._footprintClass); if (!site.selection._isIncluded(this._site.pressedEntity.index)) { var element = _Global.document.createElement("div"); element.className = _Constants._selectionHintClass; element.textContent = _Constants._SELECTION_CHECKMARK; element.style.display = 'none'; this._selectionHint.insertBefore(element, this._selectionHint.firstElementChild); } this._selectionHintTracker[uniqueID(selectionHint)] = 1; } else { this._selectionHintTracker[uniqueID(selectionHint)]++; } }, _removeSelectionHint: function ItemEventsHandler_removeSelectionHint(selectionHint) { if (selectionHint) { var refCount = --this._selectionHintTracker[uniqueID(selectionHint)]; if (!refCount) { delete this._selectionHintTracker[uniqueID(selectionHint)]; if (!this._site.customFootprintParent) { _ElementUtilities.removeClass(selectionHint, _Constants._footprintClass); var hintCheckMark = getElementWithClass(selectionHint, _Constants._selectionHintClass); if (hintCheckMark) { hintCheckMark.parentNode.removeChild(hintCheckMark); } } else if (selectionHint.parentNode) { selectionHint.parentNode.removeChild(selectionHint); } } } }, _releasedElement: function ItemEventsHandler_releasedElement(eventObject) { return _Global.document.elementFromPoint(eventObject.clientX, eventObject.clientY); }, _applyUIInBatches: function ItemEventsHandler_applyUIInBatches(work) { var that = this; this._work.push(work); if (!this._paintedThisFrame) { applyUI(); } function applyUI() { if (that._work.length > 0) { that._flushUIBatches(); that._paintedThisFrame = _BaseUtils._requestAnimationFrame(applyUI.bind(that)); } else { that._paintedThisFrame = null; } } }, _flushUIBatches: function ItemEventsHandler_flushUIBatches() { if (this._work.length > 0) { var workItems = this._work; this._work = []; for (var i = 0; i < workItems.length; i++) { workItems[i](); } } }, _selectionAllowed: function ItemEventsHandler_selectionAllowed(itemIndex) { var item = (itemIndex !== undefined ? this._site.itemAtIndex(itemIndex) : null), itemSelectable = !(item && _ElementUtilities.hasClass(item, _Constants._nonSelectableClass)); return itemSelectable && this._site.selectionMode !== _UI.SelectionMode.none; }, _multiSelection: function ItemEventsHandler_multiSelection() { return this._site.selectionMode === _UI.SelectionMode.multi; }, _selectOnTap: function ItemEventsHandler_selectOnTap() { return this._site.tapBehavior === _UI.TapBehavior.toggleSelect || this._site.tapBehavior === _UI.TapBehavior.directSelect; }, _staticMode: function ItemEventsHandler_staticMode(isHeader) { if (isHeader) { return this._site.headerTapBehavior === _UI.GroupHeaderTapBehavior.none; } else { return this._site.tapBehavior === _UI.TapBehavior.none && this._site.selectionMode === _UI.SelectionMode.none; } }, }, { // Avoids unnecessary UIA selection events by only updating aria-selected if it has changed setAriaSelected: function ItemEventsHandler_setAriaSelected(itemElement, isSelected) { var ariaSelected = (itemElement.getAttribute("aria-selected") === "true"); if (isSelected !== ariaSelected) { itemElement.setAttribute("aria-selected", isSelected); } }, renderSelection: function ItemEventsHandler_renderSelection(itemBox, element, selected, aria, container) { if (!ItemEventsHandler._selectionTemplate) { ItemEventsHandler._selectionTemplate = []; ItemEventsHandler._selectionTemplate.push(createNodeWithClass(_Constants._selectionBackgroundClass)); ItemEventsHandler._selectionTemplate.push(createNodeWithClass(_Constants._selectionBorderClass)); ItemEventsHandler._selectionTemplate.push(createNodeWithClass(_Constants._selectionCheckmarkBackgroundClass)); var checkmark = createNodeWithClass(_Constants._selectionCheckmarkClass); checkmark.textContent = _Constants._SELECTION_CHECKMARK; ItemEventsHandler._selectionTemplate.push(checkmark); } // Update the selection rendering if necessary if (selected !== _ElementUtilities._isSelectionRendered(itemBox)) { if (selected) { itemBox.insertBefore(ItemEventsHandler._selectionTemplate[0].cloneNode(true), itemBox.firstElementChild); for (var i = 1, len = ItemEventsHandler._selectionTemplate.length; i < len; i++) { itemBox.appendChild(ItemEventsHandler._selectionTemplate[i].cloneNode(true)); } } else { var nodes = itemBox.querySelectorAll(_ElementUtilities._selectionPartsSelector); for (var i = 0, len = nodes.length; i < len; i++) { itemBox.removeChild(nodes[i]); } } _ElementUtilities[selected ? "addClass" : "removeClass"](itemBox, _Constants._selectedClass); if (container) { _ElementUtilities[selected ? "addClass" : "removeClass"](container, _Constants._selectedClass); } } // To allow itemPropertyChange to work properly, aria needs to be updated after the selection visuals are added to the itemBox if (aria) { ItemEventsHandler.setAriaSelected(element, selected); } }, }); return ItemEventsHandler; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ListView/_SelectionManager',[ 'exports', '../../Core/_Global', '../../Core/_Base', '../../Promise', '../../_Signal', '../../Utilities/_UI', '../ItemContainer/_Constants' ], function selectionManagerInit(exports, _Global, _Base, Promise, _Signal, _UI, _Constants) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _ItemSet: _Base.Namespace._lazy(function () { var _ItemSet = _Base.Class.define(function _ItemSet_ctor(listView, ranges, count) { this._listView = listView; this._ranges = ranges; this._itemsCount = count; }); _ItemSet.prototype = { getRanges: function () { var ranges = []; for (var i = 0, len = this._ranges.length; i < len; i++) { var range = this._ranges[i]; ranges.push({ firstIndex: range.firstIndex, lastIndex: range.lastIndex, firstKey: range.firstKey, lastKey: range.lastKey }); } return ranges; }, getItems: function () { return exports.getItemsFromRanges(this._listView._itemsManager.dataSource, this._ranges); }, isEverything: function () { return this.count() === this._itemsCount; }, count: function () { var count = 0; for (var i = 0, len = this._ranges.length; i < len; i++) { var range = this._ranges[i]; count += range.lastIndex - range.firstIndex + 1; } return count; }, getIndices: function () { var indices = []; for (var i = 0, len = this._ranges.length; i < len; i++) { var range = this._ranges[i]; for (var n = range.firstIndex; n <= range.lastIndex; n++) { indices.push(n); } } return indices; } }; return _ItemSet; }), getItemsFromRanges: function (dataSource, ranges) { var listBinding = dataSource.createListBinding(), promises = []; function getIndices() { var indices = []; for (var i = 0, len = ranges.length; i < len; i++) { var range = ranges[i]; for (var j = range.firstIndex; j <= range.lastIndex; j++) { indices.push(j); } } return Promise.wrap(indices); } return getIndices().then(function (indices) { for (var i = 0; i < indices.length; i++) { promises.push(listBinding.fromIndex(indices[i])); } return Promise.join(promises).then(function (items) { listBinding.release(); return items; }); }); }, _Selection: _Base.Namespace._lazy(function () { function isEverythingRange(ranges) { return ranges && ranges.firstIndex === 0 && ranges.lastIndex === Number.MAX_VALUE; } return _Base.Class.derive(exports._ItemSet, function (listView, indexesAndRanges) { this._listView = listView; this._itemsCount = -1; this._ranges = []; if (indexesAndRanges) { this.set(indexesAndRanges); } }, { clear: function () { /// /// /// Clears the selection. /// /// /// A Promise that is fulfilled when the clear operation completes. /// /// this._releaseRanges(this._ranges); this._ranges = []; return Promise.wrap(); }, set: function (items) { /// /// /// Clears the current selection and replaces it with the specified items. /// /// /// The indexes or keys of the items that make up the selection. /// You can provide different types of objects for the items parameter: /// you can specify an index, a key, or a range of indexes. /// It can also be an array that contains one or more of these objects. /// /// /// A Promise that is fulfilled when the operation completes. /// /// // A range with lastIndex set to Number.MAX_VALUE used to mean selectAll. Passing such range to set was equivalent to selectAll. This code preserves this behavior. if (!isEverythingRange(items)) { this._releaseRanges(this._ranges); this._ranges = []; var that = this; return this._execute("_set", items).then(function () { that._ranges.sort(function (left, right) { return left.firstIndex - right.firstIndex; }); return that._ensureKeys(); }).then(function () { return that._ensureCount(); }); } else { return this.selectAll(); } }, add: function (items) { /// /// /// Adds one or more items to the selection. /// /// /// The indexes or keys of the items to add. /// You can provide different types of objects for the items parameter: /// you can specify an index, a key, or a range of indexes. /// It can also be an array that contains one or more of these objects. /// /// /// A Promise that is fulfilled when the operation completes. /// /// if (!isEverythingRange(items)) { var that = this; return this._execute("_add", items).then(function () { return that._ensureKeys(); }).then(function () { return that._ensureCount(); }); } else { return this.selectAll(); } }, remove: function (items) { /// /// /// Removes the specified items from the selection. /// /// /// The indexes or keys of the items to remove. You can provide different types of objects for the items parameter: /// you can specify an index, a key, or a range of indexes. /// It can also be an array that contains one or more of these objects. /// /// /// A Promise that is fulfilled when the operation completes. /// /// var that = this; return this._execute("_remove", items).then(function () { return that._ensureKeys(); }); }, selectAll: function () { /// /// /// Adds all the items in the ListView to the selection. /// /// /// A Promise that is fulfilled when the operation completes. /// /// var that = this; return that._ensureCount().then(function () { if (that._itemsCount) { var range = { firstIndex: 0, lastIndex: that._itemsCount - 1, }; that._retainRange(range); that._releaseRanges(that._ranges); that._ranges = [range]; return that._ensureKeys(); } }); }, _execute: function (operation, items) { var that = this, keysSupported = !!that._getListBinding().fromKey, array = Array.isArray(items) ? items : [items], promises = [Promise.wrap()]; function toRange(type, first, last) { var retVal = {}; retVal["first" + type] = first; retVal["last" + type] = last; return retVal; } function handleKeys(range) { var binding = that._getListBinding(); var promise = Promise.join([binding.fromKey(range.firstKey), binding.fromKey(range.lastKey)]).then(function (items) { if (items[0] && items[1]) { range.firstIndex = items[0].index; range.lastIndex = items[1].index; that[operation](range); } return range; }); promises.push(promise); } for (var i = 0, len = array.length; i < len; i++) { var item = array[i]; if (typeof item === "number") { this[operation](toRange("Index", item, item)); } else if (item) { if (keysSupported && item.key !== undefined) { handleKeys(toRange("Key", item.key, item.key)); } else if (keysSupported && item.firstKey !== undefined && item.lastKey !== undefined) { handleKeys(toRange("Key", item.firstKey, item.lastKey)); } else if (item.index !== undefined && typeof item.index === "number") { this[operation](toRange("Index", item.index, item.index)); } else if (item.firstIndex !== undefined && item.lastIndex !== undefined && typeof item.firstIndex === "number" && typeof item.lastIndex === "number") { this[operation](toRange("Index", item.firstIndex, item.lastIndex)); } } } return Promise.join(promises); }, _set: function (range) { this._retainRange(range); this._ranges.push(range); }, _add: function (newRange) { var that = this, prev = null, range, inserted; var merge = function (left, right) { if (right.lastIndex > left.lastIndex) { left.lastIndex = right.lastIndex; left.lastKey = right.lastKey; if (left.lastPromise) { left.lastPromise.release(); } left.lastPromise = that._getListBinding().fromIndex(left.lastIndex).retain(); } }; var mergeWithPrev; for (var i = 0, len = this._ranges.length; i < len; i++) { range = this._ranges[i]; if (newRange.firstIndex < range.firstIndex) { mergeWithPrev = prev && newRange.firstIndex < (prev.lastIndex + 1); if (mergeWithPrev) { inserted = i - 1; merge(prev, newRange); } else { this._insertRange(i, newRange); inserted = i; } break; } else if (newRange.firstIndex === range.firstIndex) { merge(range, newRange); inserted = i; break; } prev = range; } if (inserted === undefined) { var last = this._ranges.length ? this._ranges[this._ranges.length - 1] : null, mergeWithLast = last && newRange.firstIndex < (last.lastIndex + 1); if (mergeWithLast) { merge(last, newRange); } else { this._retainRange(newRange); this._ranges.push(newRange); } } else { prev = null; for (i = inserted + 1, len = this._ranges.length; i < len; i++) { range = this._ranges[i]; if (newRange.lastIndex < range.firstIndex) { mergeWithPrev = prev && prev.lastIndex > newRange.lastIndex; if (mergeWithPrev) { merge(this._ranges[inserted], prev); } this._removeRanges(inserted + 1, i - inserted - 1); break; } else if (newRange.lastIndex === range.firstIndex) { merge(this._ranges[inserted], range); this._removeRanges(inserted + 1, i - inserted); break; } prev = range; } if (i >= len) { merge(this._ranges[inserted], this._ranges[len - 1]); this._removeRanges(inserted + 1, len - inserted - 1); } } }, _remove: function (toRemove) { var that = this; function retainPromise(index) { return that._getListBinding().fromIndex(index).retain(); } // This method is called when a range needs to be unselected. It is inspecting every range in the current selection comparing // it to the range which is being unselected and it is building an array of new selected ranges var ranges = []; for (var i = 0, len = this._ranges.length; i < len; i++) { var range = this._ranges[i]; if (range.lastIndex < toRemove.firstIndex || range.firstIndex > toRemove.lastIndex) { // No overlap with the unselected range ranges.push(range); } else if (range.firstIndex < toRemove.firstIndex && range.lastIndex >= toRemove.firstIndex && range.lastIndex <= toRemove.lastIndex) { // The end of this range is being unselected ranges.push({ firstIndex: range.firstIndex, firstKey: range.firstKey, firstPromise: range.firstPromise, lastIndex: toRemove.firstIndex - 1, lastPromise: retainPromise(toRemove.firstIndex - 1) }); range.lastPromise.release(); } else if (range.lastIndex > toRemove.lastIndex && range.firstIndex >= toRemove.firstIndex && range.firstIndex <= toRemove.lastIndex) { // The beginning of this range is being unselected ranges.push({ firstIndex: toRemove.lastIndex + 1, firstPromise: retainPromise(toRemove.lastIndex + 1), lastIndex: range.lastIndex, lastKey: range.lastKey, lastPromise: range.lastPromise }); range.firstPromise.release(); } else if (range.firstIndex < toRemove.firstIndex && range.lastIndex > toRemove.lastIndex) { // The middle part of this range is being unselected ranges.push({ firstIndex: range.firstIndex, firstKey: range.firstKey, firstPromise: range.firstPromise, lastIndex: toRemove.firstIndex - 1, lastPromise: retainPromise(toRemove.firstIndex - 1), }); ranges.push({ firstIndex: toRemove.lastIndex + 1, firstPromise: retainPromise(toRemove.lastIndex + 1), lastIndex: range.lastIndex, lastKey: range.lastKey, lastPromise: range.lastPromise }); } else { // The whole range is being unselected range.firstPromise.release(); range.lastPromise.release(); } } this._ranges = ranges; }, _ensureKeys: function () { var promises = [Promise.wrap()]; var that = this; var ensureKey = function (which, range) { var keyProperty = which + "Key"; if (!range[keyProperty]) { var promise = range[which + "Promise"]; promise.then(function (item) { if (item) { range[keyProperty] = item.key; } }); return promise; } else { return Promise.wrap(); } }; for (var i = 0, len = this._ranges.length; i < len; i++) { var range = this._ranges[i]; promises.push(ensureKey("first", range)); promises.push(ensureKey("last", range)); } Promise.join(promises).then(function () { that._ranges = that._ranges.filter(function (range) { return range.firstKey && range.lastKey; }); }); return Promise.join(promises); }, _mergeRanges: function (target, source) { target.lastIndex = source.lastIndex; target.lastKey = source.lastKey; }, _isIncluded: function (index) { if (this.isEverything()) { return true; } else { for (var i = 0, len = this._ranges.length; i < len; i++) { var range = this._ranges[i]; if (range.firstIndex <= index && index <= range.lastIndex) { return true; } } return false; } }, _ensureCount: function () { var that = this; return this._listView._itemsCount().then(function (count) { that._itemsCount = count; }); }, _insertRange: function (index, newRange) { this._retainRange(newRange); this._ranges.splice(index, 0, newRange); }, _removeRanges: function (index, howMany) { for (var i = 0; i < howMany; i++) { this._releaseRange(this._ranges[index + i]); } this._ranges.splice(index, howMany); }, _retainRange: function (range) { if (!range.firstPromise) { range.firstPromise = this._getListBinding().fromIndex(range.firstIndex).retain(); } if (!range.lastPromise) { range.lastPromise = this._getListBinding().fromIndex(range.lastIndex).retain(); } }, _retainRanges: function () { for (var i = 0, len = this._ranges.length; i < len; i++) { this._retainRange(this._ranges[i]); } }, _releaseRange: function (range) { range.firstPromise.release(); range.lastPromise.release(); }, _releaseRanges: function (ranges) { for (var i = 0, len = ranges.length; i < len; ++i) { this._releaseRange(ranges[i]); } }, _getListBinding: function () { return this._listView._itemsManager._listBinding; } }, { supportedForProcessing: false, }); }), // This component is responsible for holding selection state _SelectionManager: _Base.Namespace._lazy(function () { var _SelectionManager = function (listView) { this._listView = listView; this._selected = new exports._Selection(this._listView); // Don't rename this member. Some apps reference it. this._pivot = _Constants._INVALID_INDEX; this._focused = { type: _UI.ObjectType.item, index: 0 }; this._pendingChange = Promise.wrap(); }; _SelectionManager.prototype = { count: function () { /// /// /// Returns the number of items in the selection. /// /// /// The number of items in the selection. /// /// return this._selected.count(); }, getIndices: function () { /// /// /// Returns a list of the indexes for the items in the selection. /// /// /// The list of indexes for the items in the selection as an array of Number objects. /// /// return this._selected.getIndices(); }, getItems: function () { /// /// /// Returns an array that contains the items in the selection. /// /// /// A Promise that contains an array of the requested IItem objects. /// /// return this._selected.getItems(); }, getRanges: function () { /// /// /// Gets an array of the index ranges for the selected items. /// /// /// An array that contains an ISelectionRange object for each index range in the selection. /// /// return this._selected.getRanges(); }, isEverything: function () { /// /// /// Returns a value that indicates whether the selection contains every item in the data source. /// /// /// true if the selection contains every item in the data source; otherwise, false. /// /// return this._selected.isEverything(); }, set: function (items) { /// /// /// Clears the current selection and replaces it with the specified items. /// /// /// The indexes or keys of the items that make up the selection. /// You can provide different types of objects for the items parameter: /// you can specify an index, a key, or a range of indexes. /// It can also be an array that contains one or more of these objects. /// /// /// A Promise that is fulfilled when the operation completes. /// /// var that = this, signal = new _Signal(); return this._synchronize(signal).then(function () { var newSelection = new exports._Selection(that._listView); return newSelection.set(items).then( function () { that._set(newSelection); signal.complete(); }, function (error) { newSelection.clear(); signal.complete(); return Promise.wrapError(error); } ); }); }, clear: function () { /// /// /// Clears the selection. /// /// /// A Promise that is fulfilled when the clear operation completes. /// /// var that = this, signal = new _Signal(); return this._synchronize(signal).then(function () { var newSelection = new exports._Selection(that._listView); return newSelection.clear().then( function () { that._set(newSelection); signal.complete(); }, function (error) { newSelection.clear(); signal.complete(); return Promise.wrapError(error); } ); }); }, add: function (items) { /// /// /// Adds one or more items to the selection. /// /// /// The indexes or keys of the items to add. /// You can provide different types of objects for the items parameter: /// you can specify an index, a key, or a range of indexes. /// It can also be an array that contains one or more of these objects. /// /// /// A Promise that is fulfilled when the operation completes. /// /// var that = this, signal = new _Signal(); return this._synchronize(signal).then(function () { var newSelection = that._cloneSelection(); return newSelection.add(items).then( function () { that._set(newSelection); signal.complete(); }, function (error) { newSelection.clear(); signal.complete(); return Promise.wrapError(error); } ); }); }, remove: function (items) { /// /// /// Removes the specified items from the selection. /// /// /// The indexes or keys of the items to remove. You can provide different types of objects for the items parameter: /// you can specify an index, a key, or a range of indexes. /// It can also be an array that contains one or more of these objects. /// /// /// A Promise that is fulfilled when the operation completes. /// /// var that = this, signal = new _Signal(); return this._synchronize(signal).then(function () { var newSelection = that._cloneSelection(); return newSelection.remove(items).then( function () { that._set(newSelection); signal.complete(); }, function (error) { newSelection.clear(); signal.complete(); return Promise.wrapError(error); } ); }); }, selectAll: function () { /// /// /// Adds all the items in the ListView to the selection. /// /// /// A Promise that is fulfilled when the operation completes. /// /// var that = this, signal = new _Signal(); return this._synchronize(signal).then(function () { var newSelection = new exports._Selection(that._listView); return newSelection.selectAll().then( function () { that._set(newSelection); signal.complete(); }, function (error) { newSelection.clear(); signal.complete(); return Promise.wrapError(error); } ); }); }, _synchronize: function (signal) { var that = this; return this._listView._versionManager.unlocked.then(function () { var currentPendingChange = that._pendingChange; that._pendingChange = Promise.join([currentPendingChange, signal.promise]).then(function () { }); return currentPendingChange; }); }, _reset: function () { this._pivot = _Constants._INVALID_INDEX; this._setFocused({ type: _UI.ObjectType.item, index: 0 }, this._keyboardFocused()); this._pendingChange.cancel(); this._pendingChange = Promise.wrap(); this._selected.clear(); this._selected = new exports._Selection(this._listView); }, _dispose: function () { this._selected.clear(); this._selected = null; this._listView = null; }, _set: function (newSelection) { var that = this; return this._fireSelectionChanging(newSelection).then(function (approved) { if (approved) { that._selected.clear(); that._selected = newSelection; that._listView._updateSelection(); that._fireSelectionChanged(); } else { newSelection.clear(); } return approved; }); }, _fireSelectionChanging: function (newSelection) { var eventObject = _Global.document.createEvent("CustomEvent"), newSelectionUpdated = Promise.wrap(); eventObject.initCustomEvent("selectionchanging", true, true, { newSelection: newSelection, preventTapBehavior: function () { }, setPromise: function (promise) { /// /// /// Used to inform the ListView that asynchronous work is being performed, and that this /// event handler should not be considered complete until the promise completes. /// /// /// The promise to wait for. /// /// newSelectionUpdated = promise; } }); var approved = this._listView._element.dispatchEvent(eventObject); return newSelectionUpdated.then(function () { return approved; }); }, _fireSelectionChanged: function () { var eventObject = _Global.document.createEvent("CustomEvent"); eventObject.initCustomEvent("selectionchanged", true, false, null); this._listView._element.dispatchEvent(eventObject); }, _getFocused: function () { return { type: this._focused.type, index: this._focused.index }; }, _setFocused: function (entity, keyboardFocused) { this._focused = { type: entity.type, index: entity.index }; this._focusedByKeyboard = keyboardFocused; }, _keyboardFocused: function () { return this._focusedByKeyboard; }, _updateCount: function (count) { this._selected._itemsCount = count; }, _isIncluded: function (index) { return this._selected._isIncluded(index); }, _cloneSelection: function () { var newSelection = new exports._Selection(this._listView); newSelection._ranges = this._selected.getRanges(); newSelection._itemsCount = this._selected._itemsCount; newSelection._retainRanges(); return newSelection; } }; _SelectionManager.supportedForProcessing = false; return _SelectionManager; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ListView/_BrowseMode',[ 'exports', '../../Core/_Global', '../../Core/_Base', '../../Core/_BaseUtils', '../../Animations', '../../Promise', '../../Utilities/_ElementUtilities', '../../Utilities/_UI', '../ItemContainer/_Constants', '../ItemContainer/_ItemEventsHandler', './_SelectionManager' ], function browseModeInit(exports, _Global, _Base, _BaseUtils, Animations, Promise, _ElementUtilities, _UI, _Constants, _ItemEventsHandler, _SelectionManager) { "use strict"; var transformName = _BaseUtils._browserStyleEquivalents["transform"].scriptName; // This component is responsible for handling input in Browse Mode. // When the user clicks on an item in this mode itemInvoked event is fired. _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _SelectionMode: _Base.Namespace._lazy(function () { function clampToRange(first, last, x) { return Math.max(first, Math.min(last, x)); } function dispatchKeyboardNavigating(element, oldEntity, newEntity) { var navigationEvent = _Global.document.createEvent("CustomEvent"); navigationEvent.initCustomEvent("keyboardnavigating", true, true, { oldFocus: oldEntity.index, oldFocusType: oldEntity.type, newFocus: newEntity.index, newFocusType: newEntity.type }); return element.dispatchEvent(navigationEvent); } var _SelectionMode = _Base.Class.define(function _SelectionMode_ctor(modeSite) { this.inboundFocusHandled = false; this._pressedContainer = null; this._pressedItemBox = null; this._pressedHeader = null; this._pressedEntity = { type: _UI.ObjectType.item, index: _Constants._INVALID_INDEX }; this._pressedPosition = null; this.initialize(modeSite); },{ _dispose: function () { if (this._itemEventsHandler) { this._itemEventsHandler.dispose(); } if (this._setNewFocusItemOffsetPromise) { this._setNewFocusItemOffsetPromise.cancel(); } }, initialize: function (modeSite) { this.site = modeSite; this._keyboardNavigationHandlers = {}; this._keyboardAcceleratorHandlers = {}; var site = this.site, that = this; this._itemEventsHandler = new _ItemEventsHandler._ItemEventsHandler(Object.create({ containerFromElement: function (element) { return site._view.items.containerFrom(element); }, indexForItemElement: function (element) { return site._view.items.index(element); }, indexForHeaderElement: function (element) { return site._groups.index(element); }, itemBoxAtIndex: function (index) { return site._view.items.itemBoxAt(index); }, itemAtIndex: function (index) { return site._view.items.itemAt(index); }, headerAtIndex: function (index) { return site._groups.group(index).header; }, headerFromElement: function (element) { return site._groups.headerFrom(element); }, containerAtIndex: function (index) { return site._view.items.containerAt(index); }, isZombie: function () { return site._isZombie(); }, getItemPosition: function (entity) { return site._getItemPosition(entity); }, rtl: function () { return site._rtl(); }, fireInvokeEvent: function (entity, itemElement) { return that._fireInvokeEvent(entity, itemElement); }, verifySelectionAllowed: function (index) { return that._verifySelectionAllowed(index); }, changeFocus: function (newFocus, skipSelection, ctrlKeyDown, skipEnsureVisible, keyboardFocused) { return site._changeFocus(newFocus, skipSelection, ctrlKeyDown, skipEnsureVisible, keyboardFocused); }, selectRange: function (firstIndex, lastIndex, additive) { return that._selectRange(firstIndex, lastIndex, additive); } }, { pressedEntity: { enumerable: true, get: function () { return that._pressedEntity; }, set: function (value) { that._pressedEntity = value; } }, pressedContainerScaleTransform: { enumerable: true, get: function () { return that._pressedContainerScaleTransform; }, set: function (value) { that._pressedContainerScaleTransform = value; } }, pressedContainer: { enumerable: true, get: function () { return that._pressedContainer; }, set: function (value) { that._pressedContainer = value; } }, pressedItemBox: { enumerable: true, get: function () { return that._pressedItemBox; }, set: function (value) { that._pressedItemBox = value; } }, pressedHeader: { enumerable: true, get: function () { return that._pressedHeader; }, set: function (value) { return that._pressedHeader = value; } }, pressedPosition: { enumerable: true, get: function () { return that._pressedPosition; }, set: function (value) { that._pressedPosition = value; } }, pressedElement: { enumerable: true, set: function (value) { that._pressedElement = value; } }, swipeBehavior: { enumerable: true, get: function () { return site._swipeBehavior; } }, eventHandlerRoot: { enumerable: true, get: function () { return site._viewport; } }, selectionMode: { enumerable: true, get: function () { return site._selectionMode; } }, accessibleItemClass: { enumerable: true, get: function () { // CSS class of the element with the aria role return _Constants._itemClass; } }, canvasProxy: { enumerable: true, get: function () { return site._canvasProxy; } }, tapBehavior: { enumerable: true, get: function () { return site._tap; } }, headerTapBehavior: { enumerable: true, get: function () { return site._groupHeaderTap; } }, draggable: { enumerable: true, get: function () { return site.itemsDraggable || site.itemsReorderable; } }, selection: { enumerable: true, get: function () { return site._selection; } }, horizontal: { enumerable: true, get: function () { return site._horizontal(); } }, customFootprintParent: { enumerable: true, get: function () { return null; } } })); function createArrowHandler(direction, clampToBounds) { var handler = function (oldFocus) { return modeSite._view.getAdjacent(oldFocus, direction); }; handler.clampToBounds = clampToBounds; return handler; } var Key = _ElementUtilities.Key; this._keyboardNavigationHandlers[Key.upArrow] = createArrowHandler(Key.upArrow); this._keyboardNavigationHandlers[Key.downArrow] = createArrowHandler(Key.downArrow); this._keyboardNavigationHandlers[Key.leftArrow] = createArrowHandler(Key.leftArrow); this._keyboardNavigationHandlers[Key.rightArrow] = createArrowHandler(Key.rightArrow); this._keyboardNavigationHandlers[Key.pageUp] = createArrowHandler(Key.pageUp, true); this._keyboardNavigationHandlers[Key.pageDown] = createArrowHandler(Key.pageDown, true); this._keyboardNavigationHandlers[Key.home] = function (oldFocus) { return Promise.wrap({ type: oldFocus.type, index: 0 }); }; this._keyboardNavigationHandlers[Key.end] = function (oldFocus) { if (oldFocus.type === _UI.ObjectType.groupHeader) { return Promise.wrap({ type: oldFocus.type, index: site._groups.length() - 1 }); } else { // Get the index of the last container return that.site._view.finalItem().then(function (index) { return { type: oldFocus.type, index: index }; }, function (error) { return Promise.wrapError(error); }); } }; this._keyboardAcceleratorHandlers[Key.a] = function () { if (that.site._multiSelection()) { that._selectAll(); } }; }, staticMode: function SelectionMode_staticMode() { return this.site._tap === _UI.TapBehavior.none && this.site._selectionMode === _UI.SelectionMode.none; }, itemUnrealized: function SelectionMode_itemUnrealized(index, itemBox) { if (this._pressedEntity.type === _UI.ObjectType.groupHeader) { return; } if (this._pressedEntity.index === index) { this._resetPointerDownState(); } if (this._itemBeingDragged(index)) { for (var i = this._draggedItemBoxes.length - 1; i >= 0; i--) { if (this._draggedItemBoxes[i] === itemBox) { _ElementUtilities.removeClass(itemBox, _Constants._dragSourceClass); this._draggedItemBoxes.splice(i, 1); } } } }, _fireInvokeEvent: function SelectionMode_fireInvokeEvent(entity, itemElement) { if (!itemElement) { return; } var that = this; function fireInvokeEventImpl(dataSource, isHeader) { var listBinding = dataSource.createListBinding(), promise = listBinding.fromIndex(entity.index), eventName = isHeader ? "groupheaderinvoked" : "iteminvoked"; promise.done(function () { listBinding.release(); }); var eventObject = _Global.document.createEvent("CustomEvent"); eventObject.initCustomEvent(eventName, true, true, isHeader ? { groupHeaderPromise: promise, groupHeaderIndex: entity.index } : { itemPromise: promise, itemIndex: entity.index }); // If preventDefault was not called, call the default action on the site if (itemElement.dispatchEvent(eventObject)) { that.site._defaultInvoke(entity); } } if (entity.type === _UI.ObjectType.groupHeader) { if (this.site._groupHeaderTap === _UI.GroupHeaderTapBehavior.invoke && entity.index !== _Constants._INVALID_INDEX) { fireInvokeEventImpl(this.site.groupDataSource, true); } } else { if (this.site._tap !== _UI.TapBehavior.none && entity.index !== _Constants._INVALID_INDEX) { fireInvokeEventImpl(this.site.itemDataSource, false); } } }, _verifySelectionAllowed: function SelectionMode_verifySelectionAllowed(entity) { if (entity.type === _UI.ObjectType.groupHeader) { return { canSelect: false, canTapSelect: false }; } var itemIndex = entity.index; var site = this.site; var item = this.site._view.items.itemAt(itemIndex); if (site._selectionAllowed() && (site._selectOnTap() || site._swipeBehavior === _UI.SwipeBehavior.select) && !(item && _ElementUtilities.hasClass(item, _Constants._nonSelectableClass))) { var selected = site._selection._isIncluded(itemIndex), single = !site._multiSelection(), newSelection = site._selection._cloneSelection(); if (selected) { if (single) { newSelection.clear(); } else { newSelection.remove(itemIndex); } } else { if (single) { newSelection.set(itemIndex); } else { newSelection.add(itemIndex); } } var eventObject = _Global.document.createEvent("CustomEvent"), newSelectionUpdated = Promise.wrap(), completed = false, preventTap = false, included; eventObject.initCustomEvent("selectionchanging", true, true, { newSelection: newSelection, preventTapBehavior: function () { preventTap = true; }, setPromise: function (promise) { /// /// /// Used to inform the ListView that asynchronous work is being performed, and that this /// event handler should not be considered complete until the promise completes. /// /// /// The promise to wait for. /// /// newSelectionUpdated = promise; } }); var defaultBehavior = site._element.dispatchEvent(eventObject); newSelectionUpdated.then(function () { completed = true; included = newSelection._isIncluded(itemIndex); newSelection.clear(); }); var canSelect = defaultBehavior && completed && (selected || included); return { canSelect: canSelect, canTapSelect: canSelect && !preventTap }; } else { return { canSelect: false, canTapSelect: false }; } }, _containedInElementWithClass: function SelectionMode_containedInElementWithClass(element, className) { if (element.parentNode) { var matches = element.parentNode.querySelectorAll("." + className + ", ." + className + " *"); for (var i = 0, len = matches.length; i < len; i++) { if (matches[i] === element) { return true; } } } return false; }, _isDraggable: function SelectionMode_isDraggable(element) { return (!this._containedInElementWithClass(element, _Constants._nonDraggableClass)); }, _isInteractive: function SelectionMode_isInteractive(element) { return this._containedInElementWithClass(element, "win-interactive"); }, _resetPointerDownState: function SelectionMode_resetPointerDownState() { this._itemEventsHandler.resetPointerDownState(); }, onMSManipulationStateChanged: function (eventObject) { this._itemEventsHandler.onMSManipulationStateChanged(eventObject); }, onPointerDown: function SelectionMode_onPointerDown(eventObject) { this._itemEventsHandler.onPointerDown(eventObject); }, onclick: function SelectionMode_onclick(eventObject) { this._itemEventsHandler.onClick(eventObject); }, onPointerUp: function SelectionMode_onPointerUp(eventObject) { this._itemEventsHandler.onPointerUp(eventObject); }, onPointerCancel: function SelectionMode_onPointerCancel(eventObject) { this._itemEventsHandler.onPointerCancel(eventObject); }, onLostPointerCapture: function SelectionMode_onLostPointerCapture(eventObject) { this._itemEventsHandler.onLostPointerCapture(eventObject); }, onContextMenu: function SelectionMode_onContextMenu(eventObject) { this._itemEventsHandler.onContextMenu(eventObject); }, onMSHoldVisual: function SelectionMode_onMSHoldVisual(eventObject) { this._itemEventsHandler.onMSHoldVisual(eventObject); }, onDataChanged: function SelectionMode_onDataChanged(eventObject) { this._itemEventsHandler.onDataChanged(eventObject); }, _removeTransform: function SelectionMode_removeTransform(element, transform) { if (transform && element.style[transformName].indexOf(transform) !== -1) { element.style[transformName] = element.style[transformName].replace(transform, ""); } }, _selectAll: function SelectionMode_selectAll() { var unselectableRealizedItems = []; this.site._view.items.each(function (index, item) { if (item && _ElementUtilities.hasClass(item, _Constants._nonSelectableClass)) { unselectableRealizedItems.push(index); } }); this.site._selection.selectAll(); if (unselectableRealizedItems.length > 0) { this.site._selection.remove(unselectableRealizedItems); } }, _selectRange: function SelectionMode_selectRange(firstIndex, lastIndex, additive) { var ranges = []; var currentStartRange = -1; for (var i = firstIndex; i <= lastIndex; i++) { var item = this.site._view.items.itemAt(i); if (item && _ElementUtilities.hasClass(item, _Constants._nonSelectableClass)) { if (currentStartRange !== -1) { ranges.push({ firstIndex: currentStartRange, lastIndex: i - 1 }); currentStartRange = -1; } } else if (currentStartRange === -1) { currentStartRange = i; } } if (currentStartRange !== -1) { ranges.push({ firstIndex: currentStartRange, lastIndex: lastIndex }); } if (ranges.length > 0) { this.site._selection[additive ? "add" : "set"](ranges); } }, onDragStart: function SelectionMode_onDragStart(eventObject) { this._pressedEntity = { type: _UI.ObjectType.item, index: this.site._view.items.index(eventObject.target) }; this.site._selection._pivot = _Constants._INVALID_INDEX; // Drag shouldn't be initiated when the user holds down the mouse on a win-interactive element and moves. // The problem is that the dragstart event's srcElement+target will both be an itembox (which has draggable=true), so we can't check for win-interactive in the dragstart event handler. // The itemEventsHandler sets our _pressedElement field on MSPointerDown, so we use that instead when checking for interactive. if (this._pressedEntity.index !== _Constants._INVALID_INDEX && (this.site.itemsDraggable || this.site.itemsReorderable) && !this.site._view.animating && this._isDraggable(eventObject.target) && (!this._pressedElement || !this._isInteractive(this._pressedElement))) { this._dragging = true; this._dragDataTransfer = eventObject.dataTransfer; this._pressedPosition = _ElementUtilities._getCursorPos(eventObject); this._dragInfo = null; this._lastEnteredElement = eventObject.target; if (this.site._selection._isIncluded(this._pressedEntity.index)) { this._dragInfo = this.site.selection; } else { this._draggingUnselectedItem = true; this._dragInfo = new _SelectionManager._Selection(this.site, [{ firstIndex: this._pressedEntity.index, lastIndex: this._pressedEntity.index }]); } var dropTarget = this.site.itemsReorderable; var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent("itemdragstart", true, false, { dataTransfer: eventObject.dataTransfer, dragInfo: this._dragInfo }); // Firefox requires setData to be called on the dataTransfer object in order for DnD to continue. // Firefox also has an issue rendering the item's itemBox+element, so we need to use setDragImage, using the item's container, to get it to render. eventObject.dataTransfer.setData("text", ""); if (eventObject.dataTransfer.setDragImage) { var pressedItemData = this.site._view.items.itemDataAt(this._pressedEntity.index); if (pressedItemData && pressedItemData.container) { var rect = pressedItemData.container.getBoundingClientRect(); eventObject.dataTransfer.setDragImage(pressedItemData.container, eventObject.clientX - rect.left, eventObject.clientY - rect.top); } } this.site.element.dispatchEvent(event); if (this.site.itemsDraggable && !this.site.itemsReorderable) { if (!this._firedDragEnter) { if (this._fireDragEnterEvent(eventObject.dataTransfer)) { dropTarget = true; this._dragUnderstood = true; } } } if (dropTarget) { this._addedDragOverClass = true; _ElementUtilities.addClass(this.site._element, _Constants._dragOverClass); } this._draggedItemBoxes = []; var that = this; // A dragged element can be removed from the DOM by a number of actions - datasource removes/changes, being scrolled outside of the realized range, etc. // The dragend event is fired on the original source element of the drag. If that element isn't in the DOM, though, the dragend event will only be fired on the element // itself and not bubble up through the ListView's tree to the _viewport element where all the other drag event handlers are. // The dragend event handler has to be added to the event's srcElement so that we always receive the event, even when the source element is unrealized. var sourceElement = eventObject.target; sourceElement.addEventListener("dragend", function itemDragEnd(eventObject) { sourceElement.removeEventListener("dragend", itemDragEnd); that.onDragEnd(eventObject); }); // We delay setting the opacity of the dragged items so that IE has time to create a thumbnail before me make them invisible _BaseUtils._yieldForDomModification(function () { if (that._dragging) { var indicesSelected = that._dragInfo.getIndices(); for (var i = 0, len = indicesSelected.length; i < len; i++) { var itemData = that.site._view.items.itemDataAt(indicesSelected[i]); if (itemData && itemData.itemBox) { that._addDragSourceClass(itemData.itemBox); } } } }); } else { eventObject.preventDefault(); } }, onDragEnter: function (eventObject) { var eventHandled = this._dragUnderstood; this._lastEnteredElement = eventObject.target; if (this._exitEventTimer) { _Global.clearTimeout(this._exitEventTimer); this._exitEventTimer = 0; } if (!this._firedDragEnter) { if (this._fireDragEnterEvent(eventObject.dataTransfer)) { eventHandled = true; } } if (eventHandled || (this._dragging && this.site.itemsReorderable)) { eventObject.preventDefault(); this._dragUnderstood = true; if (!this._addedDragOverClass) { this._addedDragOverClass = true; _ElementUtilities.addClass(this.site._element, _Constants._dragOverClass); } } this._pointerLeftRegion = false; }, onDragLeave: function (eventObject) { if (eventObject.target === this._lastEnteredElement) { this._pointerLeftRegion = true; this._handleExitEvent(); } }, fireDragUpdateEvent: function () { var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent("itemdragchanged", true, false, { dataTransfer: this._dragDataTransfer, dragInfo: this._dragInfo }); this.site.element.dispatchEvent(event); }, _fireDragEnterEvent: function (dataTransfer) { var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent("itemdragenter", true, true, { dataTransfer: dataTransfer }); // The end developer must tell a ListView when a drag can be understood by calling preventDefault() on the event we fire var dropTarget = (!this.site.element.dispatchEvent(event)); this._firedDragEnter = true; return dropTarget; }, _fireDragBetweenEvent: function (index, insertAfterIndex, dataTransfer) { var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent("itemdragbetween", true, true, { index: index, insertAfterIndex: insertAfterIndex, dataTransfer: dataTransfer }); return this.site.element.dispatchEvent(event); }, _fireDropEvent: function (index, insertAfterIndex, dataTransfer) { var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent("itemdragdrop", true, true, { index: index, insertAfterIndex: insertAfterIndex, dataTransfer: dataTransfer }); return this.site.element.dispatchEvent(event); }, _handleExitEvent: function () { if (this._exitEventTimer) { _Global.clearTimeout(this._exitEventTimer); this._exitEventTimer = 0; } var that = this; this._exitEventTimer = _Global.setTimeout(function () { if (that.site._disposed) { return; } if (that._pointerLeftRegion) { that.site._layout.dragLeave && that.site._layout.dragLeave(); that._pointerLeftRegion = false; that._dragUnderstood = false; that._lastEnteredElement = null; that._lastInsertPoint = null; that._dragBetweenDisabled = false; if (that._firedDragEnter) { var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent("itemdragleave", true, false, { }); that.site.element.dispatchEvent(event); that._firedDragEnter = false; } if (that._addedDragOverClass) { that._addedDragOverClass = false; _ElementUtilities.removeClass(that.site._element, _Constants._dragOverClass); } that._exitEventTimer = 0; that._stopAutoScroll(); } }, 40); }, _getEventPositionInElementSpace: function (element, eventObject) { var elementRect = { left: 0, top: 0 }; try { elementRect = element.getBoundingClientRect(); } catch (err) { } var computedStyle = _Global.getComputedStyle(element, null), paddingLeft = parseInt(computedStyle["paddingLeft"]), paddingTop = parseInt(computedStyle["paddingTop"]), borderLeft = parseInt(computedStyle["borderLeftWidth"]), borderTop = parseInt(computedStyle["borderTopWidth"]), clientX = eventObject.clientX, clientY = eventObject.clientY; var position = { x: +clientX === clientX ? (clientX - elementRect.left - paddingLeft - borderLeft) : 0, y: +clientY === clientY ? (clientY - elementRect.top - paddingTop - borderTop) : 0 }; if (this.site._rtl()) { position.x = (elementRect.right - elementRect.left) - position.x; } return position; }, _getPositionInCanvasSpace: function (eventObject) { var scrollLeft = this.site._horizontal() ? this.site.scrollPosition : 0, scrollTop = this.site._horizontal() ? 0 : this.site.scrollPosition, position = this._getEventPositionInElementSpace(this.site.element, eventObject); return { x: position.x + scrollLeft, y: position.y + scrollTop }; }, _itemBeingDragged: function (itemIndex) { if (!this._dragging) { return false; } return ((this._draggingUnselectedItem && this._dragInfo._isIncluded(itemIndex)) || (!this._draggingUnselectedItem && this.site._isSelected(itemIndex))); }, _addDragSourceClass: function (itemBox) { this._draggedItemBoxes.push(itemBox); _ElementUtilities.addClass(itemBox, _Constants._dragSourceClass); if (itemBox.parentNode) { _ElementUtilities.addClass(itemBox.parentNode, _Constants._footprintClass); } }, renderDragSourceOnRealizedItem: function (itemIndex, itemBox) { if (this._itemBeingDragged(itemIndex)) { this._addDragSourceClass(itemBox); } }, onDragOver: function (eventObject) { if (!this._dragUnderstood) { return; } this._pointerLeftRegion = false; eventObject.preventDefault(); var cursorPositionInCanvas = this._getPositionInCanvasSpace(eventObject), cursorPositionInRoot = this._getEventPositionInElementSpace(this.site.element, eventObject); this._checkAutoScroll(cursorPositionInRoot.x, cursorPositionInRoot.y); if (this.site._layout.hitTest) { if (this._autoScrollFrame) { if (this._lastInsertPoint) { this.site._layout.dragLeave(); this._lastInsertPoint = null; } } else { var insertPoint = this.site._view.hitTest(cursorPositionInCanvas.x, cursorPositionInCanvas.y); insertPoint.insertAfterIndex = clampToRange(-1, this.site._cachedCount - 1, insertPoint.insertAfterIndex); if (!this._lastInsertPoint || this._lastInsertPoint.insertAfterIndex !== insertPoint.insertAfterIndex || this._lastInsertPoint.index !== insertPoint.index) { this._dragBetweenDisabled = !this._fireDragBetweenEvent(insertPoint.index, insertPoint.insertAfterIndex, eventObject.dataTransfer); if (!this._dragBetweenDisabled) { this.site._layout.dragOver(cursorPositionInCanvas.x, cursorPositionInCanvas.y, this._dragInfo); } else { this.site._layout.dragLeave(); } } this._lastInsertPoint = insertPoint; } } }, _clearDragProperties: function () { if (this._addedDragOverClass) { this._addedDragOverClass = false; _ElementUtilities.removeClass(this.site._element, _Constants._dragOverClass); } if (this._draggedItemBoxes) { for (var i = 0, len = this._draggedItemBoxes.length; i < len; i++) { _ElementUtilities.removeClass(this._draggedItemBoxes[i], _Constants._dragSourceClass); if (this._draggedItemBoxes[i].parentNode) { _ElementUtilities.removeClass(this._draggedItemBoxes[i].parentNode, _Constants._footprintClass); } } this._draggedItemBoxes = []; } this.site._layout.dragLeave(); this._dragging = false; this._dragInfo = null; this._draggingUnselectedItem = false; this._dragDataTransfer = null; this._lastInsertPoint = null; this._resetPointerDownState(); this._lastEnteredElement = null; this._dragBetweenDisabled = false; this._firedDragEnter = false; this._dragUnderstood = false; this._stopAutoScroll(); }, onDragEnd: function () { var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent("itemdragend", true, false, {}); this.site.element.dispatchEvent(event); this._clearDragProperties(); }, _findFirstAvailableInsertPoint: function (selectedItems, startIndex, searchForwards) { var indicesSelected = selectedItems.getIndices(), dropIndexInSelection = -1, count = this.site._cachedCount, selectionCount = indicesSelected.length, startIndexInSelection = -1, dropIndex = startIndex; for (var i = 0; i < selectionCount; i++) { if (indicesSelected[i] === dropIndex) { dropIndexInSelection = i; startIndexInSelection = i; break; } } while (dropIndexInSelection >= 0 && dropIndex >= 0) { if (searchForwards) { dropIndex++; if (dropIndexInSelection < selectionCount && indicesSelected[dropIndexInSelection + 1] === dropIndex && dropIndex < count) { dropIndexInSelection++; } else if (dropIndex >= count) { // If we hit the end of the list when looking for a new location ahead of our start index, it means everything from the starting point // to the end is selected, so no valid index can be located to move the items. We need to start searching again, moving backwards // from the starting location, to find the first available insert location to move the selected items. searchForwards = false; dropIndex = startIndex; dropIndexInSelection = startIndexInSelection; } else { dropIndexInSelection = -1; } } else { dropIndex--; if (dropIndexInSelection > 0 && indicesSelected[dropIndexInSelection - 1] === dropIndex) { dropIndexInSelection--; } else { dropIndexInSelection = -1; } } } return dropIndex; }, _reorderItems: function (dropIndex, reorderedItems, reorderingUnselectedItem, useMoveBefore, ensureVisibleAtEnd) { var site = this.site; var updateSelection = function updatedSelectionOnDrop(items) { // Update selection if the items were selected. If there is a range with length > 0 a move operation // on the first or last item removes the range. if (!reorderingUnselectedItem) { site._selection.set({ firstKey: items[0].key, lastKey: items[items.length - 1].key }); } else { site._selection.remove({ key: items[0].key }); } if (ensureVisibleAtEnd) { site.ensureVisible(site._selection._getFocused()); } }; reorderedItems.getItems().then(function (items) { var ds = site.itemDataSource; if (dropIndex === -1) { ds.beginEdits(); for (var i = items.length - 1; i >= 0; i--) { ds.moveToStart(items[i].key); } ds.endEdits(); updateSelection(items); } else { var listBinding = ds.createListBinding(); listBinding.fromIndex(dropIndex).then(function (item) { listBinding.release(); ds.beginEdits(); if (useMoveBefore) { for (var i = 0, len = items.length; i < len; i++) { ds.moveBefore(items[i].key, item.key); } } else { for (var i = items.length - 1; i >= 0; i--) { ds.moveAfter(items[i].key, item.key); } } ds.endEdits(); updateSelection(items); }); } }); }, onDrop: function SelectionMode_onDrop(eventObject) { // If the listview or the handler of the drop event we fire triggers a reorder, the dragged items can end up having different container nodes than what they started with. // Because of that, we need to remove the footprint class from the item boxes' containers before we do any processing of the drop event. if (this._draggedItemBoxes) { for (var i = 0, len = this._draggedItemBoxes.length; i < len; i++) { if (this._draggedItemBoxes[i].parentNode) { _ElementUtilities.removeClass(this._draggedItemBoxes[i].parentNode, _Constants._footprintClass); } } } if (!this._dragBetweenDisabled) { var cursorPosition = this._getPositionInCanvasSpace(eventObject); var dropLocation = this.site._view.hitTest(cursorPosition.x, cursorPosition.y), dropIndex = clampToRange(-1, this.site._cachedCount - 1, dropLocation.insertAfterIndex), allowDrop = true; // We don't fire dragBetween events during autoscroll, so if a user drops during autoscroll, we need to get up to date information // on the drop location, and fire dragBetween before the insert so that the developer can prevent the drop if they choose. if (!this._lastInsertPoint || this._lastInsertPoint.insertAfterIndex !== dropIndex || this._lastInsertPoint.index !== dropLocation.index) { allowDrop = this._fireDragBetweenEvent(dropLocation.index, dropIndex, eventObject.dataTransfer); } if (allowDrop) { this._lastInsertPoint = null; this.site._layout.dragLeave(); if (this._fireDropEvent(dropLocation.index, dropIndex, eventObject.dataTransfer) && this._dragging && this.site.itemsReorderable) { if (this._dragInfo.isEverything() || this.site._groupsEnabled()) { return; } dropIndex = this._findFirstAvailableInsertPoint(this._dragInfo, dropIndex, false); this._reorderItems(dropIndex, this._dragInfo, this._draggingUnselectedItem); } } } this._clearDragProperties(); eventObject.preventDefault(); }, _checkAutoScroll: function (x, y) { var viewportSize = this.site._getViewportLength(), horizontal = this.site._horizontal(), cursorPositionInViewport = (horizontal ? x : y), canvasSize = this.site._viewport[horizontal ? "scrollWidth" : "scrollHeight"], scrollPosition = Math.floor(this.site.scrollPosition), travelRate = 0; if (cursorPositionInViewport < _Constants._AUTOSCROLL_THRESHOLD) { travelRate = cursorPositionInViewport - _Constants._AUTOSCROLL_THRESHOLD; } else if (cursorPositionInViewport > (viewportSize - _Constants._AUTOSCROLL_THRESHOLD)) { travelRate = (cursorPositionInViewport - (viewportSize - _Constants._AUTOSCROLL_THRESHOLD)); } travelRate = Math.round((travelRate / _Constants._AUTOSCROLL_THRESHOLD) * (_Constants._MAX_AUTOSCROLL_RATE - _Constants._MIN_AUTOSCROLL_RATE)); // If we're at the edge of the content, we don't need to keep scrolling. We'll set travelRate to 0 to stop the autoscroll timer. if ((scrollPosition === 0 && travelRate < 0) || (scrollPosition >= (canvasSize - viewportSize) && travelRate > 0)) { travelRate = 0; } if (travelRate === 0) { if (this._autoScrollDelay) { _Global.clearTimeout(this._autoScrollDelay); this._autoScrollDelay = 0; } } else { if (!this._autoScrollDelay && !this._autoScrollFrame) { var that = this; this._autoScrollDelay = _Global.setTimeout(function () { if (that._autoScrollRate) { that._lastDragTimeout = _BaseUtils._now(); var nextFrame = function () { if ((!that._autoScrollRate && that._autoScrollFrame) || that.site._disposed) { that._stopAutoScroll(); } else { // Timeout callbacks aren't reliably timed, so extra math is needed to figure out how far the scroll position should move since the last callback var currentTime = _BaseUtils._now(); var delta = that._autoScrollRate * ((currentTime - that._lastDragTimeout) / 1000); delta = (delta < 0 ? Math.min(-1, delta) : Math.max(1, delta)); var newScrollPos = {}; newScrollPos[that.site._scrollProperty] = that.site._viewportScrollPosition + delta; _ElementUtilities.setScrollPosition(that.site._viewport, newScrollPos); that._lastDragTimeout = currentTime; that._autoScrollFrame = _BaseUtils._requestAnimationFrame(nextFrame); } }; that._autoScrollFrame = _BaseUtils._requestAnimationFrame(nextFrame); } }, _Constants._AUTOSCROLL_DELAY); } } this._autoScrollRate = travelRate; }, _stopAutoScroll: function () { if (this._autoScrollDelay) { _Global.clearTimeout(this._autoScrollDelay); this._autoScrollDelay = 0; } this._autoScrollRate = 0; this._autoScrollFrame = 0; }, onKeyDown: function SelectionMode_onKeyDown(eventObject) { var that = this, site = this.site, swipeEnabled = site._swipeBehavior === _UI.SwipeBehavior.select, view = site._view, oldEntity = site._selection._getFocused(), handled = true, ctrlKeyDown = eventObject.ctrlKey; function setNewFocus(newEntity, skipSelection, clampToBounds) { function setNewFocusImpl(maxIndex) { var moveView = true, invalidIndex = false; // Since getKeyboardNavigatedItem is purely geometry oriented, it can return us out of bounds numbers, so this check is necessary if (clampToBounds) { newEntity.index = Math.max(0, Math.min(maxIndex, newEntity.index)); } else if (newEntity.index < 0 || newEntity.index > maxIndex) { invalidIndex = true; } if (!invalidIndex && (oldEntity.index !== newEntity.index || oldEntity.type !== newEntity.type)) { var changeFocus = dispatchKeyboardNavigating(site._element, oldEntity, newEntity); if (changeFocus) { moveView = false; // If the oldEntity is completely off-screen then we mimic the desktop // behavior. This is consistent with navbar keyboarding. if (that._setNewFocusItemOffsetPromise) { that._setNewFocusItemOffsetPromise.cancel(); } site._batchViewUpdates(_Constants._ViewChange.realize, _Constants._ScrollToPriority.high, function () { that._setNewFocusItemOffsetPromise = site._getItemOffset(oldEntity, true).then(function (range) { range = site._convertFromCanvasCoordinates(range); var oldItemOffscreen = range.end <= site.scrollPosition || range.begin >= site.scrollPosition + site._getViewportLength() - 1; that._setNewFocusItemOffsetPromise = site._getItemOffset(newEntity).then(function (range) { that._setNewFocusItemOffsetPromise = null; var retVal = { position: site.scrollPosition, direction: "right" }; if (oldItemOffscreen) { // oldEntity is completely off-screen site._selection._setFocused(newEntity, true); range = site._convertFromCanvasCoordinates(range); if (newEntity.index > oldEntity.index) { retVal.direction = "right"; retVal.position = range.end - site._getViewportLength(); } else { retVal.direction = "left"; retVal.position = range.begin; } } site._changeFocus(newEntity, skipSelection, ctrlKeyDown, oldItemOffscreen, true); if (!oldItemOffscreen) { return Promise.cancel; } else { return retVal; } }, function (error) { site._changeFocus(newEntity, skipSelection, ctrlKeyDown, true, true); return Promise.wrapError(error); }); return that._setNewFocusItemOffsetPromise; }, function (error) { site._changeFocus(newEntity, skipSelection, ctrlKeyDown, true, true); return Promise.wrapError(error); }); return that._setNewFocusItemOffsetPromise; }, true); } } // When a key is pressed, we want to make sure the current focus is in view. If the keypress is changing to a new valid index, // _changeFocus will handle moving the viewport for us. If the focus isn't moving, though, we need to put the view back on // the current item ourselves and call setFocused(oldFocus, true) to make sure that the listview knows the focused item was // focused via keyboard and renders the rectangle appropriately. if (moveView) { site._selection._setFocused(oldEntity, true); site.ensureVisible(oldEntity); } if (invalidIndex) { return { type: _UI.ObjectType.item, index: _Constants._INVALID_INDEX }; } else { return newEntity; } } // We need to get the final item in the view so that we don't try setting focus out of bounds. if (newEntity.type !== _UI.ObjectType.groupHeader) { return view.finalItem().then(setNewFocusImpl); } else { return Promise.wrap(site._groups.length() - 1).then(setNewFocusImpl); } } var Key = _ElementUtilities.Key, keyCode = eventObject.keyCode, rtl = site._rtl(); if (!this._isInteractive(eventObject.target)) { if (eventObject.ctrlKey && !eventObject.altKey && !eventObject.shiftKey && this._keyboardAcceleratorHandlers[keyCode]) { this._keyboardAcceleratorHandlers[keyCode](); } if (site.itemsReorderable && (!eventObject.ctrlKey && eventObject.altKey && eventObject.shiftKey && oldEntity.type === _UI.ObjectType.item) && (keyCode === Key.leftArrow || keyCode === Key.rightArrow || keyCode === Key.upArrow || keyCode === Key.downArrow)) { var selection = site._selection, focusedIndex = oldEntity.index, movingUnselectedItem = false, processReorder = true; if (!selection.isEverything()) { if (!selection._isIncluded(focusedIndex)) { var item = site._view.items.itemAt(focusedIndex); // Selected items should never be marked as non draggable, so we only need to check for nonDraggableClass when trying to reorder an unselected item. if (item && _ElementUtilities.hasClass(item, _Constants._nonDraggableClass)) { processReorder = false; } else { movingUnselectedItem = true; selection = new _SelectionManager._Selection(this.site, [{ firstIndex: focusedIndex, lastIndex: focusedIndex }]); } } if (processReorder) { var dropIndex = focusedIndex; if (keyCode === Key.rightArrow) { dropIndex += (rtl ? -1 : 1); } else if (keyCode === Key.leftArrow) { dropIndex += (rtl ? 1 : -1); } else if (keyCode === Key.upArrow) { dropIndex--; } else { dropIndex++; } // If the dropIndex is larger than the original index, we're trying to move items forward, so the search for the first unselected item to insert after should move forward. var movingAhead = (dropIndex > focusedIndex), searchForward = movingAhead; if (movingAhead && dropIndex >= this.site._cachedCount) { // If we're at the end of the list and trying to move items forward, dropIndex should be >= cachedCount. // That doesn't mean we don't have to do any reordering, though. A selection could be broken down into // a few blocks. We need to make the selection contiguous after this reorder, so we've got to search backwards // to find the first unselected item, then move everything in the selection after it. searchForward = false; dropIndex = this.site._cachedCount - 1; } dropIndex = this._findFirstAvailableInsertPoint(selection, dropIndex, searchForward); dropIndex = Math.min(Math.max(-1, dropIndex), this.site._cachedCount - 1); var reportedInsertAfterIndex = dropIndex - (movingAhead || dropIndex === -1 ? 0 : 1), reportedIndex = dropIndex, groupsEnabled = this.site._groupsEnabled(); if (groupsEnabled) { // The indices we picked for the index/insertAfterIndex to report in our events is always correct in an ungrouped list, // and mostly correct in a grouped list. The only problem occurs when you move an item (or items) ahead into a new group, // or back into a previous group, such that the items should be the first/last in the group. Take this list as an example: // [Group A] [a] [b] [c] [Group B] [d] [e] // When [d] is focused, right/down arrow reports index: 4, insertAfterIndex: 4, which is right -- it means move [d] after [e]. // Similarily, when [c] is focused and left/up is pressed, we report index: 1, insertAfterIndex: 0 -- move [c] to after [a]. // Take note that index does not tell us where focus is / what item is being moved. // Like mouse/touch DnD, index tells us what the dragBetween slots would be were we to animate a dragBetween. // The problem cases are moving backwards into a previous group, or forward into the next group. // If [c] were focused and the user pressed right/down, we would report index: 3, insertAfterIndex: 3. In other words, move [c] after [d]. // That's not right at all - [c] needs to become the first element of [Group B]. When we're moving ahead, then, and our dropIndex // is the first index of a new group, we adjust insertAfterIndex to be dropIndex - 1. Now we'll report index:3, insertAfterIndex: 2, which means // [c] is now the first element of [Group B], rather than the last element of [Group A]. This is exactly the same as what we would report when // the user mouse/touch drags [c] right before [d]. // Similarily, when [d] is focused and we press left/up, without the logic below we would report index: 2, insertAfterIndex: 1, so we'd try to move // [d] ahead of [b]. Again, [d] first needs the opportunity to become the last element in [Group A], so we adjust the insertAfterIndex up by 1. // We then will report index:2, insertAfterIndex:2, meaning insert [d] in [Group A] after [c], which again mimics the mouse/touch API. var groups = this.site._groups, groupIndex = (dropIndex > -1 ? groups.groupFromItem(dropIndex) : 0); if (movingAhead) { if (groups.group(groupIndex).startIndex === dropIndex) { reportedInsertAfterIndex--; } } else if (groupIndex < (groups.length() - 1) && dropIndex === (groups.group(groupIndex + 1).startIndex - 1)) { reportedInsertAfterIndex++; } } if (this._fireDragBetweenEvent(reportedIndex, reportedInsertAfterIndex, null) && this._fireDropEvent(reportedIndex, reportedInsertAfterIndex, null)) { if (groupsEnabled) { return; } this._reorderItems(dropIndex, selection, movingUnselectedItem, !movingAhead, true); } } } } else if (!eventObject.altKey) { if (this._keyboardNavigationHandlers[keyCode]) { this._keyboardNavigationHandlers[keyCode](oldEntity).then(function (newEntity) { var clampToBounds = that._keyboardNavigationHandlers[keyCode].clampToBounds; if (newEntity.type !== _UI.ObjectType.groupHeader && eventObject.shiftKey && site._selectionAllowed() && site._multiSelection()) { // Shift selection should work when shift or shift+ctrl are depressed if (site._selection._pivot === _Constants._INVALID_INDEX) { site._selection._pivot = oldEntity.index; } setNewFocus(newEntity, true, clampToBounds).then(function (newEntity) { if (newEntity.index !== _Constants._INVALID_INDEX) { var firstIndex = Math.min(newEntity.index, site._selection._pivot), lastIndex = Math.max(newEntity.index, site._selection._pivot), additive = (eventObject.ctrlKey || site._tap === _UI.TapBehavior.toggleSelect); that._selectRange(firstIndex, lastIndex, additive); } }); } else { site._selection._pivot = _Constants._INVALID_INDEX; setNewFocus(newEntity, false, clampToBounds); } }); } else if (!eventObject.ctrlKey && keyCode === Key.enter) { var element = oldEntity.type === _UI.ObjectType.groupHeader ? site._groups.group(oldEntity.index).header : site._view.items.itemBoxAt(oldEntity.index); if (element) { if (oldEntity.type === _UI.ObjectType.groupHeader) { this._pressedHeader = element; this._pressedItemBox = null; this._pressedContainer = null; } else { this._pressedItemBox = element; this._pressedContainer = site._view.items.containerAt(oldEntity.index); this._pressedHeader = null; } var allowed = this._verifySelectionAllowed(oldEntity); if (allowed.canTapSelect) { this._itemEventsHandler.handleTap(oldEntity); } this._fireInvokeEvent(oldEntity, element); } } else if (oldEntity.type !== _UI.ObjectType.groupHeader && ((eventObject.ctrlKey && keyCode === Key.enter) || (swipeEnabled && eventObject.shiftKey && keyCode === Key.F10) || (swipeEnabled && keyCode === Key.menu) || keyCode === Key.space)) { // Swipe emulation this._itemEventsHandler.handleSwipeBehavior(oldEntity.index); site._changeFocus(oldEntity, true, ctrlKeyDown, false, true); } else if (keyCode === Key.escape && site._selection.count() > 0) { site._selection._pivot = _Constants._INVALID_INDEX; site._selection.clear(); } else { handled = false; } } else { handled = false; } this._keyDownHandled = handled; if (handled) { eventObject.stopPropagation(); eventObject.preventDefault(); } } if (keyCode === Key.tab) { this.site._keyboardFocusInbound = true; } }, onKeyUp: function (eventObject) { if (this._keyDownHandled) { eventObject.stopPropagation(); eventObject.preventDefault(); } }, onTabEntered: function (eventObject) { if (this.site._groups.length() === 0) { return; } var site = this.site, focused = site._selection._getFocused(), forward = eventObject.detail; // We establish whether focus is incoming on the ListView by checking keyboard focus and the srcElement. // If the ListView did not have keyboard focus, then it is definitely incoming since keyboard focus is cleared // on blur which works for 99% of all scenarios. When the ListView is the only tabbable element on the page, // then tabbing out of the ListView will make focus wrap around and focus the ListView again. The blur event is // handled after TabEnter, so the keyboard focus flag is not yet cleared. Therefore, we examine the srcElement and see // if it is the _viewport since it is the first tabbable element in the ListView DOM tree. var inboundFocus = !site._hasKeyboardFocus || eventObject.target === site._viewport; if (inboundFocus) { this.inboundFocusHandled = true; // We tabbed into the ListView focused.index = (focused.index === _Constants._INVALID_INDEX ? 0 : focused.index); if (forward || !this.site._supportsGroupHeaderKeyboarding) { // We tabbed into the ListView from before the ListView, so focus should go to items var entity = { type: _UI.ObjectType.item }; if (focused.type === _UI.ObjectType.groupHeader) { entity.index = site._groupFocusCache.getIndexForGroup(focused.index); if (dispatchKeyboardNavigating(site._element, focused, entity)) { site._changeFocus(entity, true, false, false, true); } else { site._changeFocus(focused, true, false, false, true); } } else { entity.index = focused.index; site._changeFocus(entity, true, false, false, true); } eventObject.preventDefault(); } else { // We tabbed into the ListView from after the ListView, focus should go to headers var entity = { type: _UI.ObjectType.groupHeader }; if (focused.type !== _UI.ObjectType.groupHeader) { entity.index = site._groups.groupFromItem(focused.index); if (dispatchKeyboardNavigating(site._element, focused, entity)) { site._changeFocus(entity, true, false, false, true); } else { site._changeFocus(focused, true, false, false, true); } } else { entity.index = focused.index; site._changeFocus(entity, true, false, false, true); } eventObject.preventDefault(); } } }, onTabExiting: function (eventObject) { if (!this.site._supportsGroupHeaderKeyboarding || this.site._groups.length() === 0) { return; } var site = this.site, focused = site._selection._getFocused(), forward = eventObject.detail; if (forward && focused.type !== _UI.ObjectType.groupHeader) { // Tabbing and we were focusing an item, go to headers var entity = { type: _UI.ObjectType.groupHeader, index: site._groups.groupFromItem(focused.index) }; if (dispatchKeyboardNavigating(site._element, focused, entity)) { site._changeFocus(entity, true, false, false, true); eventObject.preventDefault(); } } else if (!forward && focused.type === _UI.ObjectType.groupHeader) { // Shift tabbing and we were focusing a header, go to items var entity = { type: _UI.ObjectType.item, index: site._groupFocusCache.getIndexForGroup(focused.index) }; if (dispatchKeyboardNavigating(site._element, focused, entity)) { site._changeFocus(entity, true, false, false, true); eventObject.preventDefault(); } } } }); return _SelectionMode; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ListView/_ErrorMessages',[ 'exports', '../../Core/_Base', '../../Core/_Resources' ], function errorMessagesInit(exports, _Base, _Resources) { "use strict"; _Base.Namespace._moduleDefine(exports, null, { modeIsInvalid: { get: function () { return "Invalid argument: mode must be one of following values: 'none', 'single' or 'multi'."; } }, loadingBehaviorIsDeprecated: { get: function () { return "Invalid configuration: loadingBehavior is deprecated. The control will default this property to 'randomAccess'. Please refer to the 'ListView loading behaviors' SDK Sample for guidance on how to implement incremental load behavior."; } }, pagesToLoadIsDeprecated: { get: function () { return "Invalid configuration: pagesToLoad is deprecated. The control will not use this property. Please refer to the 'ListView loading behaviors' SDK Sample for guidance on how to implement incremental load behavior."; } }, pagesToLoadThresholdIsDeprecated: { get: function () { return "Invalid configuration: pagesToLoadThreshold is deprecated. The control will not use this property. Please refer to the 'ListView loading behaviors' SDK Sample for guidance on how to implement incremental load behavior."; } }, automaticallyLoadPagesIsDeprecated: { get: function () { return "Invalid configuration: automaticallyLoadPages is deprecated. The control will default this property to false. Please refer to the 'ListView loading behaviors' SDK Sample for guidance on how to implement incremental load behavior."; } }, invalidTemplate: { get: function () { return "Invalid template: Templates must be created before being passed to the ListView, and must contain a valid tree of elements."; } }, loadMorePagesIsDeprecated: { get: function () { return "loadMorePages is deprecated. Invoking this function will not have any effect. Please refer to the 'ListView loading behaviors' SDK Sample for guidance on how to implement incremental load behavior."; } }, disableBackdropIsDeprecated: { get: function () { return "Invalid configuration: disableBackdrop is deprecated. Style: .win-listview .win-container.win-backdrop { background-color:transparent; } instead."; } }, backdropColorIsDeprecated: { get: function () { return "Invalid configuration: backdropColor is deprecated. Style: .win-listview .win-container.win-backdrop { rgba(155,155,155,0.23); } instead."; } }, itemInfoIsDeprecated: { get: function () { return "GridLayout.itemInfo may be altered or unavailable in future versions. Instead, use CellSpanningLayout."; } }, groupInfoIsDeprecated: { get: function () { return "GridLayout.groupInfo may be altered or unavailable in future versions. Instead, use CellSpanningLayout."; } }, resetItemIsDeprecated: { get: function () { return "resetItem may be altered or unavailable in future versions. Instead, mark the element as disposable using WinJS.Utilities.markDisposable."; } }, resetGroupHeaderIsDeprecated: { get: function () { return "resetGroupHeader may be altered or unavailable in future versions. Instead, mark the header element as disposable using WinJS.Utilities.markDisposable."; } }, maxRowsIsDeprecated: { get: function () { return "GridLayout.maxRows may be altered or unavailable in future versions. Instead, use the maximumRowsOrColumns property."; } } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ListView/_GroupFocusCache',[ 'exports', '../../Core/_Base' ], function GroupFocusCacheInit(exports, _Base) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _GroupFocusCache: _Base.Namespace._lazy(function () { return _Base.Class.define(function GroupFocusCache_ctor(listView) { this._listView = listView; this.clear(); }, { // We store indices as strings in the cache so index=0 does not evaluate to false as // when we check for the existance of an index in the cache. The index is converted // back into a number when calling getIndexForGroup updateCache: function (groupKey, itemKey, itemIndex) { itemIndex = "" + itemIndex; this._itemToIndex[itemKey] = itemIndex; this._groupToItem[groupKey] = itemKey; }, deleteItem: function (itemKey) { if (!this._itemToIndex[itemKey]) { return; } var that = this; var keys = Object.keys(this._groupToItem); for (var i = 0, len = keys.length; i < len; i++) { var key = keys[i]; if (that._groupToItem[key] === itemKey) { that.deleteGroup(key); break; } } }, deleteGroup: function (groupKey) { var itemKey = this._groupToItem[groupKey]; if (itemKey) { delete this._itemToIndex[itemKey]; } delete this._groupToItem[groupKey]; }, updateItemIndex: function (itemKey, itemIndex) { if (!this._itemToIndex[itemKey]) { return; } this._itemToIndex[itemKey] = "" + itemIndex; }, getIndexForGroup: function (groupIndex) { var groupKey = this._listView._groups.group(groupIndex).key; var itemKey = this._groupToItem[groupKey]; if (itemKey && this._itemToIndex[itemKey]) { return +this._itemToIndex[itemKey]; } else { return this._listView._groups.fromKey(groupKey).group.startIndex; } }, clear: function () { this._groupToItem = {}; this._itemToIndex = {}; } }); }), _UnsupportedGroupFocusCache: _Base.Namespace._lazy(function () { return _Base.Class.define(null, { updateCache: function () { }, deleteItem: function () { }, deleteGroup: function () { }, updateItemIndex: function () { }, getIndexForGroup: function () { return 0; }, clear: function () { } }); }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ListView/_GroupsContainer',[ 'exports', '../../Core/_Base', '../../Promise', '../../Utilities/_Dispose', '../../Utilities/_ElementUtilities', '../../Utilities/_ItemsManager', '../../Utilities/_UI', '../ItemContainer/_Constants' ], function groupsContainerInit(exports, _Base, Promise, _Dispose, _ElementUtilities, _ItemsManager, _UI, _Constants) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _GroupsContainerBase: _Base.Namespace._lazy(function () { return _Base.Class.define(function () { }, { index: function (element) { var header = this.headerFrom(element); if (header) { for (var i = 0, len = this.groups.length; i < len; i++) { if (header === this.groups[i].header) { return i; } } } return _Constants._INVALID_INDEX; }, headerFrom: function (element) { while (element && !_ElementUtilities.hasClass(element, _Constants._headerClass)) { element = element.parentNode; } return element; }, requestHeader: function GroupsContainerBase_requestHeader(index) { this._waitingHeaderRequests = this._waitingHeaderRequests || {}; if (!this._waitingHeaderRequests[index]) { this._waitingHeaderRequests[index] = []; } var that = this; return new Promise(function (complete) { var group = that.groups[index]; if (group && group.header) { complete(group.header); } else { that._waitingHeaderRequests[index].push(complete); } }); }, notify: function GroupsContainerBase_notify(index, header) { if (this._waitingHeaderRequests && this._waitingHeaderRequests[index]) { var requests = this._waitingHeaderRequests[index]; for (var i = 0, len = requests.length; i < len; i++) { requests[i](header); } this._waitingHeaderRequests[index] = []; } }, groupFromImpl: function GroupsContainerBase_groupFromImpl(fromGroup, toGroup, comp) { if (toGroup < fromGroup) { return null; } var center = fromGroup + Math.floor((toGroup - fromGroup) / 2), centerGroup = this.groups[center]; if (comp(centerGroup, center)) { return this.groupFromImpl(fromGroup, center - 1, comp); } else if (center < toGroup && !comp(this.groups[center + 1], center + 1)) { return this.groupFromImpl(center + 1, toGroup, comp); } else { return center; } }, groupFrom: function GroupsContainerBase_groupFrom(comp) { if (this.groups.length > 0) { var lastGroupIndex = this.groups.length - 1, lastGroup = this.groups[lastGroupIndex]; if (!comp(lastGroup, lastGroupIndex)) { return lastGroupIndex; } else { return this.groupFromImpl(0, this.groups.length - 1, comp); } } else { return null; } }, groupFromItem: function GroupsContainerBase_groupFromItem(itemIndex) { return this.groupFrom(function (group) { return itemIndex < group.startIndex; }); }, groupFromOffset: function GroupsContainerBase_groupFromOffset(offset) { return this.groupFrom(function (group) { return offset < group.offset; }); }, group: function GroupsContainerBase_getGroup(index) { return this.groups[index]; }, length: function GroupsContainerBase_length() { return this.groups.length; }, cleanUp: function GroupsContainerBase_cleanUp() { if (this.listBinding) { for (var i = 0, len = this.groups.length; i < len; i++) { var group = this.groups[i]; if (group.userData) { this.listBinding.releaseItem(group.userData); } } this.listBinding.release(); } }, _dispose: function GroupsContainerBase_dispose() { this.cleanUp(); }, synchronizeGroups: function GroupsContainerBase_synchronizeGroups() { var that = this; this.pendingChanges = []; this.ignoreChanges = true; return this.groupDataSource.invalidateAll().then(function () { return Promise.join(that.pendingChanges); }).then(function () { if (that._listView._ifZombieDispose()) { return Promise.cancel; } }).then( function () { that.ignoreChanges = false; }, function (error) { that.ignoreChanges = false; return Promise.wrapError(error); } ); }, fromKey: function GroupsContainerBase_fromKey(key) { for (var i = 0, len = this.groups.length; i < len; i++) { var group = this.groups[i]; if (group.key === key) { return { group: group, index: i }; } } return null; }, fromHandle: function GroupsContainerBase_fromHandle(handle) { for (var i = 0, len = this.groups.length; i < len; i++) { var group = this.groups[i]; if (group.handle === handle) { return { group: group, index: i }; } } return null; } }); }), _UnvirtualizedGroupsContainer: _Base.Namespace._lazy(function () { return _Base.Class.derive(exports._GroupsContainerBase, function (listView, groupDataSource) { this._listView = listView; this.groupDataSource = groupDataSource; this.groups = []; this.pendingChanges = []; this.dirty = true; var that = this, notificationHandler = { beginNotifications: function GroupsContainer_beginNotifications() { that._listView._versionManager.beginNotifications(); }, endNotifications: function GroupsContainer_endNotifications() { that._listView._versionManager.endNotifications(); if (that._listView._ifZombieDispose()) { return; } if (!that.ignoreChanges && that._listView._groupsChanged) { that._listView._scheduleUpdate(); } }, indexChanged: function GroupsContainer_indexChanged() { that._listView._versionManager.receivedNotification(); if (that._listView._ifZombieDispose()) { return; } this.scheduleUpdate(); }, itemAvailable: function GroupsContainer_itemAvailable() { }, countChanged: function GroupsContainer_countChanged(newCount) { that._listView._versionManager.receivedNotification(); that._listView._writeProfilerMark("groupCountChanged(" + newCount + "),info"); if (that._listView._ifZombieDispose()) { return; } this.scheduleUpdate(); }, changed: function GroupsContainer_changed(newItem) { that._listView._versionManager.receivedNotification(); if (that._listView._ifZombieDispose()) { return; } var groupEntry = that.fromKey(newItem.key); if (groupEntry) { that._listView._writeProfilerMark("groupChanged(" + groupEntry.index + "),info"); groupEntry.group.userData = newItem; groupEntry.group.startIndex = newItem.firstItemIndexHint; this.markToRemove(groupEntry.group); } this.scheduleUpdate(); }, removed: function GroupsContainer_removed(itemHandle) { that._listView._versionManager.receivedNotification(); that._listView._groupRemoved(itemHandle); if (that._listView._ifZombieDispose()) { return; } var groupEntry = that.fromHandle(itemHandle); if (groupEntry) { that._listView._writeProfilerMark("groupRemoved(" + groupEntry.index + "),info"); that.groups.splice(groupEntry.index, 1); var index = that.groups.indexOf(groupEntry.group, groupEntry.index); if (index > -1) { that.groups.splice(index, 1); } this.markToRemove(groupEntry.group); } this.scheduleUpdate(); }, inserted: function GroupsContainer_inserted(itemPromise, previousHandle, nextHandle) { that._listView._versionManager.receivedNotification(); if (that._listView._ifZombieDispose()) { return; } that._listView._writeProfilerMark("groupInserted,info"); var notificationHandler = this; itemPromise.retain().then(function (item) { var index; if (!previousHandle && !nextHandle && !that.groups.length) { index = 0; } else { index = notificationHandler.findIndex(previousHandle, nextHandle); } if (index !== -1) { var newGroup = { key: item.key, startIndex: item.firstItemIndexHint, userData: item, handle: itemPromise.handle }; that.groups.splice(index, 0, newGroup); } notificationHandler.scheduleUpdate(); }); that.pendingChanges.push(itemPromise); }, moved: function GroupsContainer_moved(itemPromise, previousHandle, nextHandle) { that._listView._versionManager.receivedNotification(); if (that._listView._ifZombieDispose()) { return; } that._listView._writeProfilerMark("groupMoved,info"); var notificationHandler = this; itemPromise.then(function (item) { var newIndex = notificationHandler.findIndex(previousHandle, nextHandle), groupEntry = that.fromKey(item.key); if (groupEntry) { that.groups.splice(groupEntry.index, 1); if (newIndex !== -1) { if (groupEntry.index < newIndex) { newIndex--; } groupEntry.group.key = item.key; groupEntry.group.userData = item; groupEntry.group.startIndex = item.firstItemIndexHint; that.groups.splice(newIndex, 0, groupEntry.group); } } else if (newIndex !== -1) { var newGroup = { key: item.key, startIndex: item.firstItemIndexHint, userData: item, handle: itemPromise.handle }; that.groups.splice(newIndex, 0, newGroup); itemPromise.retain(); } notificationHandler.scheduleUpdate(); }); that.pendingChanges.push(itemPromise); }, reload: function GroupsContainer_reload() { that._listView._versionManager.receivedNotification(); if (that._listView._ifZombieDispose()) { return; } that._listView._processReload(); }, markToRemove: function GroupsContainer_markToRemove(group) { if (group.header) { var header = group.header; group.header = null; group.left = -1; group.width = -1; group.decorator = null; group.tabIndex = -1; header.tabIndex = -1; that._listView._groupsToRemove[_ElementUtilities._uniqueID(header)] = { group: group, header: header }; } }, scheduleUpdate: function GroupsContainer_scheduleUpdate() { that.dirty = true; if (!that.ignoreChanges) { that._listView._groupsChanged = true; } }, findIndex: function GroupsContainer_findIndex(previousHandle, nextHandle) { var index = -1, groupEntry; if (previousHandle) { groupEntry = that.fromHandle(previousHandle); if (groupEntry) { index = groupEntry.index + 1; } } if (index === -1 && nextHandle) { groupEntry = that.fromHandle(nextHandle); if (groupEntry) { index = groupEntry.index; } } return index; }, removeElements: function GroupsContainer_removeElements(group) { if (group.header) { var parentNode = group.header.parentNode; if (parentNode) { _Dispose.disposeSubTree(group.header); parentNode.removeChild(group.header); } group.header = null; group.left = -1; group.width = -1; } } }; this.listBinding = this.groupDataSource.createListBinding(notificationHandler); }, { initialize: function UnvirtualizedGroupsContainer_initialize() { if (this.initializePromise) { this.initializePromise.cancel(); } this._listView._writeProfilerMark("GroupsContainer_initialize,StartTM"); var that = this; this.initializePromise = this.groupDataSource.getCount().then(function (count) { var promises = []; for (var i = 0; i < count; i++) { promises.push(that.listBinding.fromIndex(i).retain()); } return Promise.join(promises); }).then( function (groups) { that.groups = []; for (var i = 0, len = groups.length; i < len; i++) { var group = groups[i]; that.groups.push({ key: group.key, startIndex: group.firstItemIndexHint, handle: group.handle, userData: group, }); } that._listView._writeProfilerMark("GroupsContainer_initialize groups(" + groups.length + "),info"); that._listView._writeProfilerMark("GroupsContainer_initialize,StopTM"); }, function (error) { that._listView._writeProfilerMark("GroupsContainer_initialize,StopTM"); return Promise.wrapError(error); }); return this.initializePromise; }, renderGroup: function UnvirtualizedGroupsContainer_renderGroup(index) { if (this._listView.groupHeaderTemplate) { var group = this.groups[index]; return Promise.wrap(this._listView._groupHeaderRenderer(Promise.wrap(group.userData))).then(_ItemsManager._normalizeRendererReturn); } else { return Promise.wrap(null); } }, setDomElement: function UnvirtualizedGroupsContainer_setDomElement(index, headerElement) { this.groups[index].header = headerElement; this.notify(index, headerElement); }, removeElements: function UnvirtualizedGroupsContainer_removeElements() { var elements = this._listView._groupsToRemove || {}, keys = Object.keys(elements), focusedItemPurged = false; var focused = this._listView._selection._getFocused(); for (var i = 0, len = keys.length; i < len; i++) { var group = elements[keys[i]], header = group.header, groupData = group.group; if (!focusedItemPurged && focused.type === _UI.ObjectType.groupHeader && groupData.userData.index === focused.index) { this._listView._unsetFocusOnItem(); focusedItemPurged = true; } if (header) { var parentNode = header.parentNode; if (parentNode) { _Dispose._disposeElement(header); parentNode.removeChild(header); } } } if (focusedItemPurged) { this._listView._setFocusOnItem(focused); } this._listView._groupsToRemove = {}; }, resetGroups: function UnvirtualizedGroupsContainer_resetGroups() { var groups = this.groups.slice(0); for (var i = 0, len = groups.length; i < len; i++) { var group = groups[i]; if (this.listBinding && group.userData) { this.listBinding.releaseItem(group.userData); } } // Set the lengths to zero to clear the arrays, rather than setting = [], which re-instantiates this.groups.length = 0; this.dirty = true; } }); }), _NoGroups: _Base.Namespace._lazy(function () { return _Base.Class.derive(exports._GroupsContainerBase, function (listView) { this._listView = listView; this.groups = [{ startIndex: 0 }]; this.dirty = true; }, { synchronizeGroups: function () { return Promise.wrap(); }, addItem: function () { return Promise.wrap(this.groups[0]); }, resetGroups: function () { this.groups = [{ startIndex: 0 }]; delete this.pinnedItem; delete this.pinnedOffset; this.dirty = true; }, renderGroup: function () { return Promise.wrap(null); }, ensureFirstGroup: function () { return Promise.wrap(this.groups[0]); }, groupOf: function () { return Promise.wrap(this.groups[0]); }, removeElements: function () { } }); }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ListView/_Helpers',[ 'exports', '../../Core/_Base', '../../Animations' ], function helpersInit(exports, _Base, Animations) { "use strict"; function nodeListToArray(nodeList) { return Array.prototype.slice.call(nodeList); } function repeat(markup, count) { return new Array(count + 1).join(markup); } _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _nodeListToArray: nodeListToArray, _repeat: repeat, _ListViewAnimationHelper: { fadeInElement: function (element) { return Animations.fadeIn(element); }, fadeOutElement: function (element) { return Animations.fadeOut(element); }, animateEntrance: function (canvas, firstEntrance) { return Animations.enterContent(canvas, [{ left: firstEntrance ? "100px" : "40px", top: "0px", rtlflip: true }], { mechanism: "transition" }); }, } }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ListView/_ItemsContainer',[ 'exports', '../../Core/_Base', '../../Promise', '../../Utilities/_ElementUtilities', '../ItemContainer/_Constants' ], function itemsContainerInit(exports, _Base, Promise, _ElementUtilities, _Constants) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _ItemsContainer: _Base.Namespace._lazy(function () { var _ItemsContainer = function (site) { this.site = site; this._itemData = {}; this.waitingItemRequests = {}; }; _ItemsContainer.prototype = { requestItem: function ItemsContainer_requestItem(itemIndex) { if (!this.waitingItemRequests[itemIndex]) { this.waitingItemRequests[itemIndex] = []; } var that = this; var promise = new Promise(function (complete) { var itemData = that._itemData[itemIndex]; if (itemData && !itemData.detached && itemData.element) { complete(itemData.element); } else { that.waitingItemRequests[itemIndex].push(complete); } }); return promise; }, removeItem: function (index) { delete this._itemData[index]; }, removeItems: function ItemsContainer_removeItems() { this._itemData = {}; this.waitingItemRequests = {}; }, setItemAt: function ItemsContainer_setItemAt(itemIndex, itemData) { this._itemData[itemIndex] = itemData; if (!itemData.detached) { this.notify(itemIndex, itemData); } }, notify: function ItemsContainer_notify(itemIndex, itemData) { if (this.waitingItemRequests[itemIndex]) { var requests = this.waitingItemRequests[itemIndex]; for (var i = 0; i < requests.length; i++) { requests[i](itemData.element); } this.waitingItemRequests[itemIndex] = []; } }, elementAvailable: function ItemsContainer_elementAvailable(itemIndex) { var itemData = this._itemData[itemIndex]; itemData.detached = false; this.notify(itemIndex, itemData); }, itemAt: function ItemsContainer_itemAt(itemIndex) { var itemData = this._itemData[itemIndex]; return itemData ? itemData.element : null; }, itemDataAt: function ItemsContainer_itemDataAt(itemIndex) { return this._itemData[itemIndex]; }, containerAt: function ItemsContainer_containerAt(itemIndex) { var itemData = this._itemData[itemIndex]; return itemData ? itemData.container : null; }, itemBoxAt: function ItemsContainer_itemBoxAt(itemIndex) { var itemData = this._itemData[itemIndex]; return itemData ? itemData.itemBox : null; }, itemBoxFrom: function ItemsContainer_containerFrom(element) { while (element && !_ElementUtilities.hasClass(element, _Constants._itemBoxClass)) { element = element.parentNode; } return element; }, containerFrom: function ItemsContainer_containerFrom(element) { while (element && !_ElementUtilities.hasClass(element, _Constants._containerClass)) { element = element.parentNode; } return element; }, index: function ItemsContainer_index(element) { var item = this.containerFrom(element); if (item) { for (var index in this._itemData) { if (this._itemData[index].container === item) { return parseInt(index, 10); } } } return _Constants._INVALID_INDEX; }, each: function ItemsContainer_each(callback) { for (var index in this._itemData) { if (this._itemData.hasOwnProperty(index)) { var itemData = this._itemData[index]; callback(parseInt(index, 10), itemData.element, itemData); } } }, eachIndex: function ItemsContainer_each(callback) { for (var index in this._itemData) { if (callback(parseInt(index, 10))) { break; } } }, count: function ItemsContainer_count() { return Object.keys(this._itemData).length; } }; return _ItemsContainer; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ListView/_Layouts',[ 'exports', '../../Core/_Global', '../../Core/_Base', '../../Core/_BaseUtils', '../../Core/_ErrorFromName', '../../Core/_Resources', '../../Core/_WriteProfilerMark', '../../Animations/_TransitionAnimation', '../../Promise', '../../Scheduler', '../../_Signal', '../../Utilities/_Dispose', '../../Utilities/_ElementUtilities', '../../Utilities/_SafeHtml', '../../Utilities/_UI', '../ItemContainer/_Constants', './_ErrorMessages' ], function layouts2Init(exports, _Global, _Base, _BaseUtils, _ErrorFromName, _Resources, _WriteProfilerMark, _TransitionAnimation, Promise, Scheduler, _Signal, _Dispose, _ElementUtilities, _SafeHtml, _UI, _Constants, _ErrorMessages) { "use strict"; var Key = _ElementUtilities.Key, uniqueID = _ElementUtilities._uniqueID; var strings = { get itemInfoIsInvalid() { return "Invalid argument: An itemInfo function must be provided which returns an object with numeric width and height properties."; }, get groupInfoResultIsInvalid() { return "Invalid result: groupInfo result for cell spanning groups must include the following numeric properties: cellWidth and cellHeight."; } }; // // Helpers for dynamic CSS rules // // Rule deletions are delayed until the next rule insertion. This helps the // scenario where a ListView changes layouts. By doing the rule manipulations // in a single synchronous block, IE will do 1 layout pass instead of 2. // // Dynamic CSS rules will be added to this style element var layoutStyleElem = _Global.document.createElement("style"); _Global.document.head.appendChild(layoutStyleElem); var nextCssClassId = 0, staleClassNames = []; // The prefix for the class name should not contain dashes function uniqueCssClassName(prefix) { return "_win-dynamic-" + prefix + "-" + (nextCssClassId++); } var browserStyleEquivalents = _BaseUtils._browserStyleEquivalents; var transformNames = browserStyleEquivalents["transform"]; var transitionScriptName = _BaseUtils._browserStyleEquivalents["transition"].scriptName; var dragBetweenTransition = transformNames.cssName + " cubic-bezier(0.1, 0.9, 0.2, 1) 167ms"; var dragBetweenDistance = 12; // Removes the dynamic CSS rules corresponding to the classes in staleClassNames // from the DOM. function flushDynamicCssRules() { var rules = layoutStyleElem.sheet.cssRules, classCount = staleClassNames.length, i, j, ruleSuffix; for (i = 0; i < classCount; i++) { ruleSuffix = "." + staleClassNames[i] + " "; for (j = rules.length - 1; j >= 0; j--) { if (rules[j].selectorText.indexOf(ruleSuffix) !== -1) { layoutStyleElem.sheet.deleteRule(j); } } } staleClassNames = []; } // Creates a dynamic CSS rule and adds it to the DOM. uniqueToken is a class name // which uniquely identifies a set of related rules. These rules may be removed // using deleteDynamicCssRule. uniqueToken should be created using uniqueCssClassName. function addDynamicCssRule(uniqueToken, site, selector, body) { flushDynamicCssRules(); var rule = "." + _Constants._listViewClass + " ." + uniqueToken + " " + selector + " { " + body + "}"; var perfId = "_addDynamicCssRule:" + uniqueToken + ",info"; if (site) { site._writeProfilerMark(perfId); } else { _WriteProfilerMark("WinJS.UI.ListView:Layout" + perfId); } layoutStyleElem.sheet.insertRule(rule, 0); } // Marks the CSS rules corresponding to uniqueToken for deletion. The rules // should have been added by addDynamicCssRule. function deleteDynamicCssRule(uniqueToken) { staleClassNames.push(uniqueToken); } // // Helpers shared by all layouts // // Clamps x to the range first <= x <= last function clampToRange(first, last, x) { return Math.max(first, Math.min(last, x)); } function getDimension(element, property) { return _ElementUtilities.convertToPixels(element, _Global.getComputedStyle(element, null)[property]); } // Returns the sum of the margin, border, and padding for the side of the // element specified by side. side can be "Left", "Right", "Top", or "Bottom". function getOuter(side, element) { return getDimension(element, "margin" + side) + getDimension(element, "border" + side + "Width") + getDimension(element, "padding" + side); } // Returns the total height of element excluding its content height function getOuterHeight(element) { return getOuter("Top", element) + getOuter("Bottom", element); } // Returns the total width of element excluding its content width function getOuterWidth(element) { return getOuter("Left", element) + getOuter("Right", element); } function forEachContainer(itemsContainer, callback) { if (itemsContainer.items) { for (var i = 0, len = itemsContainer.items.length; i < len; i++) { callback(itemsContainer.items[i], i); } } else { for (var b = 0, index = 0; b < itemsContainer.itemsBlocks.length; b++) { var block = itemsContainer.itemsBlocks[b]; for (var i = 0, len = block.items.length; i < len; i++) { callback(block.items[i], index++); } } } } function containerFromIndex(itemsContainer, index) { if (index < 0) { return null; } if (itemsContainer.items) { return (index < itemsContainer.items.length ? itemsContainer.items[index] : null); } else { var blockSize = itemsContainer.itemsBlocks[0].items.length, blockIndex = Math.floor(index / blockSize), offset = index % blockSize; return (blockIndex < itemsContainer.itemsBlocks.length && offset < itemsContainer.itemsBlocks[blockIndex].items.length ? itemsContainer.itemsBlocks[blockIndex].items[offset] : null); } } function getItemsContainerTree(itemsContainer, tree) { var itemsContainerTree; for (var i = 0, treeLength = tree.length; i < treeLength; i++) { if (tree[i].itemsContainer.element === itemsContainer) { itemsContainerTree = tree[i].itemsContainer; break; } } return itemsContainerTree; } function getItemsContainerLength(itemsContainer) { var blocksCount, itemsCount; if (itemsContainer.itemsBlocks) { blocksCount = itemsContainer.itemsBlocks.length; if (blocksCount > 0) { itemsCount = (itemsContainer.itemsBlocks[0].items.length * (blocksCount - 1)) + itemsContainer.itemsBlocks[blocksCount - 1].items.length; } else { itemsCount = 0; } } else { itemsCount = itemsContainer.items.length; } return itemsCount; } var environmentDetails = null; // getEnvironmentSupportInformation does one-time checks on several browser-specific environment details (both to check the existence of styles, // and also to see if some environments have layout bugs the ListView needs to work around). function getEnvironmentSupportInformation(site) { if (!environmentDetails) { var surface = _Global.document.createElement("div"); surface.style.width = "500px"; surface.style.visibility = "hidden"; // Set up the DOM var flexRoot = _Global.document.createElement("div"); flexRoot.style.cssText += "width: 500px; height: 200px; display: -webkit-flex; display: flex"; _SafeHtml.setInnerHTMLUnsafe(flexRoot, "
" + "
" + "
" + "
" + "
"); surface.appendChild(flexRoot); // Read from the DOM and detect the bugs site.viewport.insertBefore(surface, site.viewport.firstChild); var canMeasure = surface.offsetWidth > 0, expectedWidth = 200; if (canMeasure) { // If we can't measure now (e.g. ListView is display:none), leave environmentDetails as null // so that we do the detection later when the app calls recalculateItemPosition/forceLayout. environmentDetails = { supportsCSSGrid: !!("-ms-grid-row" in _Global.document.documentElement.style), // Detects Chrome flex issue 345433: Incorrect sizing for nested flexboxes // https://code.google.com/p/chromium/issues/detail?id=345433 // With nested flexboxes, the inner flexbox's width is proportional to the number of elements intead // of the number of columns. nestedFlexTooLarge: flexRoot.firstElementChild.offsetWidth > expectedWidth, // Detects Firefox issue 995020 // https://bugzilla.mozilla.org/show_bug.cgi?id=995020 // The three squares we're adding to the nested flexbox should increase the size of the nestedFlex to be 200 pixels wide. This is the case in IE but // currently not in Firefox. In Firefox, the third square will move to the next column, but the container's width won't update for it. nestedFlexTooSmall: flexRoot.firstElementChild.offsetWidth < expectedWidth }; } // Clean up the DOM site.viewport.removeChild(surface); } return environmentDetails; } _Base.Namespace._moduleDefine(exports, "WinJS.UI", { Layout: _Base.Class.define(function Layout_ctor() { /// /// /// Creates a new Layout object. /// /// /// The set of options to be applied initially to the new Layout object. /// /// /// The new Layout object. /// /// }), _LayoutCommon: _Base.Namespace._lazy(function () { return _Base.Class.derive(exports.Layout, null, { /// /// Gets or sets the position of group headers relative to their items. /// The default value is "top". /// groupHeaderPosition: { enumerable: true, get: function () { return this._groupHeaderPosition; }, set: function (position) { this._groupHeaderPosition = position; this._invalidateLayout(); } }, // Implementation of part of ILayout interface initialize: function _LayoutCommon_initialize(site, groupsEnabled) { site._writeProfilerMark("Layout:initialize,info"); if (!this._inListMode) { _ElementUtilities.addClass(site.surface, _Constants._gridLayoutClass); } if (this._backdropColorClassName) { _ElementUtilities.addClass(site.surface, this._backdropColorClassName); } if (this._disableBackdropClassName) { _ElementUtilities.addClass(site.surface, this._disableBackdropClassName); } this._groups = []; this._groupMap = {}; this._oldGroupHeaderPosition = null; this._usingStructuralNodes = false; this._site = site; this._groupsEnabled = groupsEnabled; this._resetAnimationCaches(true); }, /// /// Gets or sets the orientation for the layout. /// The default value is "horizontal". /// orientation: { enumerable: true, get: function () { return this._orientation; }, set: function (orientation) { this._orientation = orientation; this._horizontal = (orientation === "horizontal"); this._invalidateLayout(); } }, uninitialize: function _LayoutCommon_uninitialize() { var perfId = "Layout:uninitialize,info"; function cleanGroups(groups) { var len = groups.length, i; for (i = 0; i < len; i++) { groups[i].cleanUp(true); } } this._elementsToMeasure = {}; if (this._site) { this._site._writeProfilerMark(perfId); _ElementUtilities.removeClass(this._site.surface, _Constants._gridLayoutClass); _ElementUtilities.removeClass(this._site.surface, _Constants._headerPositionTopClass); _ElementUtilities.removeClass(this._site.surface, _Constants._headerPositionLeftClass); _ElementUtilities.removeClass(this._site.surface, _Constants._structuralNodesClass); this._site.surface.style.cssText = ""; if (this._groups) { cleanGroups(this._groups); this._groups = null; this._groupMap = null; } if (this._layoutPromise) { this._layoutPromise.cancel(); this._layoutPromise = null; } this._resetMeasurements(); this._oldGroupHeaderPosition = null; this._usingStructuralNodes = null; // The properties given to us by the app (_groupInfo, _itemInfo, // _groupHeaderPosition) are not cleaned up so that the values are // remembered if the layout is reused. if (this._backdropColorClassName) { _ElementUtilities.removeClass(this._site.surface, this._backdropColorClassName); deleteDynamicCssRule(this._backdropColorClassName); this._backdropColorClassName = null; } if (this._disableBackdropClassName) { _ElementUtilities.removeClass(this._site.surface, this._disableBackdropClassName); deleteDynamicCssRule(this._disableBackdropClassName); this._disableBackdropClassName = null; } this._site = null; this._groupsEnabled = null; if (this._animationsRunning) { this._animationsRunning.cancel(); } this._animatingItemsBlocks = {}; } else { _WriteProfilerMark("WinJS.UI.ListView:" + perfId); } }, numberOfItemsPerItemsBlock: { get: function _LayoutCommon_getNumberOfItemsPerItemsBlock() { function allGroupsAreUniform() { var groupCount = that._site.groupCount, i; for (i = 0; i < groupCount; i++) { if (that._isCellSpanning(i)) { return false; } } return true; } var that = this; return that._measureItem(0).then(function () { if (that._sizes.viewportContentSize !== that._getViewportCrossSize()) { that._viewportSizeChanged(that._getViewportCrossSize()); } if (!that._envInfo.nestedFlexTooLarge && // Disabling structural nodes works around this issue !that._envInfo.nestedFlexTooSmall && allGroupsAreUniform()) { that._usingStructuralNodes = exports._LayoutCommon._barsPerItemsBlock > 0; return exports._LayoutCommon._barsPerItemsBlock * that._itemsPerBar; } else { that._usingStructuralNodes = false; return null; } }); } }, layout: function _LayoutCommon_layout(tree, changedRange, modifiedItems, modifiedGroups) { // changedRange implies that the minimum amount of work the layout needs to do is as follows: // - It needs to lay out group shells (header containers and items containers) from // firstChangedGroup thru lastGroup. // - It needs to ask firstChangedGroup thru lastChangedGroup to lay out their // contents (i.e. win-containers). // - For each group included in the changedRange, it needs to lay out its // contents (i.e. win-containers) from firstChangedItem thru lastItem. var that = this; var site = that._site, layoutPerfId = "Layout.layout", realizedRangePerfId = layoutPerfId + ":realizedRange", realizedRangePromise; that._site._writeProfilerMark(layoutPerfId + ",StartTM"); that._site._writeProfilerMark(realizedRangePerfId + ",StartTM"); // Receives an items container's tree and returns a normalized copy. // This allows us to hold on to a snapshot of the tree without // worrying that items may have been unexpectedly inserted/ // removed/moved. The returned tree always appears as though // structural nodes are disabled. function copyItemsContainerTree(itemsContainer) { function copyItems(itemsContainer) { if (that._usingStructuralNodes) { var items = []; itemsContainer.itemsBlocks.forEach(function (itemsBlock) { items = items.concat(itemsBlock.items.slice(0)); }); return items; } else { return itemsContainer.items.slice(0); } } return { element: itemsContainer.element, items: copyItems(itemsContainer) }; } // Updates the GridLayout's internal state to reflect the current tree. // Similarly tells each group to update its internal state via prepareLayout. // After this function runs, the ILayout functions will return results that // are appropriate for the current tree. function updateGroups() { function createGroup(groupInfo, itemsContainer) { var GroupType = (groupInfo.enableCellSpanning ? Groups.CellSpanningGroup : Groups.UniformGroup); return new GroupType(that, itemsContainer); } var oldRealizedItemRange = (that._groups.length > 0 ? that._getRealizationRange() : null), newGroups = [], prepared = [], cleanUpDom = {}, newGroupMap = {}, currentIndex = 0, len = tree.length, i; for (i = 0; i < len; i++) { var oldChangedRealizedRangeInGroup = null, groupInfo = that._getGroupInfo(i), groupKey = that._site.groupFromIndex(i).key, oldGroup = that._groupMap[groupKey], wasCellSpanning = oldGroup instanceof Groups.CellSpanningGroup, isCellSpanning = groupInfo.enableCellSpanning; if (oldGroup) { if (wasCellSpanning !== isCellSpanning) { // The group has changed types so DOM needs to be cleaned up cleanUpDom[groupKey] = true; } else { // Compute the range of changed items that is within the group's realized range var firstChangedIndexInGroup = Math.max(0, changedRange.firstIndex - oldGroup.startIndex), oldRealizedItemRangeInGroup = that._rangeForGroup(oldGroup, oldRealizedItemRange); if (oldRealizedItemRangeInGroup && firstChangedIndexInGroup <= oldRealizedItemRangeInGroup.lastIndex) { // The old changed realized range is non-empty oldChangedRealizedRangeInGroup = { firstIndex: Math.max(firstChangedIndexInGroup, oldRealizedItemRangeInGroup.firstIndex), lastIndex: oldRealizedItemRangeInGroup.lastIndex }; } } } var group = createGroup(groupInfo, tree[i].itemsContainer.element); var prepareLayoutPromise; if (group.prepareLayoutWithCopyOfTree) { prepareLayoutPromise = group.prepareLayoutWithCopyOfTree(copyItemsContainerTree(tree[i].itemsContainer), oldChangedRealizedRangeInGroup, oldGroup, { groupInfo: groupInfo, startIndex: currentIndex, }); } else { prepareLayoutPromise = group.prepareLayout(getItemsContainerLength(tree[i].itemsContainer), oldChangedRealizedRangeInGroup, oldGroup, { groupInfo: groupInfo, startIndex: currentIndex, }); } prepared.push(prepareLayoutPromise); currentIndex += group.count; newGroups.push(group); newGroupMap[groupKey] = group; } return Promise.join(prepared).then(function () { var currentOffset = 0; for (var i = 0, len = newGroups.length; i < len; i++) { var group = newGroups[i]; group.offset = currentOffset; currentOffset += that._getGroupSize(group); } // Clean up deleted groups Object.keys(that._groupMap).forEach(function (deletedKey) { var skipDomCleanUp = !cleanUpDom[deletedKey]; that._groupMap[deletedKey].cleanUp(skipDomCleanUp); }); that._groups = newGroups; that._groupMap = newGroupMap; }); } // When doRealizedRange is true, this function is synchronous and has no return value. // When doRealizedRange is false, this function is asynchronous and returns a promise. function layoutGroupContent(groupIndex, realizedItemRange, doRealizedRange) { var group = that._groups[groupIndex], firstChangedIndexInGroup = Math.max(0, changedRange.firstIndex - group.startIndex), realizedItemRangeInGroup = that._rangeForGroup(group, realizedItemRange), beforeRealizedRange; if (doRealizedRange) { group.layoutRealizedRange(firstChangedIndexInGroup, realizedItemRangeInGroup); } else { if (!realizedItemRangeInGroup) { beforeRealizedRange = (group.startIndex + group.count - 1 < realizedItemRange.firstIndex); } return group.layoutUnrealizedRange(firstChangedIndexInGroup, realizedItemRangeInGroup, beforeRealizedRange); } } // Synchronously lays out: // - Realized and unrealized group shells (header containers and items containers). // This is needed so that each realized group will be positioned at the correct offset. // - Realized items. function layoutRealizedRange() { if (that._groups.length === 0) { return; } var realizedItemRange = that._getRealizationRange(), len = tree.length, i, firstChangedGroup = site.groupIndexFromItemIndex(changedRange.firstIndex); for (i = firstChangedGroup; i < len; i++) { layoutGroupContent(i, realizedItemRange, true); that._layoutGroup(i); } } // Asynchronously lays out the unrealized items function layoutUnrealizedRange() { if (that._groups.length === 0) { return Promise.wrap(); } var realizedItemRange = that._getRealizationRange(), // Last group before the realized range which contains 1 or more unrealized items lastGroupBefore = site.groupIndexFromItemIndex(realizedItemRange.firstIndex - 1), // First group after the realized range which contains 1 or more unrealized items firstGroupAfter = site.groupIndexFromItemIndex(realizedItemRange.lastIndex + 1), firstChangedGroup = site.groupIndexFromItemIndex(changedRange.firstIndex), layoutPromises = [], groupCount = that._groups.length; var stop = false; var before = lastGroupBefore; var after = Math.max(firstChangedGroup, firstGroupAfter); after = Math.max(before + 1, after); while (!stop) { stop = true; if (before >= firstChangedGroup) { layoutPromises.push(layoutGroupContent(before, realizedItemRange, false)); stop = false; before--; } if (after < groupCount) { layoutPromises.push(layoutGroupContent(after, realizedItemRange, false)); stop = false; after++; } } return Promise.join(layoutPromises); } realizedRangePromise = that._measureItem(0).then(function () { _ElementUtilities[that._usingStructuralNodes ? "addClass" : "removeClass"](that._site.surface, _Constants._structuralNodesClass); if (that._sizes.viewportContentSize !== that._getViewportCrossSize()) { that._viewportSizeChanged(that._getViewportCrossSize()); } // Move deleted elements to their original positions before calling updateGroups can be slow. that._cacheRemovedElements(modifiedItems, that._cachedItemRecords, that._cachedInsertedItemRecords, that._cachedRemovedItems, false); that._cacheRemovedElements(modifiedGroups, that._cachedHeaderRecords, that._cachedInsertedHeaderRecords, that._cachedRemovedHeaders, true); return updateGroups(); }).then(function () { that._syncDomWithGroupHeaderPosition(tree); var surfaceLength = 0; if (that._groups.length > 0) { var lastGroup = that._groups[that._groups.length - 1]; surfaceLength = lastGroup.offset + that._getGroupSize(lastGroup); } // Explicitly set the surface width/height. This maintains backwards // compatibility with the original layouts by allowing the items // to be shifted through surface margins. if (that._horizontal) { if (that._groupsEnabled && that._groupHeaderPosition === HeaderPosition.left) { site.surface.style.cssText += ";height:" + that._sizes.surfaceContentSize + "px;-ms-grid-columns: (" + that._sizes.headerContainerWidth + "px auto)[" + tree.length + "]"; } else { site.surface.style.height = that._sizes.surfaceContentSize + "px"; } if (that._envInfo.nestedFlexTooLarge || that._envInfo.nestedFlexTooSmall) { site.surface.style.width = surfaceLength + "px"; } } else { if (that._groupsEnabled && that._groupHeaderPosition === HeaderPosition.top) { site.surface.style.cssText += ";width:" + that._sizes.surfaceContentSize + "px;-ms-grid-rows: (" + that._sizes.headerContainerHeight + "px auto)[" + tree.length + "]"; } else { site.surface.style.width = that._sizes.surfaceContentSize + "px"; } if (that._envInfo.nestedFlexTooLarge || that._envInfo.nestedFlexTooSmall) { site.surface.style.height = surfaceLength + "px"; } } layoutRealizedRange(); that._layoutAnimations(modifiedItems, modifiedGroups); that._site._writeProfilerMark(realizedRangePerfId + ":complete,info"); that._site._writeProfilerMark(realizedRangePerfId + ",StopTM"); }, function (error) { that._site._writeProfilerMark(realizedRangePerfId + ":canceled,info"); that._site._writeProfilerMark(realizedRangePerfId + ",StopTM"); return Promise.wrapError(error); }); that._layoutPromise = realizedRangePromise.then(function () { return layoutUnrealizedRange().then(function () { that._site._writeProfilerMark(layoutPerfId + ":complete,info"); that._site._writeProfilerMark(layoutPerfId + ",StopTM"); }, function (error) { that._site._writeProfilerMark(layoutPerfId + ":canceled,info"); that._site._writeProfilerMark(layoutPerfId + ",StopTM"); return Promise.wrapError(error); }); }); return { realizedRangeComplete: realizedRangePromise, layoutComplete: that._layoutPromise }; }, itemsFromRange: function _LayoutCommon_itemsFromRange(firstPixel, lastPixel) { if (this._rangeContainsItems(firstPixel, lastPixel)) { return { firstIndex: this._firstItemFromRange(firstPixel), lastIndex: this._lastItemFromRange(lastPixel) }; } else { return { firstIndex: 0, lastIndex: -1 }; } }, getAdjacent: function _LayoutCommon_getAdjacent(currentItem, pressedKey) { var that = this, groupIndex = that._site.groupIndexFromItemIndex(currentItem.index), group = that._groups[groupIndex], adjustedKey = that._adjustedKeyForOrientationAndBars(that._adjustedKeyForRTL(pressedKey), group instanceof Groups.CellSpanningGroup); if (currentItem.type === _UI.ObjectType.groupHeader) { if (pressedKey === Key.pageUp || pressedKey === Key.pageDown) { // We treat page up and page down keys as if an item had focus currentItem = { type: _UI.ObjectType.item, index: this._groups[currentItem.index].startIndex }; } else { switch (adjustedKey) { case Key.leftArrow: return { type: _UI.ObjectType.groupHeader, index: Math.max(0, currentItem.index - 1) }; case Key.rightArrow: return { type: _UI.ObjectType.groupHeader, index: Math.min(that._groups.length - 1, currentItem.index + 1) }; } return currentItem; } } function handleArrowKeys() { var currentItemInGroup = { type: currentItem.type, index: currentItem.index - group.startIndex }, newItem = group.getAdjacent(currentItemInGroup, adjustedKey); if (newItem === "boundary") { var prevGroup = that._groups[groupIndex - 1], nextGroup = that._groups[groupIndex + 1], lastGroupIndex = that._groups.length - 1; if (adjustedKey === Key.leftArrow) { if (groupIndex === 0) { // We're at the beginning of the first group so stay put return currentItem; } else if (prevGroup instanceof Groups.UniformGroup && group instanceof Groups.UniformGroup) { // Moving between uniform groups so maintain the row/column if possible var coordinates = that._indexToCoordinate(currentItemInGroup.index); var currentSlot = (that._horizontal ? coordinates.row : coordinates.column), indexOfLastBar = Math.floor((prevGroup.count - 1) / that._itemsPerBar), startOfLastBar = indexOfLastBar * that._itemsPerBar; // first cell of last bar return { type: _UI.ObjectType.item, index: prevGroup.startIndex + Math.min(prevGroup.count - 1, startOfLastBar + currentSlot) }; } else { // Moving to or from a cell spanning group so go to the last item return { type: _UI.ObjectType.item, index: group.startIndex - 1 }; } } else if (adjustedKey === Key.rightArrow) { if (groupIndex === lastGroupIndex) { // We're at the end of the last group so stay put return currentItem; } else if (group instanceof Groups.UniformGroup && nextGroup instanceof Groups.UniformGroup) { // Moving between uniform groups so maintain the row/column if possible var coordinates = that._indexToCoordinate(currentItemInGroup.index), currentSlot = (that._horizontal ? coordinates.row : coordinates.column); return { type: _UI.ObjectType.item, index: nextGroup.startIndex + Math.min(nextGroup.count - 1, currentSlot) }; } else { // Moving to or from a cell spanning group so go to the first item return { type: _UI.ObjectType.item, index: nextGroup.startIndex }; } } else { return currentItem; } } else { newItem.index += group.startIndex; return newItem; } } switch (that._adjustedKeyForRTL(pressedKey)) { case Key.upArrow: case Key.leftArrow: case Key.downArrow: case Key.rightArrow: return handleArrowKeys(); default: return exports._LayoutCommon.prototype._getAdjacentForPageKeys.call(that, currentItem, pressedKey); } }, hitTest: function _LayoutCommon_hitTest(x, y) { var sizes = this._sizes, result; // Make the coordinates relative to grid layout's content box x -= sizes.layoutOriginX; y -= sizes.layoutOriginY; var groupIndex = this._groupFromOffset(this._horizontal ? x : y), group = this._groups[groupIndex]; // Make the coordinates relative to the margin box of the group's items container if (this._horizontal) { x -= group.offset; } else { y -= group.offset; } if (this._groupsEnabled) { if (this._groupHeaderPosition === HeaderPosition.left) { x -= sizes.headerContainerWidth; } else { // Headers above y -= sizes.headerContainerHeight; } } result = group.hitTest(x, y); result.index += group.startIndex; result.insertAfterIndex += group.startIndex; return result; }, // Animation cycle: // // Edits // --- UpdateTree Realize // | | --- /\/\ // | | | | | | // ------------------------------------------------------- Time // | | | | | | | | // --- | | --- ---/\/\/\/\/\/\/\/\/ // setupAni | | layoutAni endAni (animations) // ---/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\ // layout (outside realized range) // // // When there is a modification to the DataSource, the first thing that happens is setupAnimations is // called with the current tree. This allows us to cache the locations of the existing items. // // The next 3 steps have to be completely synchronous otherwise users will see intermediate states and // items will blink or jump between locations. // // ListView modifies the DOM tree. A container is added/removed from the group's itemsContainer for each // item added/removed to the group. The existing itemBoxes are shuffled between the different containers. // The itemBoxes for the removed items will be removed from the containers completely. Since the DOM tree // has been modified we have to apply a transform to position the itemBoxes at their original location. We // compare the new locations with the cached locations to figure out how far to translate the itemBoxes. // Also the removed items need to be placed back in the DOM without affecting layout (by using position // absolute) so that they also do not jump or blink. // // We only tranform and add back removed items for items which were on screen or are now on screen. // // Now the ListView can realize other items asynchronously. The items to realize are items which have been // inserted into the DataSource or items which are in the realization range because a removal has occurred // or the user has scroll slightly. // // During the realization pass the user may scroll. If they scroll to a range outside of the realization // range the items will just appear in the correct location without any animations. If they scroll to a // location within the old realization range we still have the items and they will animate correctly. // // During the realization pass another data source edit can occur. A realization pass is unable to run when // the tree and layout are out of sync. Otherwise it may try to request item at index X and get item at // index X + 1. This means that if another data source edit occurs before endAnimations is called we // restart the whole animation cycle. To group the animations between the two edits we do not reset the // caches of item box locations. We could add to it if there were items outside of the range however they // will only play half of the animation and will probably look just as ugly as not playing the animation at // all. This means setupAnimations will just be a no op in this scenario. // // This also shows that batching data source edits and only changing the data source when in loadingstate // "complete" is still a large performance win. // // Once the realization pass has finished ListView calls executeAnimations. This is where the layout // effectively fades out the removed items (and then removes them from the dom), moves the itemBoxes back // to translate(0,0), and fades in the inserted itemBoxes. ListView waits for the executeAnimations promise // to complete before allowing more data source edits to trigger another animation cycle. // // If a resize occurs during the animation cycle the animations will be canceled and items will jump to // their final positions. setupAnimations: function _LayoutCommon_setupAnimations() { // This function is called after a data source change so that we can cache the locations // of the realized items. if (this._groups.length === 0) { // No animations if we haven't measured before this._resetAnimationCaches(); return; } if (Object.keys(this._cachedItemRecords).length) { // Ignore the second call. return; } this._site._writeProfilerMark("Animation:setupAnimations,StartTM"); var realizationRange = this._getRealizationRange(); var tree = this._site.tree; var itemIndex = 0; var horizontal = (this.orientation === "horizontal"); for (var i = 0, treeLength = tree.length; i < treeLength; i++) { var groupBundle = tree[i]; var groupHasAtleastOneItemRealized = false; var group = this._groups[i]; var groupIsCellSpanning = group instanceof Groups.CellSpanningGroup; var groupOffset = (group ? group.offset : 0); forEachContainer(groupBundle.itemsContainer, function (container, j) { // Don't try to cache something outside of the realization range. if (realizationRange.firstIndex <= itemIndex && realizationRange.lastIndex >= itemIndex) { groupHasAtleastOneItemRealized = true; if (!this._cachedItemRecords[itemIndex]) { var itemPosition = this._getItemPositionForAnimations(itemIndex, i, j); var row = itemPosition.row; var column = itemPosition.column; var left = itemPosition.left; var top = itemPosition.top; // Setting both old and new variables now in case layoutAnimations is called multiple times. this._cachedItemRecords[itemIndex] = { oldRow: row, oldColumn: column, oldLeft: left, oldTop: top, width: itemPosition.width, height: itemPosition.height, element: container, inCellSpanningGroup: groupIsCellSpanning }; } } itemIndex++; }.bind(this)); if (groupHasAtleastOneItemRealized) { var groupIndex = i; if (!this._cachedHeaderRecords[groupIndex]) { var headerPosition = this._getHeaderPositionForAnimations(groupIndex); this._cachedHeaderRecords[groupIndex] = { oldLeft: headerPosition.left, oldTop: headerPosition.top, width: headerPosition.width, height: headerPosition.height, element: groupBundle.header, }; } if (!this._cachedGroupRecords[uniqueID(groupBundle.itemsContainer.element)]) { this._cachedGroupRecords[uniqueID(groupBundle.itemsContainer.element)] = { oldLeft: horizontal ? groupOffset : 0, left: horizontal ? groupOffset : 0, oldTop: horizontal ? 0 : groupOffset, top: horizontal ? 0 : groupOffset, element: groupBundle.itemsContainer.element, }; } } } this._site._writeProfilerMark("Animation:setupAnimations,StopTM"); }, _layoutAnimations: function _LayoutCommon_layoutAnimations(modifiedItems, modifiedGroups) { // This function is called after the DOM tree has been modified to match the data source. // In this function we update the cached records and apply transforms to hide the modifications // from the user. We will remove the transforms via animations in execute animation. if (!Object.keys(this._cachedItemRecords).length && !Object.keys(this._cachedGroupRecords).length && !Object.keys(this._cachedHeaderRecords).length) { return; } this._site._writeProfilerMark("Animation:layoutAnimation,StartTM"); this._updateAnimationCache(modifiedItems, modifiedGroups); var realizationRange = this._getRealizationRange(); var tree = this._site.tree; var itemIndex = 0; var horizontal = (this.orientation === "horizontal"); for (var i = 0, treeLength = tree.length; i < treeLength; i++) { var groupBundle = tree[i]; var group = this._groups[i]; var groupIsCellSpanning = group instanceof Groups.CellSpanningGroup; var groupOffset = (group ? group.offset : 0); var groupMovementX = 0; var groupMovementY = 0; var cachedGroupRecord = this._cachedGroupRecords[uniqueID(groupBundle.itemsContainer.element)]; if (cachedGroupRecord) { if (horizontal) { groupMovementX = cachedGroupRecord.oldLeft - groupOffset; } else { groupMovementY = cachedGroupRecord.oldTop - groupOffset; } } forEachContainer(groupBundle.itemsContainer, function (container, j) { // Don't try to cache something outside of the realization range. if (realizationRange.firstIndex <= itemIndex && realizationRange.lastIndex >= itemIndex) { var cachedItemRecord = this._cachedItemRecords[itemIndex]; if (cachedItemRecord) { var itemPosition = this._getItemPositionForAnimations(itemIndex, i, j); var row = itemPosition.row; var column = itemPosition.column; var left = itemPosition.left; var top = itemPosition.top; cachedItemRecord.inCellSpanningGroup = cachedItemRecord.inCellSpanningGroup || groupIsCellSpanning; // If the item has moved we need to update the cache and apply a transform to make it // appear like it has not moved yet. if (cachedItemRecord.oldRow !== row || cachedItemRecord.oldColumn !== column || cachedItemRecord.oldTop !== top || cachedItemRecord.oldLeft !== left) { cachedItemRecord.row = row; cachedItemRecord.column = column; cachedItemRecord.left = left; cachedItemRecord.top = top; var xOffset = cachedItemRecord.oldLeft - cachedItemRecord.left - groupMovementX; var yOffset = cachedItemRecord.oldTop - cachedItemRecord.top - groupMovementY; xOffset = (this._site.rtl ? -1 : 1) * xOffset; cachedItemRecord.xOffset = xOffset; cachedItemRecord.yOffset = yOffset; if (xOffset !== 0 || yOffset !== 0) { var element = cachedItemRecord.element; cachedItemRecord.needsToResetTransform = true; element.style[transitionScriptName] = ""; element.style[transformNames.scriptName] = "translate(" + xOffset + "px," + yOffset + "px)"; } var itemsBlock = container.parentNode; if (_ElementUtilities.hasClass(itemsBlock, _Constants._itemsBlockClass)) { this._animatingItemsBlocks[uniqueID(itemsBlock)] = itemsBlock; } } } else { // Treat items that came from outside of the realization range into the realization range // as a "Move" which means fade it in. this._cachedInsertedItemRecords[itemIndex] = container; container.style[transitionScriptName] = ""; container.style.opacity = 0; } } itemIndex++; }.bind(this)); var groupIndex = i; var cachedHeader = this._cachedHeaderRecords[groupIndex]; if (cachedHeader) { var headerPosition = this._getHeaderPositionForAnimations(groupIndex); // Note: If a group changes width we allow the header to immediately grow/shrink instead of // animating it. However if the header is removed we stick the header to the last known size. cachedHeader.height = headerPosition.height; cachedHeader.width = headerPosition.width; if (cachedHeader.oldLeft !== headerPosition.left || cachedHeader.oldTop !== headerPosition.top) { cachedHeader.left = headerPosition.left; cachedHeader.top = headerPosition.top; var xOffset = cachedHeader.oldLeft - cachedHeader.left; var yOffset = cachedHeader.oldTop - cachedHeader.top; xOffset = (this._site.rtl ? -1 : 1) * xOffset; if (xOffset !== 0 || yOffset !== 0) { cachedHeader.needsToResetTransform = true; var headerContainer = cachedHeader.element; headerContainer.style[transitionScriptName] = ""; headerContainer.style[transformNames.scriptName] = "translate(" + xOffset + "px," + yOffset + "px)"; } } } if (cachedGroupRecord) { if ((horizontal && cachedGroupRecord.left !== groupOffset) || (!horizontal && cachedGroupRecord.top !== groupOffset)) { var element = cachedGroupRecord.element; if (groupMovementX === 0 && groupMovementY === 0) { if (cachedGroupRecord.needsToResetTransform) { cachedGroupRecord.needsToResetTransform = false; element.style[transformNames.scriptName] = ""; } } else { var groupOffsetX = (this._site.rtl ? -1 : 1) * groupMovementX, groupOffsetY = groupMovementY; cachedGroupRecord.needsToResetTransform = true; element.style[transitionScriptName] = ""; element.style[transformNames.scriptName] = "translate(" + groupOffsetX + "px, " + groupOffsetY + "px)"; } } } } if (this._inListMode || this._itemsPerBar === 1) { var itemsBlockKeys = Object.keys(this._animatingItemsBlocks); for (var b = 0, blockKeys = itemsBlockKeys.length; b < blockKeys; b++) { this._animatingItemsBlocks[itemsBlockKeys[b]].style.overflow = 'visible'; } } this._site._writeProfilerMark("Animation:layoutAnimation,StopTM"); }, executeAnimations: function _LayoutCommon_executeAnimations() { // This function is called when we should perform an animation to reveal the true location of the items. // We fade out removed items, fade in added items, and move items which need to be shifted. If they moved // across columns we do a reflow animation. var animationSignal = new _Signal(); // Only animate the items on screen. this._filterInsertedElements(); this._filterMovedElements(); this._filterRemovedElements(); if (this._insertedElements.length === 0 && this._removedElements.length === 0 && this._itemMoveRecords.length === 0 && this._moveRecords.length === 0) { // Nothing to animate. this._resetAnimationCaches(true); animationSignal.complete(); return animationSignal.promise; } this._animationsRunning = animationSignal.promise; var slowAnimations = exports.Layout._debugAnimations || exports.Layout._slowAnimations; var site = this._site; var insertedElements = this._insertedElements; var removedElements = this._removedElements; var itemMoveRecords = this._itemMoveRecords; var moveRecords = this._moveRecords; var removeDelay = 0; var moveDelay = 0; var addDelay = 0; var currentAnimationPromise = null; var pendingTransitionPromises = []; var hasMultisizeMove = false; var hasReflow = false; var minOffset = 0; var maxOffset = 0; var itemContainersToExpand = {}; var upOutDist = 0; var downOutDist = 0; var upInDist = 0; var downInDist = 0; var reflowItemRecords = []; var horizontal = (this.orientation === "horizontal"); var oldReflowLayoutProperty = horizontal ? "oldColumn" : "oldRow", reflowLayoutProperty = horizontal ? "column" : "row", oldReflowLayoutPosition = horizontal ? "oldTop" : "oldLeft", reflowLayoutPosition = horizontal ? "top" : "left"; var animatingItemsBlocks = this._animatingItemsBlocks; for (var i = 0, len = itemMoveRecords.length; i < len; i++) { var cachedItemRecord = itemMoveRecords[i]; if (cachedItemRecord.inCellSpanningGroup) { hasMultisizeMove = true; break; } } var that = this; function startAnimations() { removePhase(); if (hasMultisizeMove) { cellSpanningFadeOutMove(); } else { if (that._itemsPerBar > 1) { var maxDistance = that._itemsPerBar * that._sizes.containerCrossSize + that._getHeaderSizeContentAdjustment() + that._sizes.containerMargins[horizontal ? "top" : (site.rtl ? "right" : "left")] + (horizontal ? that._sizes.layoutOriginY : that._sizes.layoutOriginX); for (var i = 0, len = itemMoveRecords.length; i < len; i++) { var cachedItemRecord = itemMoveRecords[i]; if (cachedItemRecord[oldReflowLayoutProperty] > cachedItemRecord[reflowLayoutProperty]) { upOutDist = Math.max(upOutDist, cachedItemRecord[oldReflowLayoutPosition] + cachedItemRecord[horizontal ? "height" : "width"]); upInDist = Math.max(upInDist, maxDistance - cachedItemRecord[reflowLayoutPosition]); hasReflow = true; reflowItemRecords.push(cachedItemRecord); } else if (cachedItemRecord[oldReflowLayoutProperty] < cachedItemRecord[reflowLayoutProperty]) { downOutDist = Math.max(downOutDist, maxDistance - cachedItemRecord[oldReflowLayoutPosition]); downInDist = Math.max(downInDist, cachedItemRecord[reflowLayoutPosition] + cachedItemRecord[horizontal ? "height" : "width"]); reflowItemRecords.push(cachedItemRecord); hasReflow = true; } } } if (site.rtl && !horizontal) { upOutDist *= -1; upInDist *= -1; downOutDist *= -1; downInDist *= -1; } if (hasReflow) { reflowPhase(that._itemsPerBar); } else { directMovePhase(); } } } if (exports.Layout._debugAnimations) { _BaseUtils._requestAnimationFrame(function () { startAnimations(); }); } else { startAnimations(); } function waitForNextPhase(nextPhaseCallback) { currentAnimationPromise = Promise.join(pendingTransitionPromises); currentAnimationPromise.done(function () { pendingTransitionPromises = []; // The success is called even if the animations are canceled due to the WinJS.UI.executeTransition // API. To deal with that we check the animationSignal variable. If it is null the animations were // canceled so we shouldn't continue. if (animationSignal) { if (exports.Layout._debugAnimations) { _BaseUtils._requestAnimationFrame(function () { nextPhaseCallback(); }); } else { nextPhaseCallback(); } } }); } function removePhase() { if (removedElements.length) { site._writeProfilerMark("Animation:setupRemoveAnimation,StartTM"); moveDelay += 60; addDelay += 60; var removeDuration = 120; if (slowAnimations) { removeDuration *= 10; } pendingTransitionPromises.push(_TransitionAnimation.executeTransition(removedElements, [{ property: "opacity", delay: removeDelay, duration: removeDuration, timing: "linear", to: 0, skipStylesReset: true }])); site._writeProfilerMark("Animation:setupRemoveAnimation,StopTM"); } } function cellSpanningFadeOutMove() { site._writeProfilerMark("Animation:cellSpanningFadeOutMove,StartTM"); // For multisize items which move we fade out and then fade in (opacity 1->0->1) var moveElements = []; for (var i = 0, len = itemMoveRecords.length; i < len; i++) { var cachedItemRecord = itemMoveRecords[i]; var container = cachedItemRecord.element; moveElements.push(container); } // Including groups and headers. for (var i = 0, len = moveRecords.length; i < len; i++) { var cachedItemRecord = moveRecords[i]; var container = cachedItemRecord.element; moveElements.push(container); } var fadeOutDuration = 120; if (slowAnimations) { fadeOutDuration *= 10; } pendingTransitionPromises.push(_TransitionAnimation.executeTransition(moveElements, { property: "opacity", delay: removeDelay, duration: fadeOutDuration, timing: "linear", to: 0 })); waitForNextPhase(cellSpanningFadeInMove); site._writeProfilerMark("Animation:cellSpanningFadeOutMove,StopTM"); } function cellSpanningFadeInMove() { site._writeProfilerMark("Animation:cellSpanningFadeInMove,StartTM"); addDelay = 0; var moveElements = []; // Move them to their final location. for (var i = 0, len = itemMoveRecords.length; i < len; i++) { var cachedItemRecord = itemMoveRecords[i]; var container = cachedItemRecord.element; container.style[transformNames.scriptName] = ""; moveElements.push(container); } // Including groups and headers. for (var i = 0, len = moveRecords.length; i < len; i++) { var cachedItemRecord = moveRecords[i]; var container = cachedItemRecord.element; container.style[transformNames.scriptName] = ""; moveElements.push(container); } var fadeInDuration = 120; if (slowAnimations) { fadeInDuration *= 10; } // For multisize items which move we fade out and then fade in (opacity 1->0->1) pendingTransitionPromises.push(_TransitionAnimation.executeTransition(moveElements, { property: "opacity", delay: addDelay, duration: fadeInDuration, timing: "linear", to: 1 })); site._writeProfilerMark("Animation:cellSpanningFadeInMove,StopTM"); addPhase(); } function reflowPhase(itemsPerBar) { site._writeProfilerMark("Animation:setupReflowAnimation,StartTM"); var itemContainersLastBarIndices = {}; for (var i = 0, len = reflowItemRecords.length; i < len; i++) { var reflowItemRecord = reflowItemRecords[i]; var xOffset = reflowItemRecord.xOffset; var yOffset = reflowItemRecord.yOffset; if (reflowItemRecord[oldReflowLayoutProperty] > reflowItemRecord[reflowLayoutProperty]) { if (horizontal) { yOffset -= upOutDist; } else { xOffset -= upOutDist; } } else if (reflowItemRecord[oldReflowLayoutProperty] < reflowItemRecord[reflowLayoutProperty]) { if (horizontal) { yOffset += downOutDist; } else { xOffset += downOutDist; } } var container = reflowItemRecord.element; minOffset = Math.min(minOffset, horizontal ? xOffset : yOffset); maxOffset = Math.max(maxOffset, horizontal ? xOffset : yOffset); var itemsContainer = container.parentNode; if (!_ElementUtilities.hasClass(itemsContainer, "win-itemscontainer")) { itemsContainer = itemsContainer.parentNode; } // The itemscontainer element is always overflow:hidden for two reasons: // 1) Better panning performance // 2) When there is margin betweeen the itemscontainer and the surface elements, items that // reflow should not be visible while they travel long distances or overlap with headers. // This introduces an issue when updateTree makes the itemscontainer smaller, but we need its size // to remain the same size during the execution of the animation to avoid having some of the animated // items being clipped. This is only an issue when items from the last column (in horizontal mode) or row // (in vertical mode) of the group will reflow. Therefore, we change the padding so that the contents are larger, // and then use margin to reverse the size change. We don't do this expansion when it is unnecessary because the // layout/formatting caused by these style changes has significant cost when the group has thousands of items. var lastBarIndex = itemContainersLastBarIndices[uniqueID(itemsContainer)]; if (!lastBarIndex) { var count = getItemsContainerLength(getItemsContainerTree(itemsContainer, site.tree)); itemContainersLastBarIndices[uniqueID(itemsContainer)] = lastBarIndex = Math.ceil(count / itemsPerBar) - 1; } if (reflowItemRecords[i][horizontal ? "column" : "row"] === lastBarIndex) { itemContainersToExpand[uniqueID(itemsContainer)] = itemsContainer; } var reflowDuration = 80; if (slowAnimations) { reflowDuration *= 10; } pendingTransitionPromises.push(_TransitionAnimation.executeTransition(container, { property: transformNames.cssName, delay: moveDelay, duration: reflowDuration, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "translate(" + xOffset + "px," + yOffset + "px)" })); } var itemContainerKeys = Object.keys(itemContainersToExpand); for (var i = 0, len = itemContainerKeys.length; i < len; i++) { var itemContainer = itemContainersToExpand[itemContainerKeys[i]]; if (site.rtl && horizontal) { itemContainer.style.paddingLeft = (-1 * minOffset) + 'px'; itemContainer.style.marginLeft = minOffset + 'px'; } else { itemContainer.style[horizontal ? "paddingRight" : "paddingBottom"] = maxOffset + 'px'; itemContainer.style[horizontal ? "marginRight" : "marginBottom"] = '-' + maxOffset + 'px'; } } var itemsBlockKeys = Object.keys(animatingItemsBlocks); for (var i = 0, len = itemsBlockKeys.length; i < len; i++) { animatingItemsBlocks[itemsBlockKeys[i]].classList.add(_Constants._clipClass); } waitForNextPhase(afterReflowPhase); site._writeProfilerMark("Animation:setupReflowAnimation,StopTM"); } function cleanupItemsContainers() { // Reset the styles used to obtain overflow-y: hidden overflow-x: visible. var itemContainerKeys = Object.keys(itemContainersToExpand); for (var i = 0, len = itemContainerKeys.length; i < len; i++) { var itemContainer = itemContainersToExpand[itemContainerKeys[i]]; if (site.rtl && horizontal) { itemContainer.style.paddingLeft = ''; itemContainer.style.marginLeft = ''; } else { itemContainer.style[horizontal ? "paddingRight" : "paddingBottom"] = ''; itemContainer.style[horizontal ? "marginRight" : "marginBottom"] = ''; } } itemContainersToExpand = {}; var itemsBlockKeys = Object.keys(animatingItemsBlocks); for (var i = 0, len = itemsBlockKeys.length; i < len; i++) { var itemsBlock = animatingItemsBlocks[itemsBlockKeys[i]]; itemsBlock.style.overflow = ''; itemsBlock.classList.remove(_Constants._clipClass); } } function afterReflowPhase() { site._writeProfilerMark("Animation:prepareReflowedItems,StartTM"); // Position the items at the edge ready to slide in. for (var i = 0, len = reflowItemRecords.length; i < len; i++) { var reflowItemRecord = reflowItemRecords[i]; var xOffset = 0, yOffset = 0; if (reflowItemRecord[oldReflowLayoutProperty] > reflowItemRecord[reflowLayoutProperty]) { if (horizontal) { yOffset = upInDist; } else { xOffset = upInDist; } } else if (reflowItemRecord[oldReflowLayoutProperty] < reflowItemRecord[reflowLayoutProperty]) { if (horizontal) { yOffset = -1 * downInDist; } else { xOffset = -1 * downInDist; } } reflowItemRecord.element.style[transitionScriptName] = ""; reflowItemRecord.element.style[transformNames.scriptName] = "translate(" + xOffset + "px," + yOffset + "px)"; } site._writeProfilerMark("Animation:prepareReflowedItems,StopTM"); if (exports.Layout._debugAnimations) { _BaseUtils._requestAnimationFrame(function () { directMovePhase(true); }); } else { directMovePhase(true); } } function directMovePhase(fastMode) { // For groups and items which move we transition them from transform: translate(Xpx,Ypx) to translate(0px,0px). var duration = 200; if (fastMode) { duration = 150; moveDelay = 0; addDelay = 0; } if (slowAnimations) { duration *= 10; } if (itemMoveRecords.length > 0 || moveRecords.length > 0) { site._writeProfilerMark("Animation:setupMoveAnimation,StartTM"); var moveElements = []; for (var i = 0, len = moveRecords.length; i < len; i++) { var container = moveRecords[i].element; moveElements.push(container); } for (var i = 0, len = itemMoveRecords.length; i < len; i++) { var container = itemMoveRecords[i].element; moveElements.push(container); } pendingTransitionPromises.push(_TransitionAnimation.executeTransition(moveElements, { property: transformNames.cssName, delay: moveDelay, duration: duration, timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", to: "" })); addDelay += 80; site._writeProfilerMark("Animation:setupMoveAnimation,StopTM"); } addPhase(); } function addPhase() { if (insertedElements.length > 0) { site._writeProfilerMark("Animation:setupInsertAnimation,StartTM"); var addDuration = 120; if (slowAnimations) { addDuration *= 10; } pendingTransitionPromises.push(_TransitionAnimation.executeTransition(insertedElements, [{ property: "opacity", delay: addDelay, duration: addDuration, timing: "linear", to: 1 }])); site._writeProfilerMark("Animation:setupInsertAnimation,StopTM"); } waitForNextPhase(completePhase); } function completePhase() { site._writeProfilerMark("Animation:cleanupAnimations,StartTM"); cleanupItemsContainers(); for (var i = 0, len = removedElements.length; i < len; i++) { var container = removedElements[i]; if (container.parentNode) { _Dispose._disposeElement(container); container.parentNode.removeChild(container); } } site._writeProfilerMark("Animation:cleanupAnimations,StopTM"); that._animationsRunning = null; animationSignal.complete(); } this._resetAnimationCaches(true); // The PVL animation library completes sucessfully even if you cancel an animation. // If the animation promise passed to layout is canceled we should cancel the PVL animations and // set a marker for them to be ignored. animationSignal.promise.then(null, function () { // Since it was canceled make sure we still clean up the styles. cleanupItemsContainers(); for (var i = 0, len = moveRecords.length; i < len; i++) { var container = moveRecords[i].element; container.style[transformNames.scriptName] = ''; container.style.opacity = 1; } for (var i = 0, len = itemMoveRecords.length; i < len; i++) { var container = itemMoveRecords[i].element; container.style[transformNames.scriptName] = ''; container.style.opacity = 1; } for (var i = 0, len = insertedElements.length; i < len; i++) { insertedElements[i].style.opacity = 1; } for (var i = 0, len = removedElements.length; i < len; i++) { var container = removedElements[i]; if (container.parentNode) { _Dispose._disposeElement(container); container.parentNode.removeChild(container); } } this._animationsRunning = null; animationSignal = null; currentAnimationPromise && currentAnimationPromise.cancel(); }.bind(this)); return animationSignal.promise; }, dragOver: function _LayoutCommon_dragOver(x, y, dragInfo) { // The coordinates passed to dragOver should be in ListView's viewport space. 0,0 should be the top left corner of the viewport's padding. var indicesAffected = this.hitTest(x, y), groupAffected = (this._groups ? this._site.groupIndexFromItemIndex(indicesAffected.index) : 0), itemsContainer = this._site.tree[groupAffected].itemsContainer, itemsCount = getItemsContainerLength(itemsContainer), indexOffset = (this._groups ? this._groups[groupAffected].startIndex : 0), visibleRange = this._getVisibleRange(); indicesAffected.index -= indexOffset; indicesAffected.insertAfterIndex -= indexOffset; visibleRange.firstIndex = Math.max(visibleRange.firstIndex - indexOffset - 1, 0); visibleRange.lastIndex = Math.min(visibleRange.lastIndex - indexOffset + 1, itemsCount); var indexAfter = Math.max(Math.min(itemsCount - 1, indicesAffected.insertAfterIndex), -1), indexBefore = Math.min(indexAfter + 1, itemsCount); if (dragInfo) { for (var i = indexAfter; i >= visibleRange.firstIndex; i--) { if (!dragInfo._isIncluded(i + indexOffset)) { indexAfter = i; break; } else if (i === visibleRange.firstIndex) { indexAfter = -1; } } for (var i = indexBefore; i < visibleRange.lastIndex; i++) { if (!dragInfo._isIncluded(i + indexOffset)) { indexBefore = i; break; } else if (i === (visibleRange.lastIndex - 1)) { indexBefore = itemsCount; } } } var elementBefore = containerFromIndex(itemsContainer, indexBefore), elementAfter = containerFromIndex(itemsContainer, indexAfter); if (this._animatedDragItems) { for (var i = 0, len = this._animatedDragItems.length; i < len; i++) { var item = this._animatedDragItems[i]; if (item) { item.style[transitionScriptName] = this._site.animationsDisabled ? "" : dragBetweenTransition; item.style[transformNames.scriptName] = ""; } } } this._animatedDragItems = []; var horizontal = this.orientation === "horizontal", inListMode = this._inListMode || this._itemsPerBar === 1; if (this._groups && this._groups[groupAffected] instanceof Groups.CellSpanningGroup) { inListMode = this._groups[groupAffected]._slotsPerColumn === 1; } var horizontalTransform = 0, verticalTransform = 0; // In general, items should slide in the direction perpendicular to the layout's orientation. // In a horizontal layout, items are laid out top to bottom, left to right. For any two neighboring items in this layout, we want to move the first item up and the second down // to denote that any inserted item would go between those two. // Similarily, vertical layout should have the first item move left and the second move right. // List layout is a special case. A horizontal list layout can only lay things out left to right, so it should slide the two items left and right like a vertical grid. // A vertical list can only lay things out top to bottom, so it should slide items up and down like a horizontal grid. // In other words: Apply horizontal transformations if we're a vertical grid or horizontal list, otherwise use vertical transformations. if ((!horizontal && !inListMode) || (horizontal && inListMode)) { horizontalTransform = this._site.rtl ? -dragBetweenDistance : dragBetweenDistance; } else { verticalTransform = dragBetweenDistance; } if (elementBefore) { elementBefore.style[transitionScriptName] = this._site.animationsDisabled ? "" : dragBetweenTransition; elementBefore.style[transformNames.scriptName] = "translate(" + horizontalTransform + "px, " + verticalTransform + "px)"; this._animatedDragItems.push(elementBefore); } if (elementAfter) { elementAfter.style[transitionScriptName] = this._site.animationsDisabled ? "" : dragBetweenTransition; elementAfter.style[transformNames.scriptName] = "translate(" + (-horizontalTransform) + "px, -" + verticalTransform + "px)"; this._animatedDragItems.push(elementAfter); } }, dragLeave: function _LayoutCommon_dragLeave() { if (this._animatedDragItems) { for (var i = 0, len = this._animatedDragItems.length; i < len; i++) { this._animatedDragItems[i].style[transitionScriptName] = this._site.animationsDisabled ? "" : dragBetweenTransition; this._animatedDragItems[i].style[transformNames.scriptName] = ""; } } this._animatedDragItems = []; }, // Private methods _setMaxRowsOrColumns: function _LayoutCommon_setMaxRowsOrColumns(value) { if (value === this._maxRowsOrColumns || this._inListMode) { return; } // If container size is unavailable then we do not need to compute itemsPerBar // as it will be computed along with the container size. if (this._sizes && this._sizes.containerSizeLoaded) { this._itemsPerBar = Math.floor(this._sizes.maxItemsContainerContentSize / this._sizes.containerCrossSize); if (value) { this._itemsPerBar = Math.min(this._itemsPerBar, value); } this._itemsPerBar = Math.max(1, this._itemsPerBar); } this._maxRowsOrColumns = value; this._invalidateLayout(); }, _getItemPosition: function _LayoutCommon_getItemPosition(itemIndex) { if (this._groupsEnabled) { var groupIndex = Math.min(this._groups.length - 1, this._site.groupIndexFromItemIndex(itemIndex)), group = this._groups[groupIndex], itemOfGroupIndex = itemIndex - group.startIndex; return this._getItemPositionForAnimations(itemIndex, groupIndex, itemOfGroupIndex); } else { return this._getItemPositionForAnimations(itemIndex, 0, itemIndex); } }, _getRealizationRange: function _LayoutCommon_getRealizationRange() { var realizedRange = this._site.realizedRange; return { firstIndex: this._firstItemFromRange(realizedRange.firstPixel), lastIndex: this._lastItemFromRange(realizedRange.lastPixel) }; }, _getVisibleRange: function _LayoutCommon_getVisibleRange() { var visibleRange = this._site.visibleRange; return { firstIndex: this._firstItemFromRange(visibleRange.firstPixel), lastIndex: this._lastItemFromRange(visibleRange.lastPixel) }; }, _resetAnimationCaches: function _LayoutCommon_resetAnimationCaches(skipReset) { if (!skipReset) { // Caches with move transforms: this._resetStylesForRecords(this._cachedGroupRecords); this._resetStylesForRecords(this._cachedItemRecords); this._resetStylesForRecords(this._cachedHeaderRecords); // Caches with insert transforms: this._resetStylesForInsertedRecords(this._cachedInsertedItemRecords); this._resetStylesForInsertedRecords(this._cachedInsertedHeaderRecords); // Caches with insert transforms: this._resetStylesForRemovedRecords(this._cachedRemovedItems); this._resetStylesForRemovedRecords(this._cachedRemovedHeaders); var itemsBlockKeys = Object.keys(this._animatingItemsBlocks); for (var i = 0, len = itemsBlockKeys.length; i < len; i++) { var itemsBlock = this._animatingItemsBlocks[itemsBlockKeys[i]]; itemsBlock.style.overflow = ''; itemsBlock.classList.remove(_Constants._clipClass); } } this._cachedGroupRecords = {}; this._cachedItemRecords = {}; this._cachedHeaderRecords = {}; this._cachedInsertedItemRecords = {}; this._cachedInsertedHeaderRecords = {}; this._cachedRemovedItems = []; this._cachedRemovedHeaders = []; this._animatingItemsBlocks = {}; }, _cacheRemovedElements: function _LayoutCommon_cacheRemovedElements(modifiedElements, cachedRecords, cachedInsertedRecords, removedElements, areHeaders) { var leftStr = "left"; if (this._site.rtl) { leftStr = "right"; } // Offset between the container's content box and its margin box var outerX, outerY; if (areHeaders) { outerX = this._sizes.headerContainerOuterX; outerY = this._sizes.headerContainerOuterY; } else { outerX = this._sizes.containerMargins[leftStr]; outerY = this._sizes.containerMargins.top; } // Cache the removed boxes and place them back in the DOM with position absolute // so that they do not appear like they have moved. for (var i = 0, len = modifiedElements.length; i < len; i++) { var modifiedElementLookup = modifiedElements[i]; if (modifiedElementLookup.newIndex === -1) { var container = modifiedElementLookup.element; var cachedItemRecord = cachedRecords[modifiedElementLookup.oldIndex]; if (cachedItemRecord) { cachedItemRecord.element = container; // This item can no longer be a moved item. delete cachedRecords[modifiedElementLookup.oldIndex]; container.style.position = "absolute"; container.style[transitionScriptName] = ""; container.style.top = cachedItemRecord.oldTop - outerY + "px"; container.style[leftStr] = cachedItemRecord.oldLeft - outerX + "px"; container.style.width = cachedItemRecord.width + "px"; container.style.height = cachedItemRecord.height + "px"; container.style[transformNames.scriptName] = ""; this._site.surface.appendChild(container); removedElements.push(cachedItemRecord); } if (cachedInsertedRecords[modifiedElementLookup.oldIndex]) { delete cachedInsertedRecords[modifiedElementLookup.oldIndex]; } } } }, _cacheInsertedElements: function _LayoutCommon_cacheInsertedItems(modifiedElements, cachedInsertedRecords, cachedRecords) { var newCachedInsertedRecords = {}; for (var i = 0, len = modifiedElements.length; i < len; i++) { var modifiedElementLookup = modifiedElements[i]; var wasInserted = cachedInsertedRecords[modifiedElementLookup.oldIndex]; if (wasInserted) { delete cachedInsertedRecords[modifiedElementLookup.oldIndex]; } if (wasInserted || modifiedElementLookup.oldIndex === -1 || modifiedElementLookup.moved) { var cachedRecord = cachedRecords[modifiedElementLookup.newIndex]; if (cachedRecord) { delete cachedRecords[modifiedElementLookup.newIndex]; } var modifiedElement = modifiedElementLookup.element; newCachedInsertedRecords[modifiedElementLookup.newIndex] = modifiedElement; modifiedElement.style[transitionScriptName] = ""; modifiedElement.style[transformNames.scriptName] = ""; modifiedElement.style.opacity = 0; } } var keys = Object.keys(cachedInsertedRecords); for (var i = 0, len = keys.length; i < len; i++) { newCachedInsertedRecords[keys[i]] = cachedInsertedRecords[keys[i]]; } return newCachedInsertedRecords; }, _resetStylesForRecords: function _LayoutCommon_resetStylesForRecords(recordsHash) { var recordKeys = Object.keys(recordsHash); for (var i = 0, len = recordKeys.length; i < len; i++) { var record = recordsHash[recordKeys[i]]; if (record.needsToResetTransform) { record.element.style[transformNames.scriptName] = ""; record.needsToResetTransform = false; } } }, _resetStylesForInsertedRecords: function _LayoutCommon_resetStylesForInsertedRecords(insertedRecords) { var insertedRecordKeys = Object.keys(insertedRecords); for (var i = 0, len = insertedRecordKeys.length; i < len; i++) { var insertedElement = insertedRecords[insertedRecordKeys[i]]; insertedElement.style.opacity = 1; } }, _resetStylesForRemovedRecords: function _LayoutCommon_resetStylesForRemovedRecords(removedElements) { for (var i = 0, len = removedElements.length; i < len; i++) { var container = removedElements[i].element; if (container.parentNode) { _Dispose._disposeElement(container); container.parentNode.removeChild(container); } } }, _updateAnimationCache: function _LayoutCommon_updateAnimationCache(modifiedItems, modifiedGroups) { // ItemBoxes can change containers so we have to start them back without transforms // and then update them again. ItemsContainers don't need to do this. this._resetStylesForRecords(this._cachedItemRecords); this._resetStylesForRecords(this._cachedHeaderRecords); // Go through all the inserted records and reset their insert transforms. this._resetStylesForInsertedRecords(this._cachedInsertedItemRecords); this._resetStylesForInsertedRecords(this._cachedInsertedHeaderRecords); var existingContainers = {}; var realizationRange = this._getRealizationRange(); var tree = this._site.tree; for (var i = 0, itemIndex = 0, treeLength = tree.length; i < treeLength; i++) { forEachContainer(tree[i].itemsContainer, function (container) { if (realizationRange.firstIndex <= itemIndex && realizationRange.lastIndex >= itemIndex) { existingContainers[uniqueID(container)] = true; } itemIndex++; }); } // Update the indicies before the insert since insert needs the new containers. function updateIndicies(modifiedElements, cachedRecords) { var updatedCachedRecords = {}; for (var i = 0, len = modifiedElements.length; i < len; i++) { var modifiedElementLookup = modifiedElements[i]; var cachedRecord = cachedRecords[modifiedElementLookup.oldIndex]; if (cachedRecord) { updatedCachedRecords[modifiedElementLookup.newIndex] = cachedRecord; cachedRecord.element = modifiedElementLookup.element; delete cachedRecords[modifiedElementLookup.oldIndex]; } } var cachedRecordKeys = Object.keys(cachedRecords); for (var i = 0, len = cachedRecordKeys.length; i < len; i++) { var key = cachedRecordKeys[i], record = cachedRecords[key]; // We need to filter out containers which were removed from the DOM. If container's item // wasn't realized container can be removed without adding record to modifiedItems. if (!record.element || existingContainers[uniqueID(record.element)]) { updatedCachedRecords[key] = record; } } return updatedCachedRecords; } this._cachedItemRecords = updateIndicies(modifiedItems, this._cachedItemRecords); this._cachedHeaderRecords = updateIndicies(modifiedGroups, this._cachedHeaderRecords); this._cachedInsertedItemRecords = this._cacheInsertedElements(modifiedItems, this._cachedInsertedItemRecords, this._cachedItemRecords); this._cachedInsertedHeaderRecords = this._cacheInsertedElements(modifiedGroups, this._cachedInsertedHeaderRecords, this._cachedHeaderRecords); }, _filterRemovedElements: function _LayoutCommon_filterRemovedElements() { this._removedElements = []; if (this._site.animationsDisabled) { this._resetStylesForRemovedRecords(this._cachedRemovedItems); this._resetStylesForRemovedRecords(this._cachedRemovedHeaders); return; } var that = this; var oldLeftStr = this.orientation === "horizontal" ? "oldLeft" : "oldTop"; var widthStr = this.orientation === "horizontal" ? "width" : "height"; var visibleFirstPixel = this._site.scrollbarPos; var visibleLastPixel = visibleFirstPixel + this._site.viewportSize[widthStr] - 1; function filterRemovedElements(removedRecordArray, removedElementsArray) { for (var i = 0, len = removedRecordArray.length; i < len; i++) { var removedItem = removedRecordArray[i]; var container = removedItem.element; if (removedItem[oldLeftStr] + removedItem[widthStr] - 1 < visibleFirstPixel || removedItem[oldLeftStr] > visibleLastPixel || !that._site.viewport.contains(container)) { if (container.parentNode) { _Dispose._disposeElement(container); container.parentNode.removeChild(container); } } else { removedElementsArray.push(container); } } } filterRemovedElements(this._cachedRemovedItems, this._removedElements); filterRemovedElements(this._cachedRemovedHeaders, this._removedElements); }, _filterInsertedElements: function _LayoutCommon_filterInsertedElements() { this._insertedElements = []; if (this._site.animationsDisabled) { this._resetStylesForInsertedRecords(this._cachedInsertedItemRecords); this._resetStylesForInsertedRecords(this._cachedInsertedHeaderRecords); return; } var that = this; var visibleRange = this._getVisibleRange(); function filterInsertedElements(cachedInsertedRecords, insertedElementsArray) { var recordKeys = Object.keys(cachedInsertedRecords); for (var i = 0, len = recordKeys.length; i < len; i++) { var itemIndex = recordKeys[i]; var insertedRecord = cachedInsertedRecords[itemIndex]; if (itemIndex < visibleRange.firstIndex || itemIndex > visibleRange.lastIndex || that._site.viewport.contains(insertedRecord.element)) { insertedRecord.style.opacity = 1; } else { insertedElementsArray.push(insertedRecord); } } } filterInsertedElements(this._cachedInsertedItemRecords, this._insertedElements); filterInsertedElements(this._cachedInsertedHeaderRecords, this._insertedElements); }, _filterMovedElements: function _LayoutCommon_filterMovedElements() { var that = this; // This filters all the items and groups down which could have moved to just the items on screen. // The items which are not going to animate are immediately shown in their correct final location. var oldLeftStr = this.orientation === "horizontal" ? "oldLeft" : "oldTop"; var leftStr = this.orientation === "horizontal" ? "left" : "top"; var widthStr = this.orientation === "horizontal" ? "width" : "height"; var realizationRange = this._getRealizationRange(); var visibleFirstPixel = this._site.scrollbarPos; var visibleLastPixel = visibleFirstPixel + this._site.viewportSize[widthStr] - 1; // ItemMove can reflow across column or fade in/out due to multisize. this._itemMoveRecords = []; this._moveRecords = []; if (!this._site.animationsDisabled) { var tree = this._site.tree; var itemIndex = 0; for (var i = 0, treeLength = tree.length; i < treeLength; i++) { var groupBundle = tree[i]; var groupHasItemToAnimate = false; forEachContainer(groupBundle.itemsContainer, function () { if (realizationRange.firstIndex <= itemIndex && realizationRange.lastIndex >= itemIndex) { var cachedItemRecord = this._cachedItemRecords[itemIndex]; if (cachedItemRecord) { var shouldAnimate = ((cachedItemRecord[oldLeftStr] + cachedItemRecord[widthStr] - 1 >= visibleFirstPixel && cachedItemRecord[oldLeftStr] <= visibleLastPixel) || (cachedItemRecord[leftStr] + cachedItemRecord[widthStr] - 1 >= visibleFirstPixel && cachedItemRecord[leftStr] <= visibleLastPixel)) && that._site.viewport.contains(cachedItemRecord.element); if (shouldAnimate) { groupHasItemToAnimate = true; if (cachedItemRecord.needsToResetTransform) { this._itemMoveRecords.push(cachedItemRecord); delete this._cachedItemRecords[itemIndex]; } } } } itemIndex++; }.bind(this)); var groupIndex = i; var cachedHeaderRecord = this._cachedHeaderRecords[groupIndex]; if (cachedHeaderRecord) { if (groupHasItemToAnimate && cachedHeaderRecord.needsToResetTransform) { this._moveRecords.push(cachedHeaderRecord); delete this._cachedHeaderRecords[groupIndex]; } } var cachedGroupRecord = this._cachedGroupRecords[uniqueID(groupBundle.itemsContainer.element)]; if (cachedGroupRecord) { if (groupHasItemToAnimate && cachedGroupRecord.needsToResetTransform) { this._moveRecords.push(cachedGroupRecord); delete this._cachedGroupRecords[uniqueID(groupBundle.itemsContainer.element)]; } } } } // Reset transform for groups and items that were never on screen. this._resetStylesForRecords(this._cachedGroupRecords); this._resetStylesForRecords(this._cachedItemRecords); this._resetStylesForRecords(this._cachedHeaderRecords); }, _getItemPositionForAnimations: function _LayoutCommon_getItemPositionForAnimations(itemIndex, groupIndex, itemOfGroupIndex) { // Top/Left are used to know if the item has moved and also used to position the item if removed. // Row/Column are used to know if a reflow animation should occur // Height/Width are used when positioning a removed item without impacting layout. // The returned rectangle refers to the win-container's border/padding/content box. Coordinates // are relative to the viewport. var group = this._groups[groupIndex]; var itemPosition = group.getItemPositionForAnimations(itemOfGroupIndex); var groupOffset = (this._groups[groupIndex] ? this._groups[groupIndex].offset : 0); var headerWidth = (this._groupsEnabled && this._groupHeaderPosition === HeaderPosition.left ? this._sizes.headerContainerWidth : 0); var headerHeight = (this._groupsEnabled && this._groupHeaderPosition === HeaderPosition.top ? this._sizes.headerContainerHeight : 0); itemPosition.left += this._sizes.layoutOriginX + headerWidth + this._sizes.itemsContainerOuterX; itemPosition.top += this._sizes.layoutOriginY + headerHeight + this._sizes.itemsContainerOuterY; itemPosition[this._horizontal ? "left" : "top"] += groupOffset; return itemPosition; }, _getHeaderPositionForAnimations: function (groupIndex) { // Top/Left are used to know if the item has moved. // Height/Width are used when positioning a removed item without impacting layout. // The returned rectangle refers to the header container's content box. Coordinates // are relative to the viewport. var headerPosition; if (this._groupsEnabled) { var width = this._sizes.headerContainerWidth - this._sizes.headerContainerOuterWidth, height = this._sizes.headerContainerHeight - this._sizes.headerContainerOuterHeight; if (this._groupHeaderPosition === HeaderPosition.left && !this._horizontal) { height = this._groups[groupIndex].getItemsContainerSize() - this._sizes.headerContainerOuterHeight; } else if (this._groupHeaderPosition === HeaderPosition.top && this._horizontal) { width = this._groups[groupIndex].getItemsContainerSize() - this._sizes.headerContainerOuterWidth; } var offsetX = this._horizontal ? this._groups[groupIndex].offset : 0, offsetY = this._horizontal ? 0 : this._groups[groupIndex].offset; headerPosition = { top: this._sizes.layoutOriginY + offsetY + this._sizes.headerContainerOuterY, left: this._sizes.layoutOriginX + offsetX + this._sizes.headerContainerOuterX, height: height, width: width }; } else { headerPosition = { top: 0, left: 0, height: 0, width: 0 }; } return headerPosition; }, _rangeContainsItems: function _LayoutCommon_rangeContainsItems(firstPixel, lastPixel) { if (this._groups.length === 0) { return false; } else { var lastGroup = this._groups[this._groups.length - 1], lastPixelOfLayout = this._sizes.layoutOrigin + lastGroup.offset + this._getGroupSize(lastGroup) - 1; return lastPixel >= 0 && firstPixel <= lastPixelOfLayout; } }, _itemFromOffset: function _LayoutCommon_itemFromOffset(offset, options) { // supported options are: // - wholeItem: when set to true the fully visible item is returned // - last: if 1 the last item is returned. if 0 the first var that = this; if (this._groups.length === 0) { return 0; } function assignItemMargins(offset) { if (!options.wholeItem) { // This makes it such that if a container's margin is on screen but all of its // content is off screen then we'll treat the container as being off screen. var marginPropLast = (that._horizontal ? (that._site.rtl ? "right" : "left") : "top"), marginPropFirst = (that._horizontal ? (that._site.rtl ? "left" : "right") : "bottom"); if (options.last) { // When looking for the *last* item, treat all container margins // as belonging to the container *before* the margin. return offset - that._sizes.containerMargins[marginPropLast]; } else { // When looking for the *first* item, treat all container margins // as belonging to the container *after* the margin. return offset + that._sizes.containerMargins[marginPropFirst]; } } return offset; } // Assign the headers and margins to the appropriate groups. function assignGroupMarginsAndHeaders(offset) { if (options.last) { // When looking for the *last* group, the *trailing* header and margin belong to the group. return offset - that._getHeaderSizeGroupAdjustment() - that._sizes.itemsContainerOuterStart; } else { // When looking for the *first* group, the *leading* header and margin belong to the group. // No need to make any adjustments to offset because the correct header and margin // already belong to the group. return offset; } } options = options || {}; // Make offset relative to layout's content box offset -= this._sizes.layoutOrigin; offset = assignItemMargins(offset); var groupIndex = this._groupFromOffset(assignGroupMarginsAndHeaders(offset)), group = this._groups[groupIndex]; // Make offset relative to the margin box of the group's items container offset -= group.offset; offset -= this._getHeaderSizeGroupAdjustment(); return group.startIndex + group.itemFromOffset(offset, options); }, _firstItemFromRange: function _LayoutCommon_firstItemFromRange(firstPixel, options) { // supported options are: // - wholeItem: when set to true the first fully visible item is returned options = options || {}; options.last = 0; return this._itemFromOffset(firstPixel, options); }, _lastItemFromRange: function _LayoutCommon_lastItemFromRange(lastPixel, options) { // supported options are: // - wholeItem: when set to true the last fully visible item is returned options = options || {}; options.last = 1; return this._itemFromOffset(lastPixel, options); }, _adjustedKeyForRTL: function _LayoutCommon_adjustedKeyForRTL(key) { if (this._site.rtl) { if (key === Key.leftArrow) { key = Key.rightArrow; } else if (key === Key.rightArrow) { key = Key.leftArrow; } } return key; }, _adjustedKeyForOrientationAndBars: function _LayoutCommon_adjustedKeyForOrientationAndBars(key, cellSpanningGroup) { var newKey = key; // Don't support cell spanning if (cellSpanningGroup) { return key; } // First, convert the key into a virtual form based off of horizontal layouts. // In a horizontal layout, left/right keys switch between columns (AKA "bars"), and // up/down keys switch between rows (AKA "slots"). // In vertical mode, we want up/down to switch between rows (AKA "bars" when vertical), // and left/right to switch between columns (AKA "slots" when vertical). // The first step is to convert keypresses in vertical so that up/down always correspond to moving between slots, // and left/right moving between bars. if (!this._horizontal) { switch (newKey) { case Key.leftArrow: newKey = Key.upArrow; break; case Key.rightArrow: newKey = Key.downArrow; break; case Key.upArrow: newKey = Key.leftArrow; break; case Key.downArrow: newKey = Key.rightArrow; break; } } // Next, if we only have one item per bar, we'll make the change-slots-key the same as the change-bars-key if (this._itemsPerBar === 1) { if (newKey === Key.upArrow) { newKey = Key.leftArrow; } else if (newKey === Key.downArrow) { newKey = Key.rightArrow; } } return newKey; }, _getAdjacentForPageKeys: function _LayoutCommon_getAdjacentForPageKeys(currentItem, pressedKey) { var containerMargins = this._sizes.containerMargins, marginSum = (this.orientation === "horizontal" ? containerMargins.left + containerMargins.right : containerMargins.top + containerMargins.bottom); var viewportLength = this._site.viewportSize[this.orientation === "horizontal" ? "width" : "height"], firstPixel = this._site.scrollbarPos, lastPixel = firstPixel + viewportLength - 1 - containerMargins[(this.orientation === "horizontal" ? "right" : "bottom")], newFocus; // Handles page up by attempting to choose the first fully visible item // on the current page. If that item already has focus, chooses the // first item on the previous page. Page down is handled similarly. var firstIndex = this._firstItemFromRange(firstPixel, { wholeItem: true }), lastIndex = this._lastItemFromRange(lastPixel, { wholeItem: false }), currentItemPosition = this._getItemPosition(currentItem.index); var offscreen = false; if (currentItem.index < firstIndex || currentItem.index > lastIndex) { offscreen = true; if (this.orientation === "horizontal") { firstPixel = currentItemPosition.left - marginSum; } else { firstPixel = currentItemPosition.top - marginSum; } lastPixel = firstPixel + viewportLength - 1; firstIndex = this._firstItemFromRange(firstPixel, { wholeItem: true }); lastIndex = this._lastItemFromRange(lastPixel, { wholeItem: false }); } if (pressedKey === Key.pageUp) { if (!offscreen && firstIndex !== currentItem.index) { return { type: _UI.ObjectType.item, index: firstIndex }; } var end; if (this.orientation === "horizontal") { end = currentItemPosition.left + currentItemPosition.width + marginSum + containerMargins.left; } else { end = currentItemPosition.top + currentItemPosition.height + marginSum + containerMargins.bottom; } var firstIndexOnPrevPage = this._firstItemFromRange(end - viewportLength, { wholeItem: true }); if (currentItem.index === firstIndexOnPrevPage) { // The current item is so big that it spanned from the previous page, so we want to at least // move to the previous item. newFocus = Math.max(0, currentItem.index - this._itemsPerBar); } else { newFocus = firstIndexOnPrevPage; } } else { if (!offscreen && lastIndex !== currentItem.index) { return { type: _UI.ObjectType.item, index: lastIndex }; } // We need to subtract twice the marginSum from the item's starting position because we need to // consider that ensureVisible will scroll the viewport to include the new items margin as well // which may push the current item just off screen. var start; if (this.orientation === "horizontal") { start = currentItemPosition.left - marginSum - containerMargins.right; } else { start = currentItemPosition.top - marginSum - containerMargins.bottom; } var lastIndexOnNextPage = Math.max(0, this._lastItemFromRange(start + viewportLength - 1, { wholeItem: true })); if (currentItem.index === lastIndexOnNextPage) { // The current item is so big that it spans across the next page, so we want to at least // move to the next item. It is also ok to blindly increment this index w/o bound checking // since the browse mode clamps the bounds for page keys. This way we do not have to // asynchronoulsy request the count here. newFocus = currentItem.index + this._itemsPerBar; } else { newFocus = lastIndexOnNextPage; } } return { type: _UI.ObjectType.item, index: newFocus }; }, _isCellSpanning: function _LayoutCommon_isCellSpanning(groupIndex) { var group = this._site.groupFromIndex(groupIndex), groupInfo = this._groupInfo; if (groupInfo) { return !!(typeof groupInfo === "function" ? groupInfo(group) : groupInfo).enableCellSpanning; } else { return false; } }, // Can only be called after measuring has been completed _getGroupInfo: function _LayoutCommon_getGroupInfo(groupIndex) { var group = this._site.groupFromIndex(groupIndex), groupInfo = this._groupInfo, margins = this._sizes.containerMargins, adjustedInfo = { enableCellSpanning: false }; groupInfo = (typeof groupInfo === "function" ? groupInfo(group) : groupInfo); if (groupInfo) { if (groupInfo.enableCellSpanning && (+groupInfo.cellWidth !== groupInfo.cellWidth || +groupInfo.cellHeight !== groupInfo.cellHeight)) { throw new _ErrorFromName("WinJS.UI.GridLayout.GroupInfoResultIsInvalid", strings.groupInfoResultIsInvalid); } adjustedInfo = { enableCellSpanning: !!groupInfo.enableCellSpanning, cellWidth: groupInfo.cellWidth + margins.left + margins.right, cellHeight: groupInfo.cellHeight + margins.top + margins.bottom }; } return adjustedInfo; }, // itemIndex is optional _getItemInfo: function _LayoutCommon_getItemInfo(itemIndex) { var result; if (!this._itemInfo || typeof this._itemInfo !== "function") { if (this._useDefaultItemInfo) { result = this._defaultItemInfo(itemIndex); } else { throw new _ErrorFromName("WinJS.UI.GridLayout.ItemInfoIsInvalid", strings.itemInfoIsInvalid); } } else { result = this._itemInfo(itemIndex); } return Promise.as(result).then(function (size) { if (!size || +size.width !== size.width || +size.height !== size.height) { throw new _ErrorFromName("WinJS.UI.GridLayout.ItemInfoIsInvalid", strings.itemInfoIsInvalid); } return size; }); }, _defaultItemInfo: function _LayoutCommon_defaultItemInfo(itemIndex) { var that = this; return this._site.renderItem(this._site.itemFromIndex(itemIndex)).then(function (element) { that._elementsToMeasure[itemIndex] = { element: element }; return that._measureElements(); }).then( function () { var entry = that._elementsToMeasure[itemIndex], size = { width: entry.width, height: entry.height }; delete that._elementsToMeasure[itemIndex]; return size; }, function (error) { delete that._elementsToMeasure[itemIndex]; return Promise.wrapError(error); } ); }, _getGroupSize: function _LayoutCommon_getGroupSize(group) { var headerContainerMinSize = 0; if (this._groupsEnabled) { if (this._horizontal && this._groupHeaderPosition === HeaderPosition.top) { headerContainerMinSize = this._sizes.headerContainerMinWidth; } else if (!this._horizontal && this._groupHeaderPosition === HeaderPosition.left) { headerContainerMinSize = this._sizes.headerContainerMinHeight; } } return Math.max(headerContainerMinSize, group.getItemsContainerSize() + this._getHeaderSizeGroupAdjustment()); }, // offset should be relative to the grid layout's content box _groupFromOffset: function _LayoutCommon_groupFromOffset(offset) { return offset < this._groups[0].offset ? 0 : this._groupFrom(function (group) { return offset < group.offset; }); }, _groupFromImpl: function _LayoutCommon_groupFromImpl(fromGroup, toGroup, comp) { if (toGroup < fromGroup) { return null; } var center = fromGroup + Math.floor((toGroup - fromGroup) / 2), centerGroup = this._groups[center]; if (comp(centerGroup, center)) { return this._groupFromImpl(fromGroup, center - 1, comp); } else if (center < toGroup && !comp(this._groups[center + 1], center + 1)) { return this._groupFromImpl(center + 1, toGroup, comp); } else { return center; } }, _groupFrom: function _LayoutCommon_groupFrom(comp) { if (this._groups.length > 0) { var lastGroupIndex = this._groups.length - 1, lastGroup = this._groups[lastGroupIndex]; if (!comp(lastGroup, lastGroupIndex)) { return lastGroupIndex; } else { return this._groupFromImpl(0, this._groups.length - 1, comp); } } else { return null; } }, _invalidateLayout: function _LayoutCommon_invalidateLayout() { if (this._site) { this._site.invalidateLayout(); } }, _resetMeasurements: function _LayoutCommon_resetMeasurements() { if (this._measuringPromise) { this._measuringPromise.cancel(); this._measuringPromise = null; } if (this._containerSizeClassName) { _ElementUtilities.removeClass(this._site.surface, this._containerSizeClassName); deleteDynamicCssRule(this._containerSizeClassName); this._containerSizeClassName = null; } this._sizes = null; this._resetAnimationCaches(); }, _measureElements: function _LayoutCommon_measureElements() { // batching measurements to minimalize number of layout passes if (!this._measuringElements) { var that = this; // Schedule a job so that: // 1. Calls to _measureElements are batched. // 2. that._measuringElements is set before the promise handler is executed // (that._measuringElements is used within the handler). that._measuringElements = Scheduler.schedulePromiseHigh(null, "WinJS.UI.GridLayout._measuringElements").then( function measure() { that._site._writeProfilerMark("_measureElements,StartTM"); var surface = that._createMeasuringSurface(), itemsContainer = _Global.document.createElement("div"), site = that._site, measuringElements = that._measuringElements, elementsToMeasure = that._elementsToMeasure, stopMeasuring = false; itemsContainer.className = _Constants._itemsContainerClass + " " + _Constants._laidOutClass; // This code is executed by CellSpanningGroups where styling is configured for –ms-grid. Let's satisfy these assumptions itemsContainer.style.cssText += ";display: -ms-grid" + ";-ms-grid-column: 1" + ";-ms-grid-row: 1"; var keys = Object.keys(elementsToMeasure), len, i; for (i = 0, len = keys.length; i < len; i++) { var element = elementsToMeasure[keys[i]].element; element.style["-ms-grid-column"] = i + 1; element.style["-ms-grid-row"] = i + 1; itemsContainer.appendChild(element); } surface.appendChild(itemsContainer); site.viewport.insertBefore(surface, site.viewport.firstChild); // Reading from the DOM may cause the app's resize handler to // be run synchronously which may invalidate this measuring // operation. When this happens, stop measuring. measuringElements.then(null, function () { stopMeasuring = true; }); for (i = 0, len = keys.length; i < len && !stopMeasuring; i++) { var entry = elementsToMeasure[keys[i]], item = entry.element.querySelector("." + _Constants._itemClass); entry.width = _ElementUtilities.getTotalWidth(item); entry.height = _ElementUtilities.getTotalHeight(item); } if (surface.parentNode) { surface.parentNode.removeChild(surface); } if (measuringElements === that._measuringElements) { that._measuringElements = null; } site._writeProfilerMark("_measureElements,StopTM"); }, function (error) { that._measuringElements = null; return Promise.wrapError(error); } ); } return this._measuringElements; }, _ensureEnvInfo: function _LayoutCommon_ensureEnvInfo() { if (!this._envInfo) { this._envInfo = getEnvironmentSupportInformation(this._site); if (this._envInfo && !this._envInfo.supportsCSSGrid) { _ElementUtilities.addClass(this._site.surface, _Constants._noCSSGrid); } } return !!this._envInfo; }, _createMeasuringSurface: function _LayoutCommon_createMeasuringSurface() { var surface = _Global.document.createElement("div"); surface.style.cssText = "visibility: hidden" + ";-ms-grid-columns: auto" + ";-ms-grid-rows: auto" + ";-ms-flex-align: start" + ";-webkit-align-items: flex-start" + ";align-items: flex-start"; surface.className = _Constants._scrollableClass + " " + (this._inListMode ? _Constants._listLayoutClass : _Constants._gridLayoutClass); if (!this._envInfo.supportsCSSGrid) { _ElementUtilities.addClass(surface, _Constants._noCSSGrid); } if (this._groupsEnabled) { if (this._groupHeaderPosition === HeaderPosition.top) { _ElementUtilities.addClass(surface, _Constants._headerPositionTopClass); } else { _ElementUtilities.addClass(surface, _Constants._headerPositionLeftClass); } } return surface; }, // Assumes that the size of the item at the specified index is representative // of the size of all items, measures it, and stores the measurements in // this._sizes. If necessary, also: // - Creates a CSS rule to give the containers a height and width // - Stores the name associated with the rule in this._containerSizeClassName // - Adds the class name associated with the rule to the surface _measureItem: function _LayoutCommon_measureItem(index) { var that = this; var perfId = "Layout:measureItem"; var site = that._site; var measuringPromise = that._measuringPromise; // itemPromise is optional. It is provided when taking a second attempt at measuring. function measureItemImpl(index, itemPromise) { var secondTry = !!itemPromise, elementPromises = {}, itemPromise, left = site.rtl ? "right" : "left"; return site.itemCount.then(function (count) { if (!count || (that._groupsEnabled && !site.groupCount)) { return Promise.cancel; } itemPromise = itemPromise || site.itemFromIndex(index); elementPromises.container = site.renderItem(itemPromise); if (that._groupsEnabled) { elementPromises.headerContainer = site.renderHeader(that._site.groupFromIndex(site.groupIndexFromItemIndex(index))); } return Promise.join(elementPromises); }).then(function (elements) { // Reading from the DOM is tricky because each read may trigger a resize handler which // may invalidate this layout object. To make it easier to minimize bugs in this edge case: // 1. All DOM reads for _LayoutCommon_measureItem should be contained within this function. // 2. This function should remain as simple as possible. Stick to DOM reads, avoid putting // logic in here, and cache all needed instance variables at the top of the function. // // Returns null if the measuring operation was invalidated while reading from the DOM. // Otherwise, returns an object containing the measurements. function readMeasurementsFromDOM() { var horizontal = that._horizontal; var groupsEnabled = that._groupsEnabled; var stopMeasuring = false; // Reading from the DOM may cause the app's resize handler to // be run synchronously which may invalidate this measuring // operation. When this happens, stop measuring. measuringPromise.then(null, function () { stopMeasuring = true; }); var firstElementOnSurfaceMargins = getMargins(firstElementOnSurface); var firstElementOnSurfaceOffsetX = site.rtl ? (site.viewport.offsetWidth - (firstElementOnSurface.offsetLeft + firstElementOnSurface.offsetWidth)) : firstElementOnSurface.offsetLeft; var firstElementOnSurfaceOffsetY = firstElementOnSurface.offsetTop; var sizes = { // These will be set by _viewportSizeChanged viewportContentSize: 0, surfaceContentSize: 0, maxItemsContainerContentSize: 0, surfaceOuterHeight: getOuterHeight(surface), surfaceOuterWidth: getOuterWidth(surface), // Origin of the grid layout's content in viewport coordinates layoutOriginX: firstElementOnSurfaceOffsetX - firstElementOnSurfaceMargins[left], layoutOriginY: firstElementOnSurfaceOffsetY - firstElementOnSurfaceMargins.top, itemsContainerOuterHeight: getOuterHeight(itemsContainer), itemsContainerOuterWidth: getOuterWidth(itemsContainer), // Amount of space between the items container's margin and its content itemsContainerOuterX: getOuter(site.rtl ? "Right" : "Left", itemsContainer), itemsContainerOuterY: getOuter("Top", itemsContainer), itemsContainerMargins: getMargins(itemsContainer), itemBoxOuterHeight: getOuterHeight(itemBox), itemBoxOuterWidth: getOuterWidth(itemBox), containerOuterHeight: getOuterHeight(elements.container), containerOuterWidth: getOuterWidth(elements.container), emptyContainerContentHeight: _ElementUtilities.getContentHeight(emptyContainer), emptyContainerContentWidth: _ElementUtilities.getContentWidth(emptyContainer), containerMargins: getMargins(elements.container), // containerWidth/Height are computed when a uniform group is detected containerWidth: 0, containerHeight: 0, // true when both containerWidth and containerHeight have been measured containerSizeLoaded: false }; if (groupsEnabled) { // Amount of space between the header container's margin and its content sizes.headerContainerOuterX = getOuter(site.rtl ? "Right" : "Left", elements.headerContainer), sizes.headerContainerOuterY = getOuter("Top", elements.headerContainer), sizes.headerContainerOuterWidth = getOuterWidth(elements.headerContainer); sizes.headerContainerOuterHeight = getOuterHeight(elements.headerContainer); sizes.headerContainerWidth = _ElementUtilities.getTotalWidth(elements.headerContainer); sizes.headerContainerHeight = _ElementUtilities.getTotalHeight(elements.headerContainer); sizes.headerContainerMinWidth = getDimension(elements.headerContainer, "minWidth") + sizes.headerContainerOuterWidth; sizes.headerContainerMinHeight = getDimension(elements.headerContainer, "minHeight") + sizes.headerContainerOuterHeight; } var measurements = { // Measurements which are needed after measureItem has returned. sizes: sizes, // Measurements which are only needed within measureItem. viewportContentWidth: _ElementUtilities.getContentWidth(site.viewport), viewportContentHeight: _ElementUtilities.getContentHeight(site.viewport), containerContentWidth: _ElementUtilities.getContentWidth(elements.container), containerContentHeight: _ElementUtilities.getContentHeight(elements.container), containerWidth: _ElementUtilities.getTotalWidth(elements.container), containerHeight: _ElementUtilities.getTotalHeight(elements.container) }; measurements.viewportCrossSize = measurements[horizontal ? "viewportContentHeight" : "viewportContentWidth"]; site.readyToMeasure(); return stopMeasuring ? null : measurements; } function cleanUp() { if (surface.parentNode) { surface.parentNode.removeChild(surface); } } var surface = that._createMeasuringSurface(), itemsContainer = _Global.document.createElement("div"), emptyContainer = _Global.document.createElement("div"), itemBox = elements.container.querySelector("." + _Constants._itemBoxClass), groupIndex = site.groupIndexFromItemIndex(index); emptyContainer.className = _Constants._containerClass; itemsContainer.className = _Constants._itemsContainerClass + " " + _Constants._laidOutClass; // Use display=inline-block so that the width sizes to content when not in list mode. // When in grid mode, put items container and header container in different rows and columns so that the size of the items container does not affect the size of the header container and vice versa. // Use the same for list mode when headers are inline with item containers. // When headers are to the left of a vertical list, or above a horizontal list, put the rows/columns they would be in when laid out normally // into the CSS text for measuring. We have to do this because list item containers should take up 100% of the space left over in the surface // once the group's header is laid out. var itemsContainerRow = 1, itemsContainerColumn = 1, headerContainerRow = 2, headerContainerColumn = 2, firstElementOnSurface = itemsContainer, addHeaderFirst = false; if (that._inListMode && that._groupsEnabled) { if (that._horizontal && that._groupHeaderPosition === HeaderPosition.top) { itemsContainerRow = 2; headerContainerColumn = 1; headerContainerRow = 1; firstElementOnSurface = elements.headerContainer; addHeaderFirst = true; } else if (!that._horizontal && that._groupHeaderPosition === HeaderPosition.left) { itemsContainerColumn = 2; headerContainerColumn = 1; headerContainerRow = 1; firstElementOnSurface = elements.headerContainer; addHeaderFirst = true; } } // ListMode needs to use display block to proprerly measure items in vertical mode, and display flex to properly measure items in horizontal mode itemsContainer.style.cssText += ";display: " + (that._inListMode ? ((that._horizontal ? "flex" : "block") + "; overflow: hidden") : "inline-block") + ";vertical-align:top" + ";-ms-grid-column: " + itemsContainerColumn + ";-ms-grid-row: " + itemsContainerRow; if (!that._inListMode) { elements.container.style.display = "inline-block"; } if (that._groupsEnabled) { elements.headerContainer.style.cssText += ";display: inline-block" + ";-ms-grid-column: " + headerContainerColumn + ";-ms-grid-row: " + headerContainerRow; _ElementUtilities.addClass(elements.headerContainer, _Constants._laidOutClass + " " + _Constants._groupLeaderClass); if ((that._groupHeaderPosition === HeaderPosition.top && that._horizontal) || (that._groupHeaderPosition === HeaderPosition.left && !that._horizontal)) { _ElementUtilities.addClass(itemsContainer, _Constants._groupLeaderClass); } } if (addHeaderFirst) { surface.appendChild(elements.headerContainer); } itemsContainer.appendChild(elements.container); itemsContainer.appendChild(emptyContainer); surface.appendChild(itemsContainer); if (!addHeaderFirst && that._groupsEnabled) { surface.appendChild(elements.headerContainer); } site.viewport.insertBefore(surface, site.viewport.firstChild); var measurements = readMeasurementsFromDOM(); if (!measurements) { // While reading from the DOM, the measuring operation was invalidated. Bail out. cleanUp(); return Promise.cancel; } else if ((that._horizontal && measurements.viewportContentHeight === 0) || (!that._horizontal && measurements.viewportContentWidth === 0)) { // ListView is invisible so we can't measure. Return a canceled promise. cleanUp(); return Promise.cancel; } else if (!secondTry && !that._isCellSpanning(groupIndex) && (measurements.containerContentWidth === 0 || measurements.containerContentHeight === 0)) { // win-container has no size. For backwards compatibility, wait for the item promise and then try measuring again. cleanUp(); return itemPromise.then(function () { return measureItemImpl(index, itemPromise); }); } else { var sizes = that._sizes = measurements.sizes; // Wrappers for orientation-specific properties. // Sizes prefaced with "cross" refer to the sizes orthogonal to the current layout orientation. Sizes without a preface are in the orientation's direction. Object.defineProperties(sizes, { surfaceOuterCrossSize: { get: function () { return (that._horizontal ? sizes.surfaceOuterHeight : sizes.surfaceOuterWidth); }, enumerable: true }, layoutOrigin: { get: function () { return (that._horizontal ? sizes.layoutOriginX : sizes.layoutOriginY); }, enumerable: true }, itemsContainerOuterSize: { get: function () { return (that._horizontal ? sizes.itemsContainerOuterWidth : sizes.itemsContainerOuterHeight); }, enumerable: true }, itemsContainerOuterCrossSize: { get: function () { return (that._horizontal ? sizes.itemsContainerOuterHeight : sizes.itemsContainerOuterWidth); }, enumerable: true }, itemsContainerOuterStart: { get: function () { return (that._horizontal ? sizes.itemsContainerOuterX : sizes.itemsContainerOuterY); }, enumerable: true }, itemsContainerOuterCrossStart: { get: function () { return (that._horizontal ? sizes.itemsContainerOuterY : sizes.itemsContainerOuterX); }, enumerable: true }, containerCrossSize: { get: function () { return (that._horizontal ? sizes.containerHeight : sizes.containerWidth); }, enumerable: true }, containerSize: { get: function () { return (that._horizontal ? sizes.containerWidth : sizes.containerHeight); }, enumerable: true }, }); // If the measured group is uniform, measure the container height // and width now. Otherwise, compute them thru itemInfo on demand (via _ensureContainerSize). if (!that._isCellSpanning(groupIndex)) { if (that._inListMode) { var itemsContainerContentSize = measurements.viewportCrossSize - sizes.surfaceOuterCrossSize - that._getHeaderSizeContentAdjustment() - sizes.itemsContainerOuterCrossSize; if (that._horizontal) { sizes.containerHeight = itemsContainerContentSize; sizes.containerWidth = measurements.containerWidth; } else { sizes.containerHeight = measurements.containerHeight; sizes.containerWidth = itemsContainerContentSize; } } else { sizes.containerWidth = measurements.containerWidth; sizes.containerHeight = measurements.containerHeight; } sizes.containerSizeLoaded = true; } that._createContainerStyleRule(); that._viewportSizeChanged(measurements.viewportCrossSize); cleanUp(); } }); } if (!measuringPromise) { site._writeProfilerMark(perfId + ",StartTM"); // Use a signal to guarantee that measuringPromise is set before the promise // handler is executed (measuringPromise is referenced within measureItemImpl). var promiseStoredSignal = new _Signal(); that._measuringPromise = measuringPromise = promiseStoredSignal.promise.then(function () { if (that._ensureEnvInfo()) { return measureItemImpl(index); } else { // Couldn't get envInfo. ListView is invisible. Bail out. return Promise.cancel; } }).then(function () { site._writeProfilerMark(perfId + ":complete,info"); site._writeProfilerMark(perfId + ",StopTM"); }, function (error) { // The purpose of the measuring promise is so that we only // measure once. If measuring fails, clear the promise because // we still need to measure. that._measuringPromise = null; site._writeProfilerMark(perfId + ":canceled,info"); site._writeProfilerMark(perfId + ",StopTM"); return Promise.wrapError(error); }); promiseStoredSignal.complete(); } return measuringPromise; }, _getHeaderSizeGroupAdjustment: function () { if (this._groupsEnabled) { if (this._horizontal && this._groupHeaderPosition === HeaderPosition.left) { return this._sizes.headerContainerWidth; } else if (!this._horizontal && this._groupHeaderPosition === HeaderPosition.top) { return this._sizes.headerContainerHeight; } } return 0; }, _getHeaderSizeContentAdjustment: function () { if (this._groupsEnabled) { if (this._horizontal && this._groupHeaderPosition === HeaderPosition.top) { return this._sizes.headerContainerHeight; } else if (!this._horizontal && this._groupHeaderPosition === HeaderPosition.left) { return this._sizes.headerContainerWidth; } } return 0; }, // Horizontal layouts lay items out top to bottom, left to right, whereas vertical layouts lay items out left to right, top to bottom. // The viewport size is the size layouts use to determine how many items can be placed in one bar, so it should be cross to the // orientation. _getViewportCrossSize: function () { return this._site.viewportSize[this._horizontal ? "height" : "width"]; }, // viewportContentSize is the new viewport size _viewportSizeChanged: function _LayoutCommon_viewportSizeChanged(viewportContentSize) { var sizes = this._sizes; sizes.viewportContentSize = viewportContentSize; sizes.surfaceContentSize = viewportContentSize - sizes.surfaceOuterCrossSize; sizes.maxItemsContainerContentSize = sizes.surfaceContentSize - sizes.itemsContainerOuterCrossSize - this._getHeaderSizeContentAdjustment(); // This calculation is for uniform layouts if (sizes.containerSizeLoaded && !this._inListMode) { this._itemsPerBar = Math.floor(sizes.maxItemsContainerContentSize / sizes.containerCrossSize); if (this.maximumRowsOrColumns) { this._itemsPerBar = Math.min(this._itemsPerBar, this.maximumRowsOrColumns); } this._itemsPerBar = Math.max(1, this._itemsPerBar); } else { if (this._inListMode) { sizes[this._horizontal ? "containerHeight" : "containerWidth"] = sizes.maxItemsContainerContentSize; } this._itemsPerBar = 1; } // Ignore animations if height changed this._resetAnimationCaches(); }, _createContainerStyleRule: function _LayoutCommon_createContainerStyleRule() { // Adding CSS rules is expensive. Add a rule to provide a // height and width for containers if the app hasn't provided one. var sizes = this._sizes; if (!this._containerSizeClassName && sizes.containerSizeLoaded && (sizes.emptyContainerContentHeight === 0 || sizes.emptyContainerContentWidth === 0)) { var width = sizes.containerWidth - sizes.containerOuterWidth + "px", height = sizes.containerHeight - sizes.containerOuterHeight + "px"; if (this._inListMode) { if (this._horizontal) { height = "calc(100% - " + (sizes.containerMargins.top + sizes.containerMargins.bottom) + "px)"; } else { width = "auto"; } } if (!this._containerSizeClassName) { this._containerSizeClassName = uniqueCssClassName("containersize"); _ElementUtilities.addClass(this._site.surface, this._containerSizeClassName); } var ruleSelector = "." + _Constants._containerClass, ruleBody = "width:" + width + ";height:" + height + ";"; addDynamicCssRule(this._containerSizeClassName, this._site, ruleSelector, ruleBody); } }, // Computes container width and height if they haven't been computed yet. This // should happen when the first uniform group is created. _ensureContainerSize: function _LayoutCommon_ensureContainerSize(group) { var sizes = this._sizes; if (!sizes.containerSizeLoaded && !this._ensuringContainerSize) { var promise; if ((!this._itemInfo || typeof this._itemInfo !== "function") && this._useDefaultItemInfo) { var margins = sizes.containerMargins; promise = Promise.wrap({ width: group.groupInfo.cellWidth - margins.left - margins.right, height: group.groupInfo.cellHeight - margins.top - margins.bottom }); } else { promise = this._getItemInfo(); } var that = this; this._ensuringContainerSize = promise.then(function (itemSize) { sizes.containerSizeLoaded = true; sizes.containerWidth = itemSize.width + sizes.itemBoxOuterWidth + sizes.containerOuterWidth; sizes.containerHeight = itemSize.height + sizes.itemBoxOuterHeight + sizes.containerOuterHeight; if (!that._inListMode) { that._itemsPerBar = Math.floor(sizes.maxItemsContainerContentSize / sizes.containerCrossSize); if (that.maximumRowsOrColumns) { that._itemsPerBar = Math.min(that._itemsPerBar, that.maximumRowsOrColumns); } that._itemsPerBar = Math.max(1, that._itemsPerBar); } else { that._itemsPerBar = 1; } that._createContainerStyleRule(); }); promise.done( function () { that._ensuringContainerSize = null; }, function () { that._ensuringContainerSize = null; } ); return promise; } else { return this._ensuringContainerSize ? this._ensuringContainerSize : Promise.wrap(); } }, _indexToCoordinate: function _LayoutCommon_indexToCoordinate(index, itemsPerBar) { itemsPerBar = itemsPerBar || this._itemsPerBar; var bar = Math.floor(index / itemsPerBar); if (this._horizontal) { return { column: bar, row: index - bar * itemsPerBar }; } else { return { row: bar, column: index - bar * itemsPerBar }; } }, // Empty ranges are represented by null. Non-empty ranges are represented by // an object with 2 properties: firstIndex and lastIndex. _rangeForGroup: function _LayoutCommon_rangeForGroup(group, range) { var first = group.startIndex, last = first + group.count - 1; if (!range || range.firstIndex > last || range.lastIndex < first) { // There isn't any overlap between range and the group's indices return null; } else { return { firstIndex: Math.max(0, range.firstIndex - first), lastIndex: Math.min(group.count - 1, range.lastIndex - first) }; } }, _syncDomWithGroupHeaderPosition: function _LayoutCommon_syncDomWithGroupHeaderPosition(tree) { if (this._groupsEnabled && this._oldGroupHeaderPosition !== this._groupHeaderPosition) { // this._oldGroupHeaderPosition may refer to top, left, or null. It will be null // the first time this function is called which means that no styles have to be // removed. var len = tree.length, i; // Remove styles associated with old group header position if (this._oldGroupHeaderPosition === HeaderPosition.top) { _ElementUtilities.removeClass(this._site.surface, _Constants._headerPositionTopClass); // maxWidth must be cleared because it is used with headers in the top position but not the left position. // The _groupLeaderClass must be removed from the itemsContainer element because the associated styles // should only be applied to it when headers are in the top position. if (this._horizontal) { for (i = 0; i < len; i++) { tree[i].header.style.maxWidth = ""; _ElementUtilities.removeClass(tree[i].itemsContainer.element, _Constants._groupLeaderClass); } } else { this._site.surface.style.msGridRows = ""; } } else if (this._oldGroupHeaderPosition === HeaderPosition.left) { _ElementUtilities.removeClass(this._site.surface, _Constants._headerPositionLeftClass); // msGridColumns is cleared for a similar reason as maxWidth if (!this._horizontal) { for (i = 0; i < len; i++) { tree[i].header.style.maxHeight = ""; _ElementUtilities.removeClass(tree[i].itemsContainer.element, _Constants._groupLeaderClass); } } this._site.surface.style.msGridColumns = ""; } // Add styles associated with new group header position if (this._groupHeaderPosition === HeaderPosition.top) { _ElementUtilities.addClass(this._site.surface, _Constants._headerPositionTopClass); if (this._horizontal) { for (i = 0; i < len; i++) { _ElementUtilities.addClass(tree[i].itemsContainer.element, _Constants._groupLeaderClass); } } } else { _ElementUtilities.addClass(this._site.surface, _Constants._headerPositionLeftClass); if (!this._horizontal) { for (i = 0; i < len; i++) { _ElementUtilities.addClass(tree[i].itemsContainer.element, _Constants._groupLeaderClass); } } } this._oldGroupHeaderPosition = this._groupHeaderPosition; } }, _layoutGroup: function _LayoutCommon_layoutGroup(index) { var group = this._groups[index], groupBundle = this._site.tree[index], headerContainer = groupBundle.header, itemsContainer = groupBundle.itemsContainer.element, sizes = this._sizes, groupCrossSize = group.getItemsContainerCrossSize(); if (this._groupsEnabled) { if (this._horizontal) { if (this._groupHeaderPosition === HeaderPosition.top) { // Horizontal with headers above // var headerContainerMinContentWidth = sizes.headerContainerMinWidth - sizes.headerContainerOuterWidth, itemsContainerContentWidth = group.getItemsContainerSize() - sizes.headerContainerOuterWidth; headerContainer.style.maxWidth = Math.max(headerContainerMinContentWidth, itemsContainerContentWidth) + "px"; if (this._envInfo.supportsCSSGrid) { headerContainer.style.msGridColumn = index + 1; itemsContainer.style.msGridColumn = index + 1; } else { headerContainer.style.height = (sizes.headerContainerHeight - sizes.headerContainerOuterHeight) + "px"; itemsContainer.style.height = (groupCrossSize - sizes.itemsContainerOuterHeight) + "px"; // If the itemsContainer is too small, the next group's header runs the risk of appearing below the current group's items. // We need to add a margin to the bottom of the itemsContainer to prevent that from happening. itemsContainer.style.marginBottom = sizes.itemsContainerMargins.bottom + (sizes.maxItemsContainerContentSize - groupCrossSize + sizes.itemsContainerOuterHeight) + "px"; } // itemsContainers only get the _groupLeaderClass when header position is top. _ElementUtilities.addClass(itemsContainer, _Constants._groupLeaderClass); } else { // Horizontal with headers on the left // if (this._envInfo.supportsCSSGrid) { headerContainer.style.msGridColumn = index * 2 + 1; itemsContainer.style.msGridColumn = index * 2 + 2; } else { headerContainer.style.width = sizes.headerContainerWidth - sizes.headerContainerOuterWidth + "px"; headerContainer.style.height = (groupCrossSize - sizes.headerContainerOuterHeight) + "px"; itemsContainer.style.height = (groupCrossSize - sizes.itemsContainerOuterHeight) + "px"; } } } else { if (this._groupHeaderPosition === HeaderPosition.left) { // Vertical with headers on the left // var headerContainerMinContentHeight = sizes.headerContainerMinHeight - sizes.headerContainerOuterHeight, itemsContainerContentHeight = group.getItemsContainerSize() - sizes.headerContainerOuterHeight; headerContainer.style.maxHeight = Math.max(headerContainerMinContentHeight, itemsContainerContentHeight) + "px"; if (this._envInfo.supportsCSSGrid) { headerContainer.style.msGridRow = index + 1; itemsContainer.style.msGridRow = index + 1; } else { headerContainer.style.width = (sizes.headerContainerWidth - sizes.headerContainerOuterWidth) + "px"; itemsContainer.style.width = (groupCrossSize - sizes.itemsContainerOuterWidth) + "px"; // If the itemsContainer is too small, the next group's header runs the risk of appearing to the side of the current group's items. // We need to add a margin to the right of the itemsContainer to prevent that from happening (or the left margin, in RTL). itemsContainer.style["margin" + (this._site.rtl ? "Left" : "Right")] = (sizes.itemsContainerMargins[(this._site.rtl ? "left" : "right")] + (sizes.maxItemsContainerContentSize - groupCrossSize + sizes.itemsContainerOuterWidth)) + "px"; } // itemsContainers only get the _groupLeaderClass when header position is left. _ElementUtilities.addClass(itemsContainer, _Constants._groupLeaderClass); } else { // Vertical with headers above // headerContainer.style.msGridRow = index * 2 + 1; // It's important to explicitly set the container height in vertical list mode with headers above, since we use flow layout. // When the header's content is taken from the DOM, the headerContainer will shrink unless it has a height set. if (this._inListMode) { headerContainer.style.height = (sizes.headerContainerHeight - sizes.headerContainerOuterHeight) + "px"; } else { if (this._envInfo.supportsCSSGrid) { itemsContainer.style.msGridRow = index * 2 + 2; } else { headerContainer.style.height = sizes.headerContainerHeight - sizes.headerContainerOuterHeight + "px"; headerContainer.style.width = (groupCrossSize - sizes.headerContainerOuterWidth) + "px"; itemsContainer.style.width = (groupCrossSize - sizes.itemsContainerOuterWidth) + "px"; } } } } // Header containers always get the _groupLeaderClass. _ElementUtilities.addClass(headerContainer, _Constants._laidOutClass + " " + _Constants._groupLeaderClass); } _ElementUtilities.addClass(itemsContainer, _Constants._laidOutClass); } }, { // The maximum number of rows or columns of win-containers to put into each items block. // A row/column cannot be split across multiple items blocks. win-containers // are grouped into items blocks in order to mitigate the costs of the platform doing // a layout in response to insertions and removals of win-containers. _barsPerItemsBlock: 4 }); }), // // Layouts // _LegacyLayout: _Base.Namespace._lazy(function () { return _Base.Class.derive(exports._LayoutCommon, null, { /// /// Gets or sets a value that indicates whether the layout should disable the backdrop feature /// which avoids blank areas while panning in a virtualized list. /// /// disableBackdrop is deprecated. Style: .win-listview .win-container.win-backdrop { background-color:transparent; } instead. /// /// disableBackdrop: { get: function _LegacyLayout_disableBackdrop_get() { return this._backdropDisabled || false; }, set: function _LegacyLayout_disableBackdrop_set(value) { _ElementUtilities._deprecated(_ErrorMessages.disableBackdropIsDeprecated); value = !!value; if (this._backdropDisabled !== value) { this._backdropDisabled = value; if (this._disableBackdropClassName) { deleteDynamicCssRule(this._disableBackdropClassName); this._site && _ElementUtilities.removeClass(this._site.surface, this._disableBackdropClassName); this._disableBackdropClassName = null; } this._disableBackdropClassName = uniqueCssClassName("disablebackdrop"); this._site && _ElementUtilities.addClass(this._site.surface, this._disableBackdropClassName); if (value) { var ruleSelector = ".win-container.win-backdrop", ruleBody = "background-color:transparent;"; addDynamicCssRule(this._disableBackdropClassName, this._site, ruleSelector, ruleBody); } } } }, /// /// Gets or sets the fill color for the default pattern used for the backdrops. /// The default value is "rgba(155,155,155,0.23)". /// /// backdropColor is deprecated. Style: .win-listview .win-container.win-backdrop { rgba(155,155,155,0.23); } instead. /// /// backdropColor: { get: function _LegacyLayout_backdropColor_get() { return this._backdropColor || "rgba(155,155,155,0.23)"; }, set: function _LegacyLayout_backdropColor_set(value) { _ElementUtilities._deprecated(_ErrorMessages.backdropColorIsDeprecated); if (value && this._backdropColor !== value) { this._backdropColor = value; if (this._backdropColorClassName) { deleteDynamicCssRule(this._backdropColorClassName); this._site && _ElementUtilities.removeClass(this._site.surface, this._backdropColorClassName); this._backdropColorClassName = null; } this._backdropColorClassName = uniqueCssClassName("backdropcolor"); this._site && _ElementUtilities.addClass(this._site.surface, this._backdropColorClassName); var ruleSelector = ".win-container.win-backdrop", ruleBody = "background-color:" + value + ";"; addDynamicCssRule(this._backdropColorClassName, this._site, ruleSelector, ruleBody); } } } }); }), GridLayout: _Base.Namespace._lazy(function () { return _Base.Class.derive(exports._LegacyLayout, function (options) { /// /// /// Creates a new GridLayout. /// /// /// An object that contains one or more property/value pairs to apply to the new control. Each property of the options /// object corresponds to one of the control's properties or events. /// /// /// The new GridLayout. /// /// options = options || {}; // Executing setters to display compatibility warning this.itemInfo = options.itemInfo; this.groupInfo = options.groupInfo; this._maxRowsOrColumns = 0; this._useDefaultItemInfo = true; this._elementsToMeasure = {}; this._groupHeaderPosition = options.groupHeaderPosition || HeaderPosition.top; this.orientation = options.orientation || "horizontal"; if (options.maxRows) { this.maxRows = +options.maxRows; } if (options.maximumRowsOrColumns) { this.maximumRowsOrColumns = +options.maximumRowsOrColumns; } }, { // Public /// /// Gets the maximum number of rows or columns, depending on the orientation, that should present before it introduces wrapping to the layout. /// A value of 0 indicates that there is no maximum. The default value is 0. /// maximumRowsOrColumns: { get: function () { return this._maxRowsOrColumns; }, set: function (value) { this._setMaxRowsOrColumns(value); } }, /// /// Gets or sets the maximum number of rows displayed by the ListView. /// /// WinJS.UI.GridLayout.maxRows may be altered or unavailable after the Windows Library for JavaScript 2.0. Instead, use the maximumRowsOrColumns property. /// /// maxRows: { get: function () { return this.maximumRowsOrColumns; }, set: function (maxRows) { _ElementUtilities._deprecated(_ErrorMessages.maxRowsIsDeprecated); this.maximumRowsOrColumns = maxRows; } }, /// /// Determines the size of the item and whether /// the item should be placed in a new column. /// /// GridLayout.itemInfo may be altered or unavailable in future versions. Instead, use CellSpanningLayout. /// /// itemInfo: { enumerable: true, get: function () { return this._itemInfo; }, set: function (itemInfo) { itemInfo && _ElementUtilities._deprecated(_ErrorMessages.itemInfoIsDeprecated); this._itemInfo = itemInfo; this._invalidateLayout(); } }, /// /// Indicates whether a group has cell spanning items and specifies the dimensions of the cell. /// /// GridLayout.groupInfo may be altered or unavailable in future versions. Instead, use CellSpanningLayout. /// /// groupInfo: { enumerable: true, get: function () { return this._groupInfo; }, set: function (groupInfo) { groupInfo && _ElementUtilities._deprecated(_ErrorMessages.groupInfoIsDeprecated); this._groupInfo = groupInfo; this._invalidateLayout(); } } }); }) }); var Groups = _Base.Namespace.defineWithParent(null, null, { UniformGroupBase: _Base.Namespace._lazy(function () { return _Base.Class.define(null, { cleanUp: function UniformGroupBase_cleanUp() { }, itemFromOffset: function UniformGroupBase_itemFromOffset(offset, options) { // supported options are: // - wholeItem: when set to true the fully visible item is returned // - last: if 1 the last item is returned. if 0 the first options = options || {}; var sizes = this._layout._sizes; // Make offset relative to the items container's content box offset -= sizes.itemsContainerOuterStart; if (options.wholeItem) { offset += (options.last ? -1 : 1) * (sizes.containerSize - 1); } var lastIndexOfGroup = this.count - 1, lastBar = Math.floor(lastIndexOfGroup / this._layout._itemsPerBar), bar = clampToRange(0, lastBar, Math.floor(offset / sizes.containerSize)), index = (bar + options.last) * this._layout._itemsPerBar - options.last; return clampToRange(0, this.count - 1, index); }, hitTest: function UniformGroupBase_hitTest(x, y) { var horizontal = this._layout._horizontal, itemsPerBar = this._layout._itemsPerBar, useListSemantics = this._layout._inListMode || itemsPerBar === 1, directionalLocation = horizontal ? x : y, crossLocation = horizontal ? y : x, sizes = this._layout._sizes; directionalLocation -= sizes.itemsContainerOuterStart; crossLocation -= sizes.itemsContainerOuterCrossStart; var bar = Math.floor(directionalLocation / sizes.containerSize); var slotInBar = clampToRange(0, itemsPerBar - 1, Math.floor(crossLocation / sizes.containerCrossSize)); var index = Math.max(-1, bar * itemsPerBar + slotInBar); // insertAfterIndex is determined by which half of the target element the mouse cursor is currently in. // The trouble is that we can cut the element in half horizontally or cut it in half vertically. // Which one we choose depends on the order that elements are laid out in the grid. // A horizontal grid with multiple rows per column will lay items out starting from top to bottom, and move left to right. // A vertical list is just a horizontal grid with an infinite number of rows per column, so it follows the same order. // In both of these cases, each item is cut in half horizontally, since for any item n, n-1 should be above it and n+1 below (ignoring column changes). // A vertical grid lays items out left to right, top to bottom, and a horizontal list left to right (with infinite items per row). // In this case for item n, n-1 is on the left and n+1 on the right, so we cut the item in half vertically. var insertAfterSlot; if ((!horizontal && useListSemantics) || (horizontal && !useListSemantics)) { insertAfterSlot = (y - sizes.containerHeight / 2) / sizes.containerHeight; } else { insertAfterSlot = (x - sizes.containerWidth / 2) / sizes.containerWidth; } if (useListSemantics) { insertAfterSlot = Math.floor(insertAfterSlot); return { index: index, insertAfterIndex: (insertAfterSlot >= 0 && index >= 0 ? insertAfterSlot : -1) }; } insertAfterSlot = clampToRange(-1, itemsPerBar - 1, insertAfterSlot); var insertAfterIndex; if (insertAfterSlot < 0) { insertAfterIndex = bar * itemsPerBar - 1; } else { insertAfterIndex = bar * itemsPerBar + Math.floor(insertAfterSlot); } return { index: clampToRange(-1, this.count - 1, index), insertAfterIndex: clampToRange(-1, this.count - 1, insertAfterIndex) }; }, getAdjacent: function UniformGroupBase_getAdjacent(currentItem, pressedKey) { var index = currentItem.index, currentBar = Math.floor(index / this._layout._itemsPerBar), currentSlot = index % this._layout._itemsPerBar, newFocus; switch (pressedKey) { case Key.upArrow: newFocus = (currentSlot === 0 ? "boundary" : index - 1); break; case Key.downArrow: var isLastIndexOfGroup = (index === this.count - 1), inLastSlot = (this._layout._itemsPerBar > 1 && currentSlot === this._layout._itemsPerBar - 1); newFocus = (isLastIndexOfGroup || inLastSlot ? "boundary" : index + 1); break; case Key.leftArrow: newFocus = (currentBar === 0 && this._layout._itemsPerBar > 1 ? "boundary" : index - this._layout._itemsPerBar); break; case Key.rightArrow: var lastIndexOfGroup = this.count - 1, lastBar = Math.floor(lastIndexOfGroup / this._layout._itemsPerBar); newFocus = (currentBar === lastBar ? "boundary" : Math.min(index + this._layout._itemsPerBar, this.count - 1)); break; } return (newFocus === "boundary" ? newFocus : { type: _UI.ObjectType.item, index: newFocus }); }, getItemsContainerSize: function UniformGroupBase_getItemsContainerSize() { var sizes = this._layout._sizes, barCount = Math.ceil(this.count / this._layout._itemsPerBar); return barCount * sizes.containerSize + sizes.itemsContainerOuterSize; }, getItemsContainerCrossSize: function UniformGroupBase_getItemsContainerCrossSize() { var sizes = this._layout._sizes; return this._layout._itemsPerBar * sizes.containerCrossSize + sizes.itemsContainerOuterCrossSize; }, getItemPositionForAnimations: function UniformGroupBase_getItemPositionForAnimations(itemIndex) { // Top/Left are used to know if the item has moved and also used to position the item if removed. // Row/Column are used to know if a reflow animation should occur // Height/Width are used when positioning a removed item without impacting layout. // The returned rectangle refers to the win-container's border/padding/content box. Coordinates // are relative to group's items container. var sizes = this._layout._sizes; var leftStr = this._layout._site.rtl ? "right" : "left"; var containerMargins = this._layout._sizes.containerMargins; var coordinates = this._layout._indexToCoordinate(itemIndex); var itemPosition = { row: coordinates.row, column: coordinates.column, top: containerMargins.top + coordinates.row * sizes.containerHeight, left: containerMargins[leftStr] + coordinates.column * sizes.containerWidth, height: sizes.containerHeight - sizes.containerMargins.top - sizes.containerMargins.bottom, width: sizes.containerWidth - sizes.containerMargins.left - sizes.containerMargins.right }; return itemPosition; } }); }), // // Groups for GridLayout // // Each group implements a 3 function layout interface. The interface is used // whenever GridLayout has to do a layout. The interface consists of: // - prepareLayout/prepareLayoutWithCopyOfTree: Called 1st. Group should update all of its internal // layout state. It should not modify the DOM. Group should implement either prepareLayout or // prepareLayoutWithCopyOfTree. The former is preferable because the latter is expensive as calling // it requires copying the group's tree. Implementing prepareLayoutWithCopyOfTree is necessary when // the group is manually laying out items and is laying out unrealized items asynchronously // (e.g. CellSpanningGroup). This requires a copy of the tree from the previous layout pass. // - layoutRealizedRange: Called 2nd. Group should update the DOM so that // the realized range reflects the internal layout state computed during // prepareLayout. // - layoutUnrealizedRange: Called 3rd. Group should update the DOM for the items // outside of the realized range. This function returns a promise so // it can do its work asynchronously. When the promise completes, layout will // be done. // // The motivation for this interface is perceived performance. If groups had just 1 // layout function, all items would have to be laid out before any animations could // begin. With this interface, animations can begin playing after // layoutRealizedRange is called. // // Each group also implements a cleanUp function which is called when the group is // no longer needed so that it can clean up the DOM and its resources. After cleanUp // is called, the group object cannnot be reused. // UniformGroup: _Base.Namespace._lazy(function () { return _Base.Class.derive(Groups.UniformGroupBase, function UniformGroup_ctor(layout, itemsContainer) { this._layout = layout; this._itemsContainer = itemsContainer; _ElementUtilities.addClass(this._itemsContainer, layout._inListMode ? _Constants._uniformListLayoutClass : _Constants._uniformGridLayoutClass); }, { cleanUp: function UniformGroup_cleanUp(skipDomCleanUp) { if (!skipDomCleanUp) { _ElementUtilities.removeClass(this._itemsContainer, _Constants._uniformGridLayoutClass); _ElementUtilities.removeClass(this._itemsContainer, _Constants._uniformListLayoutClass); this._itemsContainer.style.height = this._itemsContainer.style.width = ""; } this._itemsContainer = null; this._layout = null; this.groupInfo = null; this.startIndex = null; this.offset = null; this.count = null; }, prepareLayout: function UniformGroup_prepareLayout(itemsCount, oldChangedRealizedRange, oldState, updatedProperties) { this.groupInfo = updatedProperties.groupInfo; this.startIndex = updatedProperties.startIndex; this.count = itemsCount; return this._layout._ensureContainerSize(this); }, layoutRealizedRange: function UniformGroup_layoutRealizedRange() { // Explicitly set the items container size. This is required so that the // surface, which is a grid, will have its width sized to content. var sizes = this._layout._sizes; this._itemsContainer.style[this._layout._horizontal ? "width" : "height"] = this.getItemsContainerSize() - sizes.itemsContainerOuterSize + "px"; this._itemsContainer.style[this._layout._horizontal ? "height" : "width"] = (this._layout._inListMode ? sizes.maxItemsContainerContentSize + "px" : this._layout._itemsPerBar * sizes.containerCrossSize + "px"); }, layoutUnrealizedRange: function UniformGroup_layoutUnrealizedRange() { return Promise.wrap(); } }); }), UniformFlowGroup: _Base.Namespace._lazy(function () { return _Base.Class.derive(Groups.UniformGroupBase, function UniformFlowGroup_ctor(layout, tree) { this._layout = layout; this._itemsContainer = tree.element; _ElementUtilities.addClass(this._itemsContainer, layout._inListMode ? _Constants._uniformListLayoutClass : _Constants._uniformGridLayoutClass); }, { cleanUp: function UniformFlowGroup_cleanUp(skipDomCleanUp) { if (!skipDomCleanUp) { _ElementUtilities.removeClass(this._itemsContainer, _Constants._uniformListLayoutClass); _ElementUtilities.removeClass(this._itemsContainer, _Constants._uniformGridLayoutClass); this._itemsContainer.style.height = ""; } }, layout: function UniformFlowGroup_layout() { this._layout._site._writeProfilerMark("Layout:_UniformFlowGroup:setItemsContainerHeight,info"); this._itemsContainer.style.height = this.count * this._layout._sizes.containerHeight + "px"; } }); }), CellSpanningGroup: _Base.Namespace._lazy(function () { return _Base.Class.define(function CellSpanningGroup_ctor(layout, itemsContainer) { this._layout = layout; this._itemsContainer = itemsContainer; _ElementUtilities.addClass(this._itemsContainer, _Constants._cellSpanningGridLayoutClass); this.resetMap(); }, { cleanUp: function CellSpanningGroup_cleanUp(skipDomCleanUp) { if (!skipDomCleanUp) { this._cleanContainers(); _ElementUtilities.removeClass(this._itemsContainer, _Constants._cellSpanningGridLayoutClass); this._itemsContainer.style.cssText = ""; } this._itemsContainer = null; if (this._layoutPromise) { this._layoutPromise.cancel(); this._layoutPromise = null; } this.resetMap(); this._slotsPerColumn = null; this._offScreenSlotsPerColumn = null; this._items = null; this._layout = null; this._containersToHide = null; this.groupInfo = null; this.startIndex = null; this.offset = null; this.count = null; }, prepareLayoutWithCopyOfTree: function CellSpanningGroup_prepareLayoutWithCopyOfTree(tree, oldChangedRealizedRange, oldState, updatedProperties) { var that = this; var i; // Remember the items in the old realized item range that changed. // During layoutRealizedRange, they either need to be relaid out or hidden. this._containersToHide = {}; if (oldChangedRealizedRange) { for (i = oldChangedRealizedRange.firstIndex; i <= oldChangedRealizedRange.lastIndex; i++) { this._containersToHide[uniqueID(oldState._items[i])] = oldState._items[i]; } } // Update public properties this.groupInfo = updatedProperties.groupInfo; this.startIndex = updatedProperties.startIndex; this.count = tree.items.length; this._items = tree.items; this._slotsPerColumn = Math.floor(this._layout._sizes.maxItemsContainerContentSize / this.groupInfo.cellHeight); if (this._layout.maximumRowsOrColumns) { this._slotsPerColumn = Math.min(this._slotsPerColumn, this._layout.maximumRowsOrColumns); } this._slotsPerColumn = Math.max(this._slotsPerColumn, 1); this.resetMap(); var itemInfoPromises = new Array(this.count); for (i = 0; i < this.count; i++) { itemInfoPromises[i] = this._layout._getItemInfo(this.startIndex + i); } return Promise.join(itemInfoPromises).then(function (itemInfos) { itemInfos.forEach(function (itemInfo, index) { that.addItemToMap(index, itemInfo); }); }); }, layoutRealizedRange: function CellSpanningGroup_layoutRealizedRange(firstChangedIndex, realizedRange) { // Lay out the changed items within the realized range if (realizedRange) { var firstChangedRealizedIndex = Math.max(firstChangedIndex, realizedRange.firstIndex), i; for (i = firstChangedRealizedIndex; i <= realizedRange.lastIndex; i++) { this._layoutItem(i); delete this._containersToHide[uniqueID(this._items[i])]; } } // Hide the old containers that are in the realized range but weren't relaid out Object.keys(this._containersToHide).forEach(function (id) { _ElementUtilities.removeClass(this._containersToHide[id], _Constants._laidOutClass); }.bind(this)); this._containersToHide = {}; // Explicitly set the items container height. This is required so that the // surface, which is a grid, will have its width sized to content. this._itemsContainer.style.cssText += ";width:" + (this.getItemsContainerSize() - this._layout._sizes.itemsContainerOuterSize) + "px;height:" + this._layout._sizes.maxItemsContainerContentSize + "px;-ms-grid-columns: (" + this.groupInfo.cellWidth + "px)[" + this.getColumnCount() + "];-ms-grid-rows: (" + this.groupInfo.cellHeight + "px)[" + (this._slotsPerColumn + this._offScreenSlotsPerColumn) + "]"; }, layoutUnrealizedRange: function CellSpanningGroup_layoutUnrealizedRange(firstChangedIndex, realizedRange, beforeRealizedRange) { var that = this; var layoutJob; that._layoutPromise = new Promise(function (complete) { function completeLayout() { layoutJob = null; complete(); } function schedule(fn) { return Scheduler.schedule(fn, Scheduler.Priority.normal, null, "WinJS.UI.GridLayout.CellSpanningGroup.LayoutUnrealizedRange"); } // These loops are built to lay out the items that are closest to // the realized range first. if (realizedRange) { var stop = false; // For laying out the changed items that are before the realized range var before = realizedRange.firstIndex - 1; // For laying out the changed items that are after the realized range var after = Math.max(firstChangedIndex, realizedRange.lastIndex + 1); after = Math.max(before + 1, after); // Alternate between laying out items before and after the realized range layoutJob = schedule(function unrealizedRangeWork(info) { while (!stop) { if (info.shouldYield) { info.setWork(unrealizedRangeWork); return; } stop = true; if (before >= firstChangedIndex) { that._layoutItem(before); before--; stop = false; } if (after < that.count) { that._layoutItem(after); after++; stop = false; } } completeLayout(); }); } else if (beforeRealizedRange) { // The items we are laying out come before the realized range. // so lay them out in descending order. var i = that.count - 1; layoutJob = schedule(function beforeRealizedRangeWork(info) { for (; i >= firstChangedIndex; i--) { if (info.shouldYield) { info.setWork(beforeRealizedRangeWork); return; } that._layoutItem(i); } completeLayout(); }); } else { // The items we are laying out come after the realized range // so lay them out in ascending order. var i = firstChangedIndex; layoutJob = schedule(function afterRealizedRangeWork(info) { for (; i < that.count; i++) { if (info.shouldYield) { info.setWork(afterRealizedRangeWork); return; } that._layoutItem(i); } completeLayout(); }); } }, function () { // Cancellation handler for that._layoutPromise layoutJob && layoutJob.cancel(); layoutJob = null; }); return that._layoutPromise; }, itemFromOffset: function CellSpanningGroup_itemFromOffset(offset, options) { // supported options are: // - wholeItem: when set to true the fully visible item is returned // - last: if 1 the last item is returned. if 0 the first options = options || {}; var sizes = this._layout._sizes, margins = sizes.containerMargins; // Make offset relative to the items container's content box offset -= sizes.itemsContainerOuterX; offset -= ((options.last ? 1 : -1) * margins[options.last ? "left" : "right"]); var value = this.indexFromOffset(offset, options.wholeItem, options.last).item; return clampToRange(0, this.count - 1, value); }, getAdjacent: function CellSpanningGroup_getAdjacent(currentItem, pressedKey) { var index, originalIndex; index = originalIndex = currentItem.index; var newIndex, inMap, inMapIndex; if (this.lastAdjacent === index) { inMapIndex = this.lastInMapIndex; } else { inMapIndex = this.findItem(index); } do { var column = Math.floor(inMapIndex / this._slotsPerColumn), row = inMapIndex - column * this._slotsPerColumn, lastColumn = Math.floor((this.occupancyMap.length - 1) / this._slotsPerColumn); switch (pressedKey) { case Key.upArrow: if (row > 0) { inMapIndex--; } else { return { type: _UI.ObjectType.item, index: originalIndex }; } break; case Key.downArrow: if (row + 1 < this._slotsPerColumn) { inMapIndex++; } else { return { type: _UI.ObjectType.item, index: originalIndex }; } break; case Key.leftArrow: inMapIndex = (column > 0 ? inMapIndex - this._slotsPerColumn : -1); break; case Key.rightArrow: inMapIndex = (column < lastColumn ? inMapIndex + this._slotsPerColumn : this.occupancyMap.length); break; } inMap = inMapIndex >= 0 && inMapIndex < this.occupancyMap.length; if (inMap) { newIndex = this.occupancyMap[inMapIndex] ? this.occupancyMap[inMapIndex].index : undefined; } } while (inMap && (index === newIndex || newIndex === undefined)); this.lastAdjacent = newIndex; this.lastInMapIndex = inMapIndex; return (inMap ? { type: _UI.ObjectType.item, index: newIndex } : "boundary"); }, hitTest: function CellSpanningGroup_hitTest(x, y) { var sizes = this._layout._sizes, itemIndex = 0; // Make the coordinates relative to the items container's content box x -= sizes.itemsContainerOuterX; y -= sizes.itemsContainerOuterY; if (this.occupancyMap.length > 0) { var result = this.indexFromOffset(x, false, 0); var counter = Math.min(this._slotsPerColumn - 1, Math.floor(y / this.groupInfo.cellHeight)), curr = result.index, lastValidIndex = curr; while (counter-- > 0) { curr++; if (this.occupancyMap[curr]) { lastValidIndex = curr; } } if (!this.occupancyMap[lastValidIndex]) { lastValidIndex--; } itemIndex = this.occupancyMap[lastValidIndex].index; } var itemSize = this.getItemSize(itemIndex), itemLeft = itemSize.column * this.groupInfo.cellWidth, itemTop = itemSize.row * this.groupInfo.cellHeight, useListSemantics = this._slotsPerColumn === 1, insertAfterIndex = itemIndex; if ((useListSemantics && (x < (itemLeft + itemSize.contentWidth / 2))) || (!useListSemantics && (y < (itemTop + itemSize.contentHeight / 2)))) { insertAfterIndex--; } return { type: _UI.ObjectType.item, index: clampToRange(0, this.count - 1, itemIndex), insertAfterIndex: clampToRange(-1, this.count - 1, insertAfterIndex) }; }, getItemsContainerSize: function CellSpanningGroup_getItemsContainerSize() { var sizes = this._layout._sizes; return this.getColumnCount() * this.groupInfo.cellWidth + sizes.itemsContainerOuterSize; }, getItemsContainerCrossSize: function CellSpanningGroup_getItemsContainerCrossSize() { var sizes = this._layout._sizes; return sizes.maxItemsContainerContentSize + sizes.itemsContainerOuterCrossSize; }, getItemPositionForAnimations: function CellSpanningGroup_getItemPositionForAnimations(itemIndex) { // Top/Left are used to know if the item has moved and also used to position the item if removed. // Row/Column are used to know if a reflow animation should occur // Height/Width are used when positioning a removed item without impacting layout. // The returned rectangle refers to the win-container's border/padding/content box. Coordinates // are relative to group's items container. var leftStr = this._layout._site.rtl ? "right" : "left"; var containerMargins = this._layout._sizes.containerMargins; var coordinates = this.getItemSize(itemIndex); var groupInfo = this.groupInfo; var itemPosition = { row: coordinates.row, column: coordinates.column, top: containerMargins.top + coordinates.row * groupInfo.cellHeight, left: containerMargins[leftStr] + coordinates.column * groupInfo.cellWidth, height: coordinates.contentHeight, width: coordinates.contentWidth }; return itemPosition; }, _layoutItem: function CellSpanningGroup_layoutItem(index) { var entry = this.getItemSize(index); this._items[index].style.cssText += ";-ms-grid-row:" + (entry.row + 1) + ";-ms-grid-column:" + (entry.column + 1) + ";-ms-grid-row-span:" + entry.rows + ";-ms-grid-column-span:" + entry.columns + ";height:" + entry.contentHeight + "px;width:" + entry.contentWidth + "px"; _ElementUtilities.addClass(this._items[index], _Constants._laidOutClass); return this._items[index]; }, _cleanContainers: function CellSpanningGroup_cleanContainers() { var items = this._items, len = items.length, i; for (i = 0; i < len; i++) { items[i].style.cssText = ""; _ElementUtilities.removeClass(items[i], _Constants._laidOutClass); } }, // Occupancy map getColumnCount: function CellSpanningGroup_getColumnCount() { return Math.ceil(this.occupancyMap.length / this._slotsPerColumn); }, getOccupancyMapItemCount: function CellSpanningGroup_getOccupancyMapItemCount() { var index = -1; // Use forEach as the map may be sparse this.occupancyMap.forEach(function (item) { if (item.index > index) { index = item.index; } }); return index + 1; }, coordinateToIndex: function CellSpanningGroup_coordinateToIndex(c, r) { return c * this._slotsPerColumn + r; }, markSlotAsFull: function CellSpanningGroup_markSlotAsFull(index, itemEntry) { var coordinates = this._layout._indexToCoordinate(index, this._slotsPerColumn), toRow = coordinates.row + itemEntry.rows; for (var r = coordinates.row; r < toRow && r < this._slotsPerColumn; r++) { for (var c = coordinates.column, toColumn = coordinates.column + itemEntry.columns; c < toColumn; c++) { this.occupancyMap[this.coordinateToIndex(c, r)] = itemEntry; } } this._offScreenSlotsPerColumn = Math.max(this._offScreenSlotsPerColumn, toRow - this._slotsPerColumn); }, isSlotEmpty: function CellSpanningGroup_isSlotEmpty(itemSize, row, column) { for (var r = row, toRow = row + itemSize.rows; r < toRow; r++) { for (var c = column, toColumn = column + itemSize.columns; c < toColumn; c++) { if ((r >= this._slotsPerColumn) || (this.occupancyMap[this.coordinateToIndex(c, r)] !== undefined)) { return false; } } } return true; }, findEmptySlot: function CellSpanningGroup_findEmptySlot(startIndex, itemSize, newColumn) { var coordinates = this._layout._indexToCoordinate(startIndex, this._slotsPerColumn), startRow = coordinates.row, lastColumn = Math.floor((this.occupancyMap.length - 1) / this._slotsPerColumn); if (newColumn) { for (var c = coordinates.column + 1; c <= lastColumn; c++) { if (this.isSlotEmpty(itemSize, 0, c)) { return this.coordinateToIndex(c, 0); } } } else { for (var c = coordinates.column; c <= lastColumn; c++) { for (var r = startRow; r < this._slotsPerColumn; r++) { if (this.isSlotEmpty(itemSize, r, c)) { return this.coordinateToIndex(c, r); } } startRow = 0; } } return (lastColumn + 1) * this._slotsPerColumn; }, findItem: function CellSpanningGroup_findItem(index) { for (var inMapIndex = index, len = this.occupancyMap.length; inMapIndex < len; inMapIndex++) { var entry = this.occupancyMap[inMapIndex]; if (entry && entry.index === index) { return inMapIndex; } } return inMapIndex; }, getItemSize: function CellSpanningGroup_getItemSize(index) { var inMapIndex = this.findItem(index), entry = this.occupancyMap[inMapIndex], coords = this._layout._indexToCoordinate(inMapIndex, this._slotsPerColumn); if (index === entry.index) { return { row: coords.row, column: coords.column, contentWidth: entry.contentWidth, contentHeight: entry.contentHeight, columns: entry.columns, rows: entry.rows }; } else { return null; } }, resetMap: function CellSpanningGroup_resetMap() { this.occupancyMap = []; this.lastAdded = 0; this._offScreenSlotsPerColumn = 0; }, addItemToMap: function CellSpanningGroup_addItemToMap(index, itemInfo) { var that = this; function add(mapEntry, newColumn) { var inMapIndex = that.findEmptySlot(that.lastAdded, mapEntry, newColumn); that.lastAdded = inMapIndex; that.markSlotAsFull(inMapIndex, mapEntry); } var groupInfo = that.groupInfo, margins = that._layout._sizes.containerMargins, mapEntry = { index: index, contentWidth: itemInfo.width, contentHeight: itemInfo.height, columns: Math.max(1, Math.ceil((itemInfo.width + margins.left + margins.right) / groupInfo.cellWidth)), rows: Math.max(1, Math.ceil((itemInfo.height + margins.top + margins.bottom) / groupInfo.cellHeight)) }; add(mapEntry, itemInfo.newColumn); }, indexFromOffset: function CellSpanningGroup_indexFromOffset(adjustedOffset, wholeItem, last) { var measuredWidth = 0, lastItem = 0, groupInfo = this.groupInfo, index = 0; if (this.occupancyMap.length > 0) { lastItem = this.getOccupancyMapItemCount() - 1; measuredWidth = Math.ceil((this.occupancyMap.length - 1) / this._slotsPerColumn) * groupInfo.cellWidth; if (adjustedOffset < measuredWidth) { var counter = this._slotsPerColumn, index = (Math.max(0, Math.floor(adjustedOffset / groupInfo.cellWidth)) + last) * this._slotsPerColumn - last; while (!this.occupancyMap[index] && counter-- > 0) { index += (last > 0 ? -1 : 1); } return { index: index, item: this.occupancyMap[index].index }; } else { index = this.occupancyMap.length - 1; } } return { index: index, item: lastItem + (Math.max(0, Math.floor((adjustedOffset - measuredWidth) / groupInfo.cellWidth)) + last) * this._slotsPerColumn - last }; } }); }) }); _Base.Namespace._moduleDefine(exports, "WinJS.UI", { ListLayout: _Base.Namespace._lazy(function () { return _Base.Class.derive(exports._LegacyLayout, function ListLayout_ctor(options) { /// /// /// Creates a new ListLayout object. /// /// /// An object that contains one or more property/value pairs to apply to the new control. Each property of the options /// object corresponds to one of the object's properties or events. Event names must begin with "on". /// /// /// The new ListLayout object. /// /// options = options || {}; this._itemInfo = {}; this._groupInfo = {}; this._groupHeaderPosition = options.groupHeaderPosition || HeaderPosition.top; this._inListMode = true; this.orientation = options.orientation || "vertical"; }, { initialize: function ListLayout_initialize(site, groupsEnabled) { _ElementUtilities.addClass(site.surface, _Constants._listLayoutClass); exports._LegacyLayout.prototype.initialize.call(this, site, groupsEnabled); }, uninitialize: function ListLayout_uninitialize() { if (this._site) { _ElementUtilities.removeClass(this._site.surface, _Constants._listLayoutClass); } exports._LegacyLayout.prototype.uninitialize.call(this); }, layout: function ListLayout_layout(tree, changedRange, modifiedItems, modifiedGroups) { if (!this._groupsEnabled && !this._horizontal) { return this._layoutNonGroupedVerticalList(tree, changedRange, modifiedItems, modifiedGroups); } else { return exports._LegacyLayout.prototype.layout.call(this, tree, changedRange, modifiedItems, modifiedGroups); } }, _layoutNonGroupedVerticalList: function ListLayout_layoutNonGroupedVerticalList(tree, changedRange, modifiedItems, modifiedGroups) { var that = this; var perfId = "Layout:_layoutNonGroupedVerticalList"; that._site._writeProfilerMark(perfId + ",StartTM"); this._layoutPromise = that._measureItem(0).then(function () { _ElementUtilities[that._usingStructuralNodes ? "addClass" : "removeClass"](that._site.surface, _Constants._structuralNodesClass); if (that._sizes.viewportContentSize !== that._getViewportCrossSize()) { that._viewportSizeChanged(that._getViewportCrossSize()); } that._cacheRemovedElements(modifiedItems, that._cachedItemRecords, that._cachedInsertedItemRecords, that._cachedRemovedItems, false); that._cacheRemovedElements(modifiedGroups, that._cachedHeaderRecords, that._cachedInsertedHeaderRecords, that._cachedRemovedHeaders, true); var itemsContainer = tree[0].itemsContainer, group = new Groups.UniformFlowGroup(that, itemsContainer); that._groups = [group]; group.groupInfo = { enableCellSpanning: false }; group.startIndex = 0; group.count = getItemsContainerLength(itemsContainer); group.offset = 0; group.layout(); that._site._writeProfilerMark(perfId + ":setSurfaceWidth,info"); that._site.surface.style.width = that._sizes.surfaceContentSize + "px"; that._layoutAnimations(modifiedItems, modifiedGroups); that._site._writeProfilerMark(perfId + ":complete,info"); that._site._writeProfilerMark(perfId + ",StopTM"); }, function (error) { that._site._writeProfilerMark(perfId + ":canceled,info"); that._site._writeProfilerMark(perfId + ",StopTM"); return Promise.wrapError(error); }); return { realizedRangeComplete: this._layoutPromise, layoutComplete: this._layoutPromise }; }, numberOfItemsPerItemsBlock: { get: function ListLayout_getNumberOfItemsPerItemsBlock() { var that = this; // Measure when numberOfItemsPerItemsBlock is called so that we measure before ListView has created the full tree structure // which reduces the trident layout required by measure. return this._measureItem(0).then(function () { if (that._envInfo.nestedFlexTooLarge || that._envInfo.nestedFlexTooSmall) { that._usingStructuralNodes = false; } else { that._usingStructuralNodes = exports.ListLayout._numberOfItemsPerItemsBlock > 0; } return (that._usingStructuralNodes ? exports.ListLayout._numberOfItemsPerItemsBlock : null); }); } }, }, { // The maximum number of win-containers to put into each items block. win-containers // are grouped into items blocks in order to mitigate the costs of the platform doing // a layout in response to insertions and removals of win-containers. _numberOfItemsPerItemsBlock: 10 }); }), CellSpanningLayout: _Base.Namespace._lazy(function () { return _Base.Class.derive(exports._LayoutCommon, function CellSpanningLayout_ctor(options) { /// /// /// Creates a new CellSpanningLayout object. /// /// /// An object that contains one or more property/value pairs to apply to the new object. Each property of the options /// object corresponds to one of the object's properties or events. Event names must begin with "on". /// /// /// The new CellSpanningLayout object. /// /// options = options || {}; this._itemInfo = options.itemInfo; this._groupInfo = options.groupInfo; this._groupHeaderPosition = options.groupHeaderPosition || HeaderPosition.top; this._horizontal = true; this._cellSpanning = true; }, { /// /// Gets or set the maximum number of rows or columns, depending on the orientation, to display before content begins to wrap. /// A value of 0 indicates that there is no maximum. /// maximumRowsOrColumns: { get: function () { return this._maxRowsOrColumns; }, set: function (value) { this._setMaxRowsOrColumns(value); } }, /// /// Gets or sets a function that returns the width and height of an item, as well as whether /// it should appear in a new column. Setting this function improves performance because /// the ListView can allocate space for an item without having to measure it first. /// The function takes a single parameter: the index of the item to render. /// The function returns an object that has three properties: /// width: The total width of the item. /// height: The total height of the item. /// newColumn: Set to true to create a column break; otherwise, false. /// itemInfo: { enumerable: true, get: function () { return this._itemInfo; }, set: function (itemInfo) { this._itemInfo = itemInfo; this._invalidateLayout(); } }, /// /// Gets or sets a function that enables cell-spanning and establishes base cell dimensions. /// The function returns an object that has these properties: /// enableCellSpanning: Set to true to allow the ListView to contain items of multiple sizes. /// cellWidth: The width of the base cell. /// cellHeight: The height of the base cell. /// groupInfo: { enumerable: true, get: function () { return this._groupInfo; }, set: function (groupInfo) { this._groupInfo = groupInfo; this._invalidateLayout(); } }, /// /// Gets the orientation of the layout. CellSpanning layout only supports horizontal orientation. /// orientation: { enumerable: true, get: function () { return "horizontal"; } } }); }), _LayoutWrapper: _Base.Namespace._lazy(function () { return _Base.Class.define(function LayoutWrapper_ctor(layout) { this.defaultAnimations = true; // Initialize and hitTest are required this.initialize = function LayoutWrapper_initialize(site, groupsEnabled) { layout.initialize(site, groupsEnabled); }; this.hitTest = function LayoutWrapper_hitTest(x, y) { return layout.hitTest(x, y); }; // These methods are optional layout.uninitialize && (this.uninitialize = function LayoutWrapper_uninitialize() { layout.uninitialize(); }); if ("numberOfItemsPerItemsBlock" in layout) { Object.defineProperty(this, "numberOfItemsPerItemsBlock", { get: function LayoutWrapper_getNumberOfItemsPerItemsBlock() { return layout.numberOfItemsPerItemsBlock; } }); } layout._getItemPosition && (this._getItemPosition = function LayoutWrapper_getItemPosition(index) { return layout._getItemPosition(index); }); layout.itemsFromRange && (this.itemsFromRange = function LayoutWrapper_itemsFromRange(start, end) { return layout.itemsFromRange(start, end); }); layout.getAdjacent && (this.getAdjacent = function LayoutWrapper_getAdjacent(currentItem, pressedKey) { return layout.getAdjacent(currentItem, pressedKey); }); layout.dragOver && (this.dragOver = function LayoutWrapper_dragOver(x, y, dragInfo) { return layout.dragOver(x, y, dragInfo); }); layout.dragLeave && (this.dragLeave = function LayoutWrapper_dragLeave() { return layout.dragLeave(); }); var propertyDefinition = { enumerable: true, get: function () { return "vertical"; } }; if (layout.orientation !== undefined) { propertyDefinition.get = function () { return layout.orientation; }; propertyDefinition.set = function (value) { layout.orientation = value; }; } Object.defineProperty(this, "orientation", propertyDefinition); if (layout.setupAnimations || layout.executeAnimations) { this.defaultAnimations = false; this.setupAnimations = function LayoutWrapper_setupAnimations() { return layout.setupAnimations(); }; this.executeAnimations = function LayoutWrapper_executeAnimations() { return layout.executeAnimations(); }; } if (layout.layout) { if (this.defaultAnimations) { var that = this; this.layout = function LayoutWrapper_layout(tree, changedRange, modifiedItems, modifiedGroups) { var promises = normalizeLayoutPromises(layout.layout(tree, changedRange, [], [])), synchronous; promises.realizedRangeComplete.then(function () { synchronous = true; }); synchronous && that._layoutAnimations(modifiedItems, modifiedGroups); return promises; }; } else { this.layout = function LayoutWrapper_layout(tree, changedRange, modifiedItems, modifiedGroups) { return normalizeLayoutPromises(layout.layout(tree, changedRange, modifiedItems, modifiedGroups)); }; } } }, { uninitialize: function LayoutWrapper_uninitialize() { }, numberOfItemsPerItemsBlock: { get: function LayoutWrapper_getNumberOfItemsPerItemsBlock() { } }, layout: function LayoutWrapper_layout(tree, changedRange, modifiedItems, modifiedGroups) { if (this.defaultAnimations) { this._layoutAnimations(modifiedItems, modifiedGroups); } return normalizeLayoutPromises(); }, itemsFromRange: function LayoutWrapper_itemsFromRange() { return { firstIndex: 0, lastIndex: Number.MAX_VALUE }; }, getAdjacent: function LayoutWrapper_getAdjacent(currentItem, pressedKey) { switch (pressedKey) { case Key.pageUp: case Key.upArrow: case Key.leftArrow: return { type: currentItem.type, index: currentItem.index - 1 }; case Key.downArrow: case Key.rightArrow: case Key.pageDown: return { type: currentItem.type, index: currentItem.index + 1 }; } }, dragOver: function LayoutWrapper_dragOver() { }, dragLeave: function LayoutWrapper_dragLeaver() { }, setupAnimations: function LayoutWrapper_setupAnimations() { }, executeAnimations: function LayoutWrapper_executeAnimations() { }, _getItemPosition: function LayoutWrapper_getItemPosition() { }, _layoutAnimations: function LayoutWrapper_layoutAnimations() { }, }); }), }); function normalizeLayoutPromises(retVal) { if (Promise.is(retVal)) { return { realizedRangeComplete: retVal, layoutComplete: retVal }; } else if (typeof retVal === "object" && retVal && retVal.layoutComplete) { return retVal; } else { return { realizedRangeComplete: Promise.wrap(), layoutComplete: Promise.wrap() }; } } var HeaderPosition = { left: "left", top: "top" }; function getMargins(element) { return { left: getDimension(element, "marginLeft"), right: getDimension(element, "marginRight"), top: getDimension(element, "marginTop"), bottom: getDimension(element, "marginBottom") }; } // Layout, _LayoutCommon, and _LegacyLayout are defined ealier so that their fully // qualified names can be used in _Base.Class.derive. This is required by Blend. _Base.Namespace._moduleDefine(exports, "WinJS.UI", { HeaderPosition: HeaderPosition, _getMargins: getMargins }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ListView/_VirtualizeContentsView',[ 'exports', '../../Core/_Global', '../../Core/_Base', '../../Core/_BaseUtils', '../../Promise', '../../_Signal', '../../Scheduler', '../../Utilities/_Dispose', '../../Utilities/_ElementUtilities', '../../Utilities/_SafeHtml', '../../Utilities/_UI', '../ItemContainer/_Constants', '../ItemContainer/_ItemEventsHandler', './_Helpers', './_ItemsContainer' ], function virtualizeContentsViewInit(exports, _Global, _Base, _BaseUtils, Promise, _Signal, Scheduler, _Dispose, _ElementUtilities, _SafeHtml, _UI, _Constants, _ItemEventsHandler, _Helpers, _ItemsContainer) { "use strict"; function setFlow(from, to) { _ElementUtilities._setAttribute(from, "aria-flowto", to.id); _ElementUtilities._setAttribute(to, "x-ms-aria-flowfrom", from.id); } _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _VirtualizeContentsView: _Base.Namespace._lazy(function () { function cooperativeQueueWorker(info) { var workItems = info.job._workItems; var work; while (workItems.length && !info.shouldYield) { work = workItems.shift(); work(); } info.setWork(cooperativeQueueWorker); if (!workItems.length) { info.job.pause(); } } function scheduleQueueJob(priority, name) { var job = Scheduler.schedule(cooperativeQueueWorker, priority, null, name); job._workItems = []; job.addWork = function (work, head) { if (head) { this._workItems.unshift(work); } else { this._workItems.push(work); } this.resume(); }; job.clearWork = function () { this._workItems.length = 0; }; job.dispose = function () { this.cancel(); this._workItems.length = 0; }; return job; } function shouldWaitForSeZo(listView) { return listView._zooming || listView._pinching; } function waitForSeZo(listView, timeout) { // waitForSeZo will block until sezo calls endZoom on listview, or a timeout duration has elapsed to // unblock a potential deadlock between the sezo waiting on container creation, and container creation // waiting on endZoom. if (listView._isZombie()) { return Promise.wrap(); } if (shouldWaitForSeZo(listView)) { if (+timeout !== timeout) { timeout = _VirtualizeContentsView._waitForSeZoTimeoutDuration; } //To improve SeZo's zoom animation and pinch detection perf, we want to ensure unimportant task //is only run while zooming or pinching is not in progress. return Promise.timeout(_VirtualizeContentsView._waitForSeZoIntervalDuration).then(function () { timeout -= _VirtualizeContentsView._waitForSeZoIntervalDuration; if (timeout <= 0) { return true; } return waitForSeZo(listView, timeout); }); } else { return Promise.wrap(); } } function makeFunctor(scrollToFunctor) { if (typeof scrollToFunctor === "number") { var pos = scrollToFunctor; scrollToFunctor = function () { return { position: pos, direction: "right" }; }; } return scrollToFunctor; } var _VirtualizeContentsView = _Base.Class.define(function VirtualizeContentsView_ctor(listView) { this._listView = listView; this._forceRelayout = false; this.items = new _ItemsContainer._ItemsContainer(listView); this.firstIndexDisplayed = -1; this.lastIndexDisplayed = -1; this.begin = 0; this.end = 0; this._realizePass = 1; this._firstLayoutPass = true; this._runningAnimations = null; this._renderCompletePromise = Promise.wrap(); this._state = new CreatedState(this); this._createLayoutSignal(); this._createTreeBuildingSignal(); this._layoutWork = null; this._onscreenJob = scheduleQueueJob(Scheduler.Priority.aboveNormal, "on-screen items"); this._frontOffscreenJob = scheduleQueueJob(Scheduler.Priority.normal, "front off-screen items"); this._backOffscreenJob = scheduleQueueJob(Scheduler.Priority.belowNormal, "back off-screen items"); this._scrollbarPos = 0; this._direction = "right"; this._scrollToFunctor = makeFunctor(0); }, { _dispose: function VirtualizeContentsView_dispose() { this.cleanUp(); this.items = null; this._renderCompletePromise && this._renderCompletePromise.cancel(); this._renderCompletePromise = null; this._onscreenJob.dispose(); this._frontOffscreenJob.dispose(); this._backOffscreenJob.dispose(); }, _createItem: function VirtualizeContentsView_createItem(itemIndex, itemPromise, available, unavailable) { this._listView._writeProfilerMark("createItem(" + itemIndex + ") " + this._getBoundingRectString(itemIndex) + ",info"); var that = this; that._listView._itemsManager._itemFromItemPromiseThrottled(itemPromise).done( function (element) { if (element) { available(itemIndex, element, that._listView._itemsManager._recordFromElement(element)); } else { unavailable(itemIndex); } }, function (err) { unavailable(itemIndex); return Promise.wrapError(err); } ); }, _addItem: function VirtualizeContentsView_addItem(fragment, itemIndex, element, currentPass) { if (this._realizePass === currentPass) { var record = this._listView._itemsManager._recordFromElement(element); delete this._pendingItemPromises[record.itemPromise.handle]; this.items.setItemAt(itemIndex, { itemBox: null, container: null, element: element, detached: true, itemsManagerRecord: record }); } }, finalItem: function VirtualizeContentsView_finalItem() { return this.containers ? Promise.wrap(this.containers.length - 1) : Promise.cancel; }, _setSkipRealizationForChange: function (skip) { if (skip) { if (this._realizationLevel !== _VirtualizeContentsView._realizationLevel.realize) { this._realizationLevel = _VirtualizeContentsView._realizationLevel.skip; } } else { this._realizationLevel = _VirtualizeContentsView._realizationLevel.realize; } }, _realizeItems: function VirtualizeContentsView_realizeItems(fragment, begin, end, count, currentPass, scrollbarPos, direction, firstInView, lastInView, ignoreGaps) { var perfId = "_realizeItems(" + begin + "-" + (end - 1) + ") visible(" + firstInView + "-" + lastInView + ")"; this._listView._writeProfilerMark(perfId + ",StartTM"); direction = direction || "right"; var counter = end - begin; var inView = lastInView - firstInView + 1, inViewCounter = inView, rightOffscreenCount = end - lastInView - 1, leftOffscreenCount = firstInView - begin; var renderCompletePromises = []; var entranceAnimationSignal = new _Signal(); var viewportItemsRealized = new _Signal(); var frontItemsRealized = new _Signal(); var that = this; function itemIsReady(itemIndex, itemsManagerRecord) { renderCompletePromises.push(Promise._cancelBlocker(itemsManagerRecord.renderComplete)); delivered(itemIndex); } function appendItemsToDom(startIndex, endIndex) { that._listView._writeProfilerMark("_realizeItems_appendedItemsToDom,StartTM"); if (that._listView._isZombie()) { return; } function updateSwipeable(itemData, element) { if (!itemData.updatedSwipeableAttribute && (that._listView.itemsDraggable || that._listView.itemsReorderable || that._listView._swipeable)) { itemData.itemsManagerRecord.renderComplete.done(function () { if (that._realizePass === currentPass) { var dragDisabledOnItem = _ElementUtilities.hasClass(element, _Constants._nonDraggableClass), selectionDisabledOnItem = _ElementUtilities.hasClass(element, _Constants._nonSelectableClass), dragEnabled = (that._listView.itemsDraggable || that._listView.itemsReorderable), swipeSelectEnabled = (that._listView._selectionAllowed() && that._listView._swipeBehavior === _UI.SwipeBehavior.select); if (dragEnabled && !dragDisabledOnItem) { itemData.itemBox.draggable = true; } if (that._listView._swipeable && ((dragEnabled && !swipeSelectEnabled && dragDisabledOnItem) || (swipeSelectEnabled && !dragEnabled && selectionDisabledOnItem) || (dragDisabledOnItem && selectionDisabledOnItem))) { _ElementUtilities.addClass(itemData.itemBox, _Constants._nonSwipeableClass); } itemData.updatedSwipeableAttribute = true; } }); } } var itemIndex; var appendItemsCount = 0; var firstIndex = -1; var lastIndex = -1; for (itemIndex = startIndex; itemIndex <= endIndex; itemIndex++) { var itemData = that.items.itemDataAt(itemIndex); if (itemData) { var element = itemData.element, itemBox = itemData.itemBox; if (!itemBox) { itemBox = that._listView._itemBoxTemplate.cloneNode(true); itemData.itemBox = itemBox; itemBox.appendChild(element); _ElementUtilities.addClass(element, _Constants._itemClass); that._listView._setupAriaSelectionObserver(element); if (that._listView._isSelected(itemIndex)) { _ItemEventsHandler._ItemEventsHandler.renderSelection(itemBox, element, true, true); } that._listView._currentMode().renderDragSourceOnRealizedItem(itemIndex, itemBox); } updateSwipeable(itemData, element, itemBox); var container = that.getContainer(itemIndex); if (itemBox.parentNode !== container) { that._appendAndRestoreFocus(container, itemBox); appendItemsCount++; if (firstIndex < 0) { firstIndex = itemIndex; } lastIndex = itemIndex; itemData.container = container; if (that._listView._isSelected(itemIndex)) { _ElementUtilities.addClass(container, _Constants._selectedClass); } _ElementUtilities.removeClass(container, _Constants._backdropClass); // elementAvailable needs to be called after fragment.appendChild. elementAvailable fulfills a promise for items requested // by the keyboard focus handler. That handler will explicitly call .focus() on the element, so in order for // the focus handler to work, the element needs to be in a tree prior to focusing. that.items.elementAvailable(itemIndex); } } } that._listView._writeProfilerMark("_realizeItems_appendedItemsToDom,StopTM"); if (appendItemsCount > 0) { that._listView._writeProfilerMark("_realizeItems_appendedItemsToDom:" + appendItemsCount + " (" + firstIndex + "-" + lastIndex + "),info"); that._reportElementsLevel(direction); } } function removeGaps(first, last, begin, end) { if (ignoreGaps) { return; } // If we realized items 0 through 20 and then scrolled so that items 25 - 30 are on screen when we // append them to the dom we should remove items 0 - 20 from the dom so there are no gaps between the // two realized spots. // Walk backwards from the beginning and if we find an item which is missing remove the rest var foundMissing = false; while (first >= begin) { foundMissing = testGap(first, foundMissing); first--; } // Walk forwards from the end and if we find an item which is missing remove the rest foundMissing = false; while (last <= end) { foundMissing = testGap(last, foundMissing); last++; } function testGap(itemIndex, foundMissing) { // This helper method is called for each index and once an item is missing from the dom // it removes any future one it encounters. var itemData = that.items.itemDataAt(itemIndex); if (itemData) { var itemBox = itemData.itemBox; if (!itemBox || !itemBox.parentNode) { return true; } else if (foundMissing) { _ElementUtilities.addClass(itemBox.parentNode, _Constants._backdropClass); itemBox.parentNode.removeChild(itemBox); return true; } else { return false; } } else { return true; } } } function scheduleReadySignal(first, last, job, dir, head) { var promises = []; for (var i = first; i <= last; i++) { var itemData = that.items.itemDataAt(i); if (itemData) { promises.push(itemData.itemsManagerRecord.itemPromise); } } function schedule(itemIndex) { var itemData = that.items.itemDataAt(itemIndex); if (itemData) { var record = itemData.itemsManagerRecord; if (!record.readyComplete && that._realizePass === currentPass) { job.addWork(function () { if (that._listView._isZombie()) { return; } if (record.pendingReady && that._realizePass === currentPass) { that._listView._writeProfilerMark("pendingReady(" + itemIndex + "),info"); record.pendingReady(); } }, head); } } } Promise.join(promises).then(function () { if (dir === "right") { for (var i = first; i <= last; i++) { schedule(i); } } else { for (var i = last; i >= first; i--) { schedule(i); } } }); } function delivered(index) { if (that._realizePass !== currentPass) { return; } if (index >= firstInView && index <= lastInView) { if (--inViewCounter === 0) { appendItemsToDom(firstInView, lastInView); removeGaps(firstInView, lastInView, begin, end); if (that._firstLayoutPass) { scheduleReadySignal(firstInView, lastInView, that._frontOffscreenJob, direction === "right" ? "left" : "right", true); var entranceAnimation = Scheduler.schedulePromiseHigh(null, "WinJS.UI.ListView.entranceAnimation").then(function () { if (that._listView._isZombie()) { return; } that._listView._writeProfilerMark("entranceAnimation,StartTM"); var promise = that._listView._animateListEntrance(!that._firstEntranceAnimated); that._firstEntranceAnimated = true; return promise; }); that._runningAnimations = Promise.join([that._runningAnimations, entranceAnimation]); that._runningAnimations.done(function () { that._listView._writeProfilerMark("entranceAnimation,StopTM"); if (that._realizePass === currentPass) { that._runningAnimations = null; entranceAnimationSignal.complete(); } }); that._firstLayoutPass = false; if (that._listView._isCurrentZoomView) { Scheduler.requestDrain(that._onscreenJob.priority); } } else { // during scrolling ready for onscreen items after front off screen items scheduleReadySignal(firstInView, lastInView, that._frontOffscreenJob, direction); entranceAnimationSignal.complete(); } that._updateHeaders(that._listView._canvas, firstInView, lastInView + 1).done(function () { viewportItemsRealized.complete(); }); } } else if (index < firstInView) { --leftOffscreenCount; if (leftOffscreenCount % inView === 0) { appendItemsToDom(begin, firstInView - 1); } if (!leftOffscreenCount) { that._updateHeaders(that._listView._canvas, begin, firstInView).done(function () { if (direction !== "right") { frontItemsRealized.complete(); } }); scheduleReadySignal(begin, firstInView - 1, direction !== "right" ? that._frontOffscreenJob : that._backOffscreenJob, "left"); } } else if (index > lastInView) { --rightOffscreenCount; if (rightOffscreenCount % inView === 0) { appendItemsToDom(lastInView + 1, end - 1); } if (!rightOffscreenCount) { that._updateHeaders(that._listView._canvas, lastInView + 1, end).then(function () { if (direction === "right") { frontItemsRealized.complete(); } }); scheduleReadySignal(lastInView + 1, end - 1, direction === "right" ? that._frontOffscreenJob : that._backOffscreenJob, "right"); } } counter--; if (counter === 0) { that._renderCompletePromise = Promise.join(renderCompletePromises).then(null, function (e) { var error = Array.isArray(e) && e.some(function (item) { return item && !(item instanceof Error && item.name === "Canceled"); }); if (error) { // rethrow return Promise.wrapError(e); } }); (that._headerRenderPromises || Promise.wrap()).done(function () { Scheduler.schedule(function VirtualizeContentsView_async_delivered() { if (that._listView._isZombie()) { workCompleteSignal.cancel(); } else { workCompleteSignal.complete(); } }, Math.min(that._onscreenJob.priority, that._backOffscreenJob.priority), null, "WinJS.UI.ListView._allItemsRealized"); }); } } function newItemIsReady(itemIndex, element, itemsManagerRecord) { if (that._realizePass === currentPass) { var element = itemsManagerRecord.element; that._addItem(fragment, itemIndex, element, currentPass); itemIsReady(itemIndex, itemsManagerRecord); } } if (counter > 0) { var createCount = 0; var updateCount = 0; var cleanCount = 0; that.firstIndexDisplayed = firstInView; that.lastIndexDisplayed = lastInView; var isCurrentZoomView = that._listView._isCurrentZoomView; if (that._highPriorityRealize && (that._firstLayoutPass || that._hasAnimationInViewportPending)) { // startup or edits that will animate items in the viewport that._highPriorityRealize = false; that._onscreenJob.priority = Scheduler.Priority.high; that._frontOffscreenJob.priority = Scheduler.Priority.normal; that._backOffscreenJob.priority = Scheduler.Priority.belowNormal; } else if (that._highPriorityRealize) { // edits that won't animate items in the viewport that._highPriorityRealize = false; that._onscreenJob.priority = Scheduler.Priority.high; that._frontOffscreenJob.priority = Scheduler.Priority.high - 1; that._backOffscreenJob.priority = Scheduler.Priority.high - 1; } else if (isCurrentZoomView) { // scrolling that._onscreenJob.priority = Scheduler.Priority.aboveNormal; that._frontOffscreenJob.priority = Scheduler.Priority.normal; that._backOffscreenJob.priority = Scheduler.Priority.belowNormal; } else { // hidden ListView in SeZo that._onscreenJob.priority = Scheduler.Priority.belowNormal; that._frontOffscreenJob.priority = Scheduler.Priority.idle; that._backOffscreenJob.priority = Scheduler.Priority.idle; } // Create a promise to wrap the work in the queue. When the queue gets to the last item we can mark // the work promise complete and if the work promise is canceled we cancel the queue. // var workCompleteSignal = new _Signal(); // If the version manager recieves a notification we clear the work in the work queues // var cancelToken = that._listView._versionManager.cancelOnNotification(workCompleteSignal.promise); var queueStage1AfterStage0 = function (job, record) { if (record.startStage1) { record.stage0.then(function () { if (that._realizePass === currentPass && record.startStage1) { job.addWork(record.startStage1); } }); } }; var queueWork = function (job, itemIndex) { var itemData = that.items.itemDataAt(itemIndex); if (!itemData) { var itemPromise = that._listView._itemsManager._itemPromiseAtIndex(itemIndex); // Remember this pending item promise and avoid canceling it from the previous realization pass. that._pendingItemPromises[itemPromise.handle] = itemPromise; delete that._previousRealizationPendingItemPromises[itemPromise.handle]; job.addWork(function VirtualizeContentsView_realizeItemsWork() { if (that._listView._isZombie()) { return; } createCount++; that._createItem(itemIndex, itemPromise, newItemIsReady, delivered); // _createItem runs user code if (that._listView._isZombie() || that._realizePass !== currentPass) { return; } if (itemPromise.handle) { var record = that._listView._itemsManager._recordFromHandle(itemPromise.handle); queueStage1AfterStage0(job, record); } }); } }; var queueRight = function (job, first, last) { for (var itemIndex = first; itemIndex <= last; itemIndex++) { queueWork(job, itemIndex); } }; var queueLeft = function (job, first, last) { // Always build the left side in the direction away from the center. for (var itemIndex = last; itemIndex >= first; itemIndex--) { queueWork(job, itemIndex); } }; var handleExistingRange = function (job, first, last) { for (var itemIndex = first; itemIndex <= last; itemIndex++) { var itemData = that.items.itemDataAt(itemIndex); if (itemData) { var record = itemData.itemsManagerRecord; itemIsReady(itemIndex, record); updateCount++; queueStage1AfterStage0(job, record); } } }; // PendingItemPromises are the item promises which we have requested from the ItemsManager // which have not returned an element (placeholder or real). Since we only clean up items // which have an element in _unrealizeItems we need to remember these item promises. We cancel // the item promises from the previous realization iteration if those item promises are not // used for the current realization. this._previousRealizationPendingItemPromises = this._pendingItemPromises || {}; this._pendingItemPromises = {}; var emptyFront; if (direction === "left") { queueLeft(that._onscreenJob, firstInView, lastInView); queueLeft(that._frontOffscreenJob, begin, firstInView - 1); emptyFront = begin > (firstInView - 1); } else { queueRight(that._onscreenJob, firstInView, lastInView); queueRight(that._frontOffscreenJob, lastInView + 1, end - 1); emptyFront = lastInView + 1 > (end - 1); } // Anything left in _previousRealizationPendingItemPromises can be canceled here. // Note: we are doing this synchronously. If we didn't do it synchronously we would have had to merge // _previousRealizationPendingItemPromises and _pendingItemPromises together. This also has the great // benefit to cancel item promises in the backOffScreenArea which are much less important. for (var i = 0, handles = Object.keys(this._previousRealizationPendingItemPromises), len = handles.length; i < len; i++) { var handle = handles[i]; that._listView._itemsManager.releaseItemPromise(this._previousRealizationPendingItemPromises[handle]); } this._previousRealizationPendingItemPromises = {}; // Handle existing items in the second pass to make sure that raising ready signal is added to the queues after creating items handleExistingRange(that._onscreenJob, firstInView, lastInView); if (direction === "left") { handleExistingRange(that._frontOffscreenJob, begin, firstInView - 1); } else { handleExistingRange(that._frontOffscreenJob, lastInView + 1, end - 1); } var showProgress = (inViewCounter === lastInView - firstInView + 1); if (that._firstLayoutPass) { that._listView._canvas.style.opacity = 0; } else { if (showProgress) { that._listView._showProgressBar(that._listView._element, "50%", "50%"); } else { that._listView._hideProgressBar(); } } that._frontOffscreenJob.pause(); that._backOffscreenJob.pause(); viewportItemsRealized.promise.done( function () { that._frontOffscreenJob.resume(); if (emptyFront) { frontItemsRealized.complete(); } }, function () { workCompleteSignal.cancel(); } ); frontItemsRealized.promise.done(function () { that._listView._writeProfilerMark("frontItemsRealized,info"); if (direction === "left") { queueRight(that._backOffscreenJob, lastInView + 1, end - 1); handleExistingRange(that._backOffscreenJob, lastInView + 1, end - 1); } else { queueLeft(that._backOffscreenJob, begin, firstInView - 1); handleExistingRange(that._backOffscreenJob, begin, firstInView - 1); } that._backOffscreenJob.resume(); }); workCompleteSignal.promise.done( function () { that._listView._versionManager.clearCancelOnNotification(cancelToken); that._listView._writeProfilerMark(perfId + " complete(created:" + createCount + " updated:" + updateCount + "),info"); }, function (err) { that._listView._versionManager.clearCancelOnNotification(cancelToken); that._onscreenJob.clearWork(); that._frontOffscreenJob.clearWork(); that._backOffscreenJob.clearWork(); entranceAnimationSignal.cancel(); viewportItemsRealized.cancel(); that._listView._writeProfilerMark(perfId + " canceled(created:" + createCount + " updated:" + updateCount + " clean:" + cleanCount + "),info"); return Promise.wrapError(err); } ); that._listView._writeProfilerMark(perfId + ",StopTM"); return { viewportItemsRealized: viewportItemsRealized.promise, allItemsRealized: workCompleteSignal.promise, loadingCompleted: Promise.join([workCompleteSignal.promise, entranceAnimationSignal.promise]).then(function () { var promises = []; for (var i = begin; i < end; i++) { var itemData = that.items.itemDataAt(i); if (itemData) { promises.push(itemData.itemsManagerRecord.itemReadyPromise); } } return Promise._cancelBlocker(Promise.join(promises)); }) }; } else { that._listView._writeProfilerMark(perfId + ",StopTM"); return { viewportItemsRealized: Promise.wrap(), allItemsRealized: Promise.wrap(), loadingCompleted: Promise.wrap() }; } }, _setAnimationInViewportState: function VirtualizeContentsView_setAnimationInViewportState(modifiedElements) { this._hasAnimationInViewportPending = false; if (modifiedElements && modifiedElements.length > 0) { var viewportLength = this._listView._getViewportLength(), range = this._listView._layout.itemsFromRange(this._scrollbarPos, this._scrollbarPos + viewportLength - 1); for (var i = 0, len = modifiedElements.length; i < len; i++) { var modifiedElement = modifiedElements[i]; if (modifiedElement.newIndex >= range.firstIndex && modifiedElement.newIndex <= range.lastIndex && modifiedElement.newIndex !== modifiedElement.oldIndex) { this._hasAnimationInViewportPending = true; break; } } } }, _addHeader: function VirtualizeContentsView_addHeader(fragment, groupIndex) { var that = this; return this._listView._groups.renderGroup(groupIndex).then(function (header) { if (header) { header.element.tabIndex = 0; var placeholder = that._getHeaderContainer(groupIndex); if (header.element.parentNode !== placeholder) { placeholder.appendChild(header.element); _ElementUtilities.addClass(header.element, _Constants._headerClass); } that._listView._groups.setDomElement(groupIndex, header.element); } }); }, _updateHeaders: function VirtualizeContentsView_updateHeaders(fragment, begin, end) { var that = this; function updateGroup(index) { var group = that._listView._groups.group(index); if (group && !group.header) { var headerPromise = group.headerPromise; if (!headerPromise) { headerPromise = group.headerPromise = that._addHeader(fragment, index); headerPromise.done(function () { group.headerPromise = null; }, function () { group.headerPromise = null; }); } return headerPromise; } return Promise.wrap(); } this._listView._groups.removeElements(); var groupStart = this._listView._groups.groupFromItem(begin), groupIndex = groupStart, groupEnd = this._listView._groups.groupFromItem(end - 1), realizationPromises = []; if (groupIndex !== null) { for (; groupIndex <= groupEnd; groupIndex++) { realizationPromises.push(updateGroup(groupIndex)); } } function done() { that._headerRenderPromises = null; } this._headerRenderPromises = Promise.join(realizationPromises, this._headerRenderPromises).then(done, done); return this._headerRenderPromises || Promise.wrap(); }, _unrealizeItem: function VirtualizeContentsView_unrealizeItem(itemIndex) { var listView = this._listView, focusedItemPurged; this._listView._writeProfilerMark("_unrealizeItem(" + itemIndex + "),info"); var focused = listView._selection._getFocused(); if (focused.type !== _UI.ObjectType.groupHeader && focused.index === itemIndex) { listView._unsetFocusOnItem(); focusedItemPurged = true; } var itemData = this.items.itemDataAt(itemIndex), item = itemData.element, itemBox = itemData.itemBox; if (itemBox && itemBox.parentNode) { _ElementUtilities.removeClass(itemBox.parentNode, _Constants._selectedClass); _ElementUtilities.removeClass(itemBox.parentNode, _Constants._footprintClass); _ElementUtilities.addClass(itemBox.parentNode, _Constants._backdropClass); itemBox.parentNode.removeChild(itemBox); } itemData.container = null; if (listView._currentMode().itemUnrealized) { listView._currentMode().itemUnrealized(itemIndex, itemBox); } this.items.removeItem(itemIndex); // If this wasn't released by the itemsManager already, then // we remove it. This handles the special case of delete // occuring on an item that is outside of the current view, but // has not been cleaned up yet. // if (!itemData.removed) { listView._itemsManager.releaseItem(item); } _Dispose._disposeElement(item); if (focusedItemPurged) { // If the focused item was purged, we'll still want to focus on it if it comes into view sometime in the future. // calling _setFocusOnItem once the item is removed from this.items will set up a promise that will be fulfilled // if the item ever gets reloaded listView._setFocusOnItem(listView._selection._getFocused()); } }, _unrealizeGroup: function VirtualizeContentsView_unrealizeGroup(group) { var headerElement = group.header, focusedItemPurged; var focused = this._listView._selection._getFocused(); if (focused.type === _UI.ObjectType.groupHeader && this._listView._groups.group(focused.index) === group) { this._listView._unsetFocusOnItem(); focusedItemPurged = true; } if (headerElement.parentNode) { headerElement.parentNode.removeChild(headerElement); } _Dispose._disposeElement(headerElement); group.header = null; group.left = -1; group.top = -1; if (focusedItemPurged) { this._listView._setFocusOnItem(this._listView._selection._getFocused()); } }, _unrealizeItems: function VirtualizeContentsView_unrealizeItems(remove) { var that = this, removedCount = 0; this.items.eachIndex(function (index) { if (index < that.begin || index >= that.end) { that._unrealizeItem(index); return remove && ++removedCount >= remove; } }); var groups = this._listView._groups, beginGroup = groups.groupFromItem(this.begin); if (beginGroup !== null) { var endGroup = groups.groupFromItem(this.end - 1); for (var i = 0, len = groups.length() ; i < len; i++) { var group = groups.group(i); if ((i < beginGroup || i > endGroup) && group.header) { this._unrealizeGroup(group); } } } }, _unrealizeExcessiveItems: function VirtualizeContentsView_unrealizeExcessiveItems() { var realized = this.items.count(), needed = this.end - this.begin, approved = needed + this._listView._maxDeferredItemCleanup; this._listView._writeProfilerMark("_unrealizeExcessiveItems realized(" + realized + ") approved(" + approved + "),info"); if (realized > approved) { this._unrealizeItems(realized - approved); } }, _lazilyUnrealizeItems: function VirtualizeContentsView_lazilyUnrealizeItems() { this._listView._writeProfilerMark("_lazilyUnrealizeItems,StartTM"); var that = this; return waitForSeZo(this._listView).then(function () { function done() { that._listView._writeProfilerMark("_lazilyUnrealizeItems,StopTM"); } if (that._listView._isZombie()) { done(); return; } var itemsToUnrealize = []; that.items.eachIndex(function (index) { if (index < that.begin || index >= that.end) { itemsToUnrealize.push(index); } }); that._listView._writeProfilerMark("_lazilyUnrealizeItems itemsToUnrealize(" + itemsToUnrealize.length + "),info"); var groupsToUnrealize = [], groups = that._listView._groups, beginGroup = groups.groupFromItem(that.begin); if (beginGroup !== null) { var endGroup = groups.groupFromItem(that.end - 1); for (var i = 0, len = groups.length() ; i < len; i++) { var group = groups.group(i); if ((i < beginGroup || i > endGroup) && group.header) { groupsToUnrealize.push(group); } } } if (itemsToUnrealize.length || groupsToUnrealize.length) { var job; var promise = new Promise(function (complete) { function unrealizeWorker(info) { if (that._listView._isZombie()) { return; } var firstIndex = -1, lastIndex = -1, removeCount = 0, zooming = shouldWaitForSeZo(that._listView); while (itemsToUnrealize.length && !zooming && !info.shouldYield) { var itemIndex = itemsToUnrealize.shift(); that._unrealizeItem(itemIndex); removeCount++; if (firstIndex < 0) { firstIndex = itemIndex; } lastIndex = itemIndex; } that._listView._writeProfilerMark("unrealizeWorker removeItems:" + removeCount + " (" + firstIndex + "-" + lastIndex + "),info"); while (groupsToUnrealize.length && !zooming && !info.shouldYield) { that._unrealizeGroup(groupsToUnrealize.shift()); } if (itemsToUnrealize.length || groupsToUnrealize.length) { if (zooming) { info.setPromise(waitForSeZo(that._listView).then(function () { return unrealizeWorker; })); } else { info.setWork(unrealizeWorker); } } else { complete(); } } job = Scheduler.schedule(unrealizeWorker, Scheduler.Priority.belowNormal, null, "WinJS.UI.ListView._lazilyUnrealizeItems"); }); return promise.then(done, function (error) { job.cancel(); that._listView._writeProfilerMark("_lazilyUnrealizeItems canceled,info"); that._listView._writeProfilerMark("_lazilyUnrealizeItems,StopTM"); return Promise.wrapError(error); }); } else { done(); return Promise.wrap(); } }); }, _getBoundingRectString: function VirtualizeContentsView_getBoundingRectString(itemIndex) { var result; if (itemIndex >= 0 && itemIndex < this.containers.length) { var itemPos = this._listView._layout._getItemPosition(itemIndex); if (itemPos) { result = "[" + itemPos.left + "; " + itemPos.top + "; " + itemPos.width + "; " + itemPos.height + " ]"; } } return result || ""; }, _clearDeferTimeout: function VirtualizeContentsView_clearDeferTimeout() { if (this.deferTimeout) { this.deferTimeout.cancel(); this.deferTimeout = null; } if (this.deferredActionCancelToken !== -1) { this._listView._versionManager.clearCancelOnNotification(this.deferredActionCancelToken); this.deferredActionCancelToken = -1; } }, _setupAria: function VirtualizeContentsView_setupAria(timedOut) { if (this._listView._isZombie()) { return; } var that = this; function done() { that._listView._writeProfilerMark("aria work,StopTM"); } function calcLastRealizedIndexInGroup(groupIndex) { var groups = that._listView._groups, nextGroup = groups.group(groupIndex + 1); return (nextGroup ? Math.min(nextGroup.startIndex - 1, that.end - 1) : that.end - 1); } this._listView._createAriaMarkers(); return this._listView._itemsCount().then(function (count) { if (count > 0 && that.firstIndexDisplayed !== -1 && that.lastIndexDisplayed !== -1) { that._listView._writeProfilerMark("aria work,StartTM"); var startMarker = that._listView._ariaStartMarker, endMarker = that._listView._ariaEndMarker, index = that.begin, item = that.items.itemAt(that.begin), job, // These are only used when the ListView is using groups groups, startGroup, currentGroup, group, lastRealizedIndexInGroup; if (item) { _ElementUtilities._ensureId(item); if (that._listView._groupsEnabled()) { groups = that._listView._groups; startGroup = currentGroup = groups.groupFromItem(that.begin); group = groups.group(currentGroup); lastRealizedIndexInGroup = calcLastRealizedIndexInGroup(currentGroup); _ElementUtilities._ensureId(group.header); _ElementUtilities._setAttribute(group.header, "role", that._listView._headerRole); _ElementUtilities._setAttribute(group.header, "x-ms-aria-flowfrom", startMarker.id); setFlow(group.header, item); _ElementUtilities._setAttribute(group.header, "tabindex", that._listView._tabIndex); } else { _ElementUtilities._setAttribute(item, "x-ms-aria-flowfrom", startMarker.id); } return new Promise(function (completeJobPromise) { var skipWait = timedOut; job = Scheduler.schedule(function ariaWorker(jobInfo) { if (that._listView._isZombie()) { done(); return; } for (; index < that.end; index++) { if (!skipWait && shouldWaitForSeZo(that._listView)) { jobInfo.setPromise(waitForSeZo(that._listView).then(function (timedOut) { skipWait = timedOut; return ariaWorker; })); return; } else if (jobInfo.shouldYield) { jobInfo.setWork(ariaWorker); return; } item = that.items.itemAt(index); var nextItem = that.items.itemAt(index + 1); if (nextItem) { _ElementUtilities._ensureId(nextItem); } _ElementUtilities._setAttribute(item, "role", that._listView._itemRole); _ElementUtilities._setAttribute(item, "aria-setsize", count); _ElementUtilities._setAttribute(item, "aria-posinset", index + 1); _ElementUtilities._setAttribute(item, "tabindex", that._listView._tabIndex); if (that._listView._groupsEnabled()) { if (index === lastRealizedIndexInGroup || !nextItem) { var nextGroup = groups.group(currentGroup + 1); // If group is the last realized group, then nextGroup won't exist in the DOM. // When this is the case, nextItem shouldn't exist. if (nextGroup && nextGroup.header && nextItem) { _ElementUtilities._setAttribute(nextGroup.header, "tabindex", that._listView._tabIndex); _ElementUtilities._setAttribute(nextGroup.header, "role", that._listView._headerRole); _ElementUtilities._ensureId(nextGroup.header); setFlow(item, nextGroup.header); setFlow(nextGroup.header, nextItem); } else { // We're at the last group so flow to the end marker _ElementUtilities._setAttribute(item, "aria-flowto", endMarker.id); } currentGroup++; group = nextGroup; lastRealizedIndexInGroup = calcLastRealizedIndexInGroup(currentGroup); } else { // This is not the last item in the group so flow to the next item setFlow(item, nextItem); } } else if (nextItem) { // Groups are disabled so as long as we aren't at the last item, flow to the next one setFlow(item, nextItem); } else { // Groups are disabled and we're at the last item, so flow to the end marker _ElementUtilities._setAttribute(item, "aria-flowto", endMarker.id); } if (!nextItem) { break; } } that._listView._fireAccessibilityAnnotationCompleteEvent(that.begin, index, startGroup, currentGroup - 1); done(); completeJobPromise(); }, Scheduler.Priority.belowNormal, null, "WinJS.UI.ListView._setupAria"); }, function () { // Cancellation handler for promise returned by setupAria job.cancel(); done(); }); } else { // the first item is null done(); } } else { // The count is 0 return Promise.wrap(); } }); }, _setupDeferredActions: function VirtualizeContentsView_setupDeferredActions() { this._listView._writeProfilerMark("_setupDeferredActions,StartTM"); var that = this; this._clearDeferTimeout(); function cleanUp() { if (that._listView._isZombie()) { return; } that.deferTimeout = null; that._listView._versionManager.clearCancelOnNotification(that.deferredActionCancelToken); that.deferredActionCancelToken = -1; } this.deferTimeout = this._lazilyRemoveRedundantItemsBlocks().then(function () { return Promise.timeout(_Constants._DEFERRED_ACTION); }). then(function () { return waitForSeZo(that._listView); }). then(function (timedOut) { return that._setupAria(timedOut); }). then(cleanUp, function (error) { cleanUp(); return Promise.wrapError(error); }); this.deferredActionCancelToken = this._listView._versionManager.cancelOnNotification(this.deferTimeout); this._listView._writeProfilerMark("_setupDeferredActions,StopTM"); }, // Sets aria-flowto on _ariaStartMarker and x-ms-aria-flowfrom on _ariaEndMarker. The former // points to either the first visible group header or the first visible item. The latter points // to the last visible item. _updateAriaMarkers: function VirtualizeContentsView_updateAriaMarkers(listViewIsEmpty, firstIndexDisplayed, lastIndexDisplayed) { var that = this; if (this._listView._isZombie()) { return; } function getFirstVisibleItem() { return that.items.itemAt(firstIndexDisplayed); } // At a certain index, the VDS may return null for all items at that index and // higher. When this is the case, the end marker should point to the last // non-null item in the visible range. function getLastVisibleItem() { for (var i = lastIndexDisplayed; i >= firstIndexDisplayed; i--) { if (that.items.itemAt(i)) { return that.items.itemAt(i); } } return null; } this._listView._createAriaMarkers(); var startMarker = this._listView._ariaStartMarker, endMarker = this._listView._ariaEndMarker, firstVisibleItem, lastVisibleItem; if (firstIndexDisplayed !== -1 && lastIndexDisplayed !== -1 && firstIndexDisplayed <= lastIndexDisplayed) { firstVisibleItem = getFirstVisibleItem(); lastVisibleItem = getLastVisibleItem(); } if (listViewIsEmpty || !firstVisibleItem || !lastVisibleItem) { setFlow(startMarker, endMarker); this._listView._fireAccessibilityAnnotationCompleteEvent(-1, -1); } else { _ElementUtilities._ensureId(firstVisibleItem); _ElementUtilities._ensureId(lastVisibleItem); // Set startMarker's flowto if (this._listView._groupsEnabled()) { var groups = this._listView._groups, firstVisibleGroup = groups.group(groups.groupFromItem(firstIndexDisplayed)); if (firstVisibleGroup.header) { _ElementUtilities._ensureId(firstVisibleGroup.header); if (firstIndexDisplayed === firstVisibleGroup.startIndex) { _ElementUtilities._setAttribute(startMarker, "aria-flowto", firstVisibleGroup.header.id); } else { _ElementUtilities._setAttribute(startMarker, "aria-flowto", firstVisibleItem.id); } } } else { _ElementUtilities._setAttribute(startMarker, "aria-flowto", firstVisibleItem.id); } // Set endMarker's flowfrom _ElementUtilities._setAttribute(endMarker, "x-ms-aria-flowfrom", lastVisibleItem.id); } }, // Update the ARIA attributes on item that are needed so that Narrator can announce it. // item must be in the items container. updateAriaForAnnouncement: function VirtualizeContentsView_updateAriaForAnnouncement(item, count) { var index = -1; var type = _UI.ObjectType.item; if (_ElementUtilities.hasClass(item, _Constants._headerClass)) { index = this._listView._groups.index(item); type = _UI.ObjectType.groupHeader; _ElementUtilities._setAttribute(item, "role", this._listView._headerRole); _ElementUtilities._setAttribute(item, "tabindex", this._listView._tabIndex); } else { index = this.items.index(item); _ElementUtilities._setAttribute(item, "aria-setsize", count); _ElementUtilities._setAttribute(item, "aria-posinset", index + 1); _ElementUtilities._setAttribute(item, "role", this._listView._itemRole); _ElementUtilities._setAttribute(item, "tabindex", this._listView._tabIndex); } if (type === _UI.ObjectType.groupHeader) { this._listView._fireAccessibilityAnnotationCompleteEvent(-1, -1, index, index); } else { this._listView._fireAccessibilityAnnotationCompleteEvent(index, index, -1, -1); } }, _reportElementsLevel: function VirtualizeContentsView_reportElementsLevel(direction) { var items = this.items; function elementsCount(first, last) { var count = 0; for (var itemIndex = first; itemIndex <= last; itemIndex++) { var itemData = items.itemDataAt(itemIndex); if (itemData && itemData.container) { count++; } } return count; } var level; if (direction === "right") { level = Math.floor(100 * elementsCount(this.firstIndexDisplayed, this.end - 1) / (this.end - this.firstIndexDisplayed)); } else { level = Math.floor(100 * elementsCount(this.begin, this.lastIndexDisplayed) / (this.lastIndexDisplayed - this.begin + 1)); } this._listView._writeProfilerMark("elementsLevel level(" + level + "),info"); }, _createHeaderContainer: function VirtualizeContentsView_createHeaderContainer(insertAfter) { return this._createSurfaceChild(_Constants._headerContainerClass, insertAfter); }, _createItemsContainer: function VirtualizeContentsView_createItemsContainer(insertAfter) { var itemsContainer = this._createSurfaceChild(_Constants._itemsContainerClass, insertAfter); var padder = _Global.document.createElement("div"); padder.className = _Constants._padderClass; itemsContainer.appendChild(padder); return itemsContainer; }, _ensureContainerInDOM: function VirtualizeContentsView_ensureContainerInDOM(index) { var container = this.containers[index]; if (container && !this._listView._canvas.contains(container)) { this._forceItemsBlocksInDOM(index, index + 1); return true; } return false; }, _ensureItemsBlocksInDOM: function VirtualizeContentsView_ensureItemsBlocksInDOM(begin, end) { if (this._expandedRange) { var oldBegin = this._expandedRange.first.index, oldEnd = this._expandedRange.last.index + 1; if (begin <= oldBegin && end > oldBegin) { end = Math.max(end, oldEnd); } else if (begin < oldEnd && end >= oldEnd) { begin = Math.min(begin, oldBegin); } } this._forceItemsBlocksInDOM(begin, end); }, _removeRedundantItemsBlocks: function VirtualizeContentsView_removeRedundantItemsBlocks() { if (this.begin !== -1 && this.end !== -1) { this._forceItemsBlocksInDOM(this.begin, this.end); } }, _lazilyRemoveRedundantItemsBlocks: function VirtualizeContentsView_lazilyRemoveRedundantItemsBlocks() { this._listView._writeProfilerMark("_lazilyRemoveRedundantItemsBlocks,StartTM"); var that = this; return waitForSeZo(this._listView).then(function () { function done() { that._listView._writeProfilerMark("_lazilyRemoveRedundantItemsBlocks,StopTM"); } if (that._listView._isZombie()) { done(); return; } if (that._expandedRange && that.begin !== -1 && that.end !== -1 && (that._expandedRange.first.index < that.begin || that._expandedRange.last.index + 1 > that.end)) { var job; var promise = new Promise(function (complete) { function blocksCleanupWorker(info) { if (that._listView._isZombie()) { return; } var zooming = shouldWaitForSeZo(that._listView); while (that._expandedRange.first.index < that.begin && !zooming && !info.shouldYield) { var begin = Math.min(that.begin, that._expandedRange.first.index + that._blockSize * _VirtualizeContentsView._blocksToRelease); that._forceItemsBlocksInDOM(begin, that.end); } while (that._expandedRange.last.index + 1 > that.end && !zooming && !info.shouldYield) { var end = Math.max(that.end, that._expandedRange.last.index - that._blockSize * _VirtualizeContentsView._blocksToRelease); that._forceItemsBlocksInDOM(that.begin, end); } if (that._expandedRange.first.index < that.begin || that._expandedRange.last.index + 1 > that.end) { if (zooming) { info.setPromise(waitForSeZo(that._listView).then(function () { return blocksCleanupWorker; })); } else { info.setWork(blocksCleanupWorker); } } else { complete(); } } job = Scheduler.schedule(blocksCleanupWorker, Scheduler.Priority.belowNormal, null, "WinJS.UI.ListView._lazilyRemoveRedundantItemsBlocks"); }); return promise.then(done, function (error) { job.cancel(); that._listView._writeProfilerMark("_lazilyRemoveRedundantItemsBlocks canceled,info"); that._listView._writeProfilerMark("_lazilyRemoveRedundantItemsBlocks,StopTM"); return Promise.wrapError(error); }); } else { done(); return Promise.wrap(); } }); }, _forceItemsBlocksInDOM: function VirtualizeContentsView_forceItemsBlocksInDOM(begin, end) { if (!this._blockSize) { return; } var perfId = "_forceItemsBlocksInDOM begin(" + begin + ") end(" + end + "),"; this._listView._writeProfilerMark(perfId + "StartTM"); var that = this, added = 0, removed = 0, paddingProperty = "padding" + (this._listView._horizontal() ? "Left" : "Top"); function setPadder(itemsContainer, padding) { var padder = itemsContainer.element.firstElementChild; padder.style[paddingProperty] = padding; } function forEachBlock(callback) { for (var g = 0; g < that.tree.length; g++) { var itemsContainer = that.tree[g].itemsContainer; for (var b = 0, len = itemsContainer.itemsBlocks.length; b < len; b++) { if (callback(itemsContainer, itemsContainer.itemsBlocks[b])) { return; } } } } function measureItemsBlock(itemsBlock) { that._listView._writeProfilerMark("_itemsBlockExtent,StartTM"); that._listView._itemsBlockExtent = _ElementUtilities[that._listView._horizontal() ? "getTotalWidth" : "getTotalHeight"](itemsBlock.element); that._listView._writeProfilerMark("_itemsBlockExtent(" + that._listView._itemsBlockExtent + "),info"); that._listView._writeProfilerMark("_itemsBlockExtent,StopTM"); } function getItemsBlockExtent() { if (that._listView._itemsBlockExtent === -1) { // first try blocks already added to the DOM forEachBlock(function (itemsContainer, itemsBlock) { if (itemsBlock.items.length === that._blockSize && itemsBlock.element.parentNode === itemsContainer.element) { measureItemsBlock(itemsBlock); return true; } return false; }); } if (that._listView._itemsBlockExtent === -1) { forEachBlock(function (itemsContainer, itemsBlock) { if (itemsBlock.items.length === that._blockSize) { itemsContainer.element.appendChild(itemsBlock.element); measureItemsBlock(itemsBlock); itemsContainer.element.removeChild(itemsBlock.element); return true; } return false; }); } return that._listView._itemsBlockExtent; } function removeBlocks(itemsContainer, begin, end) { function remove(blockIndex) { var block = itemsContainer.itemsBlocks[blockIndex]; if (block && block.element.parentNode === itemsContainer.element) { itemsContainer.element.removeChild(block.element); removed++; } } if (Array.isArray(begin)) { begin.forEach(remove); } else { for (var i = begin; i < end; i++) { remove(i); } } } function addBlocks(itemsContainer, begin, end) { var padder = itemsContainer.element.firstElementChild, previous = padder; for (var i = begin; i < end; i++) { var block = itemsContainer.itemsBlocks[i]; if (block) { if (block.element.parentNode !== itemsContainer.element) { itemsContainer.element.insertBefore(block.element, previous.nextElementSibling); added++; } previous = block.element; } } } function collapseGroup(groupIndex) { if (groupIndex < that.tree.length) { that._listView._writeProfilerMark("collapseGroup(" + groupIndex + "),info"); var itemsContainer = that.tree[groupIndex].itemsContainer; removeBlocks(itemsContainer, 0, itemsContainer.itemsBlocks.length); setPadder(itemsContainer, ""); } } function expandGroup(groupIndex) { if (groupIndex < that.tree.length) { that._listView._writeProfilerMark("expandGroup(" + groupIndex + "),info"); var itemsContainer = that.tree[groupIndex].itemsContainer; addBlocks(itemsContainer, 0, itemsContainer.itemsBlocks.length); setPadder(itemsContainer, ""); } } function removedFromRange(oldRange, newRange) { function expand(first, last) { var array = []; for (var i = first; i <= last; i++) { array.push(i); } return array; } var newL = newRange[0]; var newR = newRange[1]; var oldL = oldRange[0]; var oldR = oldRange[1]; if (newR < oldL || newL > oldR) { return expand(oldL, oldR); } else if (newL > oldL && newR < oldR) { return expand(oldL, newL - 1).concat(expand(newR + 1, oldR)); } else if (oldL < newL) { return expand(oldL, newL - 1); } else if (oldR > newR) { return expand(newR + 1, oldR); } else { return null; } } var firstGroupIndex = this._listView._groups.groupFromItem(begin), lastGroupIndex = this._listView._groups.groupFromItem(end - 1); var firstGroup = this._listView._groups.group(firstGroupIndex), firstItemsContainer = that.tree[firstGroupIndex].itemsContainer; var firstBlock = Math.floor((begin - firstGroup.startIndex) / this._blockSize); var lastGroup = this._listView._groups.group(lastGroupIndex), lastItemsContainer = that.tree[lastGroupIndex].itemsContainer; var lastBlock = Math.floor((end - 1 - lastGroup.startIndex) / this._blockSize); // if size of structure block is needed try to obtain it before modifying the tree to avoid a layout pass if (firstBlock && that._listView._itemsBlockExtent === -1) { forEachBlock(function (itemsContainer, itemsBlock) { if (itemsBlock.items.length === that._blockSize && itemsBlock.element.parentNode === itemsContainer.element) { measureItemsBlock(itemsBlock); return true; } return false; }); } var groupsToCollapse = this._expandedRange ? removedFromRange([this._expandedRange.first.groupIndex, this._expandedRange.last.groupIndex], [firstGroupIndex, lastGroupIndex]) : null; if (groupsToCollapse) { groupsToCollapse.forEach(collapseGroup); } if (this._expandedRange && this._expandedRange.first.groupKey === firstGroup.key) { var blocksToRemove = removedFromRange([this._expandedRange.first.block, Number.MAX_VALUE], [firstBlock, Number.MAX_VALUE]); if (blocksToRemove) { removeBlocks(firstItemsContainer, blocksToRemove); } } else if (this._expandedRange && firstGroupIndex >= this._expandedRange.first.groupIndex && firstGroupIndex <= this._expandedRange.last.groupIndex) { removeBlocks(firstItemsContainer, 0, firstBlock); } if (firstGroupIndex !== lastGroupIndex) { addBlocks(firstItemsContainer, firstBlock, firstItemsContainer.itemsBlocks.length); addBlocks(lastItemsContainer, 0, lastBlock + 1); } else { addBlocks(firstItemsContainer, firstBlock, lastBlock + 1); } if (this._expandedRange && this._expandedRange.last.groupKey === lastGroup.key) { var blocksToRemove = removedFromRange([0, this._expandedRange.last.block], [0, lastBlock]); if (blocksToRemove) { removeBlocks(lastItemsContainer, blocksToRemove); } } else if (this._expandedRange && lastGroupIndex >= this._expandedRange.first.groupIndex && lastGroupIndex <= this._expandedRange.last.groupIndex) { removeBlocks(lastItemsContainer, lastBlock + 1, lastItemsContainer.itemsBlocks.length); } setPadder(firstItemsContainer, firstBlock ? firstBlock * getItemsBlockExtent() + "px" : ""); if (firstGroupIndex !== lastGroupIndex) { setPadder(lastItemsContainer, ""); } // groups between first and last for (var i = firstGroupIndex + 1; i < lastGroupIndex; i++) { expandGroup(i); } this._expandedRange = { first: { index: begin, groupIndex: firstGroupIndex, groupKey: firstGroup.key, block: firstBlock }, last: { index: end - 1, groupIndex: lastGroupIndex, groupKey: lastGroup.key, block: lastBlock }, }; this._listView._writeProfilerMark("_forceItemsBlocksInDOM groups(" + firstGroupIndex + "-" + lastGroupIndex + ") blocks(" + firstBlock + "-" + lastBlock + ") added(" + added + ") removed(" + removed + "),info"); this._listView._writeProfilerMark(perfId + "StopTM"); }, _realizePageImpl: function VirtualizeContentsView_realizePageImpl() { var that = this; var perfId = "realizePage(scrollPosition:" + this._scrollbarPos + " forceLayout:" + this._forceRelayout + ")"; this._listView._writeProfilerMark(perfId + ",StartTM"); // It's safe to skip realizePage, so we just queue up the last request to run when the version manager // get unlocked. // if (this._listView._versionManager.locked) { this._listView._versionManager.unlocked.done(function () { if (!that._listView._isZombie()) { that._listView._batchViewUpdates(_Constants._ViewChange.realize, _Constants._ScrollToPriority.low, that._listView.scrollPosition); } }); this._listView._writeProfilerMark(perfId + ",StopTM"); return Promise.cancel; } return new Promise(function (c) { var renderingCompleteSignal = new _Signal(); function complete() { c(); renderingCompleteSignal.complete(); } function viewPortPageRealized() { that._listView._hideProgressBar(); that._state.setLoadingState(that._listView._LoadingState.viewPortLoaded); if (that._executeAnimations) { that._setState(RealizingAnimatingState, renderingCompleteSignal.promise); } } function pageRealized(count) { that._updateAriaMarkers(count === 0, that.firstIndexDisplayed, that.lastIndexDisplayed); that._state.setLoadingState && that._state.setLoadingState(that._listView._LoadingState.itemsLoaded); } function finish(count) { that._listView._clearInsertedItems(); that._listView._groups.removeElements(); viewPortPageRealized(); pageRealized(count); complete(); } that._state.setLoadingState(that._listView._LoadingState.itemsLoading); if (that._firstLayoutPass) { that._listView._showProgressBar(that._listView._element, "50%", "50%"); } var count = that.containers.length; if (count) { // While the zoom animation is played we want to minimize the # of pages // being fetched to improve TtFF for SeZo scenarios var pagesToPrefetch = _VirtualizeContentsView._pagesToPrefetch; var customPagesToPrefetchMax = _VirtualizeContentsView._customPagesToPrefetchMax; var customPagesToPrefetchMin = _VirtualizeContentsView._customPagesToPrefetchMin; if (that._listView._zooming) { pagesToPrefetch = 0; customPagesToPrefetchMax = 0; customPagesToPrefetchMin = 0; } var viewportLength = that._listView._getViewportLength(); var pagesBefore, pagesAfter; if (_BaseUtils._isiOS && !_VirtualizeContentsView._disableCustomPagesPrefetch) { pagesBefore = (that._direction === "left" ? customPagesToPrefetchMax : customPagesToPrefetchMin); // Optimize the beginning of the list such that if you scroll, then change direction and start going back towards the beginning of the list, // we maintain a remainder of pages that can be added to pagesAfter. This ensures that at beginning of the list, which is the common case, // we always have customPagesToPrefetchMax ahead, even when the scrolling direction is constantly changing. var pagesShortBehind = Math.max(0, (pagesBefore - (that._scrollbarPos / viewportLength))); pagesAfter = Math.min(customPagesToPrefetchMax, pagesShortBehind + (that._direction === "right" ? customPagesToPrefetchMax : customPagesToPrefetchMin)); } else { pagesBefore = pagesToPrefetch; pagesAfter = pagesToPrefetch; } var beginningOffset = Math.max(0, that._scrollbarPos - pagesBefore * viewportLength), endingOffset = that._scrollbarPos + (1 + pagesAfter) * viewportLength; var range = that._listView._layout.itemsFromRange(beginningOffset, endingOffset - 1); if ((range.firstIndex < 0 || range.firstIndex >= count) && (range.lastIndex < 0 || range.lastIndex >= count)) { that.begin = -1; that.end = -1; that.firstIndexDisplayed = -1; that.lastIndexDisplayed = -1; finish(count); } else { var begin = _ElementUtilities._clamp(range.firstIndex, 0, count - 1), end = _ElementUtilities._clamp(range.lastIndex + 1, 0, count); var inView = that._listView._layout.itemsFromRange(that._scrollbarPos, that._scrollbarPos + viewportLength - 1), firstInView = _ElementUtilities._clamp(inView.firstIndex, 0, count - 1), lastInView = _ElementUtilities._clamp(inView.lastIndex, 0, count - 1); if (that._realizationLevel === _VirtualizeContentsView._realizationLevel.skip && !that.lastRealizePass && firstInView === that.firstIndexDisplayed && lastInView === that.lastIndexDisplayed) { that.begin = begin; that.end = begin + Object.keys(that.items._itemData).length; that._updateHeaders(that._listView._canvas, that.begin, that.end).done(function () { that.lastRealizePass = null; finish(count); }); } else if ((that._forceRelayout || begin !== that.begin || end !== that.end || firstInView !== that.firstIndexDisplayed || lastInView !== that.lastIndexDisplayed) && (begin < end) && (beginningOffset < endingOffset)) { that._listView._writeProfilerMark("realizePage currentInView(" + firstInView + "-" + lastInView + ") previousInView(" + that.firstIndexDisplayed + "-" + that.lastIndexDisplayed + ") change(" + (firstInView - that.firstIndexDisplayed) + "),info"); that._cancelRealize(); // cancelRealize changes the realizePass and resets begin/end var currentPass = that._realizePass; that.begin = begin; that.end = end; that.firstIndexDisplayed = firstInView; that.lastIndexDisplayed = lastInView; that.deletesWithoutRealize = 0; that._ensureItemsBlocksInDOM(that.begin, that.end); var realizeWork = that._realizeItems( that._listView._itemCanvas, that.begin, that.end, count, currentPass, that._scrollbarPos, that._direction, firstInView, lastInView, that._forceRelayout); that._forceRelayout = false; var realizePassWork = realizeWork.viewportItemsRealized.then(function () { viewPortPageRealized(); return realizeWork.allItemsRealized; }).then(function () { if (that._realizePass === currentPass) { return that._updateHeaders(that._listView._canvas, that.begin, that.end).then(function () { pageRealized(count); }); } }).then(function () { return realizeWork.loadingCompleted; }).then( function () { that._unrealizeExcessiveItems(); that.lastRealizePass = null; complete(); }, function (e) { if (that._realizePass === currentPass) { that.lastRealizePass = null; that.begin = -1; that.end = -1; } return Promise.wrapError(e); } ); that.lastRealizePass = Promise.join([realizeWork.viewportItemsRealized, realizeWork.allItemsRealized, realizeWork.loadingCompleted, realizePassWork]); that._unrealizeExcessiveItems(); } else if (!that.lastRealizePass) { // We are currently in the "itemsLoading" state and need to get back to "complete". The // previous realize pass has been completed so proceed to the other states. finish(count); } else { that.lastRealizePass.then(complete); } } } else { that.begin = -1; that.end = -1; that.firstIndexDisplayed = -1; that.lastIndexDisplayed = -1; finish(count); } that._reportElementsLevel(that._direction); that._listView._writeProfilerMark(perfId + ",StopTM"); }); }, realizePage: function VirtualizeContentsView_realizePage(scrollToFunctor, forceRelayout, scrollEndPromise, StateType) { this._scrollToFunctor = makeFunctor(scrollToFunctor); this._forceRelayout = this._forceRelayout || forceRelayout; this._scrollEndPromise = scrollEndPromise; this._listView._writeProfilerMark(this._state.name + "_realizePage,info"); this._state.realizePage(StateType || RealizingState); }, onScroll: function VirtualizeContentsView_onScroll(scrollToFunctor, scrollEndPromise) { this.realizePage(scrollToFunctor, false, scrollEndPromise, ScrollingState); }, reload: function VirtualizeContentsView_reload(scrollToFunctor, highPriority) { if (this._listView._isZombie()) { return; } this._scrollToFunctor = makeFunctor(scrollToFunctor); this._forceRelayout = true; this._highPriorityRealize = !!highPriority; this.stopWork(true); this._listView._writeProfilerMark(this._state.name + "_rebuildTree,info"); this._state.rebuildTree(); }, refresh: function VirtualizeContentsView_refresh(scrollToFunctor) { if (this._listView._isZombie()) { return; } this._scrollToFunctor = makeFunctor(scrollToFunctor); this._forceRelayout = true; this._highPriorityRealize = true; this.stopWork(); this._listView._writeProfilerMark(this._state.name + "_relayout,info"); this._state.relayout(); }, waitForValidScrollPosition: function VirtualizeContentsView_waitForValidScrollPosition(newPosition) { var that = this; var currentMaxScroll = this._listView._viewport[this._listView._scrollLength] - this._listView._getViewportLength(); if (newPosition > currentMaxScroll) { return that._listView._itemsCount().then(function (count) { // Wait until we have laid out enough containers to be able to set the scroll position to newPosition if (that.containers.length < count) { return Promise._cancelBlocker(that._creatingContainersWork && that._creatingContainersWork.promise).then(function () { return that._getLayoutCompleted(); }).then(function () { return newPosition; }); } else { return newPosition; } }); } else { return Promise.wrap(newPosition); } }, waitForEntityPosition: function VirtualizeContentsView_waitForEntityPosition(entity) { var that = this; this._listView._writeProfilerMark(this._state.name + "_waitForEntityPosition" + "(" + entity.type + ": " + entity.index + ")" + ",info"); return Promise._cancelBlocker(this._state.waitForEntityPosition(entity).then(function () { if ((entity.type !== _UI.ObjectType.groupHeader && entity.index >= that.containers.length) || (entity.type === _UI.ObjectType.groupHeader && that._listView._groups.group(entity.index).startIndex >= that.containers.length)) { return that._creatingContainersWork && that._creatingContainersWork.promise; } }).then(function () { return that._getLayoutCompleted(); })); }, stopWork: function VirtualizeContentsView_stopWork(stopTreeCreation) { this._listView._writeProfilerMark(this._state.name + "_stop,info"); this._state.stop(stopTreeCreation); if (this._layoutWork) { this._layoutWork.cancel(); } if (stopTreeCreation && this._creatingContainersWork) { this._creatingContainersWork.cancel(); } if (stopTreeCreation) { this._state = new CreatedState(this); } }, _cancelRealize: function VirtualizeContentsView_cancelRealize() { this._listView._writeProfilerMark("_cancelRealize,StartTM"); if (this.lastRealizePass || this.deferTimeout) { this._forceRelayout = true; } this._clearDeferTimeout(); this._realizePass++; if (this._headerRenderPromises) { this._headerRenderPromises.cancel(); this._headerRenderPromises = null; } var last = this.lastRealizePass; if (last) { this.lastRealizePass = null; this.begin = -1; this.end = -1; last.cancel(); } this._listView._writeProfilerMark("_cancelRealize,StopTM"); }, resetItems: function VirtualizeContentsView_resetItems(unparent) { if (!this._listView._isZombie()) { this.firstIndexDisplayed = -1; this.lastIndexDisplayed = -1; this._runningAnimations = null; this._executeAnimations = false; var listView = this._listView; this._firstLayoutPass = true; listView._unsetFocusOnItem(); if (listView._currentMode().onDataChanged) { listView._currentMode().onDataChanged(); } this.items.each(function (index, item) { if (unparent && item.parentNode && item.parentNode.parentNode) { item.parentNode.parentNode.removeChild(item.parentNode); } listView._itemsManager.releaseItem(item); _Dispose._disposeElement(item); }); this.items.removeItems(); this._deferredReparenting = []; if (unparent) { listView._groups.removeElements(); } listView._clearInsertedItems(); } }, reset: function VirtualizeContentsView_reset() { this.stopWork(true); this._state = new CreatedState(this); this.resetItems(); // when in the zombie state, we let disposal cleanup the ScrollView state // if (!this._listView._isZombie()) { var listView = this._listView; listView._groups.resetGroups(); listView._resetCanvas(); this.tree = null; this.keyToGroupIndex = null; this.containers = null; this._expandedRange = null; } }, cleanUp: function VirtualizeContentsView_cleanUp() { this.stopWork(true); this._runningAnimations && this._runningAnimations.cancel(); var itemsManager = this._listView._itemsManager; this.items.each(function (index, item) { itemsManager.releaseItem(item); _Dispose._disposeElement(item); }); this._listView._unsetFocusOnItem(); this.items.removeItems(); this._deferredReparenting = []; this._listView._groups.resetGroups(); this._listView._resetCanvas(); this.tree = null; this.keyToGroupIndex = null; this.containers = null; this._expandedRange = null; this.destroyed = true; }, getContainer: function VirtualizeContentsView_getContainer(itemIndex) { return this.containers[itemIndex]; }, _getHeaderContainer: function VirtualizeContentsView_getHeaderContainer(groupIndex) { return this.tree[groupIndex].header; }, _getGroups: function VirtualizeContentsView_getGroups(count) { if (this._listView._groupDataSource) { var groupsContainer = this._listView._groups.groups, groups = []; if (count) { for (var i = 0, len = groupsContainer.length; i < len; i++) { var group = groupsContainer[i], nextStartIndex = i + 1 < len ? groupsContainer[i + 1].startIndex : count; groups.push({ key: group.key, size: nextStartIndex - group.startIndex }); } } return groups; } else { return [{ key: "-1", size: count }]; } }, _createChunk: function VirtualizeContentsView_createChunk(groups, count, chunkSize) { var that = this; this._listView._writeProfilerMark("createChunk,StartTM"); function addToGroup(itemsContainer, groupSize) { var children = itemsContainer.element.children, oldSize = children.length, toAdd = Math.min(groupSize - itemsContainer.items.length, chunkSize); _SafeHtml.insertAdjacentHTMLUnsafe(itemsContainer.element, "beforeend", _Helpers._repeat("
", toAdd)); for (var i = 0; i < toAdd; i++) { var container = children[oldSize + i]; itemsContainer.items.push(container); that.containers.push(container); } } function newGroup(group) { var node = { header: that._listView._groupDataSource ? that._createHeaderContainer() : null, itemsContainer: { element: that._createItemsContainer(), items: [] } }; that.tree.push(node); that.keyToGroupIndex[group.key] = that.tree.length - 1; addToGroup(node.itemsContainer, group.size); } if (this.tree.length && this.tree.length <= groups.length) { var last = this.tree[this.tree.length - 1], finalSize = groups[this.tree.length - 1].size; // check if the last group in the tree already has all items. If not add items to this group if (last.itemsContainer.items.length < finalSize) { addToGroup(last.itemsContainer, finalSize); this._listView._writeProfilerMark("createChunk,StopTM"); return; } } if (this.tree.length < groups.length) { newGroup(groups[this.tree.length]); } this._listView._writeProfilerMark("createChunk,StopTM"); }, _createChunkWithBlocks: function VirtualizeContentsView_createChunkWithBlocks(groups, count, blockSize, chunkSize) { var that = this; this._listView._writeProfilerMark("createChunk,StartTM"); function addToGroup(itemsContainer, toAdd) { var lastExistingBlock = itemsContainer.itemsBlocks.length ? itemsContainer.itemsBlocks[itemsContainer.itemsBlocks.length - 1] : null; if (lastExistingBlock && lastExistingBlock.items.length < blockSize) { var fix = Math.min(toAdd, blockSize - lastExistingBlock.items.length); _SafeHtml.insertAdjacentHTMLUnsafe(lastExistingBlock.element, "beforeend", _Helpers._repeat("
", fix)); var oldSize = lastExistingBlock.items.length; children = lastExistingBlock.element.children; for (var j = 0; j < fix; j++) { var child = children[oldSize + j]; lastExistingBlock.items.push(child); that.containers.push(child); } toAdd -= fix; } if (toAdd > chunkSize) { toAdd = Math.min(toAdd, Math.max(1, Math.floor(chunkSize / blockSize)) * blockSize); } var blocks = Math.floor(toAdd / blockSize), lastBlockSize = toAdd % blockSize; var blockMarkup = "
" + _Helpers._repeat("
", blockSize) + "
", markup = _Helpers._repeat(blockMarkup, blocks); if (lastBlockSize) { markup += "
" + _Helpers._repeat("
", lastBlockSize) + "
"; blocks++; } var blocksTemp = _Global.document.createElement("div"); _SafeHtml.setInnerHTMLUnsafe(blocksTemp, markup); var children = blocksTemp.children; for (var i = 0; i < blocks; i++) { var block = children[i], blockNode = { element: block, items: _Helpers._nodeListToArray(block.children) }; itemsContainer.itemsBlocks.push(blockNode); for (var n = 0; n < blockNode.items.length; n++) { that.containers.push(blockNode.items[n]); } } } function newGroup(group) { var node = { header: that._listView._groupDataSource ? that._createHeaderContainer() : null, itemsContainer: { element: that._createItemsContainer(), itemsBlocks: [] } }; that.tree.push(node); that.keyToGroupIndex[group.key] = that.tree.length - 1; addToGroup(node.itemsContainer, group.size); } if (this.tree.length && this.tree.length <= groups.length) { var lastContainer = this.tree[this.tree.length - 1].itemsContainer, finalSize = groups[this.tree.length - 1].size, currentSize = 0; if (lastContainer.itemsBlocks.length) { currentSize = (lastContainer.itemsBlocks.length - 1) * blockSize + lastContainer.itemsBlocks[lastContainer.itemsBlocks.length - 1].items.length; } if (currentSize < finalSize) { addToGroup(lastContainer, finalSize - currentSize); this._listView._writeProfilerMark("createChunk,StopTM"); return; } } if (this.tree.length < groups.length) { newGroup(groups[this.tree.length]); } this._listView._writeProfilerMark("createChunk,StopTM"); }, _generateCreateContainersWorker: function VirtualizeContentsView_generateCreateContainersWorker() { var that = this, counter = 0, skipWait = false; return function work(info) { if (!that._listView._versionManager.locked) { that._listView._itemsCount().then(function (count) { var zooming = !skipWait && shouldWaitForSeZo(that._listView); if (!zooming) { if (that._listView._isZombie()) { return; } skipWait = false; var end = _BaseUtils._now() + _VirtualizeContentsView._createContainersJobTimeslice, groups = that._getGroups(count), startLength = that.containers.length, realizedToEnd = that.end === that.containers.length, chunkSize = _VirtualizeContentsView._chunkSize; do { that._blockSize ? that._createChunkWithBlocks(groups, count, that._blockSize, chunkSize) : that._createChunk(groups, count, chunkSize); counter++; } while (that.containers.length < count && _BaseUtils._now() < end); that._listView._writeProfilerMark("createContainers yields containers(" + that.containers.length + "),info"); that._listView._affectedRange.add({ start: startLength, end: that.containers.length }, count); if (realizedToEnd) { that.stopWork(); that._listView._writeProfilerMark(that._state.name + "_relayout,info"); that._state.relayout(); } else { that._listView._writeProfilerMark(that._state.name + "_layoutNewContainers,info"); that._state.layoutNewContainers(); } if (that.containers.length < count) { info.setWork(work); } else { that._listView._writeProfilerMark("createContainers completed steps(" + counter + "),info"); that._creatingContainersWork.complete(); } } else { // Waiting on zooming info.setPromise(waitForSeZo(that._listView).then(function (timedOut) { skipWait = timedOut; return work; })); } }); } else { // Version manager locked info.setPromise(that._listView._versionManager.unlocked.then(function () { return work; })); } }; }, _scheduleLazyTreeCreation: function VirtualizeContentsView_scheduleLazyTreeCreation() { return Scheduler.schedule(this._generateCreateContainersWorker(), Scheduler.Priority.idle, this, "WinJS.UI.ListView.LazyTreeCreation"); }, _createContainers: function VirtualizeContentsView_createContainers() { this.tree = null; this.keyToGroupIndex = null; this.containers = null; this._expandedRange = null; var that = this, count; return this._listView._itemsCount().then(function (c) { if (c === 0) { that._listView._hideProgressBar(); } count = c; that._listView._writeProfilerMark("createContainers(" + count + "),StartTM"); if (that._listView._groupDataSource) { return that._listView._groups.initialize(); } }).then(function () { that._listView._writeProfilerMark("numberOfItemsPerItemsBlock,StartTM"); return (count && that._listView._groups.length() ? that._listView._layout.numberOfItemsPerItemsBlock : null); }).then(function (blockSize) { that._listView._writeProfilerMark("numberOfItemsPerItemsBlock(" + blockSize + "),info"); that._listView._writeProfilerMark("numberOfItemsPerItemsBlock,StopTM"); that._listView._resetCanvas(); that.tree = []; that.keyToGroupIndex = {}; that.containers = []; that._blockSize = blockSize; var groups = that._getGroups(count); var end = _BaseUtils._now() + _VirtualizeContentsView._maxTimePerCreateContainers, chunkSize = Math.min(_VirtualizeContentsView._startupChunkSize, _VirtualizeContentsView._chunkSize); var stop; do { stop = blockSize ? that._createChunkWithBlocks(groups, count, blockSize, chunkSize) : that._createChunk(groups, count, chunkSize); } while (_BaseUtils._now() < end && that.containers.length < count && !stop); that._listView._writeProfilerMark("createContainers created(" + that.containers.length + "),info"); that._listView._affectedRange.add({ start: 0, end: that.containers.length }, count); if (that.containers.length < count) { var jobNode = that._scheduleLazyTreeCreation(); that._creatingContainersWork.promise.done(null, function () { jobNode.cancel(); }); } else { that._listView._writeProfilerMark("createContainers completed synchronously,info"); that._creatingContainersWork.complete(); } that._listView._writeProfilerMark("createContainers(" + count + "),StopTM"); }); }, _updateItemsBlocks: function VirtualizeContentsView_updateItemsBlocks(blockSize) { var that = this; var usingStructuralNodes = !!blockSize; function createNewBlock() { var element = _Global.document.createElement("div"); element.className = _Constants._itemsBlockClass; return element; } function updateGroup(itemsContainer, startIndex) { var blockElements = [], itemsCount = 0, blocks = itemsContainer.itemsBlocks, b; function rebuildItemsContainer() { itemsContainer.itemsBlocks = null; itemsContainer.items = []; for (var i = 0; i < itemsCount; i++) { var container = that.containers[startIndex + i]; itemsContainer.element.appendChild(container); itemsContainer.items.push(container); } } function rebuildItemsContainerWithBlocks() { itemsContainer.itemsBlocks = [{ element: blockElements.length ? blockElements.shift() : createNewBlock(), items: [] }]; var currentBlock = itemsContainer.itemsBlocks[0]; for (var i = 0; i < itemsCount; i++) { if (currentBlock.items.length === blockSize) { var nextBlock = blockElements.length ? blockElements.shift() : createNewBlock(); itemsContainer.itemsBlocks.push({ element: nextBlock, items: [] }); currentBlock = itemsContainer.itemsBlocks[itemsContainer.itemsBlocks.length - 1]; } var container = that.containers[startIndex + i]; currentBlock.element.appendChild(container); currentBlock.items.push(container); } itemsContainer.items = null; } if (blocks) { for (b = 0; b < blocks.length; b++) { itemsCount += blocks[b].items.length; blockElements.push(blocks[b].element); } } else { itemsCount = itemsContainer.items.length; } if (usingStructuralNodes) { rebuildItemsContainerWithBlocks(); } else { rebuildItemsContainer(); } for (b = 0; b < blockElements.length; b++) { var block = blockElements[b]; if (block.parentNode === itemsContainer.element) { itemsContainer.element.removeChild(block); } } return itemsCount; } for (var g = 0, startIndex = 0; g < this.tree.length; g++) { startIndex += updateGroup(this.tree[g].itemsContainer, startIndex); } that._blockSize = blockSize; }, _layoutItems: function VirtualizeContentsView_layoutItems() { var that = this; return this._listView._itemsCount().then(function () { return Promise.as(that._listView._layout.numberOfItemsPerItemsBlock).then(function (blockSize) { that._listView._writeProfilerMark("numberOfItemsPerItemsBlock(" + blockSize + "),info"); if (blockSize !== that._blockSize) { that._updateItemsBlocks(blockSize); that._listView._itemsBlockExtent = -1; } var affectedRange = that._listView._affectedRange.get(); var changedRange; // We accumulate all changes that occur between layouts in _affectedRange. If layout is interrupted due to additional // modifications, _affectedRange will become the union of the previous range of changes and the new range of changes // and will be passed to layout again. _affectedRange is reset whenever layout completes. if (affectedRange) { changedRange = { // _affectedRange is stored in the format [start, end), layout expects a range in the form of [firstIndex , lastIndex] // To ensure that layout can successfully use the expected range to find all of the groups which need to be re-laid out // we will pad an extra index at the front end such that layout receives [start - 1, end] in form of [lastIndex, firstIndex]. firstIndex: Math.max(affectedRange.start - 1, 0), lastIndex: Math.min(that.containers.length - 1, affectedRange.end) // Account for any constrained upper limits from lazily loaded win-container's. }; if (changedRange.firstIndex < that.containers.length || that.containers.length === 0) { return that._listView._layout.layout(that.tree, changedRange, that._modifiedElements || [], that._modifiedGroups || []); } } // There is nothing to layout. that._listView._affectedRange.clear(); return { realizedRangeComplete: Promise.wrap(), layoutComplete: Promise.wrap() }; }); }); }, updateTree: function VirtualizeContentsView_updateTree(count, delta, modifiedElements) { this._listView._writeProfilerMark(this._state.name + "_updateTree,info"); return this._state.updateTree(count, delta, modifiedElements); }, _updateTreeImpl: function VirtualizeContentsView_updateTreeImpl(count, delta, modifiedElements, skipUnrealizeItems) { this._executeAnimations = true; this._modifiedElements = modifiedElements; if (modifiedElements.handled) { return; } modifiedElements.handled = true; this._listView._writeProfilerMark("_updateTreeImpl,StartTM"); var that = this, i; if (!skipUnrealizeItems) { // If we skip unrealize items, this work will eventually happen when we reach the UnrealizingState. Sometimes, // it is appropriate to defer the unrealize work in order to optimize scenarios (e.g, edits that happen when we are // in the CompletedState, that way the animation can start sooner). this._unrealizeItems(); } function removeElements(array) { for (var i = 0, len = array.length; i < len; i++) { var itemBox = array[i]; itemBox.parentNode.removeChild(itemBox); } } for (var i = 0, len = modifiedElements.length; i < len; i++) { if (modifiedElements[i]._itemBox && modifiedElements[i]._itemBox.parentNode) { _ElementUtilities.removeClass(modifiedElements[i]._itemBox.parentNode, _Constants._selectedClass); } } this.items.each(function (index, item, itemData) { itemData.container && _ElementUtilities.removeClass(itemData.container, _Constants._selectedClass); itemData.container && _ElementUtilities.addClass(itemData.container, _Constants._backdropClass); }); var removedGroups = this._listView._updateContainers(this._getGroups(count), count, delta, modifiedElements); removeElements(removedGroups.removedHeaders); removeElements(removedGroups.removedItemsContainers); for (var i = 0, len = modifiedElements.length; i < len; i++) { var modifiedElement = modifiedElements[i]; if (modifiedElement.newIndex !== -1) { modifiedElement.element = this.getContainer(modifiedElement.newIndex); if (!modifiedElement.element) { throw "Container missing after updateContainers."; } } else { _ElementUtilities.removeClass(modifiedElement.element, _Constants._backdropClass); } } // We only need to restore focus if the current focus is within surface var activeElement = _Global.document.activeElement; if (this._listView._canvas.contains(activeElement)) { this._requireFocusRestore = activeElement; } this._deferredReparenting = []; this.items.each(function (index, item, itemData) { var container = that.getContainer(index), itemBox = itemData.itemBox; if (itemBox && container) { if (itemBox.parentNode !== container) { if (index >= that.firstIndexDisplayed && index <= that.lastIndexDisplayed) { that._appendAndRestoreFocus(container, itemBox); } else { that._deferredReparenting.push({ itemBox: itemBox, container: container }); } } _ElementUtilities.removeClass(container, _Constants._backdropClass); itemData.container = container; _ElementUtilities[that._listView.selection._isIncluded(index) ? "addClass" : "removeClass"](container, _Constants._selectedClass); if (!that._listView.selection._isIncluded(index) && _ElementUtilities.hasClass(itemBox, _Constants._selectedClass)) { _ItemEventsHandler._ItemEventsHandler.renderSelection(itemBox, itemData.element, false, true); } } }); this._listView._writeProfilerMark("_updateTreeImpl,StopTM"); }, _completeUpdateTree: function () { if (this._deferredReparenting) { var deferredCount = this._deferredReparenting.length; if (deferredCount > 0) { var perfId = "_completeReparenting(" + deferredCount + ")"; this._listView._writeProfilerMark(perfId + ",StartTM"); var deferredItem; for (var i = 0; i < deferredCount; i++) { deferredItem = this._deferredReparenting[i]; this._appendAndRestoreFocus(deferredItem.container, deferredItem.itemBox); } this._deferredReparenting = []; this._listView._writeProfilerMark(perfId + ",StopTM"); } } this._requireFocusRestore = null; }, _appendAndRestoreFocus: function VirtualizeContentsView_appendAndRestoreFocus(container, itemBox) { if (itemBox.parentNode !== container) { var activeElement; if (this._requireFocusRestore) { activeElement = _Global.document.activeElement; } if (this._requireFocusRestore && this._requireFocusRestore === activeElement && (container.contains(activeElement) || itemBox.contains(activeElement))) { this._listView._unsetFocusOnItem(); activeElement = _Global.document.activeElement; } _ElementUtilities.empty(container); container.appendChild(itemBox); if (this._requireFocusRestore && activeElement === this._listView._keyboardEventsHelper) { var focused = this._listView._selection._getFocused(); if (focused.type === _UI.ObjectType.item && this.items.itemBoxAt(focused.index) === itemBox) { _ElementUtilities._setActive(this._requireFocusRestore); this._requireFocusRestore = null; } } } }, _startAnimations: function VirtualizeContentsView_startAnimations() { this._listView._writeProfilerMark("startAnimations,StartTM"); var that = this; this._hasAnimationInViewportPending = false; var animationPromise = Promise.as(this._listView._layout.executeAnimations()).then(function () { that._listView._writeProfilerMark("startAnimations,StopTM"); }); return animationPromise; }, _setState: function VirtualizeContentsView_setState(NewStateType, arg) { if (!this._listView._isZombie()) { var prevStateName = this._state.name; this._state = new NewStateType(this, arg); this._listView._writeProfilerMark(this._state.name + "_enter from(" + prevStateName + "),info"); this._state.enter(); } }, getAdjacent: function VirtualizeContentsView_getAdjacent(currentFocus, direction) { var that = this; return this.waitForEntityPosition(currentFocus).then(function () { return that._listView._layout.getAdjacent(currentFocus, direction); }); }, hitTest: function VirtualizeContentsView_hitTest(x, y) { if (!this._realizedRangeLaidOut) { var retVal = this._listView._layout.hitTest(x, y); retVal.index = _ElementUtilities._clamp(retVal.index, -1, this._listView._cachedCount - 1, 0); retVal.insertAfterIndex = _ElementUtilities._clamp(retVal.insertAfterIndex, -1, this._listView._cachedCount - 1, 0); return retVal; } else { return { index: -1, insertAfterIndex: -1 }; } }, _createTreeBuildingSignal: function VirtualizeContentsView__createTreeBuildingSignal() { if (!this._creatingContainersWork) { this._creatingContainersWork = new _Signal(); var that = this; this._creatingContainersWork.promise.done( function () { that._creatingContainersWork = null; }, function () { that._creatingContainersWork = null; } ); } }, _createLayoutSignal: function VirtualizeContentsView_createLayoutSignal() { var that = this; if (!this._layoutCompleted) { this._layoutCompleted = new _Signal(); this._layoutCompleted.promise.done( function () { that._layoutCompleted = null; }, function () { that._layoutCompleted = null; } ); } if (!this._realizedRangeLaidOut) { this._realizedRangeLaidOut = new _Signal(); this._realizedRangeLaidOut.promise.done( function () { that._realizedRangeLaidOut = null; }, function () { that._realizedRangeLaidOut = null; } ); } }, _getLayoutCompleted: function VirtualizeContentsView_getLayoutCompleted() { return this._layoutCompleted ? Promise._cancelBlocker(this._layoutCompleted.promise) : Promise.wrap(); }, _createSurfaceChild: function VirtualizeContentsView_createSurfaceChild(className, insertAfter) { var element = _Global.document.createElement("div"); element.className = className; this._listView._canvas.insertBefore(element, insertAfter ? insertAfter.nextElementSibling : null); return element; }, _executeScrollToFunctor: function VirtualizeContentsView_executeScrollToFunctor() { var that = this; return Promise.as(this._scrollToFunctor ? this._scrollToFunctor() : null).then(function (scroll) { that._scrollToFunctor = null; scroll = scroll || {}; // _scrollbarPos is initialized to 0 in the constructor, and we only set it when a valid integer // value is passed in order to account for cases when there is not a _scrollToFunctor if (+scroll.position === scroll.position) { that._scrollbarPos = scroll.position; } that._direction = scroll.direction || "right"; }); } },{ _pagesToPrefetch: 2, _customPagesToPrefetchMax: 6, _customPagesToPrefetchMin: 2, _disableCustomPagesPrefetch: false, _waitForSeZoIntervalDuration: 100, _waitForSeZoTimeoutDuration: 500, _chunkSize: 500, _startupChunkSize: 100, _maxTimePerCreateContainers: 5, _createContainersJobTimeslice: 15, _blocksToRelease:10, _realizationLevel: { skip: "skip", realize: "realize", normal: "normal" } }); function nop() { } /* View is in this state before reload is called so during startup, after datasource change etc. */ var CreatedState = _Base.Class.define(function CreatedState_ctor(view) { this.view = view; this.view._createTreeBuildingSignal(); this.view._createLayoutSignal(); }, { name: 'CreatedState', enter: function CreatedState_enter() { this.view._createTreeBuildingSignal(); this.view._createLayoutSignal(); }, stop: nop, realizePage: nop, rebuildTree: function CreatedState_rebuildTree() { this.view._setState(BuildingState); }, relayout: function CreatedState_relayout() { this.view._setState(BuildingState); }, layoutNewContainers: nop, waitForEntityPosition: function CreatedState_waitForEntityPosition() { this.view._setState(BuildingState); return this.view._getLayoutCompleted(); }, updateTree: nop }); /* In this state View is building its DOM tree with win-container element for each item in the data set. To build the tree the view needs to know items count or for grouped case the count of groups and the count of items in each group. The view enters this state when the tree needs to be built during startup or rebuild after data source change and etc. BuildingState => LayingoutState | CreatedState */ var BuildingState = _Base.Class.define(function BuildingState_ctor(view) { this.view = view; }, { name: 'BuildingState', enter: function BuildingState_enter() { this.canceling = false; this.view._createTreeBuildingSignal(); this.view._createLayoutSignal(); var that = this; // Use a signal to guarantee that this.promise is set before the promise // handler is executed. var promiseStoredSignal = new _Signal(); this.promise = promiseStoredSignal.promise.then(function () { return that.view._createContainers(); }).then( function () { that.view._setState(LayingoutState); }, function (error) { if (!that.canceling) { // this is coming from layout. ListView is hidden. We need to raise complete and wait in initial state for further actions that.view._setState(CreatedState); that.view._listView._raiseViewComplete(); } return Promise.wrapError(error); } ); promiseStoredSignal.complete(); }, stop: function BuildingState_stop() { this.canceling = true; this.promise.cancel(); this.view._setState(CreatedState); }, realizePage: nop, rebuildTree: function BuildingState_rebuildTree() { this.canceling = true; this.promise.cancel(); this.enter(); }, relayout: nop, layoutNewContainers: nop, waitForEntityPosition: function BuildingState_waitForEntityPosition() { return this.view._getLayoutCompleted(); }, updateTree: nop }); /* In this state View waits for the layout to lay out win-container elements. The view enters this state after edits or resize. LayingoutState => RealizingState | BuildingState | CanceledState | CompletedState | LayoutCanceledState */ var LayingoutState = _Base.Class.define(function LayingoutState_ctor(view, NextStateType) { this.view = view; this.nextStateType = NextStateType || RealizingState; }, { name: 'LayingoutState', enter: function LayingoutState_enter() { var that = this; this.canceling = false; this.view._createLayoutSignal(); this.view._listView._writeProfilerMark(this.name + "_enter_layoutItems,StartTM"); // Use a signal to guarantee that this.promise is set before the promise // handler is executed. var promiseStoredSignal = new _Signal(); this.promise = promiseStoredSignal.promise.then(function () { return that.view._layoutItems(); }).then(function (layoutPromises) { // View is taking ownership of this promise and it will cancel it in stopWork that.view._layoutWork = layoutPromises.layoutComplete; return layoutPromises.realizedRangeComplete; }).then( function () { that.view._listView._writeProfilerMark(that.name + "_enter_layoutItems,StopTM"); that.view._listView._clearInsertedItems(); that.view._setAnimationInViewportState(that.view._modifiedElements); that.view._modifiedElements = []; that.view._modifiedGroups = []; that.view._realizedRangeLaidOut.complete(); that.view._layoutWork.then(function () { that.view._listView._writeProfilerMark(that.name + "_enter_layoutCompleted,info"); that.view._listView._affectedRange.clear(); that.view._layoutCompleted.complete(); }); if (!that.canceling) { that.view._setState(that.nextStateType); } }, function (error) { that.view._listView._writeProfilerMark(that.name + "_enter_layoutCanceled,info"); if (!that.canceling) { // Cancel is coming from layout itself so ListView is hidden or empty. In this case we want to raise loadingStateChanged that.view.firstIndexDisplayed = that.view.lastIndexDisplayed = -1; that.view._updateAriaMarkers(true, that.view.firstIndexDisplayed, that.view.lastIndexDisplayed); that.view._setState(CompletedState); } return Promise.wrapError(error); } ); promiseStoredSignal.complete(); if (this.canceling) { this.promise.cancel(); } }, cancelLayout: function LayingoutState_cancelLayout(switchState) { this.view._listView._writeProfilerMark(this.name + "_cancelLayout,info"); this.canceling = true; if (this.promise) { this.promise.cancel(); } if (switchState) { this.view._setState(LayoutCanceledState); } }, stop: function LayingoutState_stop() { this.cancelLayout(true); }, realizePage: nop, rebuildTree: function LayingoutState_rebuildTree() { this.cancelLayout(false); this.view._setState(BuildingState); }, relayout: function LayingoutState_relayout() { this.cancelLayout(false); this.enter(); }, layoutNewContainers: function LayingoutState_layoutNewContainers() { this.relayout(); }, waitForEntityPosition: function LayingoutState_waitForEntityPosition() { return this.view._getLayoutCompleted(); }, updateTree: function LayingoutState_updateTree(count, delta, modifiedElements) { return this.view._updateTreeImpl(count, delta, modifiedElements); } }); /* View enters this state when layout is canceled. LayoutCanceledState => LayingoutState | BuildingState */ var LayoutCanceledState = _Base.Class.define(function LayoutCanceledState_ctor(view) { this.view = view; }, { name: 'LayoutCanceledState', enter: nop, stop: nop, realizePage: function LayoutCanceledState_realizePage() { this.relayout(); }, rebuildTree: function LayoutCanceledState_rebuildTree() { this.view._setState(BuildingState); }, relayout: function LayoutCanceledState_relayout() { this.view._setState(LayingoutState); }, layoutNewContainers: function LayoutCanceledState_layoutNewContainers() { this.relayout(); }, waitForEntityPosition: function LayoutCanceledState_waitForEntityPosition() { return this.view._getLayoutCompleted(); }, updateTree: function LayoutCanceledState_updateTree(count, delta, modifiedElements) { return this.view._updateTreeImpl(count, delta, modifiedElements); } }); /* Contents of items in the current viewport and prefetch area is realized during this stage. The view enters this state when items needs to be realized for instance during initialization, edits and resize. RealizingState => RealizingAnimatingState | UnrealizingState | LayingoutState | BuildingState | CanceledState */ var RealizingState = _Base.Class.define(function RealizingState_ctor(view) { this.view = view; this.nextState = UnrealizingState; this.relayoutNewContainers = true; }, { name: 'RealizingState', enter: function RealizingState_enter() { var that = this; var promiseStoredSignal = new _Signal(); this.promise = promiseStoredSignal.promise.then(function () { return that.view._executeScrollToFunctor(); }).then(function () { that.relayoutNewContainers = false; return Promise._cancelBlocker(that.view._realizePageImpl()); }).then( function () { if (that.view._state === that) { that.view._completeUpdateTree(); that.view._listView._writeProfilerMark("RealizingState_to_UnrealizingState"); that.view._setState(that.nextState); } }, function (error) { if (that.view._state === that && !that.canceling) { that.view._listView._writeProfilerMark("RealizingState_to_CanceledState"); that.view._setState(CanceledState); } return Promise.wrapError(error); } ); promiseStoredSignal.complete(); }, stop: function RealizingState_stop() { this.canceling = true; this.promise.cancel(); this.view._cancelRealize(); this.view._setState(CanceledState); }, realizePage: function RealizingState_realizePage() { this.canceling = true; this.promise.cancel(); this.enter(); }, rebuildTree: function RealizingState_rebuildTree() { this.stop(); this.view._setState(BuildingState); }, relayout: function RealizingState_relayout() { this.stop(); this.view._setState(LayingoutState); }, layoutNewContainers: function RealizingState_layoutNewContainers() { if (this.relayoutNewContainers) { this.relayout(); } else { this.view._createLayoutSignal(); this.view._relayoutInComplete = true; } }, waitForEntityPosition: function RealizingState_waitForEntityPosition() { return this.view._getLayoutCompleted(); }, updateTree: function RealizingState_updateTree(count, delta, modifiedElements) { return this.view._updateTreeImpl(count, delta, modifiedElements); }, setLoadingState: function RealizingState_setLoadingState(state) { this.view._listView._setViewState(state); } }); /* The view enters this state when the realize pass, animations or unrealizing was canceled or after newContainers have been laid out. In this state view waits for the next call from ListViewImpl. It can be scroll, edit etc. CanceledState => RealizingState | ScrollingState | LayingoutState | BuildingState */ var CanceledState = _Base.Class.define(function CanceledState_ctor(view) { this.view = view; }, { name: 'CanceledState', enter: nop, stop: function CanceledState_stop() { // cancelRealize cancels ariaSetup which can still be in progress this.view._cancelRealize(); }, realizePage: function CanceledState_realizePage(NewStateType) { this.stop(); this.view._setState(NewStateType); }, rebuildTree: function CanceledState_rebuildTree() { this.stop(); this.view._setState(BuildingState); }, relayout: function CanceledState_relayout(NextStateType) { this.stop(); this.view._setState(LayingoutState, NextStateType); }, layoutNewContainers: function CanceledState_layoutNewContainers() { this.relayout(CanceledState); }, waitForEntityPosition: function CanceledState_waitForEntityPosition() { return this.view._getLayoutCompleted(); }, updateTree: function CanceledState_updateTree(count, delta, modifiedElements) { return this.view._updateTreeImpl(count, delta, modifiedElements); } }); /* This state is almost identical with RealizingState. Currently the difference is that in this state loadingStateChanged events aren’t raised and after complete the state is switched to ScrollingPausedState to wait until end of scrolling. ScrollingState => RealizingAnimatingState | ScrollingPausedState | LayingoutState | BuildingState | CanceledState */ var ScrollingState = _Base.Class.derive(RealizingState, function ScrollingState_ctor(view) { this.view = view; this.nextState = ScrollingPausedState; this.relayoutNewContainers = true; }, { name: 'ScrollingState', setLoadingState: function ScrollingState_setLoadingState() { } }); /* The view waits in this state for end of scrolling which for touch is signaled by MSManipulationStateChanged event and for mouse it is timeout. ScrollingPausedState => RealizingAnimatingState | ScrollingPausedState | LayingoutState | BuildingState | CanceledState */ var ScrollingPausedState = _Base.Class.derive(CanceledState, function ScrollingPausedState_ctor(view) { this.view = view; }, { name: 'ScrollingPausedState', enter: function ScrollingPausedState_enter() { var that = this; this.promise = Promise._cancelBlocker(this.view._scrollEndPromise).then(function () { that.view._setState(UnrealizingState); }); }, stop: function ScrollingPausedState_stop() { this.promise.cancel(); // cancelRealize cancels ariaSetup which can still be in progress this.view._cancelRealize(); }, }); /* In this state, view unrealizes not needed items and then waits for all renderers to complete. UnrealizingState => CompletedState | RealizingState | ScrollingState | LayingoutState | BuildingState | CanceledState */ var UnrealizingState = _Base.Class.define(function UnrealizingState_ctor(view) { this.view = view; }, { name: 'UnrealizingState', enter: function UnrealizingState_enter() { var that = this; this.promise = this.view._lazilyUnrealizeItems().then(function () { that.view._listView._writeProfilerMark("_renderCompletePromise wait starts,info"); return that.view._renderCompletePromise; }).then(function () { that.view._setState(CompletedState); }); }, stop: function UnrealizingState_stop() { // cancelRealize cancels ariaSetup which can still be in progress this.view._cancelRealize(); this.promise.cancel(); this.view._setState(CanceledState); }, realizePage: function UnrealizingState_realizePage(NewStateType) { this.promise.cancel(); this.view._setState(NewStateType); }, rebuildTree: function UnrealizingState_rebuildTree() { this.view._setState(BuildingState); }, relayout: function UnrealizingState_relayout() { this.view._setState(LayingoutState); }, layoutNewContainers: function UnrealizingState_layoutNewContainers() { this.view._createLayoutSignal(); this.view._relayoutInComplete = true; }, waitForEntityPosition: function UnrealizingState_waitForEntityPosition() { return this.view._getLayoutCompleted(); }, updateTree: function UnrealizingState_updateTree(count, delta, modifiedElements) { return this.view._updateTreeImpl(count, delta, modifiedElements); } }); /* We enter this state, when there are animations to execute, and we have already fired the viewportloaded event RealizingAnimatingState => RealizingState | UnrealizingState | LayingoutState | BuildingState | CanceledState */ var RealizingAnimatingState = _Base.Class.define(function RealizingStateAnimating_ctor(view, realizePromise) { this.view = view; this.realizePromise = realizePromise; this.realizeId = 1; }, { name: 'RealizingAnimatingState', enter: function RealizingAnimatingState_enter() { var that = this; this.animating = true; this.animatePromise = this.view._startAnimations(); this.animateSignal = new _Signal(); this.view._executeAnimations = false; this.animatePromise.done( function () { that.animating = false; if (that.modifiedElements) { that.view._updateTreeImpl(that.count, that.delta, that.modifiedElements); that.modifiedElements = null; that.view._setState(CanceledState); } else { that.animateSignal.complete(); } }, function (error) { that.animating = false; return Promise.wrapError(error); } ); this._waitForRealize(); }, _waitForRealize: function RealizingAnimatingState_waitForRealize() { var that = this; this.realizing = true; this.realizePromise.done(function () { that.realizing = false; }); var currentRealizeId = ++this.realizeId; Promise.join([this.realizePromise, this.animateSignal.promise]).done(function () { if (currentRealizeId === that.realizeId) { that.view._completeUpdateTree(); that.view._listView._writeProfilerMark("RealizingAnimatingState_to_UnrealizingState"); that.view._setState(UnrealizingState); } }); }, stop: function RealizingAnimatingState_stop(stopTreeCreation) { // always cancel realization this.realizePromise.cancel(); this.view._cancelRealize(); // animations are canceled only when tree needs to be rebuilt if (stopTreeCreation) { this.animatePromise.cancel(); this.view._setState(CanceledState); } }, realizePage: function RealizingAnimatingState_realizePage() { if (!this.modifiedElements) { var that = this; this.realizePromise = this.view._executeScrollToFunctor().then(function () { return Promise._cancelBlocker(that.view._realizePageImpl()); }); this._waitForRealize(); } }, rebuildTree: function RealizingAnimatingState_rebuildTree() { this.stop(true); this.view._setState(BuildingState); }, relayout: function RealizingAnimatingState_relayout() { // Relayout caused by edits should be stopped by updateTree but relayout can be caused by resize or containers creation and in these cases we should stop animations this.stop(true); // if tree update was waiting for animations we should do it now if (this.modifiedElements) { this.view._updateTreeImpl(this.count, this.delta, this.modifiedElements); this.modifiedElements = null; } this.view._setState(LayingoutState); }, layoutNewContainers: function RealizingAnimatingState_layoutNewContainers() { this.view._createLayoutSignal(); this.view._relayoutInComplete = true; }, waitForEntityPosition: function RealizingAnimatingState_waitForEntityPosition() { return this.view._getLayoutCompleted(); }, updateTree: function RealizingAnimatingState_updateTree(count, delta, modifiedElements) { if (this.animating) { var previousModifiedElements = this.modifiedElements; this.count = count; this.delta = delta; this.modifiedElements = modifiedElements; return previousModifiedElements ? Promise.cancel : this.animatePromise; } else { return this.view._updateTreeImpl(count, delta, modifiedElements); } }, setLoadingState: function RealizingAnimatingState_setLoadingState(state) { this.view._listView._setViewState(state); } }); /* The view enters this state when the tree is built, layout and realized after animations have finished. The layout can still laying out items outside of realized view during this stage. CompletedState => RealizingState | ScrollingState | LayingoutState | BuildingState | LayingoutNewContainersState */ var CompletedState = _Base.Class.derive(CanceledState, function CompletedState_ctor(view) { this.view = view; }, { name: 'CompletedState', enter: function CompletedState_enter() { this._stopped = false; this.view._setupDeferredActions(); this.view._realizationLevel = _VirtualizeContentsView._realizationLevel.normal; this.view._listView._raiseViewComplete(); // _raiseViewComplete will cause user event listener code to run synchronously. // If any updates are made to the Listview, this state will be stopped by the updater. // We don't want to change state to LayingoutNewContainersState if that happens. if (this.view._state === this && this.view._relayoutInComplete && !this._stopped) { this.view._setState(LayingoutNewContainersState); } }, stop: function CompletedState_stop() { this._stopped = true; // Call base class method. CanceledState.prototype.stop.call(this); }, layoutNewContainers: function CompletedState_layoutNewContainers() { this.view._createLayoutSignal(); this.view._setState(LayingoutNewContainersState); }, updateTree: function CompletedState_updateTree(count, delta, modifiedElements) { return this.view._updateTreeImpl(count, delta, modifiedElements, true); } }); /* The view waits in this state for previous layout pass to finish. LayingoutNewContainersState => RealizingState | ScrollingState | LayingoutState | BuildingState */ var LayingoutNewContainersState = _Base.Class.derive(CanceledState, function LayingoutNewContainersState(view) { this.view = view; }, { name: 'LayingoutNewContainersState', enter: function LayingoutNewContainersState_enter() { var that = this; // _layoutWork is completed when the previous layout pass is done. _getLayoutCompleted will be completed when these new containers are laid out this.promise = Promise.join([this.view.deferTimeout, this.view._layoutWork]); this.promise.then(function () { that.view._relayoutInComplete = false; that.relayout(CanceledState); }); }, stop: function LayingoutNewContainersState_stop() { // cancelRealize cancels ariaSetup which can still be in progress this.promise.cancel(); this.view._cancelRealize(); }, realizePage: function LayingoutNewContainersState_realizePage(NewStateType) { // in this state realizePage needs to run layout before realizing items this.stop(); this.view._setState(LayingoutState, NewStateType); }, layoutNewContainers: function LayingoutNewContainersState_layoutNewContainers() { this.view._createLayoutSignal(); } }); return _VirtualizeContentsView; }) }); }); define('require-style!less/desktop/controls',[],function(){}); define('require-style!less/phone/controls',[],function(){}); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ListView',[ '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Events', '../Core/_Log', '../Core/_Resources', '../Core/_WriteProfilerMark', '../Animations/_TransitionAnimation', '../BindingList', '../Promise', '../Scheduler', '../_Signal', '../Utilities/_Control', '../Utilities/_Dispose', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', '../Utilities/_ItemsManager', '../Utilities/_SafeHtml', '../Utilities/_TabContainer', '../Utilities/_UI', '../Utilities/_VersionManager', './ItemContainer/_Constants', './ItemContainer/_ItemEventsHandler', './ListView/_BrowseMode', './ListView/_ErrorMessages', './ListView/_GroupFocusCache', './ListView/_GroupsContainer', './ListView/_Helpers', './ListView/_ItemsContainer', './ListView/_Layouts', './ListView/_SelectionManager', './ListView/_VirtualizeContentsView', 'require-style!less/desktop/controls', 'require-style!less/phone/controls' ], function listViewImplInit(_Global, _Base, _BaseUtils, _ErrorFromName, _Events, _Log, _Resources, _WriteProfilerMark, _TransitionAnimation, BindingList, Promise, Scheduler, _Signal, _Control, _Dispose, _ElementUtilities, _Hoverable, _ItemsManager, _SafeHtml, _TabContainer, _UI, _VersionManager, _Constants, _ItemEventsHandler, _BrowseMode, _ErrorMessages, _GroupFocusCache, _GroupsContainer, _Helpers, _ItemsContainer, _Layouts, _SelectionManager, _VirtualizeContentsView) { "use strict"; var transformNames = _BaseUtils._browserStyleEquivalents["transform"]; var DISPOSE_TIMEOUT = 1000; var controlsToDispose = []; var disposeControlTimeout; var uniqueID = _ElementUtilities._uniqueID; function disposeControls() { var temp = controlsToDispose; controlsToDispose = []; temp = temp.filter(function (c) { if (c._isZombie()) { c._dispose(); return false; } else { return true; } }); controlsToDispose = controlsToDispose.concat(temp); } function scheduleForDispose(lv) { controlsToDispose.push(lv); disposeControlTimeout && disposeControlTimeout.cancel(); disposeControlTimeout = Promise.timeout(DISPOSE_TIMEOUT).then(disposeControls); } function getOffsetRight(element) { return element.offsetParent ? (element.offsetParent.offsetWidth - element.offsetLeft - element.offsetWidth) : 0; } var AnimationHelper = _Helpers._ListViewAnimationHelper; var strings = { get notCompatibleWithSemanticZoom() { return "ListView can only be used with SemanticZoom if randomAccess loading behavior is specified."; }, get listViewInvalidItem() { return "Item must provide index, key or description of corresponding item."; }, get listViewViewportAriaLabel() { return _Resources._getWinJSString("ui/listViewViewportAriaLabel").value; } }; var requireSupportedForProcessing = _BaseUtils.requireSupportedForProcessing; var ListViewAnimationType = { /// /// The animation plays when the ListView is first displayed. /// entrance: "entrance", /// /// The animation plays when the ListView is changing its content. /// contentTransition: "contentTransition" }; // ListView implementation _Base.Namespace.define("WinJS.UI", { /// /// Specifies whether the ListView animation is an entrance animation or a transition animation. /// /// ListViewAnimationType: ListViewAnimationType, /// /// /// Displays items in a customizable list or grid. /// /// /// /// /// ]]> /// Raised when the ListView is about to play an entrance or a transition animation. /// Raised when the user taps or clicks an item. /// Raised when the user taps or clicks a group header. /// Raised before items are selected or deselected. /// Raised after items are selected or deselected. /// Raised when the loading state changes. /// Raised when the focused item changes. /// Raised when the the user begins dragging ListView items. /// Raised when the user drags into the ListView. /// Raised when a drag operation begun in a ListView ends. /// Raised when the user drags between two ListView items. /// Raised when the user drags outside of the ListView region. /// Raised when the items being dragged are changed due to a datasource modification. /// Raised when the user drops items into the ListView. /// Raised when the accessibility attributes have been added to the ListView items. /// The entire ListView control. /// The viewport of the ListView. /// The scrollable region of the ListView. /// An item in the ListView. /// The background of a selection checkmark. /// A selection checkmark. /// The header of a group. /// The progress indicator of the ListView. /// /// /// ListView: _Base.Namespace._lazy(function () { var AffectedRange = _Base.Class.define(function () { this.clear(); }, { // Marks the union of the current affected range and range as requiring layout add: function (range, itemsCount) { range._lastKnownSizeOfData = itemsCount; // store this in order to make unions. if (!this._range) { this._range = range; } else { // Take the union of these two ranges. this._range.start = Math.min(this._range.start, range.start); // To accurately calculate the new unioned range 'end' value, we need to convert the current and new range end // positions into values that represent the remaining number of un-modified items in between the end of the range // and the end of the list of data. var previousUnmodifiedItemsFromEnd = (this._range._lastKnownSizeOfData - this._range.end); var newUnmodifiedItemsFromEnd = (range._lastKnownSizeOfData - range.end); var finalUnmodifiedItemsFromEnd = Math.min(previousUnmodifiedItemsFromEnd, newUnmodifiedItemsFromEnd); this._range._lastKnownSizeOfData = range._lastKnownSizeOfData; // Convert representation of the unioned end position back into a value which matches the above definition of _affecteRange.end this._range.end = this._range._lastKnownSizeOfData - finalUnmodifiedItemsFromEnd; } }, // Marks everything as requiring layout addAll: function () { this.add({ start: 0, end: Number.MAX_VALUE }, Number.MAX_VALUE); }, // Marks nothing as requiring layout clear: function () { this._range = null; }, get: function () { return this._range; } }); var ZoomableView = _Base.Class.define(function ZoomableView_ctor(listView) { // Constructor this._listView = listView; }, { // Public methods getPanAxis: function () { return this._listView._getPanAxis(); }, configureForZoom: function (isZoomedOut, isCurrentView, triggerZoom, prefetchedPages) { this._listView._configureForZoom(isZoomedOut, isCurrentView, triggerZoom, prefetchedPages); }, setCurrentItem: function (x, y) { this._listView._setCurrentItem(x, y); }, getCurrentItem: function () { return this._listView._getCurrentItem(); }, beginZoom: function () { return this._listView._beginZoom(); }, positionItem: function (item, position) { return this._listView._positionItem(item, position); }, endZoom: function (isCurrentView) { this._listView._endZoom(isCurrentView); }, pinching: { get: function () { return this._listView._pinching; }, set: function (value) { this._listView._pinching = value; } } }); var ListView = _Base.Class.define(function ListView_ctor(element, options) { /// /// /// Creates a new ListView. /// /// /// The DOM element that hosts the ListView control. /// /// /// An object that contains one or more property/value pairs to apply to the new control. /// Each property of the options object corresponds to one of the control's properties or events. /// Event names must begin with "on". For example, to provide a handler for the selectionchanged event, /// add a property named "onselectionchanged" to the options object and set its value to the event handler. /// /// /// The new ListView. /// /// element = element || _Global.document.createElement("div"); if (_ElementUtilities._supportsTouchActionCrossSlide) { element.classList.add(_Constants._listViewSupportsCrossSlideClass); } this._id = element.id || ""; this._writeProfilerMark("constructor,StartTM"); options = options || {}; // Attaching JS control to DOM element element.winControl = this; _ElementUtilities.addClass(element, "win-disposable"); this._affectedRange = new AffectedRange(); this._mutationObserver = new _ElementUtilities._MutationObserver(this._itemPropertyChange.bind(this)); this._versionManager = null; this._insertedItems = {}; this._element = element; this._startProperty = null; this._scrollProperty = null; this._scrollLength = null; this._scrolling = false; this._zooming = false; this._pinching = false; this._itemsManager = null; this._canvas = null; this._cachedCount = _Constants._UNINITIALIZED; this._loadingState = this._LoadingState.complete; this._firstTimeDisplayed = true; this._currentScrollPosition = 0; this._lastScrollPosition = 0; this._notificationHandlers = []; this._itemsBlockExtent = -1; this._viewportWidth = _Constants._UNINITIALIZED; this._viewportHeight = _Constants._UNINITIALIZED; this._manipulationState = _ElementUtilities._MSManipulationEvent.MS_MANIPULATION_STATE_STOPPED; this._maxDeferredItemCleanup = Number.MAX_VALUE; this._groupsToRemove = {}; this._setupInternalTree(); this._isCurrentZoomView = true; this._dragSource = false; this._reorderable = false; this._groupFocusCache = new _GroupFocusCache._UnsupportedGroupFocusCache(); this._viewChange = _Constants._ViewChange.rebuild; this._scrollToFunctor = null; this._setScrollbarPosition = false; // The view needs to be initialized after the internal tree is setup, because the view uses the canvas node immediately to insert an element in its constructor this._view = new _VirtualizeContentsView._VirtualizeContentsView(this); this._selection = new _SelectionManager._SelectionManager(this); this._createTemplates(); this._groupHeaderRenderer = _ItemsManager._trivialHtmlRenderer; this._itemRenderer = _ItemsManager._trivialHtmlRenderer; this._groupHeaderRelease = null; this._itemRelease = null; if (!options.itemDataSource) { var list = new BindingList.List(); this._dataSource = list.dataSource; } else { this._dataSource = options.itemDataSource; } this._selectionMode = _UI.SelectionMode.multi; this._tap = _UI.TapBehavior.invokeOnly; this._groupHeaderTap = _UI.GroupHeaderTapBehavior.invoke; this._swipeBehavior = _UI.SwipeBehavior.select; this._mode = new _BrowseMode._SelectionMode(this); // Call after swipeBehavior and modes are set this._setSwipeClass(); this._groups = new _GroupsContainer._NoGroups(this); this._updateItemsAriaRoles(); this._updateGroupHeadersAriaRoles(); this._element.setAttribute("aria-multiselectable", this._multiSelection()); this._element.tabIndex = -1; this._tabManager.tabIndex = this._tabIndex; if (this._element.style.position !== "absolute" && this._element.style.position !== "relative") { this._element.style.position = "relative"; } this._updateItemsManager(); if (!options.layout) { this._updateLayout(new _Layouts.GridLayout()); } this._attachEvents(); this._runningInit = true; _Control.setOptions(this, options); this._runningInit = false; this._batchViewUpdates(_Constants._ViewChange.rebuild, _Constants._ScrollToPriority.medium, 0); this._writeProfilerMark("constructor,StopTM"); }, { // Public properties /// element: { get: function () { return this._element; } }, /// /// Gets or sets an object that controls the layout of the ListView. /// layout: { get: function () { return this._layoutImpl; }, set: function (layoutObject) { this._updateLayout(layoutObject); if (!this._runningInit) { this._view.reset(); this._updateItemsManager(); this._batchViewUpdates(_Constants._ViewChange.rebuild, _Constants._ScrollToPriority.medium, 0, true); } } }, /// /// Gets or sets the number of pages to load when the user scrolls beyond the /// threshold specified by the pagesToLoadThreshold property if /// the loadingBehavior property is set to incremental. /// /// pagesToLoad is deprecated. The control will not use this property. Please refer to the 'ListView loading behaviors' SDK Sample for guidance on how to implement incremental load behavior. /// /// pagesToLoad: { get: function () { return (_VirtualizeContentsView._VirtualizeContentsView._pagesToPrefetch * 2) + 1; }, set: function () { _ElementUtilities._deprecated(_ErrorMessages.pagesToLoadIsDeprecated); } }, /// /// Gets or sets the threshold (in pages) for initiating an incremental load. When the last visible item is /// within the specified number of pages from the end of the loaded portion of the list, /// and if automaticallyLoadPages is true and loadingBehavior is set to "incremental", /// the ListView initiates an incremental load. /// /// pagesToLoadThreshold is deprecated. The control will not use this property. Please refer to the 'ListView loading behaviors' SDK Sample for guidance on how to implement incremental load behavior. /// /// pagesToLoadThreshold: { get: function () { return 0; }, set: function () { _ElementUtilities._deprecated(_ErrorMessages.pagesToLoadThresholdIsDeprecated); } }, /// /// Gets or sets the data source that contains the groups for the items in the itemDataSource. /// groupDataSource: { get: function () { return this._groupDataSource; }, set: function (newValue) { this._writeProfilerMark("set_groupDataSource,info"); var that = this; function groupStatusChanged(eventObject) { if (eventObject.detail === _UI.DataSourceStatus.failure) { that.itemDataSource = null; that.groupDataSource = null; } } if (this._groupDataSource && this._groupDataSource.removeEventListener) { this._groupDataSource.removeEventListener("statuschanged", groupStatusChanged, false); } this._groupDataSource = newValue; this._groupFocusCache = (newValue && this._supportsGroupHeaderKeyboarding) ? new _GroupFocusCache._GroupFocusCache(this) : new _GroupFocusCache._UnsupportedGroupFocusCache(); if (this._groupDataSource && this._groupDataSource.addEventListener) { this._groupDataSource.addEventListener("statuschanged", groupStatusChanged, false); } this._createGroupsContainer(); if (!this._runningInit) { this._view.reset(); this._pendingLayoutReset = true; this._pendingGroupWork = true; this._batchViewUpdates(_Constants._ViewChange.rebuild, _Constants._ScrollToPriority.medium, 0, true); } else { this._updateGroupWork(); this._resetLayout(); } } }, _updateGroupWork: function () { this._pendingGroupWork = false; if (this._groupDataSource) { _ElementUtilities.addClass(this._element, _Constants._groupsClass); } else { _ElementUtilities.removeClass(this._element, _Constants._groupsClass); } this._resetLayout(); }, /// /// Gets or sets a value that indicates whether the next set of pages is automatically loaded /// when the user scrolls beyond the number of pages specified by the /// pagesToLoadThreshold property. /// /// automaticallyLoadPages is deprecated. The control will default this property to false. Please refer to the 'ListView loading behaviors' SDK Sample for guidance on how to implement incremental load behavior. /// /// automaticallyLoadPages: { get: function () { return false; }, set: function () { _ElementUtilities._deprecated(_ErrorMessages.automaticallyLoadPagesIsDeprecated); } }, /// /// Gets or sets a value that determines how many data items are loaded into the DOM. /// /// pagesToLoadThreshold is deprecated. The control will default this property to 'randomAccess'. Please refer to the 'ListView loading behaviors' SDK Sample for guidance on how to implement incremental load behavior. /// /// loadingBehavior: { get: function () { return "randomAccess"; }, set: function () { _ElementUtilities._deprecated(_ErrorMessages.loadingBehaviorIsDeprecated); } }, /// /// Gets or sets a value that specifies how many ListView items the user can select: "none", "single", or "multi". /// selectionMode: { get: function () { return this._selectionMode; }, set: function (newMode) { if (typeof newMode === "string") { if (newMode.match(/^(none|single|multi)$/)) { if (_BaseUtils.isPhone && newMode === _UI.SelectionMode.single) { return; } this._selectionMode = newMode; this._element.setAttribute("aria-multiselectable", this._multiSelection()); this._updateItemsAriaRoles(); this._setSwipeClass(); this._configureSelectionMode(); return; } } throw new _ErrorFromName("WinJS.UI.ListView.ModeIsInvalid", _ErrorMessages.modeIsInvalid); } }, /// /// Gets or sets how the ListView reacts when the user taps or clicks an item. /// The tap or click can invoke the item, select it and invoke it, or have no /// effect. /// tapBehavior: { get: function () { return this._tap; }, set: function (tap) { if (_BaseUtils.isPhone && tap === _UI.TapBehavior.directSelect) { return; } this._tap = tap; this._updateItemsAriaRoles(); this._configureSelectionMode(); } }, /// /// Gets or sets how the ListView reacts when the user taps or clicks a group header. /// groupHeaderTapBehavior: { get: function () { return this._groupHeaderTap; }, set: function (tap) { this._groupHeaderTap = tap; this._updateGroupHeadersAriaRoles(); } }, /// /// Gets or sets how the ListView reacts to the swipe interaction. /// The swipe gesture can select the swiped items or it can /// have no effect on the current selection. /// /// swipeBehavior: { get: function () { return this._swipeBehavior; }, set: function (swipeBehavior) { this._swipeBehavior = swipeBehavior; this._setSwipeClass(); } }, /// /// Gets or sets the data source that provides items for the ListView. /// itemDataSource: { get: function () { return this._itemsManager.dataSource; }, set: function (newData) { this._writeProfilerMark("set_itemDataSource,info"); this._dataSource = newData || new BindingList.List().dataSource; this._groupFocusCache.clear(); if (!this._runningInit) { this._selection._reset(); this._cancelAsyncViewWork(true); this._updateItemsManager(); this._pendingLayoutReset = true; this._batchViewUpdates(_Constants._ViewChange.rebuild, _Constants._ScrollToPriority.medium, 0, true); } } }, /// /// Gets or sets the templating function that creates the DOM elements /// for each item in the itemDataSource. Each item can contain multiple /// DOM elements, but we recommend that it have a single root element. /// itemTemplate: { get: function () { return this._itemRenderer; }, set: function (newRenderer) { this._setRenderer(newRenderer, false); if (!this._runningInit) { this._cancelAsyncViewWork(true); this._updateItemsManager(); this._pendingLayoutReset = true; this._batchViewUpdates(_Constants._ViewChange.rebuild, _Constants._ScrollToPriority.medium, 0, true); } } }, /// /// Gets or sets the function that is called when the ListView recycles the /// element representation of an item. /// /// resetItem may be altered or unavailable in future versions. Instead, mark the element as disposable using WinJS.Utilities.markDisposable. /// /// resetItem: { get: function () { return this._itemRelease; }, set: function (release) { _ElementUtilities._deprecated(_ErrorMessages.resetItemIsDeprecated); this._itemRelease = release; } }, /// /// Gets or sets the templating function that creates the DOM elements /// for each group header in the groupDataSource. Each group header /// can contain multiple elements, but it must have a single root element. /// groupHeaderTemplate: { get: function () { return this._groupHeaderRenderer; }, set: function (newRenderer) { this._setRenderer(newRenderer, true); if (!this._runningInit) { this._cancelAsyncViewWork(true); this._pendingLayoutReset = true; this._batchViewUpdates(_Constants._ViewChange.rebuild, _Constants._ScrollToPriority.medium, 0, true); } } }, /// /// Gets or sets the function that is called when the ListView recycles the DOM element representation /// of a group header. /// /// resetGroupHeader may be altered or unavailable in future versions. Instead, mark the header element as disposable using WinJS.Utilities.markDisposable. /// /// resetGroupHeader: { get: function () { return this._groupHeaderRelease; }, set: function (release) { _ElementUtilities._deprecated(_ErrorMessages.resetGroupHeaderIsDeprecated); this._groupHeaderRelease = release; } }, /// loadingState: { get: function () { return this._loadingState; } }, /// /// Gets an ISelection object that contains the currently selected items. /// selection: { get: function () { return this._selection; } }, /// /// Gets or sets the first visible item. When setting this property, the ListView scrolls so that the /// item with the specified index is at the top of the list. /// indexOfFirstVisible: { get: function () { return this._view.firstIndexDisplayed; }, set: function (itemIndex) { if (itemIndex < 0) { return; } this._writeProfilerMark("set_indexOfFirstVisible(" + itemIndex + "),info"); this._raiseViewLoading(true); var that = this; this._batchViewUpdates(_Constants._ViewChange.realize, _Constants._ScrollToPriority.high, function () { var range; return that._entityInRange({ type: _UI.ObjectType.item, index: itemIndex }).then(function (validated) { if (!validated.inRange) { return { position: 0, direction: "left" }; } else { return that._getItemOffset({ type: _UI.ObjectType.item, index: validated.index }).then(function (r) { range = r; return that._ensureFirstColumnRange(_UI.ObjectType.item); }).then(function () { range = that._correctRangeInFirstColumn(range, _UI.ObjectType.item); range = that._convertFromCanvasCoordinates(range); return that._view.waitForValidScrollPosition(range.begin); }).then(function (begin) { var direction = (begin < that._lastScrollPosition) ? "left" : "right"; var max = that._viewport[that._scrollLength] - that._getViewportLength(); begin = _ElementUtilities._clamp(begin, 0, max); return { position: begin, direction: direction }; }); } }); }, true); } }, /// /// Gets the index of the last visible item. /// indexOfLastVisible: { get: function () { return this._view.lastIndexDisplayed; } }, /// /// Gets or sets an object that indicates which item should get keyboard focus and its focus status. /// The object has these properties: /// index: the index of the item in the itemDataSource. /// key: the key of the item in the itemDataSource. /// hasFocus: when getting this property, this value is true if the item already has focus; otherwise, it's false. /// When setting this property, set this value to true if the item should get focus immediately; otherwise, set it to /// false and the item will get focus eventually. /// showFocus: true if the item displays the focus rectangle; otherwise, false. /// currentItem: { get: function () { var focused = this._selection._getFocused(); var retVal = { index: focused.index, type: focused.type, key: null, hasFocus: !!this._hasKeyboardFocus, showFocus: false }; if (focused.type === _UI.ObjectType.groupHeader) { var group = this._groups.group(focused.index); if (group) { retVal.key = group.key; retVal.showFocus = !!(group.header && _ElementUtilities.hasClass(group.header, _Constants._itemFocusClass)); } } else { var item = this._view.items.itemAt(focused.index); if (item) { var record = this._itemsManager._recordFromElement(item); retVal.key = record.item && record.item.key; retVal.showFocus = !!item.parentNode.querySelector("." + _Constants._itemFocusOutlineClass); } } return retVal; }, set: function (data) { this._hasKeyboardFocus = data.hasFocus || this._hasKeyboardFocus; var that = this; function setItemFocused(item, isInTree, entity) { var drawKeyboardFocus = !!data.showFocus && that._hasKeyboardFocus; that._unsetFocusOnItem(isInTree); that._selection._setFocused(entity, drawKeyboardFocus); if (that._hasKeyboardFocus) { that._keyboardFocusInbound = drawKeyboardFocus; that._setFocusOnItem(entity); } else { that._tabManager.childFocus = (isInTree ? item : null); } if (entity.type !== _UI.ObjectType.groupHeader) { that._updateFocusCache(entity.index); if (that._updater) { that._updater.newSelectionPivot = entity.index; that._updater.oldSelectionPivot = -1; } that._selection._pivot = entity.index; } } if (data.key && ((data.type !== _UI.ObjectType.groupHeader && this._dataSource.itemFromKey) || (data.type === _UI.ObjectType.groupHeader && this._groupDataSource && this._groupDataSource.itemFromKey))) { if (this.oldCurrentItemKeyFetch) { this.oldCurrentItemKeyFetch.cancel(); } var dataSource = (data.type === _UI.ObjectType.groupHeader ? this._groupDataSource : this._dataSource); this.oldCurrentItemKeyFetch = dataSource.itemFromKey(data.key).then(function (item) { that.oldCurrentItemKeyFetch = null; if (item) { var element = (data.type === _UI.ObjectType.groupHeader ? that._groups.group(item.index).header : that._view.items.itemAt(item.index)); setItemFocused(element, !!element, { type: data.type || _UI.ObjectType.item, index: item.index }); } }); } else { if (data.index !== undefined) { var element; if (data.type === _UI.ObjectType.groupHeader) { var group = that._groups.group(data.index); element = group && group.header; } else { element = that._view.items.itemAt(data.index); } setItemFocused(element, !!element, { type: data.type || _UI.ObjectType.item, index: data.index }); } } } }, /// /// Gets a ZoomableView. This API supports the SemanticZoom infrastructure /// and is not intended to be used directly from your code. /// zoomableView: { get: function () { if (!this._zoomableView) { this._zoomableView = new ZoomableView(this); } return this._zoomableView; } }, /// /// Gets or sets whether the ListView's items can be dragged via drag and drop. /// /// itemsDraggable: { get: function () { return this._dragSource; }, set: function (value) { if (_BaseUtils.isPhone) { return; } if (this._dragSource !== value) { this._dragSource = value; this._setSwipeClass(); } } }, /// /// Gets or sets whether the ListView's items can be reordered within itself via drag and drop. When a ListView is marked as reorderable, its items can be dragged about inside itself, but it will not require the itemdrag events it fires to be handled. /// /// itemsReorderable: { get: function () { return this._reorderable; }, set: function (value) { if (_BaseUtils.isPhone) { return; } if (this._reorderable !== value) { this._reorderable = value; this._setSwipeClass(); } } }, /// /// Gets or sets the maximum number of realized items. /// maxDeferredItemCleanup: { get: function () { return this._maxDeferredItemCleanup; }, set: function (value) { this._maxDeferredItemCleanup = Math.max(0, +value || 0); } }, // Public methods dispose: function () { /// /// /// Disposes this ListView. /// /// this._dispose(); }, elementFromIndex: function (itemIndex) { /// /// /// Returns the DOM element that represents the item at the specified index. /// /// /// The index of the item. /// /// /// The DOM element that represents the specified item. /// /// return this._view.items.itemAt(itemIndex); }, indexOfElement: function (element) { /// /// /// Returns the index of the item that the specified DOM element displays. /// /// /// The DOM element that displays the item. /// /// /// The index of item that the specified DOM element displays. /// /// return this._view.items.index(element); }, ensureVisible: function ListView_ensureVisible(value) { /// /// /// Makes the specified item visible. The ListView scrolls to the item if needed. /// /// /// The index of the ListView item or IListViewEntity to bring into view. /// /// var type = _UI.ObjectType.item, itemIndex = value; if (+value !== value) { type = value.type; itemIndex = value.index; } this._writeProfilerMark("ensureVisible(" + type + ": " + itemIndex + "),info"); if (itemIndex < 0) { return; } this._raiseViewLoading(true); var that = this; this._batchViewUpdates(_Constants._ViewChange.realize, _Constants._ScrollToPriority.high, function () { var range; return that._entityInRange({ type: type, index: itemIndex }).then(function (validated) { if (!validated.inRange) { return { position: 0, direction: "left" }; } else { return that._getItemOffset({ type: type, index: validated.index }).then(function (r) { range = r; return that._ensureFirstColumnRange(type); }).then(function () { range = that._correctRangeInFirstColumn(range, type); var viewportLength = that._getViewportLength(), left = that._viewportScrollPosition, right = left + viewportLength, newPosition = that._viewportScrollPosition, entityWidth = range.end - range.begin; range = that._convertFromCanvasCoordinates(range); var handled = false; if (type === _UI.ObjectType.groupHeader && left <= range.begin) { // EnsureVisible on a group where the entire header is fully visible does not // scroll. This prevents tabbing from an item in a very large group to align // the scroll to the header element. var header = that._groups.group(validated.index).header; if (header) { var headerEnd; var margins = _Layouts._getMargins(header); if (that._horizontalLayout) { var rtl = that._rtl(); var headerStart = (rtl ? getOffsetRight(header) - margins.right : header.offsetLeft - margins.left); headerEnd = headerStart + header.offsetWidth + (rtl ? margins.left : margins.right); } else { headerEnd = header.offsetTop + header.offsetHeight + margins.top; } handled = headerEnd <= right; } } if (!handled) { if (entityWidth >= right - left) { // This item is larger than the viewport so we will just set // the scroll position to the beginning of the item. newPosition = range.begin; } else { if (range.begin < left) { newPosition = range.begin; } else if (range.end > right) { newPosition = range.end - viewportLength; } } } var direction = (newPosition < that._lastScrollPosition) ? "left" : "right"; var max = that._viewport[that._scrollLength] - viewportLength; newPosition = _ElementUtilities._clamp(newPosition, 0, max); return { position: newPosition, direction: direction }; }); } }); }, true); }, loadMorePages: function ListView_loadMorePages() { /// /// /// Loads the next set of pages if the ListView object's loadingBehavior is set to incremental. /// /// loadMorePages is deprecated. Invoking this function will not have any effect. Please refer to the 'ListView loading behaviors' SDK Sample for guidance on how to implement incremental load behavior. /// /// /// _ElementUtilities._deprecated(_ErrorMessages.loadMorePagesIsDeprecated); }, recalculateItemPosition: function ListView_recalculateItemPosition() { /// /// /// Repositions all the visible items in the ListView to adjust for items whose sizes have changed. Use this function or forceLayout when making the ListView visible again after you set its style.display property to "none" or after style changes have been made that affect the size or position of the ListView or its items. Unlike forceLayout, this method doesn’t recreate items and it doesn’t display entrance animation. /// /// this._writeProfilerMark("recalculateItemPosition,info"); this._forceLayoutImpl(_Constants._ViewChange.relayout); }, forceLayout: function ListView_forceLayout() { /// /// /// Forces the ListView to update its layout. Use this function or relcaculateItemPosition when making the ListView visible again after you set its style.display property to "none” or after style changes have been made that affect the size or position of the ListView or its items. /// after you set its style.display property to "none". /// /// this._writeProfilerMark("forceLayout,info"); this._forceLayoutImpl(_Constants._ViewChange.remeasure); }, _entityInRange: function ListView_entityInRange(entity) { if (entity.type === _UI.ObjectType.item) { return this._itemsCount().then(function (itemsCount) { var index = _ElementUtilities._clamp(entity.index, 0, itemsCount - 1); return { inRange: index >= 0 && index < itemsCount, index: index }; }); } else { var index = _ElementUtilities._clamp(entity.index, 0, this._groups.length() - 1); return Promise.wrap({ inRange: index >= 0 && index < this._groups.length(), index: index }); } }, _forceLayoutImpl: function ListView_forceLayoutImpl(viewChange) { var that = this; this._versionManager.unlocked.then(function () { that._writeProfilerMark("_forceLayoutImpl viewChange(" + viewChange + "),info"); that._cancelAsyncViewWork(); that._pendingLayoutReset = true; that._resizeViewport(); that._batchViewUpdates(viewChange, _Constants._ScrollToPriority.low, function () { return { position: that._lastScrollPosition, direction: "right" }; }, true, true); }); }, _configureSelectionMode: function () { if (_BaseUtils.isPhone) { if (this.tapBehavior === _UI.TapBehavior.toggleSelect && this.selectionMode === _UI.SelectionMode.multi) { _ElementUtilities.addClass(this._canvas, _Constants._selectionModeClass); } else { _ElementUtilities.removeClass(this._canvas, _Constants._selectionModeClass); } } }, _lastScrollPosition: { get: function () { return this._lastScrollPositionValue; }, set: function (position) { if (position === 0) { this._lastDirection = "right"; this._direction = "right"; this._lastScrollPositionValue = 0; } else { var currentDirection = position < this._lastScrollPositionValue ? "left" : "right"; this._direction = this._scrollDirection(position); this._lastDirection = currentDirection; this._lastScrollPositionValue = position; } } }, _supportsGroupHeaderKeyboarding: { get: function () { return this._groupDataSource; } }, _viewportScrollPosition: { get: function () { this._currentScrollPosition = _ElementUtilities.getScrollPosition(this._viewport)[this._scrollProperty]; return this._currentScrollPosition; }, set: function (value) { var newScrollPos = {}; newScrollPos[this._scrollProperty] = value; _ElementUtilities.setScrollPosition(this._viewport, newScrollPos); this._currentScrollPosition = value; } }, _canvasStart: { get: function () { return this._canvasStartValue || 0; }, set: function (value) { var transformX = this._horizontal() ? (this._rtl() ? -value : value) : 0, transformY = this._horizontal() ? 0 : value; if (value !== 0) { this._canvas.style[transformNames.scriptName] = "translate( " + transformX + "px, " + transformY + "px)"; } else { this._canvas.style[transformNames.scriptName] = ""; } this._canvasStartValue = value; } }, /// /// Gets or sets the position of the ListView's scrollbar. /// scrollPosition: { get: function () { return this._viewportScrollPosition; }, set: function (newPosition) { var that = this; this._batchViewUpdates(_Constants._ViewChange.realize, _Constants._ScrollToPriority.high, function () { return that._view.waitForValidScrollPosition(newPosition).then(function () { var max = that._viewport[that._scrollLength] - that._getViewportLength(); newPosition = _ElementUtilities._clamp(newPosition, 0, max); var direction = (newPosition < that._lastScrollPosition) ? "left" : "right"; return { position: newPosition, direction: direction }; }); }, true); } }, _setRenderer: function ListView_setRenderer(newRenderer, isGroupHeaderRenderer) { var renderer; if (!newRenderer) { if (_BaseUtils.validation) { throw new _ErrorFromName("WinJS.UI.ListView.invalidTemplate", _ErrorMessages.invalidTemplate); } renderer = _ItemsManager.trivialHtmlRenderer; } else if (typeof newRenderer === "function") { renderer = newRenderer; } else if (typeof newRenderer === "object") { if (_BaseUtils.validation && !newRenderer.renderItem) { throw new _ErrorFromName("WinJS.UI.ListView.invalidTemplate", _ErrorMessages.invalidTemplate); } renderer = newRenderer.renderItem; } if (renderer) { if (isGroupHeaderRenderer) { this._groupHeaderRenderer = renderer; } else { this._itemRenderer = renderer; } } }, _renderWithoutReuse: function ListView_renderWithoutReuse(itemPromise, oldElement) { if (oldElement) { _Dispose._disposeElement(oldElement); } var templateResult = this._itemRenderer(itemPromise); if (templateResult.then) { return templateResult.then(function (element) { element.tabIndex = 0; return element; }); } else { var element = templateResult.element || templateResult; element.tabIndex = 0; return templateResult; } }, _isInsertedItem: function ListView_isInsertedItem(itemPromise) { return !!this._insertedItems[itemPromise.handle]; }, _clearInsertedItems: function ListView_clearInsertedItems() { var keys = Object.keys(this._insertedItems); for (var i = 0, len = keys.length; i < len; i++) { this._insertedItems[keys[i]].release(); } this._insertedItems = {}; this._modifiedElements = []; this._countDifference = 0; }, // Private methods _cancelAsyncViewWork: function (stopTreeCreation) { this._view.stopWork(stopTreeCreation); }, _updateView: function ListView_updateView() { if (this._isZombie()) { return; } var that = this; function resetCache() { that._itemsBlockExtent = -1; that._firstItemRange = null; that._firstHeaderRange = null; that._itemMargins = null; that._headerMargins = null; that._canvasMargins = null; that._cachedRTL = null; // Retrieve the values before DOM modifications occur that._rtl(); } var viewChange = this._viewChange; this._viewChange = _Constants._ViewChange.realize; function functorWrapper() { that._scrollToPriority = _Constants._ScrollToPriority.uninitialized; var setScrollbarPosition = that._setScrollbarPosition; that._setScrollbarPosition = false; var position = typeof that._scrollToFunctor === "number" ? { position: that._scrollToFunctor } : that._scrollToFunctor(); return Promise.as(position).then( function (scroll) { scroll = scroll || {}; if (setScrollbarPosition && +scroll.position === scroll.position) { that._lastScrollPosition = scroll.position; that._viewportScrollPosition = scroll.position; } return scroll; }, function (error) { that._setScrollbarPosition |= setScrollbarPosition; return Promise.wrapError(error); } ); } if (viewChange === _Constants._ViewChange.rebuild) { if (this._pendingGroupWork) { this._updateGroupWork(); } if (this._pendingLayoutReset) { this._resetLayout(); } resetCache(); if (!this._firstTimeDisplayed) { this._view.reset(); } this._view.reload(functorWrapper, true); this._setFocusOnItem(this._selection._getFocused()); } else if (viewChange === _Constants._ViewChange.remeasure) { this._view.resetItems(true); this._resetLayout(); resetCache(); this._view.refresh(functorWrapper); this._setFocusOnItem(this._selection._getFocused()); } else if (viewChange === _Constants._ViewChange.relayout) { if (this._pendingLayoutReset) { this._resetLayout(); resetCache(); } this._view.refresh(functorWrapper); } else { this._view.onScroll(functorWrapper); } }, _batchViewUpdates: function ListView_batchViewUpdates(viewChange, scrollToPriority, positionFunctor, setScrollbarPosition, skipFadeout) { this._viewChange = Math.min(this._viewChange, viewChange); if (this._scrollToFunctor === null || scrollToPriority >= this._scrollToPriority) { this._scrollToPriority = scrollToPriority; this._scrollToFunctor = positionFunctor; } this._setScrollbarPosition |= !!setScrollbarPosition; if (!this._batchingViewUpdates) { this._raiseViewLoading(); var that = this; this._batchingViewUpdatesSignal = new _Signal(); this._batchingViewUpdates = Promise.any([this._batchingViewUpdatesSignal.promise, Scheduler.schedulePromiseHigh(null, "WinJS.UI.ListView._updateView")]).then(function () { if (that._isZombie()) { return; } // If we're displaying for the first time, or there were no items visible in the view, we can skip the fade out animation // and go straight to the refresh. _view.items._itemData.length is the most trustworthy way to find how many items are visible. if (that._viewChange === _Constants._ViewChange.rebuild && !that._firstTimeDisplayed && Object.keys(that._view.items._itemData).length !== 0 && !skipFadeout) { return that._fadeOutViewport(); } }).then( function () { that._batchingViewUpdates = null; that._batchingViewUpdatesSignal = null; that._updateView(); that._firstTimeDisplayed = false; }, function () { that._batchingViewUpdates = null; that._batchingViewUpdatesSignal = null; } ); } return this._batchingViewUpdatesSignal; }, _resetCanvas: function () { if (this._disposed) { return; } // Layouts do not currently support saving the scroll position when forceLayout() is called. // Layouts need to recreate the canvas because the tabManager is there and you don't want to // construct 2 instances of WinJS.UI.TabContainer for the same element. var newCanvas = _Global.document.createElement('div'); newCanvas.className = this._canvas.className; this._viewport.replaceChild(newCanvas, this._canvas); this._canvas = newCanvas; this._groupsToRemove = {}; // We reset the itemCanvas on _resetCanvas in case a ListView client uses two separate custom layouts, and each layout // changes different styles on the itemCanvas without resetting it. this._canvas.appendChild(this._canvasProxy); }, _setupInternalTree: function ListView_setupInternalTree() { _ElementUtilities.addClass(this._element, _Constants._listViewClass); _ElementUtilities[this._rtl() ? "addClass" : "removeClass"](this._element, _Constants._rtlListViewClass); this._element.innerHTML = '
' + '
' + // Create a proxy element inside the canvas so that during an MSPointerDown event we can call // msSetPointerCapture on it. This allows hover to not be passed to it which saves a large invalidation. '
' + '
' + '
' + '
' + // The keyboard event helper is a dummy node that allows us to keep getting keyboard events when a virtualized element // gets discarded. It has to be positioned in the center of the viewport, though, otherwise calling .focus() on it // can move our viewport around when we don't want it moved. // The keyboard event helper element will be skipped in the tab order if it doesn't have width+height set on it. ''; this._viewport = this._element.firstElementChild; this._canvas = this._viewport.firstElementChild; this._canvasProxy = this._canvas.firstElementChild; // The deleteWrapper div is used to maintain the scroll width (after delete(s)) until the animation is done this._deleteWrapper = this._canvas.nextElementSibling; this._keyboardEventsHelper = this._viewport.nextElementSibling; this._tabIndex = _ElementUtilities.getTabIndex(this._element); if (this._tabIndex < 0) { this._tabIndex = 0; } this._tabManager = new _TabContainer.TabContainer(this._viewport); this._tabManager.tabIndex = this._tabIndex; this._progressBar = _Global.document.createElement("progress"); _ElementUtilities.addClass(this._progressBar, _Constants._progressClass); this._progressBar.style.position = "absolute"; this._progressBar.max = 100; }, _unsetFocusOnItem: function ListView_unsetFocusOnItem(newFocusExists) { if (this._tabManager.childFocus) { this._clearFocusRectangle(this._tabManager.childFocus); } if (this._isZombie()) { return; } if (!newFocusExists) { // _setFocusOnItem may run asynchronously so prepare the keyboardEventsHelper // to receive focus. if (this._tabManager.childFocus) { this._tabManager.childFocus = null; } this._keyboardEventsHelper._shouldHaveFocus = false; // If the viewport has focus, leave it there. This will prevent focus from jumping // from the viewport to the keyboardEventsHelper when scrolling with Narrator Touch. if (_Global.document.activeElement !== this._viewport && this._hasKeyboardFocus) { this._keyboardEventsHelper._shouldHaveFocus = true; _ElementUtilities._setActive(this._keyboardEventsHelper); } } this._itemFocused = false; }, _setFocusOnItem: function ListView_setFocusOnItem(entity) { this._writeProfilerMark("_setFocusOnItem,info"); if (this._focusRequest) { this._focusRequest.cancel(); } if (this._isZombie()) { return; } var that = this; var setFocusOnItemImpl = function (item) { if (that._isZombie()) { return; } if (that._tabManager.childFocus !== item) { that._tabManager.childFocus = item; } that._focusRequest = null; if (that._hasKeyboardFocus && !that._itemFocused) { if (that._selection._keyboardFocused()) { that._drawFocusRectangle(item); } // The requestItem promise just completed so _cachedCount will // be initialized. that._view.updateAriaForAnnouncement(item, (entity.type === _UI.ObjectType.groupHeader ? that._groups.length() : that._cachedCount)); // Some consumers of ListView listen for item invoked events and hide the listview when an item is clicked. // Since keyboard interactions rely on async operations, sometimes an invoke event can be received before we get // to WinJS.Utilities._setActive(item), and the listview will be made invisible. If that happens and we call item.setActive(), an exception // is raised for trying to focus on an invisible item. Checking visibility is non-trivial, so it's best // just to catch the exception and ignore it. that._itemFocused = true; _ElementUtilities._setActive(item); } }; if (entity.type !== _UI.ObjectType.groupHeader) { this._focusRequest = this._view.items.requestItem(entity.index); } else { this._focusRequest = this._groups.requestHeader(entity.index); } this._focusRequest.then(setFocusOnItemImpl); }, _attachEvents: function ListView_attachEvents() { var that = this; function listViewHandler(eventName, caseSensitive, capture) { return { name: (caseSensitive ? eventName : eventName.toLowerCase()), handler: function (eventObject) { that["_on" + eventName](eventObject); }, capture: capture }; } function modeHandler(eventName, caseSensitive, capture) { return { capture: capture, name: (caseSensitive ? eventName : eventName.toLowerCase()), handler: function (eventObject) { var currentMode = that._mode, name = "on" + eventName; if (!that._disposed && currentMode[name]) { currentMode[name](eventObject); } } }; } function observerHandler(handlerName, attributesFilter) { return { handler: function (listOfChanges) { that["_on" + handlerName](listOfChanges); }, filter: attributesFilter }; } // Observers for specific element attribute changes var elementObservers = [ observerHandler("PropertyChange", ["dir", "style", "tabindex"]) ]; this._cachedStyleDir = this._element.style.direction; elementObservers.forEach(function (elementObserver) { new _ElementUtilities._MutationObserver(elementObserver.handler).observe(that._element, { attributes: true, attributeFilter: elementObserver.filter }); }); // KeyDown handler needs to be added explicitly via addEventListener instead of using attachEvent. // If it's not added via addEventListener, the eventObject given to us on event does not have the functions stopPropagation() and preventDefault(). var events = [ modeHandler("PointerDown"), modeHandler("click", false), modeHandler("PointerUp"), modeHandler("LostPointerCapture"), modeHandler("MSHoldVisual", true), modeHandler("PointerCancel", true), modeHandler("DragStart"), modeHandler("DragOver"), modeHandler("DragEnter"), modeHandler("DragLeave"), modeHandler("Drop"), modeHandler("ContextMenu"), modeHandler("MSManipulationStateChanged", true, true) ]; events.forEach(function (eventHandler) { _ElementUtilities._addEventListener(that._viewport, eventHandler.name, eventHandler.handler, !!eventHandler.capture); }); var elementEvents = [ listViewHandler("FocusIn", false, false), listViewHandler("FocusOut", false, false), modeHandler("KeyDown"), modeHandler("KeyUp"), listViewHandler("MSElementResize", false, false) ]; elementEvents.forEach(function (eventHandler) { _ElementUtilities._addEventListener(that._element, eventHandler.name, eventHandler.handler, !!eventHandler.capture); }); this._onMSElementResizeBound = this._onMSElementResize.bind(this); _ElementUtilities._resizeNotifier.subscribe(this._element, this._onMSElementResizeBound); var viewportEvents = [ listViewHandler("MSManipulationStateChanged", true), listViewHandler("Scroll") ]; viewportEvents.forEach(function (viewportEvent) { that._viewport.addEventListener(viewportEvent.name, viewportEvent.handler, false); }); this._viewport.addEventListener("onTabEnter", this._onTabEnter.bind(this)); this._viewport.addEventListener("onTabExit", this._onTabExit.bind(this)); this._viewport.addEventListener("onTabEntered", function (e) { that._mode.onTabEntered(e); }); this._viewport.addEventListener("onTabExiting", function (e) { that._mode.onTabExiting(e); }); }, _updateItemsManager: function ListView_updateItemsManager() { var that = this, notificationHandler = { // Following methods are used by ItemsManager beginNotifications: function ListView_beginNotifications() { }, changed: function ListView_changed(newItem, oldItem) { if (that._ifZombieDispose()) { return; } that._createUpdater(); var elementInfo = that._updater.elements[uniqueID(oldItem)]; if (elementInfo) { var selected = that.selection._isIncluded(elementInfo.index); if (selected) { that._updater.updateDrag = true; } if (oldItem !== newItem) { if (that._tabManager.childFocus === oldItem || that._updater.newFocusedItem === oldItem) { that._updater.newFocusedItem = newItem; that._tabManager.childFocus = null; } if (elementInfo.itemBox) { _ElementUtilities.addClass(newItem, _Constants._itemClass); that._setupAriaSelectionObserver(newItem); var next = oldItem.nextElementSibling; elementInfo.itemBox.removeChild(oldItem); elementInfo.itemBox.insertBefore(newItem, next); } that._setAriaSelected(newItem, selected); that._view.items.setItemAt(elementInfo.newIndex, { element: newItem, itemBox: elementInfo.itemBox, container: elementInfo.container, itemsManagerRecord: elementInfo.itemsManagerRecord }); delete that._updater.elements[uniqueID(oldItem)]; _Dispose._disposeElement(oldItem); that._updater.elements[uniqueID(newItem)] = { item: newItem, container: elementInfo.container, itemBox: elementInfo.itemBox, index: elementInfo.index, newIndex: elementInfo.newIndex, itemsManagerRecord: elementInfo.itemsManagerRecord }; } else if (elementInfo.itemBox && elementInfo.container) { _ItemEventsHandler._ItemEventsHandler.renderSelection(elementInfo.itemBox, newItem, selected, true); _ElementUtilities[selected ? "addClass" : "removeClass"](elementInfo.container, _Constants._selectedClass); } that._updater.changed = true; } for (var i = 0, len = that._notificationHandlers.length; i < len; i++) { that._notificationHandlers[i].changed(newItem, oldItem); } that._writeProfilerMark("changed,info"); }, removed: function ListView_removed(item, mirage, handle) { if (that._ifZombieDispose()) { return; } that._createUpdater(); function removeFromSelection(index) { that._updater.updateDrag = true; if (that._currentMode()._dragging && that._currentMode()._draggingUnselectedItem && that._currentMode()._dragInfo._isIncluded(index)) { that._updater.newDragInfo = new _SelectionManager._Selection(that, []); } var firstRange = that._updater.selectionFirst[index], lastRange = that._updater.selectionLast[index], range = firstRange || lastRange; if (range) { delete that._updater.selectionFirst[range.oldFirstIndex]; delete that._updater.selectionLast[range.oldLastIndex]; that._updater.selectionChanged = true; } } var insertedItem = that._insertedItems[handle]; if (insertedItem) { delete that._insertedItems[handle]; } var index; if (item) { var elementInfo = that._updater.elements[uniqueID(item)], itemObject = that._itemsManager.itemObject(item); if (itemObject) { that._groupFocusCache.deleteItem(itemObject.key); } if (elementInfo) { index = elementInfo.index; // We track removed elements for animation purposes (layout // component consumes this). // if (elementInfo.itemBox) { that._updater.removed.push({ index: index, itemBox: elementInfo.itemBox }); } that._updater.deletesCount++; // The view can't access the data from the itemsManager // anymore, so we need to flag the itemData that it // has been removed. // var itemData = that._view.items.itemDataAt(index); itemData.removed = true; delete that._updater.elements[uniqueID(item)]; } else { index = itemObject && itemObject.index; } if (that._updater.oldFocus.type !== _UI.ObjectType.groupHeader && that._updater.oldFocus.index === index) { that._updater.newFocus.index = index; // If index is too high, it'll be fixed in endNotifications that._updater.focusedItemRemoved = true; } removeFromSelection(index); } else { index = that._updater.selectionHandles[handle]; if (index === +index) { removeFromSelection(index); } } that._writeProfilerMark("removed(" + index + "),info"); that._updater.changed = true; }, updateAffectedRange: function ListView_updateAffectedRange(newerRange) { that._itemsCount().then(function (count) { // When we receive insertion notifications before all of the containers have // been created and the affected range is beyond the container range, the // affected range indices will not correspond to the indices of the containers // created by updateContainers. In this case, start the affected range at the end // of the containers so that the affected range includes any containers that get // appended due to this batch of notifications. var containerCount = that._view.containers ? that._view.containers.length : 0; newerRange.start = Math.min(newerRange.start, containerCount); that._affectedRange.add(newerRange, count); }); that._createUpdater(); that._updater.changed = true; }, indexChanged: function ListView_indexChanged(item, newIndex, oldIndex) { // We should receive at most one indexChanged notification per oldIndex // per notification cycle. if (that._ifZombieDispose()) { return; } that._createUpdater(); if (item) { var itemObject = that._itemsManager.itemObject(item); if (itemObject) { that._groupFocusCache.updateItemIndex(itemObject.key, newIndex); } var elementInfo = that._updater.elements[uniqueID(item)]; if (elementInfo) { elementInfo.newIndex = newIndex; that._updater.changed = true; } that._updater.itemsMoved = true; } if (that._currentMode()._dragging && that._currentMode()._draggingUnselectedItem && that._currentMode()._dragInfo._isIncluded(oldIndex)) { that._updater.newDragInfo = new _SelectionManager._Selection(that, [{ firstIndex: newIndex, lastIndex: newIndex }]); that._updater.updateDrag = true; } if (that._updater.oldFocus.type !== _UI.ObjectType.groupHeader && that._updater.oldFocus.index === oldIndex) { that._updater.newFocus.index = newIndex; that._updater.changed = true; } if (that._updater.oldSelectionPivot === oldIndex) { that._updater.newSelectionPivot = newIndex; that._updater.changed = true; } var range = that._updater.selectionFirst[oldIndex]; if (range) { range.newFirstIndex = newIndex; that._updater.changed = true; that._updater.updateDrag = true; } range = that._updater.selectionLast[oldIndex]; if (range) { range.newLastIndex = newIndex; that._updater.changed = true; that._updater.updateDrag = true; } }, endNotifications: function ListView_endNotifications() { that._update(); }, inserted: function ListView_inserted(itemPromise) { if (that._ifZombieDispose()) { return; } that._writeProfilerMark("inserted,info"); that._createUpdater(); that._updater.changed = true; itemPromise.retain(); that._updater.insertsCount++; that._insertedItems[itemPromise.handle] = itemPromise; }, moved: function ListView_moved(item, previous, next, itemPromise) { if (that._ifZombieDispose()) { return; } that._createUpdater(); that._updater.movesCount++; if (item) { that._updater.itemsMoved = true; var elementInfo = that._updater.elements[uniqueID(item)]; if (elementInfo) { elementInfo.moved = true; } } var index = that._updater.selectionHandles[itemPromise.handle]; if (index === +index) { that._updater.updateDrag = true; var firstRange = that._updater.selectionFirst[index], lastRange = that._updater.selectionLast[index], range = firstRange || lastRange; if (range && range.oldFirstIndex !== range.oldLastIndex) { delete that._updater.selectionFirst[range.oldFirstIndex]; delete that._updater.selectionLast[range.oldLastIndex]; that._updater.selectionChanged = true; that._updater.changed = true; } } that._writeProfilerMark("moved(" + index + "),info"); }, countChanged: function ListView_countChanged(newCount, oldCount) { if (that._ifZombieDispose()) { return; } that._writeProfilerMark("countChanged(" + newCount + "),info"); that._cachedCount = newCount; that._createUpdater(); if ((that._view.lastIndexDisplayed + 1) === oldCount) { that._updater.changed = true; } that._updater.countDifference += newCount - oldCount; }, reload: function ListView_reload() { if (that._ifZombieDispose()) { return; } that._writeProfilerMark("reload,info"); that._processReload(); } }; function statusChanged(eventObject) { if (eventObject.detail === _UI.DataSourceStatus.failure) { that.itemDataSource = null; that.groupDataSource = null; } } if (this._versionManager) { this._versionManager._dispose(); } this._versionManager = new _VersionManager._VersionManager(); this._updater = null; var ranges = this._selection.getRanges(); this._selection._selected.clear(); if (this._itemsManager) { if (this._itemsManager.dataSource && this._itemsManager.dataSource.removeEventListener) { this._itemsManager.dataSource.removeEventListener("statuschanged", statusChanged, false); } this._clearInsertedItems(); this._itemsManager.release(); } if (this._itemsCountPromise) { this._itemsCountPromise.cancel(); this._itemsCountPromise = null; } this._cachedCount = _Constants._UNINITIALIZED; this._itemsManager = _ItemsManager._createItemsManager( this._dataSource, this._renderWithoutReuse.bind(this), notificationHandler, { ownerElement: this._element, versionManager: this._versionManager, indexInView: function (index) { return (index >= that.indexOfFirstVisible && index <= that.indexOfLastVisible); }, viewCallsReady: true, profilerId: this._id }); if (this._dataSource.addEventListener) { this._dataSource.addEventListener("statuschanged", statusChanged, false); } this._selection._selected.set(ranges); }, _processReload: function () { this._affectedRange.addAll(); // Inform scroll view that a realization pass is coming so that it doesn't restart the // realization pass itself. this._cancelAsyncViewWork(true); if (this._currentMode()._dragging) { this._currentMode()._clearDragProperties(); } this._groupFocusCache.clear(); this._selection._reset(); this._updateItemsManager(); this._pendingLayoutReset = true; this._batchViewUpdates(_Constants._ViewChange.rebuild, _Constants._ScrollToPriority.low, this.scrollPosition); }, _createUpdater: function ListView_createUpdater() { if (!this._updater) { if (this.itemDataSource._isVirtualizedDataSource) { // VDS doesn't support the _updateAffectedRange notification so assume // that everything needs to be relaid out. this._affectedRange.addAll(); } this._versionManager.beginUpdating(); // Inform scroll view that a realization pass is coming so that it doesn't restart the // realization pass itself. this._cancelAsyncViewWork(); var updater = { changed: false, elements: {}, selectionFirst: {}, selectionLast: {}, selectionHandles: {}, oldSelectionPivot: { type: _UI.ObjectType.item, index: _Constants._INVALID_INDEX }, newSelectionPivot: { type: _UI.ObjectType.item, index: _Constants._INVALID_INDEX }, removed: [], selectionChanged: false, oldFocus: { type: _UI.ObjectType.item, index: _Constants._INVALID_INDEX }, newFocus: { type: _UI.ObjectType.item, index: _Constants._INVALID_INDEX }, hadKeyboardFocus: this._hasKeyboardFocus, itemsMoved: false, lastVisible: this.indexOfLastVisible, updateDrag: false, movesCount: 0, insertsCount: 0, deletesCount: 0, countDifference: 0 }; this._view.items.each(function (index, item, itemData) { updater.elements[uniqueID(item)] = { item: item, container: itemData.container, itemBox: itemData.itemBox, index: index, newIndex: index, itemsManagerRecord: itemData.itemsManagerRecord, detached: itemData.detached }; }); var selection = this._selection._selected._ranges; for (var i = 0, len = selection.length; i < len; i++) { var range = selection[i]; var newRange = { newFirstIndex: selection[i].firstIndex, oldFirstIndex: selection[i].firstIndex, newLastIndex: selection[i].lastIndex, oldLastIndex: selection[i].lastIndex }; updater.selectionFirst[newRange.oldFirstIndex] = newRange; updater.selectionLast[newRange.oldLastIndex] = newRange; updater.selectionHandles[range.firstPromise.handle] = newRange.oldFirstIndex; updater.selectionHandles[range.lastPromise.handle] = newRange.oldLastIndex; } updater.oldSelectionPivot = this._selection._pivot; updater.newSelectionPivot = updater.oldSelectionPivot; updater.oldFocus = this._selection._getFocused(); updater.newFocus = this._selection._getFocused(); this._updater = updater; } }, _synchronize: function ListView_synchronize() { var updater = this._updater; this._updater = null; this._groupsChanged = false; this._countDifference = this._countDifference || 0; if (updater && updater.changed) { if (updater.itemsMoved) { this._layout.itemsMoved && this._layout.itemsMoved(); } if (updater.removed.length) { this._layout.itemsRemoved && this._layout.itemsRemoved(updater.removed.map(function (node) { return node.itemBox; })); } if (updater.itemsMoved || updater.removed.length || Object.keys(this._insertedItems).length) { this._layout.setupAnimations && this._layout.setupAnimations(); } if (this._currentMode().onDataChanged) { this._currentMode().onDataChanged(); } var newSelection = []; for (var i in updater.selectionFirst) { if (updater.selectionFirst.hasOwnProperty(i)) { var range = updater.selectionFirst[i]; updater.selectionChanged = updater.selectionChanged || ((range.newLastIndex - range.newFirstIndex) !== (range.oldLastIndex - range.oldFirstIndex)); if (range.newFirstIndex <= range.newLastIndex) { newSelection.push({ firstIndex: range.newFirstIndex, lastIndex: range.newLastIndex }); } } } if (updater.selectionChanged) { var newSelectionItems = new _SelectionManager._Selection(this, newSelection); // We do not allow listeners to cancel the selection // change because the cancellation would also have to // prevent the deletion. this._selection._fireSelectionChanging(newSelectionItems); this._selection._selected.set(newSelection); this._selection._fireSelectionChanged(); newSelectionItems.clear(); } else { this._selection._selected.set(newSelection); } this._selection._updateCount(this._cachedCount); updater.newSelectionPivot = Math.min(this._cachedCount - 1, updater.newSelectionPivot); this._selection._pivot = (updater.newSelectionPivot >= 0 ? updater.newSelectionPivot : _Constants._INVALID_INDEX); if (updater.newFocus.type !== _UI.ObjectType.groupHeader) { updater.newFocus.index = Math.max(0, Math.min(this._cachedCount - 1, updater.newFocus.index)); } this._selection._setFocused(updater.newFocus, this._selection._keyboardFocused()); // If there are 2 edits before layoutAnimations runs we need to merge the 2 groups of modified elements. // For example: // If you start with A, B, C and add item Z to the beginning you will have // [ -1 -> 0, 0 -> 1, 1 -> 2, 2 -> 3] // However before layout is called an insert of Y to the beginning also happens you should get // [ -1 -> 0, -1 -> 1, 0 -> 2, 1 -> 3, 2 -> 4] var previousModifiedElements = this._modifiedElements || []; var previousModifiedElementsHash = {}; this._modifiedElements = []; this._countDifference += updater.countDifference; for (i = 0; i < previousModifiedElements.length; i++) { var modifiedElement = previousModifiedElements[i]; if (modifiedElement.newIndex === -1) { this._modifiedElements.push(modifiedElement); } else { previousModifiedElementsHash[modifiedElement.newIndex] = modifiedElement; } } for (i = 0; i < updater.removed.length; i++) { var removed = updater.removed[i]; var modifiedElement = previousModifiedElementsHash[removed.index]; if (modifiedElement) { delete previousModifiedElementsHash[removed.index]; } else { modifiedElement = { oldIndex: removed.index }; } modifiedElement.newIndex = -1; if (!modifiedElement._removalHandled) { modifiedElement._itemBox = removed.itemBox; } this._modifiedElements.push(modifiedElement); } var insertedKeys = Object.keys(this._insertedItems); for (i = 0; i < insertedKeys.length; i++) { this._modifiedElements.push({ oldIndex: -1, newIndex: this._insertedItems[insertedKeys[i]].index }); } this._writeProfilerMark("_synchronize:update_modifiedElements,StartTM"); var newItems = {}; for (i in updater.elements) { if (updater.elements.hasOwnProperty(i)) { var elementInfo = updater.elements[i]; newItems[elementInfo.newIndex] = { element: elementInfo.item, container: elementInfo.container, itemBox: elementInfo.itemBox, itemsManagerRecord: elementInfo.itemsManagerRecord, detached: elementInfo.detached }; var modifiedElement = previousModifiedElementsHash[elementInfo.index]; if (modifiedElement) { delete previousModifiedElementsHash[elementInfo.index]; modifiedElement.newIndex = elementInfo.newIndex; } else { modifiedElement = { oldIndex: elementInfo.index, newIndex: elementInfo.newIndex }; } modifiedElement.moved = elementInfo.moved; this._modifiedElements.push(modifiedElement); } } this._writeProfilerMark("_synchronize:update_modifiedElements,StopTM"); var previousIndices = Object.keys(previousModifiedElementsHash); for (i = 0; i < previousIndices.length; i++) { var key = previousIndices[i]; var modifiedElement = previousModifiedElementsHash[key]; if (modifiedElement.oldIndex !== -1) { this._modifiedElements.push(modifiedElement); } } this._view.items._itemData = newItems; if (updater.updateDrag && this._currentMode()._dragging) { if (!this._currentMode()._draggingUnselectedItem) { this._currentMode()._dragInfo = this._selection; } else if (updater.newDragInfo) { this._currentMode()._dragInfo = updater.newDragInfo; } this._currentMode().fireDragUpdateEvent(); } // If the focused item is removed, or the item we're trying to focus on has been moved before we can focus on it, // we need to update our focus request to get the item from the appropriate index. if (updater.focusedItemRemoved || (this._focusRequest && (updater.oldFocus.index !== updater.newFocus.index) || (updater.oldFocus.type !== updater.newFocus.type))) { this._itemFocused = false; this._setFocusOnItem(this._selection._getFocused()); } else if (updater.newFocusedItem) { // We need to restore the value of _hasKeyboardFocus because a changed item // gets removed from the DOM at the time of the notification. If the item // had focus at that time, then our value of _hasKeyboardFocus will have changed. this._hasKeyboardFocus = updater.hadKeyboardFocus; this._itemFocused = false; this._setFocusOnItem(this._selection._getFocused()); } var that = this; return this._groups.synchronizeGroups().then(function () { if (updater.newFocus.type === _UI.ObjectType.groupHeader) { updater.newFocus.index = Math.min(that._groups.length() - 1, updater.newFocus.index); if (updater.newFocus.index < 0) { // An empty listview has currentFocus = item 0 updater.newFocus = { type: _UI.ObjectType.item, index: 0 }; } that._selection._setFocused(updater.newFocus, that._selection._keyboardFocused()); } that._versionManager.endUpdating(); if (updater.deletesCount > 0) { that._updateDeleteWrapperSize(); } return that._view.updateTree(that._cachedCount, that._countDifference, that._modifiedElements); }).then(function () { return that._lastScrollPosition; }); } else { this._countDifference += updater ? updater.countDifference : 0; var that = this; return this._groups.synchronizeGroups().then(function ListView_synchronizeGroups_success_groupsChanged() { updater && that._versionManager.endUpdating(); return that._view.updateTree(that._cachedCount, that._countDifference, that._modifiedElements); }).then(function () { return that.scrollPosition; }); } }, _updateDeleteWrapperSize: function ListView_updateDeleteWrapperSize(clear) { var sizeProperty = this._horizontal() ? "width" : "height"; this._deleteWrapper.style["min-" + sizeProperty] = (clear ? 0 : this.scrollPosition + this._getViewportSize()[sizeProperty]) + "px"; }, _verifyRealizationNeededForChange: function ListView_skipRealization() { // If the updater indicates that only deletes occurred, and we have not lost a viewport full of items, // we skip realizing all the items and appending new ones until other action causes a full realize (e.g. scrolling). // var skipRealization = false; var totalInViewport = (this._view.lastIndexDisplayed || 0) - (this._view.firstIndexDisplayed || 0); var deletesOnly = this._updater && this._updater.movesCount === 0 && this._updater.insertsCount === 0 && this._updater.deletesCount > 0 && (this._updater.deletesCount === Math.abs(this._updater.countDifference)); if (deletesOnly && this._updater.elements) { // Verify that the indices of the elements in the updater are within the valid range var elementsKeys = Object.keys(this._updater.elements); for (var i = 0, len = elementsKeys.length; i < len; i++) { var element = this._updater.elements[elementsKeys[i]]; var delta = element.index - element.newIndex; if (delta < 0 || delta > this._updater.deletesCount) { deletesOnly = false; break; } } } this._view.deletesWithoutRealize = this._view.deletesWithoutRealize || 0; if (deletesOnly && (this._view.lastIndexDisplayed < this._view.end - totalInViewport) && (this._updater.deletesCount + this._view.deletesWithoutRealize) < totalInViewport) { skipRealization = true; this._view.deletesWithoutRealize += Math.abs(this._updater.countDifference); this._writeProfilerMark("skipping realization on delete,info"); } else { this._view.deletesWithoutRealize = 0; } this._view._setSkipRealizationForChange(skipRealization); }, _update: function ListView_update() { this._writeProfilerMark("update,StartTM"); if (this._ifZombieDispose()) { return; } this._updateJob = null; var that = this; if (this._versionManager.noOutstandingNotifications) { if (this._updater || this._groupsChanged) { this._cancelAsyncViewWork(); this._verifyRealizationNeededForChange(); this._synchronize().then(function (scrollbarPos) { that._writeProfilerMark("update,StopTM"); that._batchViewUpdates(_Constants._ViewChange.relayout, _Constants._ScrollToPriority.low, scrollbarPos).complete(); }); } else { // Even if nothing important changed we need to restart aria work if it was canceled. this._batchViewUpdates(_Constants._ViewChange.relayout, _Constants._ScrollToPriority.low, this._lastScrollPosition).complete(); } } }, _scheduleUpdate: function ListView_scheduleUpdate() { if (!this._updateJob) { var that = this; // Batch calls to _scheduleUpdate this._updateJob = Scheduler.schedulePromiseHigh(null, "WinJS.UI.ListView._update").then(function () { if (that._updateJob) { that._update(); } }); this._raiseViewLoading(); } }, _createGroupsContainer: function () { if (this._groups) { this._groups.cleanUp(); } if (this._groupDataSource) { this._groups = new _GroupsContainer._UnvirtualizedGroupsContainer(this, this._groupDataSource); } else { this._groups = new _GroupsContainer._NoGroups(this); } }, _createLayoutSite: function () { var that = this; return Object.create({ invalidateLayout: function () { that._pendingLayoutReset = true; var orientationChanged = (that._layout.orientation === "horizontal") !== that._horizontalLayout; that._affectedRange.addAll(); that._batchViewUpdates(_Constants._ViewChange.rebuild, _Constants._ScrollToPriority.low, orientationChanged ? 0 : that.scrollPosition, false, true); }, itemFromIndex: function (itemIndex) { return that._itemsManager._itemPromiseAtIndex(itemIndex); }, groupFromIndex: function (groupIndex) { if (that._groupsEnabled()) { return groupIndex < that._groups.length() ? that._groups.group(groupIndex).userData : null; } else { return { key: "-1" }; } }, groupIndexFromItemIndex: function (itemIndex) { // If itemIndex < 0, returns 0. If itemIndex is larger than the // biggest item index, returns the last group index. itemIndex = Math.max(0, itemIndex); return that._groups.groupFromItem(itemIndex); }, renderItem: function (itemPromise) { return Promise._cancelBlocker(that._itemsManager._itemFromItemPromise(itemPromise)).then(function (element) { if (element) { var record = that._itemsManager._recordFromElement(element); if (record.pendingReady) { record.pendingReady(); } element = element.cloneNode(true); _ElementUtilities.addClass(element, _Constants._itemClass); var itemBox = _Global.document.createElement("div"); _ElementUtilities.addClass(itemBox, _Constants._itemBoxClass); itemBox.appendChild(element); var container = _Global.document.createElement("div"); _ElementUtilities.addClass(container, _Constants._containerClass); container.appendChild(itemBox); return container; } else { return Promise.cancel; } }); }, renderHeader: function (group) { var rendered = _ItemsManager._normalizeRendererReturn(that.groupHeaderTemplate(Promise.wrap(group))); return rendered.then(function (headerRecord) { _ElementUtilities.addClass(headerRecord.element, _Constants._headerClass); var container = _Global.document.createElement("div"); _ElementUtilities.addClass(container, _Constants._headerContainerClass); container.appendChild(headerRecord.element); return container; }); }, readyToMeasure: function () { that._getViewportLength(); that._getCanvasMargins(); }, _isZombie: function () { return that._isZombie(); }, _writeProfilerMark: function (text) { that._writeProfilerMark(text); } }, { _itemsManager: { enumerable: true, get: function () { return that._itemsManager; } }, rtl: { enumerable: true, get: function () { return that._rtl(); } }, surface: { enumerable: true, get: function () { return that._canvas; } }, viewport: { enumerable: true, get: function () { return that._viewport; } }, scrollbarPos: { enumerable: true, get: function () { return that.scrollPosition; } }, viewportSize: { enumerable: true, get: function () { return that._getViewportSize(); } }, loadingBehavior: { enumerable: true, get: function () { return that.loadingBehavior; } }, animationsDisabled: { enumerable: true, get: function () { return that._animationsDisabled(); } }, tree: { enumerable: true, get: function () { return that._view.tree; } }, realizedRange: { enumerable: true, get: function () { return { firstPixel: Math.max(0, that.scrollPosition - 2 * that._getViewportLength()), lastPixel: that.scrollPosition + 3 * that._getViewportLength() - 1 }; } }, visibleRange: { enumerable: true, get: function () { return { firstPixel: that.scrollPosition, lastPixel: that.scrollPosition + that._getViewportLength() - 1 }; } }, itemCount: { enumerable: true, get: function () { return that._itemsCount(); } }, groupCount: { enumerable: true, get: function () { return that._groups.length(); } } }); }, _initializeLayout: function () { this._affectedRange.addAll(); var layoutSite = this._createLayoutSite(); this._layout.initialize(layoutSite, this._groupsEnabled()); return this._layout.orientation === "horizontal"; }, _resetLayoutOrientation: function ListView_resetLayoutOrientation(resetScrollPosition) { if (this._horizontalLayout) { this._startProperty = "left"; this._scrollProperty = "scrollLeft"; this._scrollLength = "scrollWidth"; this._deleteWrapper.style.minHeight = ""; _ElementUtilities.addClass(this._viewport, _Constants._horizontalClass); _ElementUtilities.removeClass(this._viewport, _Constants._verticalClass); if (resetScrollPosition) { this._viewport.scrollTop = 0; } } else { this._startProperty = "top"; this._scrollProperty = "scrollTop"; this._scrollLength = "scrollHeight"; this._deleteWrapper.style.minWidth = ""; _ElementUtilities.addClass(this._viewport, _Constants._verticalClass); _ElementUtilities.removeClass(this._viewport, _Constants._horizontalClass); if (resetScrollPosition) { _ElementUtilities.setScrollPosition(this._viewport, {scrollLeft: 0}); } } }, _resetLayout: function ListView_resetLayout() { this._pendingLayoutReset = false; this._affectedRange.addAll(); if (this._layout) { this._layout.uninitialize(); this._horizontalLayout = this._initializeLayout(); this._resetLayoutOrientation(); } }, _updateLayout: function ListView_updateLayout(layoutObject) { var hadPreviousLayout = false; if (this._layout) { // The old layout is reset here in case it was in the middle of animating when the layout got changed. Reset // will cancel out the animations. this._cancelAsyncViewWork(true); this._layout.uninitialize(); hadPreviousLayout = true; } var layoutImpl; if (layoutObject && typeof layoutObject.type === "function") { var LayoutCtor = requireSupportedForProcessing(layoutObject.type); layoutImpl = new LayoutCtor(layoutObject); } else if (layoutObject && (layoutObject.initialize)) { layoutImpl = layoutObject; } else { layoutImpl = new _Layouts.GridLayout(layoutObject); } hadPreviousLayout && this._resetCanvas(); this._layoutImpl = layoutImpl; this._layout = new _Layouts._LayoutWrapper(layoutImpl); hadPreviousLayout && this._unsetFocusOnItem(); this._setFocusOnItem({ type: _UI.ObjectType.item, index: 0 }); this._selection._setFocused({ type: _UI.ObjectType.item, index: 0 }); this._horizontalLayout = this._initializeLayout(); this._resetLayoutOrientation(hadPreviousLayout); if (hadPreviousLayout) { this._canvas.style.width = this._canvas.style.height = ""; } }, _currentMode: function ListView_currentMode() { return this._mode; }, _setSwipeClass: function ListView_setSwipeClass() { // We apply an -ms-touch-action style to block panning and swiping from occurring at the same time. It is // possible to pan in the margins between items and on lists without the swipe ability. // Phone does not support swipe; therefore, we don't add them swipeable CSS class. if (!_BaseUtils.isPhone && ((this._currentMode() instanceof _BrowseMode._SelectionMode && this._selectionAllowed() && this._swipeBehavior === _UI.SwipeBehavior.select) || this._dragSource || this._reorderable)) { this._swipeable = true; _ElementUtilities.addClass(this._element, _Constants._swipeableClass); } else { this._swipeable = false; _ElementUtilities.removeClass(this._element, _Constants._swipeableClass); } var dragEnabled = (this.itemsDraggable || this.itemsReorderable), swipeSelectEnabled = (this._selectionAllowed() && this._swipeBehavior === _UI.SwipeBehavior.select), swipeEnabled = this._swipeable; this._view.items.each(function (index, item, itemData) { if (itemData.itemBox) { var dragDisabledOnItem = _ElementUtilities.hasClass(item, _Constants._nonDraggableClass), selectionDisabledOnItem = _ElementUtilities.hasClass(item, _Constants._nonSelectableClass), nonSwipeable = _ElementUtilities.hasClass(itemData.itemBox, _Constants._nonSwipeableClass); itemData.itemBox.draggable = (dragEnabled && !dragDisabledOnItem); if (!swipeEnabled && nonSwipeable) { _ElementUtilities.removeClass(itemData.itemBox, _Constants._nonSwipeableClass); } else if (swipeEnabled) { var makeNonSwipeable = (dragEnabled && !swipeSelectEnabled && dragDisabledOnItem) || (swipeSelectEnabled && !dragEnabled && selectionDisabledOnItem) || (dragDisabledOnItem && selectionDisabledOnItem); if (makeNonSwipeable && !nonSwipeable) { _ElementUtilities.addClass(itemData.itemBox, _Constants._nonSwipeableClass); } else if (!makeNonSwipeable && nonSwipeable) { _ElementUtilities.removeClass(itemData.itemBox, _Constants._nonSwipeableClass); } } var makeNonSelectable = _BaseUtils.isPhone && selectionDisabledOnItem; _ElementUtilities[makeNonSelectable ? "addClass" : "removeClass"](itemData.itemBox, _Constants._nonSelectableClass); } }); }, _resizeViewport: function ListView_resizeViewport() { this._viewportWidth = _Constants._UNINITIALIZED; this._viewportHeight = _Constants._UNINITIALIZED; }, _onMSElementResize: function ListView_onResize() { this._writeProfilerMark("_onMSElementResize,info"); Scheduler.schedule(function ListView_async_msElementResize() { if (this._isZombie()) { return; } // If these values are uninitialized there is already a realization pass pending. if (this._viewportWidth !== _Constants._UNINITIALIZED && this._viewportHeight !== _Constants._UNINITIALIZED) { var newWidth = this._element.offsetWidth, newHeight = this._element.offsetHeight; if ((this._previousWidth !== newWidth) || (this._previousHeight !== newHeight)) { this._writeProfilerMark("resize (" + this._previousWidth + "x" + this._previousHeight + ") => (" + newWidth + "x" + newHeight + "),info"); this._previousWidth = newWidth; this._previousHeight = newHeight; this._resizeViewport(); var that = this; this._affectedRange.addAll(); this._batchViewUpdates(_Constants._ViewChange.relayout, _Constants._ScrollToPriority.low, function () { return { position: that.scrollPosition, direction: "right" }; }); } } }, Scheduler.Priority.max, this, "WinJS.UI.ListView._onMSElementResize"); }, _onFocusIn: function ListView_onFocusIn(event) { this._hasKeyboardFocus = true; var that = this; function moveFocusToItem(keyboardFocused) { that._changeFocus(that._selection._getFocused(), true, false, false, keyboardFocused); } // The keyboardEventsHelper object can get focus through three ways: We give it focus explicitly, in which case _shouldHaveFocus will be true, // or the item that should be focused isn't in the viewport, so keyboard focus could only go to our helper. The third way happens when // focus was already on the keyboard helper and someone alt tabbed away from and eventually back to the app. In the second case, we want to navigate // back to the focused item via changeFocus(). In the third case, we don't want to move focus to a real item. We differentiate between cases two and three // by checking if the flag _keyboardFocusInbound is true. It'll be set to true when the tab manager notifies us about the user pressing tab // to move focus into the listview. if (event.target === this._keyboardEventsHelper) { if (!this._keyboardEventsHelper._shouldHaveFocus && this._keyboardFocusInbound) { moveFocusToItem(true); } else { this._keyboardEventsHelper._shouldHaveFocus = false; } } else if (event.target === this._element) { // If someone explicitly calls .focus() on the listview element, we need to route focus to the item that should be focused moveFocusToItem(); } else { if (this._mode.inboundFocusHandled) { this._mode.inboundFocusHandled = false; return; } // In the event that .focus() is explicitly called on an element, we need to figure out what item got focus and set our state appropriately. var items = this._view.items, entity = {}, element = this._groups.headerFrom(event.target), winItem = null; if (element) { entity.type = _UI.ObjectType.groupHeader; entity.index = this._groups.index(element); } else { entity.index = items.index(event.target); entity.type = _UI.ObjectType.item; element = items.itemBoxAt(entity.index); winItem = items.itemAt(entity.index); } // In the old layouts, index will be -1 if a group header got focus if (entity.index !== _Constants._INVALID_INDEX) { if (this._keyboardFocusInbound || this._selection._keyboardFocused()) { if ((entity.type === _UI.ObjectType.groupHeader && event.target === element) || (entity.type === _UI.ObjectType.item && event.target.parentNode === element)) { // For items we check the parentNode because the srcElement is win-item and element is win-itembox, // for header, they should both be the win-groupheader this._drawFocusRectangle(element); } } if (this._tabManager.childFocus !== element && this._tabManager.childFocus !== winItem) { this._selection._setFocused(entity, this._keyboardFocusInbound || this._selection._keyboardFocused()); this._keyboardFocusInbound = false; element = entity.type === _UI.ObjectType.groupHeader ? element : items.itemAt(entity.index); this._tabManager.childFocus = element; if (that._updater) { var elementInfo = that._updater.elements[uniqueID(element)], focusIndex = entity.index; if (elementInfo && elementInfo.newIndex) { focusIndex = elementInfo.newIndex; } // Note to not set old and new focus to the same object that._updater.oldFocus = { type: entity.type, index: focusIndex }; that._updater.newFocus = { type: entity.type, index: focusIndex }; } } } } }, _onFocusOut: function ListView_onFocusOut(event) { if (this._disposed) { return; } this._hasKeyboardFocus = false; this._itemFocused = false; var element = this._view.items.itemBoxFrom(event.target) || this._groups.headerFrom(event.target); if (element) { this._clearFocusRectangle(element); } }, _onMSManipulationStateChanged: function ListView_onMSManipulationStateChanged(ev) { var that = this; function done() { that._manipulationEndSignal = null; } this._manipulationState = ev.currentState; that._writeProfilerMark("_onMSManipulationStateChanged state(" + ev.currentState + "),info"); if (this._manipulationState !== _ElementUtilities._MSManipulationEvent.MS_MANIPULATION_STATE_STOPPED && !this._manipulationEndSignal) { this._manipulationEndSignal = new _Signal(); this._manipulationEndSignal.promise.done(done, done); } if (this._manipulationState === _ElementUtilities._MSManipulationEvent.MS_MANIPULATION_STATE_STOPPED) { this._manipulationEndSignal.complete(); } }, _pendingScroll: false, _onScroll: function ListView_onScroll() { if (!this._zooming && !this._pendingScroll) { this._checkScroller(); } }, _checkScroller: function ListView_checkScroller() { if (this._isZombie()) { return; } var currentScrollPosition = this._viewportScrollPosition; if (currentScrollPosition !== this._lastScrollPosition) { this._pendingScroll = _BaseUtils._requestAnimationFrame(this._checkScroller.bind(this)); currentScrollPosition = Math.max(0, currentScrollPosition); var direction = this._scrollDirection(currentScrollPosition); this._lastScrollPosition = currentScrollPosition; this._raiseViewLoading(true); var that = this; this._view.onScroll(function () { return { position: that._lastScrollPosition, direction: direction }; }, this._manipulationEndSignal ? this._manipulationEndSignal.promise : Promise.timeout(_Constants._DEFERRED_SCROLL_END)); } else { this._pendingScroll = null; } }, _scrollDirection: function ListView_scrollDirectionl(currentScrollPosition) { var currentDirection = currentScrollPosition < this._lastScrollPosition ? "left" : "right"; // When receiving a sequence of scroll positions, the browser may give us one scroll position // which doesn't fit (e.g. the scroll positions were increasing but just this one is decreasing). // To filter out this noise, _scrollDirection and _direction are stubborn -- they only change // when we've received a sequence of 3 scroll position which all indicate the same direction. return currentDirection === this._lastDirection ? currentDirection : this._direction; }, _onTabEnter: function ListView_onTabEnter() { this._keyboardFocusInbound = true; }, _onTabExit: function ListView_onTabExit() { this._keyboardFocusInbound = false; }, _onPropertyChange: function ListView_onPropertyChange(list) { var that = this; list.forEach(function (record) { var dirChanged = false; if (record.attributeName === "dir") { dirChanged = true; } else if (record.attributeName === "style") { dirChanged = (that._cachedStyleDir !== record.target.style.direction); } if (dirChanged) { that._cachedStyleDir = record.target.style.direction; that._cachedRTL = null; _ElementUtilities[that._rtl() ? "addClass" : "removeClass"](that._element, _Constants._rtlListViewClass); that._lastScrollPosition = 0; that._viewportScrollPosition = 0; that.forceLayout(); } if (record.attributeName === "tabIndex") { var newTabIndex = that._element.tabIndex; if (newTabIndex >= 0) { that._view.items.each(function (index, item) { item.tabIndex = newTabIndex; }); that._tabIndex = newTabIndex; that._tabManager.tabIndex = newTabIndex; that._element.tabIndex = -1; } } }); }, _getCanvasMargins: function ListView_getCanvasMargins() { if (!this._canvasMargins) { this._canvasMargins = _Layouts._getMargins(this._canvas); } return this._canvasMargins; }, // Convert between canvas coordinates and viewport coordinates _convertCoordinatesByCanvasMargins: function ListView_convertCoordinatesByCanvasMargins(coordinates, conversionCallback) { function fix(field, offset) { if (coordinates[field] !== undefined) { coordinates[field] = conversionCallback(coordinates[field], offset); } } var offset; if (this._horizontal()) { offset = this._getCanvasMargins()[this._rtl() ? "right" : "left"]; fix("left", offset); } else { offset = this._getCanvasMargins().top; fix("top", offset); } fix("begin", offset); fix("end", offset); return coordinates; }, _convertFromCanvasCoordinates: function ListView_convertFromCanvasCoordinates(coordinates) { return this._convertCoordinatesByCanvasMargins(coordinates, function (coordinate, canvasMargin) { return coordinate + canvasMargin; }); }, _convertToCanvasCoordinates: function ListView_convertToCanvasCoordinates(coordinates) { return this._convertCoordinatesByCanvasMargins(coordinates, function (coordinate, canvasMargin) { return coordinate - canvasMargin; }); }, // Methods in the site interface used by ScrollView _getViewportSize: function ListView_getViewportSize() { if (this._viewportWidth === _Constants._UNINITIALIZED || this._viewportHeight === _Constants._UNINITIALIZED) { this._viewportWidth = Math.max(0, _ElementUtilities.getContentWidth(this._element)); this._viewportHeight = Math.max(0, _ElementUtilities.getContentHeight(this._element)); this._writeProfilerMark("viewportSizeDetected width:" + this._viewportWidth + " height:" + this._viewportHeight); this._previousWidth = this._element.offsetWidth; this._previousHeight = this._element.offsetHeight; } return { width: this._viewportWidth, height: this._viewportHeight }; }, _itemsCount: function ListView_itemsCount() { var that = this; function cleanUp() { that._itemsCountPromise = null; } if (this._cachedCount !== _Constants._UNINITIALIZED) { return Promise.wrap(this._cachedCount); } else { var retVal; if (!this._itemsCountPromise) { retVal = this._itemsCountPromise = this._itemsManager.dataSource.getCount().then( function (count) { if (count === _UI.CountResult.unknown) { count = 0; } that._cachedCount = count; that._selection._updateCount(that._cachedCount); return count; }, function () { return Promise.cancel; } ); this._itemsCountPromise.then(cleanUp, cleanUp); } else { retVal = this._itemsCountPromise; } return retVal; } }, _isSelected: function ListView_isSelected(index) { return this._selection._isIncluded(index); }, _LoadingState: { itemsLoading: "itemsLoading", viewPortLoaded: "viewPortLoaded", itemsLoaded: "itemsLoaded", complete: "complete" }, _raiseViewLoading: function ListView_raiseViewLoading(scrolling) { if (this._loadingState !== this._LoadingState.itemsLoading) { this._scrolling = !!scrolling; } this._setViewState(this._LoadingState.itemsLoading); }, _raiseViewComplete: function ListView_raiseViewComplete() { if (!this._disposed && !this._view.animating) { this._setViewState(this._LoadingState.complete); } }, _setViewState: function ListView_setViewState(state) { if (state !== this._loadingState) { var detail = null; // We can go from any state to itemsLoading but the rest of the states transitions must follow this // order: itemsLoading -> viewPortLoaded -> itemsLoaded -> complete. // Recursively set the previous state until you hit the current state or itemsLoading. switch (state) { case this._LoadingState.viewPortLoaded: if (!this._scheduledForDispose) { scheduleForDispose(this); this._scheduledForDispose = true; } this._setViewState(this._LoadingState.itemsLoading); break; case this._LoadingState.itemsLoaded: detail = { scrolling: this._scrolling }; this._setViewState(this._LoadingState.viewPortLoaded); break; case this._LoadingState.complete: this._setViewState(this._LoadingState.itemsLoaded); this._updateDeleteWrapperSize(true); break; } this._writeProfilerMark("loadingStateChanged:" + state + ",info"); this._loadingState = state; var eventObject = _Global.document.createEvent("CustomEvent"); eventObject.initCustomEvent("loadingstatechanged", true, false, detail); this._element.dispatchEvent(eventObject); } }, _createTemplates: function ListView_createTemplates() { function createNodeWithClass(className, skipAriaHidden) { var element = _Global.document.createElement("div"); element.className = className; if (!skipAriaHidden) { element.setAttribute("aria-hidden", true); } return element; } this._itemBoxTemplate = createNodeWithClass(_Constants._itemBoxClass, true); }, // Methods used by SelectionManager _updateSelection: function ListView_updateSelection() { var indices = this._selection.getIndices(), selectAll = this._selection.isEverything(), selectionMap = {}; if (!selectAll) { for (var i = 0, len = indices.length ; i < len; i++) { var index = indices[i]; selectionMap[index] = true; } } this._view.items.each(function (index, element, itemData) { if (itemData.itemBox && !_ElementUtilities.hasClass(itemData.itemBox, _Constants._swipeClass)) { var selected = selectAll || !!selectionMap[index]; _ItemEventsHandler._ItemEventsHandler.renderSelection(itemData.itemBox, element, selected, true); if (itemData.container) { _ElementUtilities[selected ? "addClass" : "removeClass"](itemData.container, _Constants._selectedClass); } } }); }, _getViewportLength: function ListView_getViewportLength() { return this._getViewportSize()[this._horizontal() ? "width" : "height"]; }, _horizontal: function ListView_horizontal() { return this._horizontalLayout; }, _rtl: function ListView_rtl() { if (typeof this._cachedRTL !== "boolean") { this._cachedRTL = _Global.getComputedStyle(this._element, null).direction === "rtl"; } return this._cachedRTL; }, _showProgressBar: function ListView_showProgressBar(parent, x, y) { var progressBar = this._progressBar, progressStyle = progressBar.style; if (!progressBar.parentNode) { this._fadingProgressBar = false; if (this._progressIndicatorDelayTimer) { this._progressIndicatorDelayTimer.cancel(); } var that = this; this._progressIndicatorDelayTimer = Promise.timeout(_Constants._LISTVIEW_PROGRESS_DELAY).then(function () { if (!that._isZombie()) { parent.appendChild(progressBar); AnimationHelper.fadeInElement(progressBar); that._progressIndicatorDelayTimer = null; } }); } progressStyle[this._rtl() ? "right" : "left"] = x; progressStyle.top = y; }, _hideProgressBar: function ListView_hideProgressBar() { if (this._progressIndicatorDelayTimer) { this._progressIndicatorDelayTimer.cancel(); this._progressIndicatorDelayTimer = null; } var progressBar = this._progressBar; if (progressBar.parentNode && !this._fadingProgressBar) { this._fadingProgressBar = true; var that = this; AnimationHelper.fadeOutElement(progressBar).then(function () { if (progressBar.parentNode) { progressBar.parentNode.removeChild(progressBar); } that._fadingProgressBar = false; }); } }, _getPanAxis: function () { return this._horizontal() ? "horizontal" : "vertical"; }, _configureForZoom: function (isZoomedOut, isCurrentView, triggerZoom) { if (_BaseUtils.validation) { if (!this._view.realizePage || typeof this._view.begin !== "number") { throw new _ErrorFromName("WinJS.UI.ListView.NotCompatibleWithSemanticZoom", strings.notCompatibleWithSemanticZoom); } } this._isZoomedOut = isZoomedOut; this._disableEntranceAnimation = !isCurrentView; this._isCurrentZoomView = isCurrentView; this._triggerZoom = triggerZoom; }, _setCurrentItem: function (x, y) { // First, convert the position into canvas coordinates if (this._rtl()) { x = this._viewportWidth - x; } if (this._horizontal()) { x += this.scrollPosition; } else { y += this.scrollPosition; } var result = this._view.hitTest(x, y), entity = { type: result.type ? result.type : _UI.ObjectType.item, index: result.index }; if (entity.index >= 0) { if (this._hasKeyboardFocus) { this._changeFocus(entity, true, false, true); } else { this._changeFocusPassively(entity); } } }, _getCurrentItem: function () { var focused = this._selection._getFocused(); if (focused.type === _UI.ObjectType.groupHeader) { focused = { type: _UI.ObjectType.item, index: this._groups.group(focused.index).startIndex }; } if (typeof focused.index !== "number") { // Do a hit-test in the viewport center this._setCurrentItem(0.5 * this._viewportWidth, 0.5 * this._viewportHeight); focused = this._selection._getFocused(); } var that = this; var promisePosition = this._getItemOffsetPosition(focused.index). then(function (posCanvas) { var scrollOffset = that._canvasStart; posCanvas[that._startProperty] += scrollOffset; return posCanvas; }); return Promise.join({ item: this._dataSource.itemFromIndex(focused.index), position: promisePosition }); }, _animateItemsForPhoneZoom: function () { var containersOnScreen = [], itemRows = [], promises = [], minRow = Number.MAX_VALUE, that = this; for (var i = this._view.firstIndexDisplayed, len = Math.min(this._cachedCount, this._view.lastIndexDisplayed + 1) ; i < len; i++) { promises.push(this._view.waitForEntityPosition({ type: _UI.ObjectType.item, index: i }).then(function () { containersOnScreen.push(that._view.items.containerAt(i)); var itemRow = 0; if (that.layout._getItemPosition) { var itemPosition = that.layout._getItemPosition(i); if (itemPosition.row) { itemRow = itemPosition.row; } } itemRows.push(itemRow); minRow = Math.min(itemRow, minRow); })); } function rowStaggerDelay(minRow, rows, delayBetweenRows) { return function (index) { return ((rows[index] - minRow) * delayBetweenRows); }; } function clearTransform() { for (var i = 0, len = containersOnScreen.length; i < len; i++) { containersOnScreen[i].style[transformNames.scriptName] = ""; } } return Promise.join(promises).then(function () { return (containersOnScreen.length === 0 ? Promise.wrap() : _TransitionAnimation.executeTransition( containersOnScreen, { property: transformNames.cssName, delay: rowStaggerDelay(minRow, itemRows, 30), duration: 100, timing: "ease-in-out", from: (!that._isCurrentZoomView ? "rotateX(-90deg)" : "rotateX(0deg)"), to: (!that._isCurrentZoomView ? "rotateX(0deg)" : "rotateX(90deg)") })).then(clearTransform, clearTransform); }).then(clearTransform, clearTransform); }, _beginZoom: function () { this._zooming = true; var zoomPromise = null; if (_BaseUtils.isPhone) { if (this._isZoomedOut) { this._zoomAnimationPromise && this._zoomAnimationPromise.cancel(); // The phone's zoom animations need to be handled in two different spots. // When zooming out, we need to wait for _positionItem to be called so that we have the right items in view before trying to animate. // When zooming back in, the items we need to animate are already ready (and _positionItem won't be called on the zoomed out view, since it's // being dismissed), so we play the animation in _beginZoom. if (this._isCurrentZoomView) { var that = this; var animationComplete = function animationComplete() { that._zoomAnimationPromise = null; }; this._zoomAnimationPromise = zoomPromise = this._animateItemsForPhoneZoom().then(animationComplete, animationComplete); } else { this._zoomAnimationPromise = new _Signal(); zoomPromise = this._zoomAnimationPromise.promise; } } } else { // Hide the scrollbar and extend the content beyond the ListView viewport var horizontal = this._horizontal(), scrollOffset = -this.scrollPosition; _ElementUtilities.addClass(this._viewport, horizontal ? _Constants._zoomingXClass : _Constants._zoomingYClass); this._canvasStart = scrollOffset; _ElementUtilities.addClass(this._viewport, horizontal ? _Constants._zoomingYClass : _Constants._zoomingXClass); } return zoomPromise; }, _positionItem: function (item, position) { var that = this; function positionItemAtIndex(index) { return that._getItemOffsetPosition(index).then(function positionItemAtIndex_then_ItemOffsetPosition(posCanvas) { var horizontal = that._horizontal(), canvasSize = that._viewport[horizontal ? "scrollWidth" : "scrollHeight"], viewportSize = (horizontal ? that._viewportWidth : that._viewportHeight), headerSizeProp = (horizontal ? "headerContainerWidth" : "headerContainerHeight"), layoutSizes = that.layout._sizes, headerSize = 0, scrollPosition; if (layoutSizes && layoutSizes[headerSizeProp]) { headerSize = layoutSizes[headerSizeProp]; } // Align the leading edge var start = (_BaseUtils.isPhone ? headerSize : position[that._startProperty]), startMax = viewportSize - (horizontal ? posCanvas.width : posCanvas.height); // Ensure the item ends up within the viewport start = Math.max(0, Math.min(startMax, start)); scrollPosition = posCanvas[that._startProperty] - start; // Ensure the scroll position is valid var adjustedScrollPosition = Math.max(0, Math.min(canvasSize - viewportSize, scrollPosition)), scrollAdjustment = adjustedScrollPosition - scrollPosition; scrollPosition = adjustedScrollPosition; var entity = { type: _UI.ObjectType.item, index: index }; if (that._hasKeyboardFocus) { that._changeFocus(entity, true); } else { that._changeFocusPassively(entity); } that._raiseViewLoading(true); // Since a zoom is in progress, adjust the div position if (!_BaseUtils.isPhone) { var scrollOffset = -scrollPosition; that._canvasStart = scrollOffset; } else { that._viewportScrollPosition = scrollPosition; } that._view.realizePage(scrollPosition, true); if (_BaseUtils.isPhone && that._isZoomedOut) { var animationComplete = function animationComplete() { that._zoomAnimationPromise && that._zoomAnimationPromise.complete && that._zoomAnimationPromise.complete(); that._zoomAnimationPromise = null; }; that._animateItemsForPhoneZoom().then(animationComplete, animationComplete); } return ( horizontal ? { x: scrollAdjustment, y: 0 } : { x: 0, y: scrollAdjustment } ); }); } var itemIndex = 0; if (item) { itemIndex = (this._isZoomedOut ? item.groupIndexHint : item.firstItemIndexHint); } if (typeof itemIndex === "number") { return positionItemAtIndex(itemIndex); } else { // We'll need to obtain the index from the data source var itemPromise; var key = (this._isZoomedOut ? item.groupKey : item.firstItemKey); if (typeof key === "string" && this._dataSource.itemFromKey) { itemPromise = this._dataSource.itemFromKey(key, (this._isZoomedOut ? { groupMemberKey: item.key, groupMemberIndex: item.index } : null)); } else { var description = (this._isZoomedOut ? item.groupDescription : item.firstItemDescription); if (_BaseUtils.validation) { if (description === undefined) { throw new _ErrorFromName("WinJS.UI.ListView.InvalidItem", strings.listViewInvalidItem); } } itemPromise = this._dataSource.itemFromDescription(description); } return itemPromise.then(function (item) { return positionItemAtIndex(item.index); }); } }, _endZoom: function (isCurrentView) { if (this._isZombie()) { return; } // Crop the content again and re-enable the scrollbar if (!_BaseUtils.isPhone) { var scrollOffset = this._canvasStart; _ElementUtilities.removeClass(this._viewport, _Constants._zoomingYClass); _ElementUtilities.removeClass(this._viewport, _Constants._zoomingXClass); this._canvasStart = 0; this._viewportScrollPosition = -scrollOffset; } this._disableEntranceAnimation = !isCurrentView; this._isCurrentZoomView = isCurrentView; this._zooming = false; this._view.realizePage(this.scrollPosition, false); }, _getItemOffsetPosition: function (index) { var that = this; return this._getItemOffset({ type: _UI.ObjectType.item, index: index }).then(function (position) { return that._ensureFirstColumnRange(_UI.ObjectType.item).then(function () { position = that._correctRangeInFirstColumn(position, _UI.ObjectType.item); position = that._convertFromCanvasCoordinates(position); if (that._horizontal()) { position.left = position.begin; position.width = position.end - position.begin; position.height = position.totalHeight; } else { position.top = position.begin; position.height = position.end - position.begin; position.width = position.totalWidth; } return position; }); }); }, _groupRemoved: function (key) { this._groupFocusCache.deleteGroup(key); }, _updateFocusCache: function (itemIndex) { if (this._updateFocusCacheItemRequest) { this._updateFocusCacheItemRequest.cancel(); } var that = this; this._updateFocusCacheItemRequest = this._view.items.requestItem(itemIndex).then(function () { that._updateFocusCacheItemRequest = null; var itemData = that._view.items.itemDataAt(itemIndex); var groupIndex = that._groups.groupFromItem(itemIndex); var groupKey = that._groups.group(groupIndex).key; if (itemData.itemsManagerRecord.item) { that._groupFocusCache.updateCache(groupKey, itemData.itemsManagerRecord.item.key, itemIndex); } }); }, _changeFocus: function (newFocus, skipSelection, ctrlKeyDown, skipEnsureVisible, keyboardFocused) { if (this._isZombie()) { return; } var targetItem; if (newFocus.type !== _UI.ObjectType.groupHeader) { targetItem = this._view.items.itemAt(newFocus.index); if (!skipSelection && targetItem && _ElementUtilities.hasClass(targetItem, _Constants._nonSelectableClass)) { skipSelection = true; } this._updateFocusCache(newFocus.index); } else { var group = this._groups.group(newFocus.index); targetItem = group && group.header; } this._unsetFocusOnItem(!!targetItem); this._hasKeyboardFocus = true; this._selection._setFocused(newFocus, keyboardFocused); if (!skipEnsureVisible) { this.ensureVisible(newFocus); } // _selection.set() needs to know which item has focus so we // must call it after _selection._setFocused() has been called. if (!skipSelection && this._selectFocused(ctrlKeyDown)) { this._selection.set(newFocus.index); } this._setFocusOnItem(newFocus); }, // Updates ListView's internal focus state and, if ListView currently has focus, moves // Trident's focus to the item at index newFocus. // Similar to _changeFocus except _changeFocusPassively doesn't: // - ensure the item is selected or visible // - set Trident's focus to newFocus when ListView doesn't have focus _changeFocusPassively: function (newFocus) { var targetItem; if (newFocus.type !== _UI.ObjectType.groupHeader) { targetItem = this._view.items.itemAt(newFocus.index); this._updateFocusCache(newFocus.index); } else { var group = this._groups.group(newFocus.index); targetItem = group && group.header; } this._unsetFocusOnItem(!!targetItem); this._selection._setFocused(newFocus); this._setFocusOnItem(newFocus); }, _drawFocusRectangle: function (item) { if (_ElementUtilities.hasClass(item, _Constants._headerClass)) { _ElementUtilities.addClass(item, _Constants._itemFocusClass); } else { var itemBox = this._view.items.itemBoxFrom(item); if (itemBox.querySelector("." + _Constants._itemFocusOutlineClass)) { return; } _ElementUtilities.addClass(itemBox, _Constants._itemFocusClass); var outline = _Global.document.createElement("div"); outline.className = _Constants._itemFocusOutlineClass; itemBox.appendChild(outline); } }, _clearFocusRectangle: function (item) { if (!item || this._isZombie()) { return; } var itemBox = this._view.items.itemBoxFrom(item); if (itemBox) { _ElementUtilities.removeClass(itemBox, _Constants._itemFocusClass); var outline = itemBox.querySelector("." + _Constants._itemFocusOutlineClass); if (outline) { outline.parentNode.removeChild(outline); } } else { var header = this._groups.headerFrom(item); if (header) { _ElementUtilities.removeClass(header, _Constants._itemFocusClass); } } }, _defaultInvoke: function (entity) { if (this._isZoomedOut || (_BaseUtils.isPhone && this._triggerZoom && entity.type === _UI.ObjectType.groupHeader)) { this._changeFocusPassively(entity); this._triggerZoom(); } }, _selectionAllowed: function ListView_selectionAllowed(itemIndex) { var item = (itemIndex !== undefined ? this.elementFromIndex(itemIndex) : null), itemSelectable = !(item && _ElementUtilities.hasClass(item, _Constants._nonSelectableClass)); return itemSelectable && this._selectionMode !== _UI.SelectionMode.none; }, _multiSelection: function ListView_multiSelection() { return this._selectionMode === _UI.SelectionMode.multi; }, _selectOnTap: function ListView_selectOnTap() { return this._tap === _UI.TapBehavior.toggleSelect || this._tap === _UI.TapBehavior.directSelect; }, _selectFocused: function ListView_selectFocused(ctrlKeyDown) { return this._tap === _UI.TapBehavior.directSelect && this._selectionMode === _UI.SelectionMode.multi && !ctrlKeyDown; }, _dispose: function () { if (!this._disposed) { this._disposed = true; var clear = function clear(e) { e && (e.textContent = ""); }; _ElementUtilities._resizeNotifier.unsubscribe(this._element, this._onMSElementResizeBound); this._batchingViewUpdates && this._batchingViewUpdates.cancel(); this._view && this._view._dispose && this._view._dispose(); this._mode && this._mode._dispose && this._mode._dispose(); this._groups && this._groups._dispose && this._groups._dispose(); this._selection && this._selection._dispose && this._selection._dispose(); this._layout && this._layout.uninitialize && this._layout.uninitialize(); this._itemsCountPromise && this._itemsCountPromise.cancel(); this._versionManager && this._versionManager._dispose(); this._clearInsertedItems(); this._itemsManager && this._itemsManager.release(); this._zoomAnimationPromise && this._zoomAnimationPromise.cancel(); clear(this._viewport); clear(this._canvas); clear(this._canvasProxy); this._versionManager = null; this._view = null; this._mode = null; this._element = null; this._viewport = null; this._itemsManager = null; this._canvas = null; this._canvasProxy = null; this._itemsCountPromise = null; this._scrollToFunctor = null; var index = controlsToDispose.indexOf(this); if (index >= 0) { controlsToDispose.splice(index, 1); } } }, _isZombie: function () { // determines if this ListView is no longer in the DOM or has been cleared // return this._disposed || !(this.element.firstElementChild && _Global.document.body.contains(this.element)); }, _ifZombieDispose: function () { var zombie = this._isZombie(); if (zombie && !this._disposed) { scheduleForDispose(this); } return zombie; }, _animationsDisabled: function () { if (this._viewportWidth === 0 || this._viewportHeight === 0) { return true; } return !_TransitionAnimation.isAnimationEnabled(); }, _fadeOutViewport: function ListView_fadeOutViewport() { var that = this; return new Promise(function (complete) { if (that._animationsDisabled()) { complete(); return; } if (!that._fadingViewportOut) { if (that._waitingEntranceAnimationPromise) { that._waitingEntranceAnimationPromise.cancel(); that._waitingEntranceAnimationPromise = null; } var eventDetails = that._fireAnimationEvent(ListViewAnimationType.contentTransition); that._firedAnimationEvent = true; var overflowStyle = _BaseUtils._browserStyleEquivalents["overflow-style"]; var animatedElement = overflowStyle ? that._viewport : that._canvas; if (!eventDetails.prevented) { that._fadingViewportOut = true; if (overflowStyle) { animatedElement.style[overflowStyle.scriptName] = "none"; } AnimationHelper.fadeOutElement(animatedElement).then(function () { if (that._isZombie()) { return; } that._fadingViewportOut = false; animatedElement.style.opacity = 1.0; complete(); }); } else { that._disableEntranceAnimation = true; animatedElement.style.opacity = 1.0; complete(); } } }); }, _animateListEntrance: function (firstTime) { var eventDetails = { prevented: false, animationPromise: Promise.wrap() }; var that = this; var overflowStyle = _BaseUtils._browserStyleEquivalents["overflow-style"]; var animatedElement = overflowStyle ? this._viewport : this._canvas; function resetViewOpacity() { that._canvas.style.opacity = 1; if (overflowStyle) { animatedElement.style[overflowStyle.scriptName] = ""; } } if (this._disableEntranceAnimation || this._animationsDisabled()) { resetViewOpacity(); if (this._waitingEntranceAnimationPromise) { this._waitingEntranceAnimationPromise.cancel(); this._waitingEntranceAnimationPromise = null; } return Promise.wrap(); } if (!this._firedAnimationEvent) { eventDetails = this._fireAnimationEvent(ListViewAnimationType.entrance); } else { this._firedAnimationEvent = false; } // The listview does not have an entrance animation on Phone if (eventDetails.prevented || _BaseUtils.isPhone) { resetViewOpacity(); return Promise.wrap(); } else { if (this._waitingEntranceAnimationPromise) { this._waitingEntranceAnimationPromise.cancel(); } this._canvas.style.opacity = 0; if (overflowStyle) { animatedElement.style[overflowStyle.scriptName] = "none"; } this._waitingEntranceAnimationPromise = eventDetails.animationPromise.then(function () { if (!that._isZombie()) { that._canvas.style.opacity = 1; return AnimationHelper.animateEntrance(animatedElement, firstTime).then(function () { if (!that._isZombie()) { if (overflowStyle) { animatedElement.style[overflowStyle.scriptName] = ""; } that._waitingEntranceAnimationPromise = null; } }); } }); return this._waitingEntranceAnimationPromise; } }, _fireAnimationEvent: function (type) { var animationEvent = _Global.document.createEvent("CustomEvent"), animationPromise = Promise.wrap(); animationEvent.initCustomEvent("contentanimating", true, true, { type: type }); if (type === ListViewAnimationType.entrance) { animationEvent.detail.setPromise = function (delayPromise) { animationPromise = delayPromise; }; } var prevented = !this._element.dispatchEvent(animationEvent); return { prevented: prevented, animationPromise: animationPromise }; }, // If they don't yet exist, create the start and end markers which are required // by Narrator's aria-flowto/flowfrom implementation. They mark the start and end // of ListView's set of out-of-order DOM elements and so they must surround the // headers and groups in the DOM. _createAriaMarkers: function ListView_createAriaMarkers() { if (!this._viewport.getAttribute("aria-label")) { this._viewport.setAttribute("aria-label", strings.listViewViewportAriaLabel); } if (!this._ariaStartMarker) { this._ariaStartMarker = _Global.document.createElement("div"); this._ariaStartMarker.id = uniqueID(this._ariaStartMarker); this._viewport.insertBefore(this._ariaStartMarker, this._viewport.firstElementChild); } if (!this._ariaEndMarker) { this._ariaEndMarker = _Global.document.createElement("div"); this._ariaEndMarker.id = uniqueID(this._ariaEndMarker); this._viewport.appendChild(this._ariaEndMarker); } }, // If the ListView is in static mode, then the roles of the list and items should be "list" and "listitem", respectively. // Otherwise, the roles should be "listbox" and "option." If the ARIA roles are out of sync with the ListView's // static/interactive state, update the role of the ListView and the role of each realized item. _updateItemsAriaRoles: function ListView_updateItemsAriaRoles() { var that = this; var listRole = this._element.getAttribute("role"), expectedListRole, expectedItemRole; if (this._currentMode().staticMode()) { expectedListRole = "list"; expectedItemRole = "listitem"; } else { expectedListRole = "listbox"; expectedItemRole = "option"; } if (listRole !== expectedListRole || this._itemRole !== expectedItemRole) { this._element.setAttribute("role", expectedListRole); this._itemRole = expectedItemRole; this._view.items.each(function (index, itemElement) { itemElement.setAttribute("role", that._itemRole); }); } }, _updateGroupHeadersAriaRoles: function ListView_updateGroupHeadersAriaRoles() { var headerRole = (this.groupHeaderTapBehavior === _UI.GroupHeaderTapBehavior.none ? "separator" : "link"); if (this._headerRole !== headerRole) { this._headerRole = headerRole; for (var i = 0, len = this._groups.length() ; i < len; i++) { var header = this._groups.group(i).header; if (header) { header.setAttribute("role", this._headerRole); } } } }, // Avoids unnecessary UIA selection events by only updating aria-selected if it has changed _setAriaSelected: function ListView_setAriaSelected(itemElement, isSelected) { var ariaSelected = (itemElement.getAttribute("aria-selected") === "true"); if (isSelected !== ariaSelected) { itemElement.setAttribute("aria-selected", isSelected); } }, _setupAriaSelectionObserver: function ListView_setupAriaSelectionObserver(item) { if (!item._mutationObserver) { this._mutationObserver.observe(item, { attributes: true, attributeFilter: ["aria-selected"] }); item._mutationObserver = true; } }, _itemPropertyChange: function ListView_itemPropertyChange(list) { if (this._isZombie()) { return; } var that = this; var singleSelection = that._selectionMode === _UI.SelectionMode.single; var changedItems = []; var unselectableItems = []; function revertAriaSelected(items) { items.forEach(function (entry) { entry.item.setAttribute("aria-selected", !entry.selected); }); } for (var i = 0, len = list.length; i < len; i++) { var item = list[i].target; var itemBox = that._view.items.itemBoxFrom(item); var selected = item.getAttribute("aria-selected") === "true"; // Only respond to aria-selected changes coming from UIA. This check // relies on the fact that, in renderSelection, we update the selection // visual before aria-selected. if (itemBox && (selected !== _ElementUtilities._isSelectionRendered(itemBox))) { var index = that._view.items.index(itemBox); var entry = { index: index, item: item, selected: selected }; (that._selectionAllowed(index) ? changedItems : unselectableItems).push(entry); } } if (changedItems.length > 0) { var signal = new _Signal(); that.selection._synchronize(signal).then(function () { var newSelection = that.selection._cloneSelection(); changedItems.forEach(function (entry) { if (entry.selected) { newSelection[singleSelection ? "set" : "add"](entry.index); } else { newSelection.remove(entry.index); } }); return that.selection._set(newSelection); }).then(function (approved) { if (!that._isZombie() && !approved) { // A selectionchanging event handler rejected the selection change revertAriaSelected(changedItems); } signal.complete(); }); } revertAriaSelected(unselectableItems); }, _groupsEnabled: function () { return !!this._groups.groupDataSource; }, _getItemPosition: function ListView_getItemPosition(entity, preserveItemsBlocks) { var that = this; return this._view.waitForEntityPosition(entity).then(function () { var container = (entity.type === _UI.ObjectType.groupHeader ? that._view._getHeaderContainer(entity.index) : that._view.getContainer(entity.index)); if (container) { that._writeProfilerMark("WinJS.UI.ListView:getItemPosition,info"); var itemsBlockFrom; var itemsBlockTo; if (that._view._expandedRange) { itemsBlockFrom = that._view._expandedRange.first.index; itemsBlockTo = that._view._expandedRange.last.index; } else { preserveItemsBlocks = false; } if (entity.type === _UI.ObjectType.item) { preserveItemsBlocks = !!preserveItemsBlocks; preserveItemsBlocks &= that._view._ensureContainerInDOM(entity.index); } else { preserveItemsBlocks = false; } var margins = that._getItemMargins(entity.type), position = { left: (that._rtl() ? getOffsetRight(container) - margins.right : container.offsetLeft - margins.left), top: container.offsetTop - margins.top, totalWidth: _ElementUtilities.getTotalWidth(container), totalHeight: _ElementUtilities.getTotalHeight(container), contentWidth: _ElementUtilities.getContentWidth(container), contentHeight: _ElementUtilities.getContentHeight(container) }; if (preserveItemsBlocks) { that._view._forceItemsBlocksInDOM(itemsBlockFrom, itemsBlockTo + 1); } // When a translation is applied to the surface during zooming, offsetLeft includes the canvas margins, so the left/top position will already be in canvas coordinates. // If we're not zooming, we need to convert the position to canvas coordinates before returning. return (that._zooming && that._canvasStart !== 0 ? position : that._convertToCanvasCoordinates(position)); } else { return Promise.cancel; } }); }, _getItemOffset: function ListView_getItemOffset(entity, preserveItemsBlocks) { var that = this; return this._getItemPosition(entity, preserveItemsBlocks).then(function (pos) { // _getItemOffset also includes the right/bottom margin of the previous row/column of items, so that ensureVisible/indexOfFirstVisible will jump such that // the previous row/column is directly offscreen of the target item. var margins = that._getItemMargins(entity.type); if (that._horizontal()) { var rtl = that._rtl(); pos.begin = pos.left - margins[rtl ? "left" : "right"]; pos.end = pos.left + pos.totalWidth + margins[rtl ? "right" : "left"]; } else { pos.begin = pos.top - margins.bottom; pos.end = pos.top + pos.totalHeight + margins.top; } return pos; }); }, _getItemMargins: function ListView_getItemMargins(type) { type = type || _UI.ObjectType.item; var that = this; var calculateMargins = function (className) { var item = that._canvas.querySelector("." + className), cleanup; if (!item) { item = _Global.document.createElement("div"), _ElementUtilities.addClass(item, className); that._viewport.appendChild(item); cleanup = true; } var margins = _Layouts._getMargins(item); if (cleanup) { that._viewport.removeChild(item); } return margins; }; if (type !== _UI.ObjectType.groupHeader) { return (this._itemMargins ? this._itemMargins : (this._itemMargins = calculateMargins(_Constants._containerClass))); } else { return (this._headerMargins ? this._headerMargins : (this._headerMargins = calculateMargins(_Constants._headerContainerClass))); } }, _fireAccessibilityAnnotationCompleteEvent: function ListView_fireAccessibilityAnnotationCompleteEvent(firstIndex, lastIndex, firstHeaderIndex, lastHeaderIndex) { // This event is fired in these cases: // - When the data source count is 0, it is fired after the aria markers have been // updated. The event detail will be { firstIndex: -1, lastIndex: -1 }. // - When the data source count is non-zero, it is fired after the aria markers // have been updated and the deferred work for the aria properties on the items // has completed. // - When an item gets focus. The event will be { firstIndex: indexOfItem, lastIndex: indexOfItem }. var detail = { firstIndex: firstIndex, lastIndex: lastIndex, firstHeaderIndex: (+firstHeaderIndex) || -1, lastHeaderIndex: (+lastHeaderIndex) || -1 }; var eventObject = _Global.document.createEvent("CustomEvent"); eventObject.initCustomEvent("accessibilityannotationcomplete", true, false, detail); this._element.dispatchEvent(eventObject); }, _ensureFirstColumnRange: function ListView_ensureFirstColumnRange(type) { var propName = (type === _UI.ObjectType.item ? "_firstItemRange" : "_firstHeaderRange"); if (!this[propName]) { var that = this; return this._getItemOffset({ type: type, index: 0 }, true).then(function (firstRange) { that[propName] = firstRange; }); } else { return Promise.wrap(); } }, _correctRangeInFirstColumn: function ListView_correctRangeInFirstColumn(range, type) { var firstRange = (type === _UI.ObjectType.groupHeader ? this._firstHeaderRange : this._firstItemRange); if (firstRange.begin === range.begin) { if (this._horizontal()) { range.begin = -this._getCanvasMargins()[this._rtl() ? "right" : "left"]; } else { range.begin = -this._getCanvasMargins().top; } } return range; }, _updateContainers: function ListView_updateContainers(groups, count, containersDelta, modifiedElements) { var that = this; var maxContainers = this._view.containers.length + (containersDelta > 0 ? containersDelta : 0); var newTree = []; var newKeyToGroupIndex = {}; var newContainers = []; var removedContainers = []; function createContainer() { var element = _Global.document.createElement("div"); element.className = _Constants._containerClass; return element; } function updateExistingGroupWithBlocks(groupNode, firstItem, newSize) { if (firstItem + newSize > maxContainers) { newSize = maxContainers - firstItem; } var itemsContainer = groupNode.itemsContainer, blocks = itemsContainer.itemsBlocks, lastBlock = blocks.length ? blocks[blocks.length - 1] : null, currentSize = blocks.length ? (blocks.length - 1) * that._view._blockSize + lastBlock.items.length : 0, delta = newSize - currentSize, oldSize, children; if (delta > 0) { if (lastBlock && lastBlock.items.length < that._view._blockSize) { var toAdd = Math.min(delta, that._view._blockSize - lastBlock.items.length); _SafeHtml.insertAdjacentHTMLUnsafe(lastBlock.element, "beforeend", _Helpers._repeat("
", toAdd)); oldSize = lastBlock.items.length; children = lastBlock.element.children; for (var j = 0; j < toAdd; j++) { lastBlock.items.push(children[oldSize + j]); } delta -= toAdd; } var blocksCount = Math.floor(delta / that._view._blockSize), lastBlockSize = delta % that._view._blockSize; var blockMarkup = "
" + _Helpers._repeat("
", that._view._blockSize) + "
", markup = _Helpers._repeat(blockMarkup, blocksCount); if (lastBlockSize) { markup += "
" + _Helpers._repeat("
", lastBlockSize) + "
"; blocksCount++; } var blocksTemp = _Global.document.createElement("div"); _SafeHtml.setInnerHTMLUnsafe(blocksTemp, markup); var children = blocksTemp.children; for (var j = 0; j < blocksCount; j++) { var block = children[j], blockNode = { element: block, items: _Helpers._nodeListToArray(block.children) }; itemsContainer.itemsBlocks.push(blockNode); } } else if (delta < 0) { for (var n = delta; n < 0; n++) { var container = lastBlock.items.pop(); if (!that._view._requireFocusRestore && container.contains(_Global.document.activeElement)) { that._view._requireFocusRestore = _Global.document.activeElement; that._unsetFocusOnItem(); } lastBlock.element.removeChild(container); removedContainers.push(container); if (!lastBlock.items.length) { if (itemsContainer.element === lastBlock.element.parentNode) { itemsContainer.element.removeChild(lastBlock.element); } blocks.pop(); lastBlock = blocks[blocks.length - 1]; } } } for (var j = 0, len = blocks.length; j < len; j++) { var block = blocks[j]; for (var n = 0; n < block.items.length; n++) { newContainers.push(block.items[n]); } } } function addInserted(groupNode, firstItemIndex, newSize) { var added = modifiedElements.filter(function (entry) { return (entry.oldIndex === -1 && entry.newIndex >= firstItemIndex && entry.newIndex < (firstItemIndex + newSize)); }).sort(function (left, right) { return left.newIndex - right.newIndex; }); var itemsContainer = groupNode.itemsContainer; for (var i = 0, len = added.length; i < len; i++) { var entry = added[i], offset = entry.newIndex - firstItemIndex; var container = createContainer(), next = offset < itemsContainer.items.length ? itemsContainer.items[offset] : null; itemsContainer.items.splice(offset, 0, container); itemsContainer.element.insertBefore(container, next); } } function updateExistingGroup(groupNode, firstItem, newSize) { if (firstItem + newSize > maxContainers) { newSize = maxContainers - firstItem; } var itemsContainer = groupNode.itemsContainer, delta = newSize - itemsContainer.items.length; if (delta > 0) { var children = itemsContainer.element.children, oldSize = children.length; _SafeHtml.insertAdjacentHTMLUnsafe(itemsContainer.element, "beforeend", _Helpers._repeat("
", delta)); for (var n = 0; n < delta; n++) { var container = children[oldSize + n]; itemsContainer.items.push(container); } } for (var n = delta; n < 0; n++) { var container = itemsContainer.items.pop(); itemsContainer.element.removeChild(container); removedContainers.push(container); } for (var n = 0, len = itemsContainer.items.length; n < len; n++) { newContainers.push(itemsContainer.items[n]); } } function addNewGroup(groupInfo, firstItem) { var header = that._view._createHeaderContainer(prevElement); var groupNode = { header: header, itemsContainer: { element: that._view._createItemsContainer(header), } }; groupNode.itemsContainer[that._view._blockSize ? "itemsBlocks" : "items"] = []; if (that._view._blockSize) { updateExistingGroupWithBlocks(groupNode, firstItem, groupInfo.size); } else { updateExistingGroup(groupNode, firstItem, groupInfo.size); } return groupNode; } function shift(groupNode, oldFirstItemIndex, currentFirstItemIndex, newSize) { var currentLast = currentFirstItemIndex + newSize - 1, firstShifted, delta; for (var i = 0, len = modifiedElements.length; i < len; i++) { var entry = modifiedElements[i]; if (entry.newIndex >= currentFirstItemIndex && entry.newIndex <= currentLast && entry.oldIndex !== -1) { if (firstShifted !== +firstShifted || entry.newIndex < firstShifted) { firstShifted = entry.newIndex; delta = entry.newIndex - entry.oldIndex; } } } if (firstShifted === +firstShifted) { var addedBeforeShift = 0; for (i = 0, len = modifiedElements.length; i < len; i++) { var entry = modifiedElements[i]; if (entry.newIndex >= currentFirstItemIndex && entry.newIndex < firstShifted && entry.oldIndex === -1) { addedBeforeShift++; } } var removedBeforeShift = 0, oldFirstShifted = firstShifted - delta; for (i = 0, len = modifiedElements.length; i < len; i++) { var entry = modifiedElements[i]; if (entry.oldIndex >= oldFirstItemIndex && entry.oldIndex < oldFirstShifted && entry.newIndex === -1) { removedBeforeShift++; } } delta += removedBeforeShift; delta -= addedBeforeShift; delta -= currentFirstItemIndex - oldFirstItemIndex; var itemsContainer = groupNode.itemsContainer; if (delta > 0) { var children = itemsContainer.element.children; _SafeHtml.insertAdjacentHTMLUnsafe(itemsContainer.element, "afterBegin", _Helpers._repeat("
", delta)); for (var n = 0; n < delta; n++) { var container = children[n]; itemsContainer.items.splice(n, 0, container); } } for (var n = delta; n < 0; n++) { var container = itemsContainer.items.shift(); itemsContainer.element.removeChild(container); } if (delta) { // Invalidate the layout of the entire group because we do not know the exact indices which were added/modified since they were before the realization range. that._affectedRange.add({ start: currentFirstItemIndex, end: currentFirstItemIndex + newSize }, count); } } } function flatIndexToGroupIndex(index) { var firstItem = 0; for (var i = 0, len = that._view.tree.length; i < len; i++) { var group = that._view.tree[i], size = group.itemsContainer.items.length, lastItem = firstItem + size - 1; if (index >= firstItem && index <= lastItem) { return { group: i, item: index - firstItem }; } firstItem += size; } } var oldFirstItem = []; var firstItem = 0; if (!that._view._blockSize) { for (var i = 0, len = this._view.tree.length; i < len; i++) { oldFirstItem.push(firstItem); firstItem += this._view.tree[i].itemsContainer.items.length; } } if (!that._view._blockSize) { var removed = modifiedElements.filter(function (entry) { return entry.newIndex === -1 && !entry._removalHandled; }).sort(function (left, right) { return right.oldIndex - left.oldIndex; }); for (var i = 0, len = removed.length; i < len; i++) { var entry = removed[i]; entry._removalHandled = true; var itemBox = entry._itemBox; entry._itemBox = null; var groupIndex = flatIndexToGroupIndex(entry.oldIndex); var group = this._view.tree[groupIndex.group]; var container = group.itemsContainer.items[groupIndex.item]; container.parentNode.removeChild(container); if (_ElementUtilities.hasClass(itemBox, _Constants._selectedClass)) { _ElementUtilities.addClass(container, _Constants._selectedClass); } group.itemsContainer.items.splice(groupIndex.item, 1); entry.element = container; } } this._view._modifiedGroups = []; var prevElement = this._canvasProxy; firstItem = 0; // When groups are disabled, loop thru all of the groups (there's only 1). // When groups are enabled, loop until either we exhaust all of the groups in the data source // or we exhaust all of the containers that have been created so far. for (var i = 0, len = groups.length; i < len && (!this._groupsEnabled() || firstItem < maxContainers) ; i++) { var groupInfo = groups[i], existingGroupIndex = this._view.keyToGroupIndex[groupInfo.key], existingGroup = this._view.tree[existingGroupIndex]; if (existingGroup) { if (that._view._blockSize) { updateExistingGroupWithBlocks(existingGroup, firstItem, groupInfo.size); } else { shift(existingGroup, oldFirstItem[existingGroupIndex], firstItem, groupInfo.size); addInserted(existingGroup, firstItem, groupInfo.size); updateExistingGroup(existingGroup, firstItem, groupInfo.size); } newTree.push(existingGroup); newKeyToGroupIndex[groupInfo.key] = newTree.length - 1; delete this._view.keyToGroupIndex[groupInfo.key]; prevElement = existingGroup.itemsContainer.element; this._view._modifiedGroups.push({ oldIndex: existingGroupIndex, newIndex: newTree.length - 1, element: existingGroup.header }); } else { var newGroup = addNewGroup(groupInfo, firstItem); newTree.push(newGroup); newKeyToGroupIndex[groupInfo.key] = newTree.length - 1; this._view._modifiedGroups.push({ oldIndex: -1, newIndex: newTree.length - 1, element: newGroup.header }); prevElement = newGroup.itemsContainer.element; } firstItem += groupInfo.size; } var removedItemsContainers = [], removedHeaders = [], removedGroups = this._view.keyToGroupIndex ? Object.keys(this._view.keyToGroupIndex) : []; for (var i = 0, len = removedGroups.length; i < len; i++) { var groupIndex = this._view.keyToGroupIndex[removedGroups[i]], groupNode = this._view.tree[groupIndex]; removedHeaders.push(groupNode.header); removedItemsContainers.push(groupNode.itemsContainer.element); if (this._view._blockSize) { for (var b = 0; b < groupNode.itemsContainer.itemsBlocks.length; b++) { var block = groupNode.itemsContainer.itemsBlocks[b]; for (var n = 0; n < block.items.length; n++) { removedContainers.push(block.items[n]); } } } else { for (var n = 0; n < groupNode.itemsContainer.items.length; n++) { removedContainers.push(groupNode.itemsContainer.items[n]); } } this._view._modifiedGroups.push({ oldIndex: groupIndex, newIndex: -1, element: groupNode.header }); } for (var i = 0, len = modifiedElements.length; i < len; i++) { if (modifiedElements[i].newIndex === -1 && !modifiedElements[i]._removalHandled) { modifiedElements[i]._removalHandled = true; var itemBox = modifiedElements[i]._itemBox; modifiedElements[i]._itemBox = null; var container; if (removedContainers.length) { container = removedContainers.pop(); _ElementUtilities.empty(container); } else { container = createContainer(); } if (_ElementUtilities.hasClass(itemBox, _Constants._selectedClass)) { _ElementUtilities.addClass(container, _Constants._selectedClass); } container.appendChild(itemBox); modifiedElements[i].element = container; } } this._view.tree = newTree; this._view.keyToGroupIndex = newKeyToGroupIndex; this._view.containers = newContainers; return { removedHeaders: removedHeaders, removedItemsContainers: removedItemsContainers }; }, _writeProfilerMark: function ListView_writeProfilerMark(text) { var message = "WinJS.UI.ListView:" + this._id + ":" + text; _WriteProfilerMark(message); _Log.log && _Log.log(message, null, "listviewprofiler"); } }, { // Static members triggerDispose: function () { /// /// /// Triggers the ListView disposal service manually. In normal operation this is triggered /// at ListView instantiation. However in some scenarios it may be appropriate to run /// the disposal service manually. /// /// disposeControls(); } }); _Base.Class.mix(ListView, _Events.createEventProperties( "iteminvoked", "groupheaderinvoked", "selectionchanging", "selectionchanged", "loadingstatechanged", "keyboardnavigating", "contentanimating", "itemdragstart", "itemdragenter", "itemdragend", "itemdragbetween", "itemdragleave", "itemdragchanged", "itemdragdrop", "accessibilityannotationcomplete")); _Base.Class.mix(ListView, _Control.DOMEventMixin); return ListView; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/FlipView/_Constants',[ ], function constantsInit() { "use strict"; var members = {}; members.datasourceCountChangedEvent = "datasourcecountchanged"; members.pageVisibilityChangedEvent = "pagevisibilitychanged"; members.pageSelectedEvent = "pageselected"; members.pageCompletedEvent = "pagecompleted"; return members; }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/FlipView/_PageManager',[ 'exports', '../../Core/_Global', '../../Core/_Base', '../../Core/_BaseUtils', '../../Core/_ErrorFromName', '../../Core/_Log', '../../Core/_Resources', '../../Core/_WriteProfilerMark', '../../Animations', '../../Promise', '../../_Signal', '../../Scheduler', '../../Utilities/_Dispose', '../../Utilities/_ElementUtilities', '../../Utilities/_TabContainer', './_Constants' ], function flipperPageManagerInit(exports, _Global, _Base, _BaseUtils, _ErrorFromName, _Log, _Resources, _WriteProfilerMark, Animations, Promise, _Signal, Scheduler, _Dispose, _ElementUtilities, _TabContainer, _Constants) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { // Definition of our private utility _FlipPageManager: _Base.Namespace._lazy(function () { var uniqueID = _ElementUtilities._uniqueID; var styleEquivalents = _BaseUtils._browserStyleEquivalents; var leftBufferAmount = 50, itemSelectedEventDelay = 250; var strings = { get badCurrentPage() { return "Invalid argument: currentPage must be a number greater than or equal to zero and be within the bounds of the datasource"; } }; function isFlipper(element) { var control = element.winControl; if (control && control._isFlipView) { return true; } return false; } function flipperPropertyChanged(list) { list.forEach(function (record) { var element = record.target; if (element.winControl && element.tabIndex >= 0) { element.winControl._pageManager._updateTabIndex(element.tabIndex); element.tabIndex = -1; } var that = element.winControl; if (that && that._isFlipView) { var dirChanged = false; if (record.attributeName === "dir") { dirChanged = true; } else if (record.attributeName === "style") { dirChanged = (that._cachedStyleDir !== element.style.direction); } if (dirChanged) { that._cachedStyleDir = element.style.direction; that._pageManager._rtl = _Global.getComputedStyle(that._pageManager._flipperDiv, null).direction === "rtl"; that._pageManager.resized(); } } }); } var _FlipPageManager = _Base.Class.define(function _FlipPageManager_ctor(flipperDiv, panningDiv, panningDivContainer, itemsManager, itemSpacing, environmentSupportsTouch, buttonVisibilityHandler) { // Construction this._visibleElements = []; this._flipperDiv = flipperDiv; this._panningDiv = panningDiv; this._panningDivContainer = panningDivContainer; this._buttonVisibilityHandler = buttonVisibilityHandler; this._currentPage = null; this._rtl = _Global.getComputedStyle(this._flipperDiv, null).direction === "rtl"; this._itemsManager = itemsManager; this._itemSpacing = itemSpacing; this._tabIndex = _ElementUtilities.getTabIndex(flipperDiv); if (this._tabIndex < 0) { this._tabIndex = 0; } panningDiv.tabIndex = -1; flipperDiv.tabIndex = -1; this._tabManager = new _TabContainer.TabContainer(this._panningDivContainer); this._tabManager.tabIndex = this._tabIndex; this._lastSelectedPage = null; this._lastSelectedElement = null; this._bufferSize = _FlipPageManager.flipPageBufferCount; this._cachedSize = -1; this._environmentSupportsTouch = environmentSupportsTouch; var that = this; this._panningDiv.addEventListener("keydown", function (event) { if (that._blockTabs && event.keyCode === _ElementUtilities.Key.tab) { event.stopImmediatePropagation(); event.preventDefault(); } }, true); _ElementUtilities._addEventListener(this._flipperDiv, "focusin", function (event) { if (event.target === that._flipperDiv) { if (that._currentPage.element) { _ElementUtilities._setActive(that._currentPage.element); } } }, false); new _ElementUtilities._MutationObserver(flipperPropertyChanged).observe(this._flipperDiv, { attributes: true, attributeFilter: ["dir", "style", "tabindex"] }); this._cachedStyleDir = this._flipperDiv.style.direction; this._handleManipulationStateChangedBound = this._handleManipulationStateChanged.bind(this); if (this._environmentSupportsTouch) { this._panningDivContainer.addEventListener(_BaseUtils._browserEventEquivalents["manipulationStateChanged"], this._handleManipulationStateChangedBound, true); } }, { // Public Methods initialize: function (initialIndex, isHorizontal) { var currPage = null; // Every call to offsetWidth/offsetHeight causes an switch from Script to Layout which affects // the performance of the control. The values will be cached and will be updated when a resize occurs. this._panningDivContainerOffsetWidth = this._panningDivContainer.offsetWidth; this._panningDivContainerOffsetHeight = this._panningDivContainer.offsetHeight; this._isHorizontal = isHorizontal; if (!this._currentPage) { this._bufferAriaStartMarker = _Global.document.createElement("div"); this._bufferAriaStartMarker.id = uniqueID(this._bufferAriaStartMarker); this._panningDiv.appendChild(this._bufferAriaStartMarker); this._currentPage = this._createFlipPage(null, this); currPage = this._currentPage; this._panningDiv.appendChild(currPage.pageRoot); // flipPageBufferCount is added here twice. // Once for the buffer prior to the current item, and once for the buffer ahead of the current item. var pagesToInit = 2 * this._bufferSize; for (var i = 0; i < pagesToInit; i++) { currPage = this._createFlipPage(currPage, this); this._panningDiv.appendChild(currPage.pageRoot); } this._bufferAriaEndMarker = _Global.document.createElement("div"); this._bufferAriaEndMarker.id = uniqueID(this._bufferAriaEndMarker); this._panningDiv.appendChild(this._bufferAriaEndMarker); } this._prevMarker = this._currentPage.prev.prev; if (this._itemsManager) { this.setNewItemsManager(this._itemsManager, initialIndex); } }, dispose: function () { var curPage = this._currentPage; var tmpPage = curPage; do { _Dispose._disposeElement(tmpPage.element); tmpPage = tmpPage.next; } while (tmpPage !== curPage); }, setOrientation: function (isHorizontal) { if (this._notificationsEndedSignal) { var that = this; this._notificationsEndedSignal.promise.done(function () { that._notificationsEndedSignal = null; that.setOrientation(isHorizontal); }); return; } if (isHorizontal === this._isHorizontal) { return; } this._isOrientationChanging = true; if (this._isHorizontal) { _ElementUtilities.setScrollPosition(this._panningDivContainer, { scrollLeft: this._getItemStart(this._currentPage), scrollTop: 0 }); } else { _ElementUtilities.setScrollPosition(this._panningDivContainer, { scrollLeft: 0, scrollTop: this._getItemStart(this._currentPage) }); } this._isHorizontal = isHorizontal; var containerStyle = this._panningDivContainer.style; containerStyle.overflowX = "hidden"; containerStyle.overflowY = "hidden"; var that = this; _BaseUtils._requestAnimationFrame(function () { that._isOrientationChanging = false; that._forEachPage(function (curr) { var currStyle = curr.pageRoot.style; currStyle.left = "0px"; currStyle.top = "0px"; }); containerStyle.overflowX = ((that._isHorizontal && that._environmentSupportsTouch) ? "scroll" : "hidden"); containerStyle.overflowY = ((that._isHorizontal || !that._environmentSupportsTouch) ? "hidden" : "scroll"); that._ensureCentered(); }); }, resetState: function (initialIndex) { this._writeProfilerMark("WinJS.UI.FlipView:resetState,info"); if (initialIndex !== 0) { var indexValid = this.jumpToIndex(initialIndex, true); if (!indexValid && _BaseUtils.validation) { throw new _ErrorFromName("WinJS.UI.FlipView.BadCurrentPage", strings.badCurrentPage); } return indexValid; } else { _Dispose.disposeSubTree(this._flipperDiv); this._resetBuffer(null, true); var that = this; var work = Promise.wrap(true); if (this._itemsManager) { work = that._itemsManager._firstItem().then(function (e) { that._currentPage.setElement(e); return that._fetchPreviousItems(true). then(function () { return that._fetchNextItems(); }).then(function () { that._setButtonStates(); }); }); } return work.then(function () { that._tabManager.childFocus = that._currentPage.element; that._ensureCentered(); that._itemSettledOn(); }); } }, setNewItemsManager: function (manager, initialIndex) { this._itemsManager = manager; var that = this; return this.resetState(initialIndex).then(function () { // resetState already configures the tabManager, calls _ensureCentered and _itemSettledOn when the initial index is 0 if (initialIndex !== 0) { that._tabManager.childFocus = that._currentPage.element; that._ensureCentered(); that._itemSettledOn(); } }); }, currentIndex: function () { if (!this._itemsManager) { return 0; } var index = 0; var element = (this._navigationAnimationRecord ? this._navigationAnimationRecord.newCurrentElement : this._currentPage.element); if (element) { index = this._getElementIndex(element); } return index; }, resetScrollPos: function () { this._ensureCentered(); }, scrollPosChanged: function () { if (this._hasFocus) { this._hadFocus = true; } if (!this._itemsManager || !this._currentPage.element || this._isOrientationChanging) { return; } var newPos = this._getViewportStart(), bufferEnd = (this._lastScrollPos > newPos ? this._getTailOfBuffer() : this._getHeadOfBuffer()); if (newPos === this._lastScrollPos) { return; } while (this._currentPage.element && this._getItemStart(this._currentPage) > newPos && this._currentPage.prev.element) { this._currentPage = this._currentPage.prev; this._fetchOnePrevious(bufferEnd.prev); bufferEnd = bufferEnd.prev; } while (this._currentPage.element && this._itemEnd(this._currentPage) <= newPos && this._currentPage.next.element) { this._currentPage = this._currentPage.next; this._fetchOneNext(bufferEnd.next); bufferEnd = bufferEnd.next; } this._setButtonStates(); this._checkElementVisibility(false); this._blockTabs = true; this._lastScrollPos = newPos; if (this._currentPage.element) { this._tabManager.childFocus = this._currentPage.element; } this._setListEnds(); if (!this._manipulationState && this._viewportOnItemStart()) { // Setup a timeout to invoke _itemSettledOn in cases where the scroll position is changed, and the control // does not know when it has settled on an item (e.g. 1-finger swipe with narrator touch). this._currentPage.element.setAttribute("aria-setsize", this._cachedSize); this._currentPage.element.setAttribute("aria-posinset", this.currentIndex() + 1); this._timeoutPageSelection(); } }, itemRetrieved: function (real, placeholder) { var that = this; this._forEachPage(function (curr) { if (curr.element === placeholder) { if (curr === that._currentPage || curr === that._currentPage.next) { that._changeFlipPage(curr, placeholder, real); } else { curr.setElement(real, true); } return true; } }); if (this._navigationAnimationRecord && this._navigationAnimationRecord.elementContainers) { var animatingElements = this._navigationAnimationRecord.elementContainers; for (var i = 0, len = animatingElements.length; i < len; i++) { if (animatingElements[i].element === placeholder) { that._changeFlipPage(animatingElements[i], placeholder, real); animatingElements[i].element = real; } } } this._checkElementVisibility(false); }, resized: function () { this._panningDivContainerOffsetWidth = this._panningDivContainer.offsetWidth; this._panningDivContainerOffsetHeight = this._panningDivContainer.offsetHeight; var that = this; this._forEachPage(function (curr) { curr.pageRoot.style.width = that._panningDivContainerOffsetWidth + "px"; curr.pageRoot.style.height = that._panningDivContainerOffsetHeight + "px"; }); // Call _ensureCentered to adjust all the width/height of the pages in the buffer this._ensureCentered(); this._writeProfilerMark("WinJS.UI.FlipView:resize,StopTM"); }, jumpToIndex: function (index, forceJump) { // If we force jumping to an index, we are not interested in making sure that there is distance // between the current and the new index. if (!forceJump) { if (!this._itemsManager || !this._currentPage.element || index < 0) { return Promise.wrap(false); } // If we have to keep our pages in memory, we need to iterate through every single item from our current position to the desired target var currIndex = this._getElementIndex(this._currentPage.element), distance = Math.abs(index - currIndex); if (distance === 0) { return Promise.wrap(false); } } var tail = Promise.wrap(true); var that = this; tail = tail.then(function () { var itemPromise = that._itemsManager._itemPromiseAtIndex(index); return Promise.join({ element: that._itemsManager._itemFromItemPromise(itemPromise), item: itemPromise }).then(function (v) { var elementAtIndex = v.element; // Reset the buffer regardless of whether we have elementAtIndex or not that._resetBuffer(elementAtIndex, forceJump); if (!elementAtIndex) { return false; } that._currentPage.setElement(elementAtIndex); return that._fetchNextItems(). then(function () { return that._fetchPreviousItems(true); }). then(function () { return true; }); }); }); tail = tail.then(function (v) { that._setButtonStates(); return v; }); return tail; }, startAnimatedNavigation: function (goForward, cancelAnimationCallback, completionCallback) { this._writeProfilerMark("WinJS.UI.FlipView:startAnimatedNavigation,info"); if (this._currentPage.element) { var outgoingPage = this._currentPage, incomingPage = (goForward ? this._currentPage.next : this._currentPage.prev); if (incomingPage.element) { if (this._hasFocus) { // Give focus to the panning div ONLY if anything inside the flipview control currently has // focus; otherwise, it will be lost when the current page is animated during the navigation. _ElementUtilities._setActive(this._panningDiv); } this._navigationAnimationRecord = {}; this._navigationAnimationRecord.goForward = goForward; this._navigationAnimationRecord.cancelAnimationCallback = cancelAnimationCallback; this._navigationAnimationRecord.completionCallback = completionCallback; this._navigationAnimationRecord.oldCurrentPage = outgoingPage; this._navigationAnimationRecord.newCurrentPage = incomingPage; var outgoingElement = outgoingPage.element; var incomingElement = incomingPage.element; this._navigationAnimationRecord.newCurrentElement = incomingElement; // When a page element is animated during a navigation, it is temporarily appended on a different container during the animation (see _createDiscardablePage). // However, updates in the data source can happen (change, remove, insert, etc) during the animation affecting the element that is being animated. // Therefore, the page object also maintains the elementUniqueID, and the functions that deal with re-building the internal buffer (shifting/remove/etc) // do all the comparissons, based on the page.elementUniqueID that way even if the element of the page is being animated, we are able to restore/discard it // into the internal buffer back in the correct place. outgoingPage.setElement(null, true); outgoingPage.elementUniqueID = uniqueID(outgoingElement); incomingPage.setElement(null, true); incomingPage.elementUniqueID = uniqueID(incomingElement); var outgoingFlipPage = this._createDiscardablePage(outgoingElement), incomingFlipPage = this._createDiscardablePage(incomingElement); outgoingFlipPage.pageRoot.itemIndex = this._getElementIndex(outgoingElement); incomingFlipPage.pageRoot.itemIndex = outgoingFlipPage.pageRoot.itemIndex + (goForward ? 1 : -1); outgoingFlipPage.pageRoot.style.position = "absolute"; incomingFlipPage.pageRoot.style.position = "absolute"; outgoingFlipPage.pageRoot.style.zIndex = 1; incomingFlipPage.pageRoot.style.zIndex = 2; this._setItemStart(outgoingFlipPage, 0); this._setItemStart(incomingFlipPage, 0); this._blockTabs = true; this._visibleElements.push(incomingElement); this._announceElementVisible(incomingElement); this._navigationAnimationRecord.elementContainers = [outgoingFlipPage, incomingFlipPage]; return { outgoing: outgoingFlipPage, incoming: incomingFlipPage }; } } return null; }, endAnimatedNavigation: function (goForward, outgoing, incoming) { this._writeProfilerMark("WinJS.UI.FlipView:endAnimatedNavigation,info"); if (this._navigationAnimationRecord && this._navigationAnimationRecord.oldCurrentPage && this._navigationAnimationRecord.newCurrentPage) { var outgoingRemoved = this._restoreAnimatedElement(this._navigationAnimationRecord.oldCurrentPage, outgoing); this._restoreAnimatedElement(this._navigationAnimationRecord.newCurrentPage, incoming); if (!outgoingRemoved) { // Advance only when the element in the current page was not removed because if it did, all the pages // were shifted. this._setViewportStart(this._getItemStart(goForward ? this._currentPage.next : this._currentPage.prev)); } this._navigationAnimationRecord = null; this._itemSettledOn(); } }, startAnimatedJump: function (index, cancelAnimationCallback, completionCallback) { this._writeProfilerMark("WinJS.UI.FlipView:startAnimatedJump,info"); if (this._hasFocus) { this._hadFocus = true; } if (this._currentPage.element) { var oldElement = this._currentPage.element; var oldIndex = this._getElementIndex(oldElement); var that = this; return that.jumpToIndex(index).then(function (v) { if (!v) { return null; } that._navigationAnimationRecord = {}; that._navigationAnimationRecord.cancelAnimationCallback = cancelAnimationCallback; that._navigationAnimationRecord.completionCallback = completionCallback; that._navigationAnimationRecord.oldCurrentPage = null; that._forEachPage(function (curr) { if (curr.element === oldElement) { that._navigationAnimationRecord.oldCurrentPage = curr; return true; } }); that._navigationAnimationRecord.newCurrentPage = that._currentPage; if (that._navigationAnimationRecord.newCurrentPage === that._navigationAnimationRecord.oldCurrentPage) { return null; } var newElement = that._currentPage.element; that._navigationAnimationRecord.newCurrentElement = newElement; // When a page element is animated during a jump, it is temporarily appended on a different container during the animation (see _createDiscardablePage). // However, updates in the data source can happen (change, remove, insert, etc) during the animation affecting the element that is being animated. // Therefore, the page object also maintains the elementUniqueID, and the functions that deal with re-building the internal buffer (shifting/remove/etc) // do all the comparissons, based on the page.elementUniqueID that way even if the element of the page is being animated, we are able to restore/discard it // into the internal buffer back in the correct place. that._currentPage.setElement(null, true); that._currentPage.elementUniqueID = uniqueID(newElement); if (that._navigationAnimationRecord.oldCurrentPage) { that._navigationAnimationRecord.oldCurrentPage.setElement(null, true); } var oldFlipPage = that._createDiscardablePage(oldElement), newFlipPage = that._createDiscardablePage(newElement); oldFlipPage.pageRoot.itemIndex = oldIndex; newFlipPage.pageRoot.itemIndex = index; oldFlipPage.pageRoot.style.position = "absolute"; newFlipPage.pageRoot.style.position = "absolute"; oldFlipPage.pageRoot.style.zIndex = 1; newFlipPage.pageRoot.style.zIndex = 2; that._setItemStart(oldFlipPage, 0); that._setItemStart(newFlipPage, that._itemSize(that._currentPage)); that._visibleElements.push(newElement); that._announceElementVisible(newElement); that._navigationAnimationRecord.elementContainers = [oldFlipPage, newFlipPage]; that._blockTabs = true; return { oldPage: oldFlipPage, newPage: newFlipPage }; }); } return Promise.wrap(null); }, simulateMouseWheelScroll: function (ev) { if (this._environmentSupportsTouch || this._waitingForMouseScroll) { return; } var wheelingForward; if (typeof ev.deltaY === 'number') { wheelingForward = (ev.deltaX || ev.deltaY) > 0; } else { wheelingForward = ev.wheelDelta < 0; } var targetPage = wheelingForward ? this._currentPage.next : this._currentPage.prev; if (!targetPage.element) { return; } var zoomToContent = { contentX: 0, contentY: 0, viewportX: 0, viewportY: 0 }; zoomToContent[this._isHorizontal ? "contentX" : "contentY"] = this._getItemStart(targetPage); _ElementUtilities._zoomTo(this._panningDivContainer, zoomToContent); this._waitingForMouseScroll = true; // The 100ms is added to the zoom duration to give the snap feeling where the page sticks // while scrolling _Global.setTimeout(function () { this._waitingForMouseScroll = false; }.bind(this), _ElementUtilities._zoomToDuration + 100); }, endAnimatedJump: function (oldCurr, newCurr) { this._writeProfilerMark("WinJS.UI.FlipView:endAnimatedJump,info"); if (this._navigationAnimationRecord.oldCurrentPage) { this._navigationAnimationRecord.oldCurrentPage.setElement(oldCurr.element, true); } else { if (oldCurr.element.parentNode) { oldCurr.element.parentNode.removeChild(oldCurr.element); } } this._navigationAnimationRecord.newCurrentPage.setElement(newCurr.element, true); this._navigationAnimationRecord = null; this._ensureCentered(); this._itemSettledOn(); }, inserted: function (element, prev, next, animateInsertion) { this._writeProfilerMark("WinJS.UI.FlipView:inserted,info"); var curr = this._prevMarker, passedCurrent = false, elementSuccessfullyPlaced = false; if (animateInsertion) { this._createAnimationRecord(uniqueID(element), null); this._getAnimationRecord(element).inserted = true; } if (!prev) { if (!next) { this._currentPage.setElement(element); } else { while (curr.next !== this._prevMarker && curr.elementUniqueID !== uniqueID(next)) { if (curr === this._currentPage) { passedCurrent = true; } curr = curr.next; } if (curr.elementUniqueID === uniqueID(next) && curr !== this._prevMarker) { curr.prev.setElement(element); elementSuccessfullyPlaced = true; } else { this._releaseElementIfNotAnimated(element); } } } else { do { if (curr === this._currentPage) { passedCurrent = true; } if (curr.elementUniqueID === uniqueID(prev)) { elementSuccessfullyPlaced = true; var pageShifted = curr, lastElementMoved = element, lastElementMovedUniqueID = uniqueID(element), temp; if (passedCurrent) { while (pageShifted.next !== this._prevMarker) { temp = pageShifted.next.element; lastElementMovedUniqueID = pageShifted.next.elementUniqueID; pageShifted.next.setElement(lastElementMoved, true); if (!lastElementMoved && lastElementMovedUniqueID) { // Shift the uniqueID of the page manually since its element is being animated. // This page will not contain the element until the animation completes. pageShifted.next.elementUniqueID = lastElementMovedUniqueID; } lastElementMoved = temp; pageShifted = pageShifted.next; } } else { if (curr.elementUniqueID === curr.next.elementUniqueID && curr.elementUniqueID) { pageShifted = curr.next; } while (pageShifted.next !== this._prevMarker) { temp = pageShifted.element; lastElementMovedUniqueID = pageShifted.elementUniqueID; pageShifted.setElement(lastElementMoved, true); if (!lastElementMoved && lastElementMovedUniqueID) { // Shift the uniqueID of the page manually since its element is being animated. // This page will not contain the element until the animation completes. pageShifted.elementUniqueID = lastElementMovedUniqueID; } lastElementMoved = temp; pageShifted = pageShifted.prev; } } if (lastElementMoved) { var reused = false; this._forEachPage(function (curr) { if (uniqueID(lastElementMoved) === curr.elementUniqueID) { reused = true; return true; } }); if (!reused) { this._releaseElementIfNotAnimated(lastElementMoved); } } break; } curr = curr.next; } while (curr !== this._prevMarker); } this._getAnimationRecord(element).successfullyMoved = elementSuccessfullyPlaced; this._setButtonStates(); }, changed: function (newVal, element) { this._writeProfilerMark("WinJS.UI.FlipView:changed,info"); var that = this; this._forEachPage(function (curr) { if (curr.elementUniqueID === uniqueID(element)) { var record = that._animationRecords[curr.elementUniqueID]; record.changed = true; record.oldElement = element; record.newElement = newVal; curr.element = newVal; // We set curr's element field here so that next/prev works, but we won't update the visual until endNotifications curr.elementUniqueID = uniqueID(newVal); that._animationRecords[uniqueID(newVal)] = record; return true; } }); if (this._navigationAnimationRecord && this._navigationAnimationRecord.elementContainers) { for (var i = 0, len = this._navigationAnimationRecord.elementContainers.length; i < len; i++) { var page = this._navigationAnimationRecord.elementContainers[i]; if (page && page.elementUniqueID === uniqueID(element)) { page.element = newVal; page.elementUniqueID = uniqueID(newVal); } } var newElement = this._navigationAnimationRecord.newCurrentElement; if (newElement && uniqueID(newElement) === uniqueID(element)) { this._navigationAnimationRecord.newCurrentElement = newVal; } } }, moved: function (element, prev, next) { this._writeProfilerMark("WinJS.UI.FlipView:moved,info"); var record = this._getAnimationRecord(element); if (!record) { record = this._createAnimationRecord(uniqueID(element)); } record.moved = true; this.removed(element, false, false); if (prev || next) { this.inserted(element, prev, next, false); } else { record.successfullyMoved = false; } }, removed: function (element, mirage, animateRemoval) { this._writeProfilerMark("WinJS.UI.FlipView:removed,info"); var that = this; var prevMarker = this._prevMarker; var work = Promise.wrap(); if (mirage) { var clearNext = false; this._forEachPage(function (curr) { if (curr.elementUniqueID === uniqueID(element) || clearNext) { curr.setElement(null, true); clearNext = true; } }); this._setButtonStates(); return; } if (animateRemoval) { var record = this._getAnimationRecord(element); if (record) { record.removed = true; } } if (this._currentPage.elementUniqueID === uniqueID(element)) { if (this._currentPage.next.elementUniqueID) { this._shiftLeft(this._currentPage); this._ensureCentered(); } else if (this._currentPage.prev.elementUniqueID) { this._shiftRight(this._currentPage); } else { this._currentPage.setElement(null, true); } } else if (prevMarker.elementUniqueID === uniqueID(element)) { if (prevMarker.next.element) { work = this._itemsManager._previousItem(prevMarker.next.element). then(function (e) { if (e === element) { // Because the VDS and Binding.List can send notifications in // different states we accomodate this here by fixing the case // where VDS hasn't yet removed an item when it sends a removed // or moved notification. // e = that._itemsManager._previousItem(e); } return e; }). then(function (e) { prevMarker.setElement(e, true); }); } else { prevMarker.setElement(null, true); } } else if (prevMarker.prev.elementUniqueID === uniqueID(element)) { if (prevMarker.prev.prev && prevMarker.prev.prev.element) { work = this._itemsManager._nextItem(prevMarker.prev.prev.element). then(function (e) { if (e === element) { // Because the VDS and Binding.List can send notifications in // different states we accomodate this here by fixing the case // where VDS hasn't yet removed an item when it sends a removed // or moved notification. // e = that._itemsManager._nextItem(e); } return e; }). then(function (e) { prevMarker.prev.setElement(e, true); }); } else { prevMarker.prev.setElement(null, true); } } else { var curr = this._currentPage.prev, handled = false; while (curr !== prevMarker && !handled) { if (curr.elementUniqueID === uniqueID(element)) { this._shiftRight(curr); handled = true; } curr = curr.prev; } curr = this._currentPage.next; while (curr !== prevMarker && !handled) { if (curr.elementUniqueID === uniqueID(element)) { this._shiftLeft(curr); handled = true; } curr = curr.next; } } return work.then(function () { that._setButtonStates(); }); }, reload: function () { this._writeProfilerMark("WinJS.UI.FlipView:reload,info"); this.resetState(0); }, getItemSpacing: function () { return this._itemSpacing; }, setItemSpacing: function (space) { this._itemSpacing = space; this._ensureCentered(); }, notificationsStarted: function () { this._writeProfilerMark("WinJS.UI.FlipView:changeNotifications,StartTM"); this._logBuffer(); this._notificationsStarted = this._notificationsStarted || 0; this._notificationsStarted++; // _notificationsEndedSignal is also used in the FlipView unit tests for coordination in the datasource tests this._notificationsEndedSignal = new _Signal(); this._temporaryKeys = []; this._animationRecords = {}; var that = this; this._forEachPage(function (curr) { that._createAnimationRecord(curr.elementUniqueID, curr); }); // Since the current item is defined as the left-most item in the view, the only possible elements that can be in view at any time are // the current item and the item proceeding it. We'll save these two elements for animations during the notificationsEnded cycle this._animationRecords.currentPage = this._currentPage.element; this._animationRecords.nextPage = this._currentPage.next.element; }, notificationsEnded: function () { // The animations are broken down into three parts. // First, we move everything back to where it was before the changes happened. Elements that were inserted between two pages won't have their flip pages moved. // Next, we figure out what happened to the two elements that used to be in view. If they were removed/moved, they get animated as appropriate in this order: // removed, moved // Finally, we figure out how the items that are now in view got there, and animate them as necessary, in this order: moved, inserted. // The moved animation of the last part is joined with the moved animation of the previous part, so in the end it is: // removed -> moved items in view + moved items not in view -> inserted. var that = this; this._endNotificationsWork && this._endNotificationsWork.cancel(); this._endNotificationsWork = this._ensureBufferConsistency().then(function () { var animationPromises = []; that._forEachPage(function (curr) { var record = that._getAnimationRecord(curr.element); if (record) { if (record.changed) { record.oldElement.removedFromChange = true; animationPromises.push(that._changeFlipPage(curr, record.oldElement, record.newElement)); } record.newLocation = curr.location; that._setItemStart(curr, record.originalLocation); if (record.inserted) { curr.elementRoot.style.opacity = 0.0; } } }); function flipPageFromElement(element) { var flipPage = null; that._forEachPage(function (curr) { if (curr.element === element) { flipPage = curr; return true; } }); return flipPage; } function animateOldViewportItemRemoved(record, item) { that._writeProfilerMark("WinJS.UI.FlipView:_animateOldViewportItemRemoved,info"); var removedPage = that._createDiscardablePage(item); that._setItemStart(removedPage, record.originalLocation); animationPromises.push(that._deleteFlipPage(removedPage)); } function animateOldViewportItemMoved(record, item) { that._writeProfilerMark("WinJS.UI.FlipView:_animateOldViewportItemMoved,info"); var newLocation = record.originalLocation, movedPage; if (!record.successfullyMoved) { // If the old visible item got moved, but the next/prev of that item don't match up with anything // currently in our flip page buffer, we need to figure out in which direction it moved. // The exact location doesn't matter since we'll be deleting it anyways, but we do need to // animate it going in the right direction. movedPage = that._createDiscardablePage(item); var indexMovedTo = that._getElementIndex(item); var newCurrentIndex = (that._currentPage.element ? that._getElementIndex(that._currentPage.element) : 0); newLocation += (newCurrentIndex > indexMovedTo ? -100 * that._bufferSize : 100 * that._bufferSize); } else { movedPage = flipPageFromElement(item); newLocation = record.newLocation; } if (movedPage) { that._setItemStart(movedPage, record.originalLocation); animationPromises.push(that._moveFlipPage(movedPage, function () { that._setItemStart(movedPage, newLocation); })); } } var oldCurrent = that._animationRecords.currentPage, oldCurrentRecord = that._getAnimationRecord(oldCurrent), oldNext = that._animationRecords.nextPage, oldNextRecord = that._getAnimationRecord(oldNext); if (oldCurrentRecord && oldCurrentRecord.changed) { oldCurrent = oldCurrentRecord.newElement; } if (oldNextRecord && oldNextRecord.changed) { oldNext = oldNextRecord.newElement; } if (oldCurrent !== that._currentPage.element || oldNext !== that._currentPage.next.element) { if (oldCurrentRecord && oldCurrentRecord.removed) { animateOldViewportItemRemoved(oldCurrentRecord, oldCurrent); } if (oldNextRecord && oldNextRecord.removed) { animateOldViewportItemRemoved(oldNextRecord, oldNext); } } function joinAnimationPromises() { if (animationPromises.length === 0) { animationPromises.push(Promise.wrap()); } return Promise.join(animationPromises); } that._blockTabs = true; joinAnimationPromises().then(function () { animationPromises = []; if (oldCurrentRecord && oldCurrentRecord.moved) { animateOldViewportItemMoved(oldCurrentRecord, oldCurrent); } if (oldNextRecord && oldNextRecord.moved) { animateOldViewportItemMoved(oldNextRecord, oldNext); } var newCurrRecord = that._getAnimationRecord(that._currentPage.element), newNextRecord = that._getAnimationRecord(that._currentPage.next.element); that._forEachPage(function (curr) { var record = that._getAnimationRecord(curr.element); if (record) { if (!record.inserted) { if (record.originalLocation !== record.newLocation) { if ((record !== oldCurrentRecord && record !== oldNextRecord) || (record === oldCurrentRecord && !oldCurrentRecord.moved) || (record === oldNextRecord && !oldNextRecord.moved)) { animationPromises.push(that._moveFlipPage(curr, function () { that._setItemStart(curr, record.newLocation); })); } } } else if (record !== newCurrRecord && record !== newNextRecord) { curr.elementRoot.style.opacity = 1.0; } } }); joinAnimationPromises().then(function () { animationPromises = []; if (newCurrRecord && newCurrRecord.inserted) { animationPromises.push(that._insertFlipPage(that._currentPage)); } if (newNextRecord && newNextRecord.inserted) { animationPromises.push(that._insertFlipPage(that._currentPage.next)); } joinAnimationPromises().then(function () { that._checkElementVisibility(false); that._itemSettledOn(); that._setListEnds(); that._notificationsStarted--; if (that._notificationsStarted === 0) { that._notificationsEndedSignal.complete(); } that._writeProfilerMark("WinJS.UI.FlipView:changeNotifications,StopTM"); that._logBuffer(); that._endNotificationsWork = null; }); }); }); }); }, disableTouchFeatures: function () { this._environmentSupportsTouch = false; var panningContainerStyle = this._panningDivContainer.style; this._panningDivContainer.removeEventListener(_BaseUtils._browserEventEquivalents["manipulationStateChanged"], this._handleManipulationStateChangedBound, true); panningContainerStyle.overflowX = "hidden"; panningContainerStyle.overflowY = "hidden"; var panningContainerPropertiesToClear = [ "scroll-snap-type", "scroll-snap-points-x", "scroll-snap-points-y", "scroll-limit-x-min", "scroll-limit-x-max", "scroll-limit-y-min", "scroll-limit-y-max" ]; panningContainerPropertiesToClear.forEach(function (propertyName) { var platformPropertyName = styleEquivalents[propertyName]; if (platformPropertyName) { panningContainerStyle[platformPropertyName.scriptName] = ""; } }); }, // Private methods _hasFocus: { get: function () { return this._flipperDiv.contains(_Global.document.activeElement); } }, _timeoutPageSelection: function () { var that = this; if (this._lastTimeoutRequest) { this._lastTimeoutRequest.cancel(); } this._lastTimeoutRequest = Promise.timeout(itemSelectedEventDelay).then(function () { that._itemSettledOn(); }); }, _updateTabIndex: function (newIndex) { this._forEachPage(function (curr) { if (curr.element) { curr.element.tabIndex = newIndex; } }); this._tabIndex = newIndex; this._tabManager.tabIndex = newIndex; }, _releaseElementIfNotAnimated: function (element) { var animatedRecord = this._getAnimationRecord(element); if (!(animatedRecord && (animatedRecord.changed || animatedRecord.inserted || animatedRecord.moved || animatedRecord.removed))) { this._itemsManager.releaseItem(element); } }, _getAnimationRecord: function (element) { return (element ? this._animationRecords[uniqueID(element)] : null); }, _createAnimationRecord: function (elementUniqueID, flipPage) { if (elementUniqueID) { var record = this._animationRecords[elementUniqueID] = { removed: false, changed: false, inserted: false }; if (flipPage) { record.originalLocation = flipPage.location; } return record; } }, _writeProfilerMark: function (message) { _WriteProfilerMark(message); if (this._flipperDiv.winControl.constructor._enabledDebug) { _Log.log && _Log.log(message, null, "flipviewdebug"); } }, _getElementIndex: function (element) { var index = 0; try { index = this._itemsManager.itemObject(element).index; } catch (e) { // Failures are expected in cases where items are moved and then deleted. Animations will simply animate as if the item // moved to the beginning of the list. } return index; }, _resetBuffer: function (elementToSave, skipReleases) { this._writeProfilerMark("WinJS.UI.FlipView:_resetBuffer,info"); var head = this._currentPage, curr = head; do { if ((curr.element && curr.element === elementToSave) || skipReleases) { curr.setElement(null, true); } else { curr.setElement(null); } curr = curr.next; } while (curr !== head); }, _getHeadOfBuffer: function () { return this._prevMarker.prev; }, _getTailOfBuffer: function () { return this._prevMarker; }, _insertNewFlipPage: function (prevElement) { this._writeProfilerMark("WinJS.UI.FlipView:_insertNewFlipPage,info"); var newPage = this._createFlipPage(prevElement, this); this._panningDiv.appendChild(newPage.pageRoot); return newPage; }, _fetchNextItems: function () { this._writeProfilerMark("WinJS.UI.FlipView:_fetchNextItems,info"); var tail = Promise.wrap(this._currentPage); var that = this; for (var i = 0; i < this._bufferSize; i++) { tail = tail.then(function (curr) { if (curr.next === that._prevMarker) { that._insertNewFlipPage(curr); } if (curr.element) { return that._itemsManager._nextItem(curr.element). then(function (element) { curr.next.setElement(element); return curr.next; }); } else { curr.next.setElement(null); return curr.next; } }); } return tail; }, _fetchOneNext: function (target) { this._writeProfilerMark("WinJS.UI.FlipView:_fetchOneNext,info"); var prevElement = target.prev.element; // If the target we want to fill with the next item is the end of the circular buffer but we want to keep everything in memory, we've got to increase the buffer size // so that we don't reuse prevMarker. if (this._prevMarker === target) { this._prevMarker = this._prevMarker.next; } if (!prevElement) { target.setElement(null); return; } var that = this; return this._itemsManager._nextItem(prevElement). then(function (element) { target.setElement(element); that._movePageAhead(target.prev, target); }); }, _fetchPreviousItems: function (setPrevMarker) { this._writeProfilerMark("WinJS.UI.FlipView:_fetchPreviousItems,info"); var that = this; var tail = Promise.wrap(this._currentPage); for (var i = 0; i < this._bufferSize; i++) { tail = tail.then(function (curr) { if (curr.element) { return that._itemsManager._previousItem(curr.element). then(function (element) { curr.prev.setElement(element); return curr.prev; }); } else { curr.prev.setElement(null); return curr.prev; } }); } return tail.then(function (curr) { if (setPrevMarker) { that._prevMarker = curr; } }); }, _fetchOnePrevious: function (target) { this._writeProfilerMark("WinJS.UI.FlipView:_fetchOnePrevious,info"); var nextElement = target.next.element; // If the target we want to fill with the previous item is the end of the circular buffer but we want to keep everything in memory, we've got to increase the buffer size // so that we don't reuse prevMarker. We'll add a new element to be prevMarker's prev, then set prevMarker to point to that new element. if (this._prevMarker === target.next) { this._prevMarker = this._prevMarker.prev; } if (!nextElement) { target.setElement(null); return Promise.wrap(); } var that = this; return this._itemsManager._previousItem(nextElement). then(function (element) { target.setElement(element); that._movePageBehind(target.next, target); }); }, _setButtonStates: function () { if (this._currentPage.prev.element) { this._buttonVisibilityHandler.showPreviousButton(); } else { this._buttonVisibilityHandler.hidePreviousButton(); } if (this._currentPage.next.element) { this._buttonVisibilityHandler.showNextButton(); } else { this._buttonVisibilityHandler.hideNextButton(); } }, _ensureCentered: function (delayBoundariesSet) { this._writeProfilerMark("WinJS.UI.FlipView:_ensureCentered,info"); this._setItemStart(this._currentPage, leftBufferAmount * this._viewportSize()); var curr = this._currentPage; while (curr !== this._prevMarker) { this._movePageBehind(curr, curr.prev); curr = curr.prev; } curr = this._currentPage; while (curr.next !== this._prevMarker) { this._movePageAhead(curr, curr.next); curr = curr.next; } var boundariesSet = false; if (this._lastScrollPos && !delayBoundariesSet) { this._setListEnds(); boundariesSet = true; } this._lastScrollPos = this._getItemStart(this._currentPage); this._setViewportStart(this._lastScrollPos); this._checkElementVisibility(true); this._setupSnapPoints(); if (!boundariesSet) { this._setListEnds(); } }, _ensureBufferConsistency: function () { var that = this; var currentElement = this._currentPage.element; if (!currentElement) { return Promise.wrap(); } var refreshBuffer = false; var seenUniqueIDs = {}; var seenLocations = {}; this._forEachPage(function (page) { if (page && page.elementUniqueID) { if (!seenUniqueIDs[page.elementUniqueID]) { seenUniqueIDs[page.elementUniqueID] = true; } else { refreshBuffer = true; return true; } if (page.location > 0) { if (!seenLocations[page.location]) { seenLocations[page.location] = true; } else { refreshBuffer = true; return true; } } } }); var animationKeys = Object.keys(this._animationRecords); animationKeys.forEach(function (key) { var record = that._animationRecords[key]; if (record && (record.changed || record.inserted || record.moved || record.removed)) { refreshBuffer = true; } }); if (refreshBuffer) { this._resetBuffer(null, true); this._currentPage.setElement(currentElement); return this._fetchNextItems(). then(function () { return that._fetchPreviousItems(true); }). then(function () { that._ensureCentered(); }); } else { return Promise.wrap(); } }, _shiftLeft: function (startingPoint) { this._writeProfilerMark("WinJS.UI.FlipView:_shiftLeft,info"); var curr = startingPoint, nextEl = null; while (curr !== this._prevMarker && curr.next !== this._prevMarker) { nextEl = curr.next.element; if (!nextEl && curr.next.elementUniqueID) { // Shift the uniqueID of the page manually since its element is being animated. // This page will not contain the element until the animation completes. curr.elementUniqueID = curr.next.elementUniqueID; } curr.next.setElement(null, true); curr.setElement(nextEl, true); curr = curr.next; } if (curr !== this._prevMarker && curr.prev.element) { var that = this; return this._itemsManager._nextItem(curr.prev.element). then(function (element) { curr.setElement(element); that._createAnimationRecord(curr.elementUniqueID, curr); }); } }, _logBuffer: function () { if (this._flipperDiv.winControl.constructor._enabledDebug) { _Log.log && _Log.log(this._currentPage.next.next.next.elementUniqueID + "\t@:" + this._currentPage.next.next.next.location + (this._currentPage.next.next.next.element ? ("\t" + this._currentPage.next.next.next.element.textContent) : ""), null, "flipviewdebug"); _Log.log && _Log.log(this._currentPage.next.next.next.next.elementUniqueID + "\t@:" + this._currentPage.next.next.next.next.location + (this._currentPage.next.next.next.next.element ? ("\t" + this._currentPage.next.next.next.next.element.textContent) : ""), null, "flipviewdebug"); _Log.log && _Log.log("> " + this._currentPage.elementUniqueID + "\t@:" + this._currentPage.location + (this._currentPage.element ? ("\t" + this._currentPage.element.textContent) : ""), null, "flipviewdebug"); _Log.log && _Log.log(this._currentPage.next.elementUniqueID + "\t@:" + this._currentPage.next.location + (this._currentPage.next.element ? ("\t" + this._currentPage.next.element.textContent) : ""), null, "flipviewdebug"); _Log.log && _Log.log(this._currentPage.next.next.elementUniqueID + "\t@:" + this._currentPage.next.next.location + (this._currentPage.next.next.element ? ("\t" + this._currentPage.next.next.element.textContent) : ""), null, "flipviewdebug"); var keys = Object.keys(this._itemsManager._elementMap); var bufferKeys = []; this._forEachPage(function (page) { if (page && page.elementUniqueID) { bufferKeys.push(page.elementUniqueID); } }); _Log.log && _Log.log("itemsmanager = [" + keys.join(" ") + "] flipview [" + bufferKeys.join(" ") + "]", null, "flipviewdebug"); } }, _shiftRight: function (startingPoint) { this._writeProfilerMark("WinJS.UI.FlipView:_shiftRight,info"); var curr = startingPoint, prevEl = null; while (curr !== this._prevMarker) { prevEl = curr.prev.element; if (!prevEl && curr.prev.elementUniqueID) { // Shift the uniqueID of the page manually since its element is being animated. // This page will not contain the element until the animation completes. curr.elementUniqueID = curr.prev.elementUniqueID; } curr.prev.setElement(null, true); curr.setElement(prevEl, true); curr = curr.prev; } if (curr.next.element) { var that = this; return this._itemsManager._previousItem(curr.next.element). then(function (element) { curr.setElement(element); that._createAnimationRecord(curr.elementUniqueID, curr); }); } }, _checkElementVisibility: function (viewWasReset) { var i, len; if (viewWasReset) { var currentElement = this._currentPage.element; for (i = 0, len = this._visibleElements.length; i < len; i++) { if (this._visibleElements[i] !== currentElement) { this._announceElementInvisible(this._visibleElements[i]); } } this._visibleElements = []; if (currentElement) { this._visibleElements.push(currentElement); this._announceElementVisible(currentElement); } } else { // Elements that have been removed completely from the flipper still need to raise pageVisibilityChangedEvents if they were visible prior to being removed, // so before going through all the elements we go through the ones that we knew were visible and see if they're missing a parentNode. If they are, // the elements were removed and we announce them as invisible. for (i = 0, len = this._visibleElements.length; i < len; i++) { if (!this._visibleElements[i].parentNode || this._visibleElements[i].removedFromChange) { this._announceElementInvisible(this._visibleElements[i]); } } this._visibleElements = []; var that = this; this._forEachPage(function (curr) { var element = curr.element; if (element) { if (that._itemInView(curr)) { that._visibleElements.push(element); that._announceElementVisible(element); } else { that._announceElementInvisible(element); } } }); } }, _announceElementVisible: function (element) { if (element && !element.visible) { element.visible = true; var event = _Global.document.createEvent("CustomEvent"); this._writeProfilerMark("WinJS.UI.FlipView:pageVisibilityChangedEvent(visible:true),info"); event.initCustomEvent(_Constants.pageVisibilityChangedEvent, true, false, { source: this._flipperDiv, visible: true }); element.dispatchEvent(event); } }, _announceElementInvisible: function (element) { if (element && element.visible) { element.visible = false; // Elements that have been removed from the flipper still need to fire invisible events, but they can't do that without being in the DOM. // To fix that, we add the element back into the flipper, fire the event, then remove it. var addedToDomForEvent = false; if (!element.parentNode) { addedToDomForEvent = true; this._panningDivContainer.appendChild(element); } var event = _Global.document.createEvent("CustomEvent"); this._writeProfilerMark("WinJS.UI.FlipView:pageVisibilityChangedEvent(visible:false),info"); event.initCustomEvent(_Constants.pageVisibilityChangedEvent, true, false, { source: this._flipperDiv, visible: false }); element.dispatchEvent(event); if (addedToDomForEvent) { this._panningDivContainer.removeChild(element); } } }, _createDiscardablePage: function (content) { var pageDivs = this._createPageContainer(), page = { pageRoot: pageDivs.root, elementRoot: pageDivs.elementContainer, discardable: true, element: content, elementUniqueID: uniqueID(content), discard: function () { if (page.pageRoot.parentNode) { page.pageRoot.parentNode.removeChild(page.pageRoot); } if (page.element.parentNode) { page.element.parentNode.removeChild(page.element); } } }; page.pageRoot.style.top = "0px"; page.elementRoot.appendChild(content); this._panningDiv.appendChild(page.pageRoot); return page; }, _createPageContainer: function () { var width = this._panningDivContainerOffsetWidth, height = this._panningDivContainerOffsetHeight, parentDiv = _Global.document.createElement("div"), pageStyle = parentDiv.style, flexBox = _Global.document.createElement("div"); flexBox.className = "win-item"; pageStyle.position = "absolute"; pageStyle.overflow = "hidden"; pageStyle.width = width + "px"; pageStyle.height = height + "px"; parentDiv.appendChild(flexBox); return { root: parentDiv, elementContainer: flexBox }; }, _createFlipPage: function (prev, manager) { var page = {}; page.element = null; page.elementUniqueID = null; // The flip pages are managed as a circular doubly-linked list. this.currentItem should always refer to the current item in view, and this._prevMarker marks the point // in the list where the last previous item is stored. Why a circular linked list? // The virtualized flipper reuses its flip pages. When a new item is requested, the flipper needs to reuse an old item from the buffer. In the case of previous items, // the flipper has to go all the way back to the farthest next item in the buffer and recycle it (which is why having a .prev pointer on the farthest previous item is really useful), // and in the case of the next-most item, it needs to recycle next's next (ie, the this._prevMarker). The linked structure comes in really handy when iterating through the list // and separating out prev items from next items (like removed and ensureCentered do). If we were to use a structure like an array it would be pretty messy to do that and still // maintain a buffer of recyclable items. if (!prev) { page.next = page; page.prev = page; } else { page.prev = prev; page.next = prev.next; page.next.prev = page; prev.next = page; } var pageContainer = this._createPageContainer(); page.elementRoot = pageContainer.elementContainer; page.elementRoot.style["msOverflowStyle"] = "auto"; page.pageRoot = pageContainer.root; // Sets the element to display in this flip page page.setElement = function (element, isReplacement) { if (element === undefined) { element = null; } if (element === page.element) { if (!element) { // If there are data source updates during the animation (e.g. item removed), a page element can be set to null when the shiftLeft/Right functions // call this function with a null element. However, since the element in the page is in the middle of an animation its page.elementUniqueID // is still set, so we need to explicitly clear its value so that when the animation completes, the animated element is not // restored back into the internal buffer. page.elementUniqueID = null; } return; } if (page.element) { if (!isReplacement) { manager._itemsManager.releaseItem(page.element); _Dispose._disposeElement(page.element); } } page.element = element; page.elementUniqueID = (element ? uniqueID(element) : null); _ElementUtilities.empty(page.elementRoot); if (page.element) { if (page === manager._currentPage) { manager._tabManager.childFocus = element; } if (!isFlipper(page.element)) { page.element.tabIndex = manager._tabIndex; page.element.setAttribute("role", "option"); page.element.setAttribute("aria-selected", false); if (!page.element.id) { page.element.id = uniqueID(page.element); } var setFlowAttribute = function (source, target, attributeName) { source.setAttribute(attributeName, target.id); }; var isEnd = !page.next.element || page === manager._prevMarker.prev; if (isEnd) { setFlowAttribute(page.element, manager._bufferAriaEndMarker, "aria-flowto"); setFlowAttribute(manager._bufferAriaEndMarker, page.element, "x-ms-aria-flowfrom"); } if (page !== manager._prevMarker && page.prev.element) { setFlowAttribute(page.prev.element, page.element, "aria-flowto"); setFlowAttribute(page.element, page.prev.element, "x-ms-aria-flowfrom"); } if (page.next !== manager._prevMarker && page.next.element) { setFlowAttribute(page.element, page.next.element, "aria-flowto"); setFlowAttribute(page.next.element, page.element, "x-ms-aria-flowfrom"); } if (!page.prev.element) { setFlowAttribute(page.element, manager._bufferAriaStartMarker, "x-ms-aria-flowfrom"); // aria-flowto in the start marker is configured in itemSettledOn to point to the current page in view } } page.elementRoot.appendChild(page.element); } }; return page; }, _itemInView: function (flipPage) { return this._itemEnd(flipPage) > this._getViewportStart() && this._getItemStart(flipPage) < this._viewportEnd(); }, _getViewportStart: function () { if (!this._panningDivContainer.parentNode) { return; } if (this._isHorizontal) { return _ElementUtilities.getScrollPosition(this._panningDivContainer).scrollLeft; } else { return _ElementUtilities.getScrollPosition(this._panningDivContainer).scrollTop; } }, _setViewportStart: function (newValue) { if (!this._panningDivContainer.parentNode) { return; } if (this._isHorizontal) { _ElementUtilities.setScrollPosition(this._panningDivContainer, { scrollLeft: newValue }); } else { _ElementUtilities.setScrollPosition(this._panningDivContainer, { scrollTop: newValue }); } }, _viewportEnd: function () { var element = this._panningDivContainer; if (this._isHorizontal) { if (this._rtl) { return this._getViewportStart() + this._panningDivContainerOffsetWidth; } else { return _ElementUtilities.getScrollPosition(element).scrollLeft + this._panningDivContainerOffsetWidth; } } else { return element.scrollTop + this._panningDivContainerOffsetHeight; } }, _viewportSize: function () { return this._isHorizontal ? this._panningDivContainerOffsetWidth : this._panningDivContainerOffsetHeight; }, _getItemStart: function (flipPage) { return flipPage.location; }, _setItemStart: function (flipPage, newValue) { if (this._isHorizontal) { flipPage.pageRoot.style.left = (this._rtl ? -newValue : newValue) + "px"; } else { flipPage.pageRoot.style.top = newValue + "px"; } flipPage.location = newValue; }, _itemEnd: function (flipPage) { return (this._isHorizontal ? flipPage.location + this._panningDivContainerOffsetWidth : flipPage.location + this._panningDivContainerOffsetHeight) + this._itemSpacing; }, _itemSize: function () { return this._isHorizontal ? this._panningDivContainerOffsetWidth : this._panningDivContainerOffsetHeight; }, _movePageAhead: function (referencePage, pageToPlace) { var delta = this._itemSize(referencePage) + this._itemSpacing; this._setItemStart(pageToPlace, this._getItemStart(referencePage) + delta); }, _movePageBehind: function (referencePage, pageToPlace) { var delta = this._itemSize(referencePage) + this._itemSpacing; this._setItemStart(pageToPlace, this._getItemStart(referencePage) - delta); }, _setupSnapPoints: function () { if (!this._environmentSupportsTouch) { return; } var containerStyle = this._panningDivContainer.style; containerStyle[styleEquivalents["scroll-snap-type"].scriptName] = "mandatory"; var viewportSize = this._viewportSize(); var snapInterval = viewportSize + this._itemSpacing; var propertyName = "scroll-snap-points"; var startSnap = 0; var currPos = this._getItemStart(this._currentPage); startSnap = currPos % (viewportSize + this._itemSpacing); containerStyle[styleEquivalents[(this._isHorizontal ? propertyName + "-x" : propertyName + "-y")].scriptName] = "snapInterval(" + startSnap + "px, " + snapInterval + "px)"; }, _setListEnds: function () { if (!this._environmentSupportsTouch) { return; } if (this._currentPage.element) { var containerStyle = this._panningDivContainer.style, startScroll = 0, endScroll = 0, startNonEmptyPage = this._getTailOfBuffer(), endNonEmptyPage = this._getHeadOfBuffer(), startBoundaryStyle = styleEquivalents["scroll-limit-" + (this._isHorizontal ? "x-min" : "y-min")].scriptName, endBoundaryStyle = styleEquivalents["scroll-limit-" + (this._isHorizontal ? "x-max" : "y-max")].scriptName; while (!endNonEmptyPage.element) { endNonEmptyPage = endNonEmptyPage.prev; // We started at the item before prevMarker (going backwards), so we will exit if all // the pages in the buffer are empty. if (endNonEmptyPage === this._prevMarker.prev) { break; } } while (!startNonEmptyPage.element) { startNonEmptyPage = startNonEmptyPage.next; // We started at prevMarker (going forward), so we will exit if all the pages in the // buffer are empty. if (startNonEmptyPage === this._prevMarker) { break; } } endScroll = this._getItemStart(endNonEmptyPage); startScroll = this._getItemStart(startNonEmptyPage); containerStyle[startBoundaryStyle] = startScroll + "px"; containerStyle[endBoundaryStyle] = endScroll + "px"; } }, _viewportOnItemStart: function () { return this._getItemStart(this._currentPage) === this._getViewportStart(); }, _restoreAnimatedElement: function (oldPage, discardablePage) { var removed = true; // Restore the element in the old page only if it still matches the uniqueID, and the page // does not have new updated content. If the element was removed, it won't be restore in the // old page. if (oldPage.elementUniqueID === uniqueID(discardablePage.element) && !oldPage.element) { oldPage.setElement(discardablePage.element, true); removed = false; } else { // Iterate through the pages to see if the element was moved this._forEachPage(function (curr) { if (curr.elementUniqueID === discardablePage.elementUniqueID && !curr.element) { curr.setElement(discardablePage.element, true); removed = false; } }); } return removed; }, _itemSettledOn: function () { if (this._lastTimeoutRequest) { this._lastTimeoutRequest.cancel(); this._lastTimeoutRequest = null; } var that = this; // Need to yield to the host here _BaseUtils._setImmediate(function () { if (that._viewportOnItemStart()) { that._blockTabs = false; if (that._currentPage.element) { if (that._lastSelectedElement !== that._currentPage.element) { if (that._lastSelectedPage && that._lastSelectedPage.element && !isFlipper(that._lastSelectedPage.element)) { that._lastSelectedPage.element.setAttribute("aria-selected", false); } that._lastSelectedPage = that._currentPage; that._lastSelectedElement = that._currentPage.element; if (!isFlipper(that._currentPage.element)) { that._currentPage.element.setAttribute("aria-selected", true); } // Need to schedule this: // - to be able to register for the pageselected event after instantiating the control and still get the event // - in case a FlipView navigation is triggered inside the pageselected listener (avoid reentering _itemSettledOn) Scheduler.schedule(function FlipView_dispatchPageSelectedEvent() { if (that._currentPage.element) { if (that._hasFocus || that._hadFocus) { that._hadFocus = false; _ElementUtilities._setActive(that._currentPage.element); that._tabManager.childFocus = that._currentPage.element; } var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent(_Constants.pageSelectedEvent, true, false, { source: that._flipperDiv }); that._writeProfilerMark("WinJS.UI.FlipView:pageSelectedEvent,info"); that._currentPage.element.dispatchEvent(event); // Fire the pagecompleted event when the render completes if we are still looking at the same element. // Check that the current element is not null, since the app could've triggered a navigation inside the // pageselected event handler. var originalElement = that._currentPage.element; if (originalElement) { var record = that._itemsManager._recordFromElement(originalElement, true); if (record) { record.renderComplete.then(function () { if (originalElement === that._currentPage.element) { that._currentPage.element.setAttribute("aria-setsize", that._cachedSize); that._currentPage.element.setAttribute("aria-posinset", that.currentIndex() + 1); that._bufferAriaStartMarker.setAttribute("aria-flowto", that._currentPage.element.id); event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent(_Constants.pageCompletedEvent, true, false, { source: that._flipperDiv }); that._writeProfilerMark("WinJS.UI.FlipView:pageCompletedEvent,info"); that._currentPage.element.dispatchEvent(event); } }); } } } }, Scheduler.Priority.normal, null, "WinJS.UI.FlipView._dispatchPageSelectedEvent"); } } } }); }, _forEachPage: function (callback) { var curr = this._prevMarker; do { if (callback(curr)) { break; } curr = curr.next; } while (curr !== this._prevMarker); }, _changeFlipPage: function (page, oldElement, newElement) { this._writeProfilerMark("WinJS.UI.FlipView:_changeFlipPage,info"); page.element = null; if (page.setElement) { page.setElement(newElement, true); } else { // Discardable pages that are created for animations aren't full fleged pages, and won't have some of the functions a normal page would. // changeFlipPage will be called on them when an item that's animating gets fetched. When that happens, we need to replace its element // manually, then center it. oldElement.parentNode.removeChild(oldElement); page.elementRoot.appendChild(newElement); } var style = oldElement.style; style.position = "absolute"; style.left = "0px"; style.top = "0px"; style.opacity = 1.0; page.pageRoot.appendChild(oldElement); oldElement.style.left = Math.max(0, (page.pageRoot.offsetWidth - oldElement.offsetWidth) / 2) + "px"; oldElement.style.top = Math.max(0, (page.pageRoot.offsetHeight - oldElement.offsetHeight) / 2) + "px"; return Animations.fadeOut(oldElement).then(function () { oldElement.parentNode && oldElement.parentNode.removeChild(oldElement); }); }, _deleteFlipPage: function (page) { _WriteProfilerMark("WinJS.UI.FlipView:_deleteFlipPage,info"); page.elementRoot.style.opacity = 0; var animation = Animations.createDeleteFromListAnimation([page.elementRoot]); var that = this; return animation.execute().then(function () { if (page.discardable) { page.discard(); that._itemsManager.releaseItem(page.element); } }); }, _insertFlipPage: function (page) { _WriteProfilerMark("WinJS.UI.FlipView:_insertFlipPage,info"); page.elementRoot.style.opacity = 1.0; var animation = Animations.createAddToListAnimation([page.elementRoot]); return animation.execute().then(function () { if (page.discardable) { page.discard(); } }); }, _moveFlipPage: function (page, move) { _WriteProfilerMark("WinJS.UI.FlipView:_moveFlipPage,info"); var animation = Animations.createRepositionAnimation(page.pageRoot); move(); var that = this; return animation.execute().then(function () { if (page.discardable) { page.discard(); var animationRecord = that._getAnimationRecord(page.element); if (animationRecord && !animationRecord.successfullyMoved) { // If the animationRecord was not succesfully moved, the item is now outside of the buffer, // and we can release it. that._itemsManager.releaseItem(page.element); } } }); }, _handleManipulationStateChanged: function (event) { this._manipulationState = event.currentState; if (event.currentState === 0 && event.target === this._panningDivContainer) { this._itemSettledOn(); this._ensureCentered(); } } }, { supportedForProcessing: false, }); _FlipPageManager.flipPageBufferCount = 2; // The number of items that should surround the current item as a buffer at any time return _FlipPageManager; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/FlipView',[ '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Events', '../Core/_Resources', '../Core/_WriteProfilerMark', '../Animations', '../Animations/_TransitionAnimation', '../BindingList', '../Promise', '../Scheduler', '../Utilities/_Control', '../Utilities/_Dispose', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', '../Utilities/_ItemsManager', '../Utilities/_UI', './FlipView/_Constants', './FlipView/_PageManager', 'require-style!less/desktop/controls', 'require-style!less/phone/controls' ], function flipperInit(_Global, _Base, _BaseUtils, _ErrorFromName, _Events, _Resources, _WriteProfilerMark, Animations, _TransitionAnimation, BindingList, Promise, Scheduler, _Control, _Dispose, _ElementUtilities, _Hoverable, _ItemsManager, _UI, _Constants, _PageManager) { "use strict"; _Base.Namespace.define("WinJS.UI", { /// /// /// Displays a collection, such as a set of photos, one item at a time. /// /// /// /// /// ]]> /// Raised when the number of items in the itemDataSource changes. /// Raised when a FlipView page becomes visible or invisible. /// Raised when the FlipView flips to a page. /// Raised when the FlipView flips to a page and its renderer function completes. /// The entire FlipView control. /// The general class for all FlipView navigation buttons. /// The left navigation button. /// The right navigation button. /// The top navigation button. /// The bottom navigation button. /// /// /// FlipView: _Base.Namespace._lazy(function () { // Class names var navButtonClass = "win-navbutton", flipViewClass = "win-flipview", navButtonLeftClass = "win-navleft", navButtonRightClass = "win-navright", navButtonTopClass = "win-navtop", navButtonBottomClass = "win-navbottom"; // Aria labels var previousButtonLabel = "Previous", nextButtonLabel = "Next"; var buttonFadeDelay = 3000, avoidTrapDelay = 500, leftArrowGlyph = "", rightArrowGlyph = "", topArrowGlyph = "", bottomArrowGlyph = "", animationMoveDelta = 40; function flipViewPropertyChanged(list) { var that = list[0].target.winControl; if (that && that instanceof FlipView) { if (list.some(function (record) { if (record.attributeName === "dir") { return true; } else if (record.attributeName === "style") { return (that._cachedStyleDir !== record.target.style.direction); } else { return false; } })) { that._cachedStyleDir = that._flipviewDiv.style.direction; that._rtl = _Global.getComputedStyle(that._flipviewDiv, null).direction === "rtl"; that._setupOrientation(); } } } function flipviewResized(e) { var that = e.target && e.target.winControl; if (that && that instanceof FlipView) { _WriteProfilerMark("WinJS.UI.FlipView:resize,StartTM"); that._resize(); } } var strings = { get badAxis() { return "Invalid argument: orientation must be a string, either 'horizontal' or 'vertical'"; }, get badCurrentPage() { return "Invalid argument: currentPage must be a number greater than or equal to zero and be within the bounds of the datasource"; }, get noitemsManagerForCount() { return "Invalid operation: can't get count if no dataSource has been set"; }, get badItemSpacingAmount() { return "Invalid argument: itemSpacing must be a number greater than or equal to zero"; }, get navigationDuringStateChange() { return "Error: After changing itemDataSource or itemTemplate, any navigation in the FlipView control should be delayed until the pageselected event is fired."; }, get panningContainerAriaLabel() { return _Resources._getWinJSString("ui/flipViewPanningContainerAriaLabel").value; } }; var FlipView = _Base.Class.define(function FlipView_ctor(element, options) { /// /// /// Creates a new FlipView. /// /// /// The DOM element that hosts the control. /// /// /// An object that contains one or more property/value pairs to apply to the new control. /// Each property of the options object corresponds to one of the control's properties or events. /// Event names must begin with "on". For example, to provide a handler for the pageselected event, /// add a property named "onpageselected" to the options object and set its value to the event handler. /// This parameter is optional. /// /// /// The new FlipView control. /// /// _WriteProfilerMark("WinJS.UI.FlipView:constructor,StartTM"); this._disposed = false; element = element || _Global.document.createElement("div"); var isHorizontal = true, dataSource = null, itemRenderer = _ItemsManager._trivialHtmlRenderer, initialIndex = 0, itemSpacing = 0; if (options) { // flipAxis parameter checking. Must be a string, either "horizontal" or "vertical" if (options.orientation) { if (typeof options.orientation === "string") { switch (options.orientation.toLowerCase()) { case "horizontal": isHorizontal = true; break; case "vertical": isHorizontal = false; break; } } } if (options.currentPage) { initialIndex = options.currentPage >> 0; initialIndex = initialIndex < 0 ? 0 : initialIndex; } if (options.itemDataSource) { dataSource = options.itemDataSource; } if (options.itemTemplate) { itemRenderer = this._getItemRenderer(options.itemTemplate); } if (options.itemSpacing) { itemSpacing = options.itemSpacing >> 0; itemSpacing = itemSpacing < 0 ? 0 : itemSpacing; } } if (!dataSource) { var list = new BindingList.List(); dataSource = list.dataSource; } _ElementUtilities.empty(element); // Set _flipviewDiv so the element getter works correctly, then call _setOption with eventsOnly flag on before calling _initializeFlipView // so that event listeners are added before page loading this._flipviewDiv = element; element.winControl = this; _Control._setOptions(this, options, true); this._initializeFlipView(element, isHorizontal, dataSource, itemRenderer, initialIndex, itemSpacing); _ElementUtilities.addClass(element, "win-disposable"); this._avoidTrappingTime = 0; this._windowWheelHandlerBound = this._windowWheelHandler.bind(this); _ElementUtilities._globalListener.addEventListener(element, 'wheel', this._windowWheelHandlerBound); _ElementUtilities._globalListener.addEventListener(element, 'mousewheel', this._windowWheelHandlerBound); _WriteProfilerMark("WinJS.UI.FlipView:constructor,StopTM"); }, { // Public methods dispose: function FlipView_dispose() { /// /// /// Disposes this FlipView. /// /// _WriteProfilerMark("WinJS.UI.FlipView:dispose,StopTM"); if (this._disposed) { return; } _ElementUtilities._globalListener.removeEventListener(this._flipviewDiv, 'wheel', this._windowWheelHandlerBound); _ElementUtilities._globalListener.removeEventListener(this._flipviewDiv, 'mousewheel', this._windowWheelHandlerBound); _ElementUtilities._resizeNotifier.unsubscribe(this._flipviewDiv, flipviewResized); this._disposed = true; this._pageManager.dispose(); this._itemsManager.release(); this.itemDataSource = null; }, next: function FlipView_next() { /// /// /// Navigates to the next item. /// /// /// true if the FlipView begins navigating to the next page; /// false if the FlipView is at the last page or is in the middle of another navigation animation. /// /// _WriteProfilerMark("WinJS.UI.FlipView:next,info"); var cancelAnimationCallback = this._nextAnimation ? null : this._cancelDefaultAnimation; return this._navigate(true, cancelAnimationCallback); }, previous: function FlipView_previous() { /// /// /// Navigates to the previous item. /// /// /// true if FlipView begins navigating to the previous page; /// false if the FlipView is already at the first page or is in the middle of another navigation animation. /// /// _WriteProfilerMark("WinJS.UI.FlipView:prev,info"); var cancelAnimationCallback = this._prevAnimation ? null : this._cancelDefaultAnimation; return this._navigate(false, cancelAnimationCallback); }, /// element: { get: function () { return this._flipviewDiv; } }, /// /// Gets or sets the index of the currently displayed page. The minimum value is 0 and the maximum value is one less than the total number of items returned by the data source. /// currentPage: { get: function () { return this._getCurrentIndex(); }, set: function (index) { _WriteProfilerMark("WinJS.UI.FlipView:set_currentPage,info"); if (this._pageManager._notificationsEndedSignal) { var that = this; this._pageManager._notificationsEndedSignal.promise.done(function () { that._pageManager._notificationsEndedSignal = null; that.currentPage = index; }); return; } if (this._animating && !this._cancelAnimation()) { return; } index = index >> 0; index = index < 0 ? 0 : index; if (this._refreshTimer) { this._indexAfterRefresh = index; } else { if (this._pageManager._cachedSize > 0) { index = Math.min(this._pageManager._cachedSize - 1, index); } else if (this._pageManager._cachedSize === 0) { index = 0; } var that = this; if (this._jumpingToIndex === index) { return; } var clearJumpToIndex = function () { that._jumpingToIndex = null; }; this._jumpingToIndex = index; var jumpAnimation = (this._jumpAnimation ? this._jumpAnimation : this._defaultAnimation.bind(this)), cancelAnimationCallback = (this._jumpAnimation ? null : this._cancelDefaultAnimation), completionCallback = function () { that._completeJump(); }; this._pageManager.startAnimatedJump(index, cancelAnimationCallback, completionCallback). then(function (elements) { if (elements) { that._animationsStarted(); var currElement = elements.oldPage.pageRoot; var newCurrElement = elements.newPage.pageRoot; that._contentDiv.appendChild(currElement); that._contentDiv.appendChild(newCurrElement); that._completeJumpPending = true; jumpAnimation(currElement, newCurrElement). then(function () { if (that._completeJumpPending) { completionCallback(); _WriteProfilerMark("WinJS.UI.FlipView:set_currentPage.animationComplete,info"); } }).done(clearJumpToIndex, clearJumpToIndex); } else { clearJumpToIndex(); } }, clearJumpToIndex); } } }, /// /// Gets or sets the layout orientation of the FlipView, horizontal or vertical. /// orientation: { get: function () { return this._axisAsString(); }, set: function (orientation) { _WriteProfilerMark("WinJS.UI.FlipView:set_orientation,info"); var isHorizontal = orientation === "horizontal"; if (isHorizontal !== this._isHorizontal) { this._isHorizontal = isHorizontal; this._setupOrientation(); this._pageManager.setOrientation(this._isHorizontal); } } }, /// /// Gets or sets the data source that provides the FlipView with items to display. /// The FlipView displays one item at a time, each on its own page. /// itemDataSource: { get: function () { return this._dataSource; }, set: function (dataSource) { _WriteProfilerMark("WinJS.UI.FlipView:set_itemDataSource,info"); this._dataSourceAfterRefresh = dataSource || new BindingList.List().dataSource; this._refresh(); } }, /// /// Gets or sets a WinJS.Binding.Template or a function that defines the HTML for each item's page. /// itemTemplate: { get: function () { return this._itemRenderer; }, set: function (itemTemplate) { _WriteProfilerMark("WinJS.UI.FlipView:set_itemTemplate,info"); this._itemRendererAfterRefresh = this._getItemRenderer(itemTemplate); this._refresh(); } }, /// /// Gets or sets the spacing between items, in pixels. /// itemSpacing: { get: function () { return this._pageManager.getItemSpacing(); }, set: function (spacing) { _WriteProfilerMark("WinJS.UI.FlipView:set_itemSpacing,info"); spacing = spacing >> 0; spacing = spacing < 0 ? 0 : spacing; this._pageManager.setItemSpacing(spacing); } }, count: function FlipView_count() { /// /// /// Returns the number of items in the FlipView object's itemDataSource. /// /// /// A Promise that contains the number of items in the list /// or WinJS.UI.CountResult.unknown if the count is unavailable. /// /// _WriteProfilerMark("WinJS.UI.FlipView:count,info"); var that = this; return new Promise(function (complete, error) { if (that._itemsManager) { if (that._pageManager._cachedSize === _UI.CountResult.unknown || that._pageManager._cachedSize >= 0) { complete(that._pageManager._cachedSize); } else { that._dataSource.getCount().then(function (count) { that._pageManager._cachedSize = count; complete(count); }); } } else { error(FlipView.noitemsManagerForCount); } }); }, setCustomAnimations: function FlipView_setCustomAnimations(animations) { /// /// /// Sets custom animations for the FlipView to use when navigating between pages. /// /// /// An object containing up to three fields, one for each navigation action: next, previous, and jump /// Each of those fields must be a function with this signature: function (outgoingPage, incomingPage). /// This function returns a WinJS.Promise object that completes once the animations are finished. /// If a field is null or undefined, the FlipView reverts to its default animation for that action. /// /// _WriteProfilerMark("WinJS.UI.FlipView:setCustomAnimations,info"); if (animations.next !== undefined) { this._nextAnimation = animations.next; } if (animations.previous !== undefined) { this._prevAnimation = animations.previous; } if (animations.jump !== undefined) { this._jumpAnimation = animations.jump; } }, forceLayout: function FlipView_forceLayout() { /// /// /// Forces the FlipView to update its layout. /// Use this function when making the FlipView visible again after its style.display property had been set to "none". /// /// _WriteProfilerMark("WinJS.UI.FlipView:forceLayout,info"); this._pageManager.resized(); }, // Private members _initializeFlipView: function FlipView_initializeFlipView(element, isHorizontal, dataSource, itemRenderer, initialIndex, itemSpacing) { var that = this; var flipViewInitialized = false; this._flipviewDiv = element; _ElementUtilities.addClass(this._flipviewDiv, flipViewClass); this._contentDiv = _Global.document.createElement("div"); this._panningDivContainer = _Global.document.createElement("div"); this._panningDivContainer.className = "win-surface"; this._panningDiv = _Global.document.createElement("div"); this._prevButton = _Global.document.createElement("button"); this._nextButton = _Global.document.createElement("button"); this._isHorizontal = isHorizontal; this._dataSource = dataSource; this._itemRenderer = itemRenderer; this._itemsManager = null; this._pageManager = null; var stylesRequiredForFullFeatureMode = [ "scroll-limit-x-max", "scroll-limit-x-min", "scroll-limit-y-max", "scroll-limit-y-min", "scroll-snap-type", "scroll-snap-x", "scroll-snap-y", "overflow-style", ]; var allFeaturesSupported = true, styleEquivalents = _BaseUtils._browserStyleEquivalents; for (var i = 0, len = stylesRequiredForFullFeatureMode.length; i < len; i++) { allFeaturesSupported = allFeaturesSupported && !!(styleEquivalents[stylesRequiredForFullFeatureMode[i]]); } allFeaturesSupported = allFeaturesSupported && !!_BaseUtils._browserEventEquivalents["manipulationStateChanged"]; allFeaturesSupported = allFeaturesSupported && _ElementUtilities._supportsSnapPoints; this._environmentSupportsTouch = allFeaturesSupported; var accName = this._flipviewDiv.getAttribute("aria-label"); if (!accName) { this._flipviewDiv.setAttribute("aria-label", ""); } this._flipviewDiv.setAttribute("role", "listbox"); if (!this._flipviewDiv.style.overflow) { this._flipviewDiv.style.overflow = "hidden"; } this._contentDiv.style.position = "relative"; this._contentDiv.style.zIndex = 0; this._contentDiv.style.width = "100%"; this._contentDiv.style.height = "100%"; this._panningDiv.style.position = "relative"; this._panningDivContainer.style.position = "relative"; this._panningDivContainer.style.width = "100%"; this._panningDivContainer.style.height = "100%"; this._panningDivContainer.setAttribute("role", "group"); this._panningDivContainer.setAttribute("aria-label", strings.panningContainerAriaLabel); this._contentDiv.appendChild(this._panningDivContainer); this._flipviewDiv.appendChild(this._contentDiv); this._panningDiv.style.width = "100%"; this._panningDiv.style.height = "100%"; this._setupOrientation(); function setUpButton(button) { button.setAttribute("aria-hidden", true); button.style.visibility = "hidden"; button.style.opacity = 0.0; button.tabIndex = -1; button.style.zIndex = 1000; } setUpButton(this._prevButton); setUpButton(this._nextButton); this._prevButton.setAttribute("aria-label", previousButtonLabel); this._nextButton.setAttribute("aria-label", nextButtonLabel); this._prevButton.setAttribute("type", "button"); this._nextButton.setAttribute("type", "button"); this._panningDivContainer.appendChild(this._panningDiv); this._contentDiv.appendChild(this._prevButton); this._contentDiv.appendChild(this._nextButton); this._itemsManagerCallback = { // Callbacks for itemsManager inserted: function FlipView_inserted(itemPromise, previousHandle, nextHandle) { that._itemsManager._itemFromPromise(itemPromise).then(function (element) { var previous = that._itemsManager._elementFromHandle(previousHandle); var next = that._itemsManager._elementFromHandle(nextHandle); that._pageManager.inserted(element, previous, next, true); }); }, countChanged: function FlipView_countChanged(newCount, oldCount) { that._pageManager._cachedSize = newCount; // Don't fire the datasourcecountchanged event when there is a state transition if (oldCount !== _UI.CountResult.unknown) { that._fireDatasourceCountChangedEvent(); } }, changed: function FlipView_changed(newElement, oldElement) { that._pageManager.changed(newElement, oldElement); }, moved: function FlipView_moved(element, prev, next, itemPromise) { var elementReady = function (element) { that._pageManager.moved(element, prev, next); }; // If we haven't instantiated this item yet, do so now if (!element) { that._itemsManager._itemFromPromise(itemPromise).then(elementReady); } else { elementReady(element); } }, removed: function FlipView_removed(element, mirage) { if (element) { that._pageManager.removed(element, mirage, true); } }, knownUpdatesComplete: function FlipView_knownUpdatesComplete() { }, beginNotifications: function FlipView_beginNotifications() { that._cancelAnimation(); that._pageManager.notificationsStarted(); }, endNotifications: function FlipView_endNotifications() { that._pageManager.notificationsEnded(); }, itemAvailable: function FlipView_itemAvailable(real, placeholder) { that._pageManager.itemRetrieved(real, placeholder); }, reload: function FlipView_reload() { that._pageManager.reload(); } }; if (this._dataSource) { this._itemsManager = _ItemsManager._createItemsManager(this._dataSource, this._itemRenderer, this._itemsManagerCallback, { ownerElement: this._flipviewDiv }); } this._pageManager = new _PageManager._FlipPageManager(this._flipviewDiv, this._panningDiv, this._panningDivContainer, this._itemsManager, itemSpacing, this._environmentSupportsTouch, { hidePreviousButton: function () { that._hasPrevContent = false; that._fadeOutButton("prev"); that._prevButton.setAttribute("aria-hidden", true); }, showPreviousButton: function () { that._hasPrevContent = true; that._fadeInButton("prev"); that._prevButton.setAttribute("aria-hidden", false); }, hideNextButton: function () { that._hasNextContent = false; that._fadeOutButton("next"); that._nextButton.setAttribute("aria-hidden", true); }, showNextButton: function () { that._hasNextContent = true; that._fadeInButton("next"); that._nextButton.setAttribute("aria-hidden", false); } }); this._pageManager.initialize(initialIndex, this._isHorizontal); this._dataSource.getCount().then(function (count) { that._pageManager._cachedSize = count; }); this._prevButton.addEventListener("click", function () { that.previous(); }, false); this._nextButton.addEventListener("click", function () { that.next(); }, false); new _ElementUtilities._MutationObserver(flipViewPropertyChanged).observe(this._flipviewDiv, { attributes: true, attributeFilter: ["dir", "style"] }); this._cachedStyleDir = this._flipviewDiv.style.direction; this._flipviewDiv.addEventListener("mselementresize", flipviewResized); _ElementUtilities._resizeNotifier.subscribe(this._flipviewDiv, flipviewResized); this._contentDiv.addEventListener("mouseleave", function () { that._mouseInViewport = false; }, false); var PT_TOUCH = _ElementUtilities._MSPointerEvent.MSPOINTER_TYPE_TOUCH || "touch"; function handleShowButtons(e) { if (e.pointerType !== PT_TOUCH) { that._touchInteraction = false; if (e.screenX === that._lastMouseX && e.screenY === that._lastMouseY) { return; } that._lastMouseX = e.screenX; that._lastMouseY = e.screenY; that._mouseInViewport = true; that._fadeInButton("prev"); that._fadeInButton("next"); that._fadeOutButtons(); } } function handlePointerDown(e) { if (e.pointerType === PT_TOUCH) { that._mouseInViewport = false; that._touchInteraction = true; that._fadeOutButtons(true); } else { that._touchInteraction = false; if (!that._isInteractive(e.target)) { // Disable the default behavior of the mouse wheel button to avoid auto-scroll if ((e.buttons & 4) !== 0) { e.stopPropagation(); e.preventDefault(); } } } } function handlePointerUp(e) { if (e.pointerType !== PT_TOUCH) { that._touchInteraction = false; } } if (this._environmentSupportsTouch) { _ElementUtilities._addEventListener(this._contentDiv, "pointerdown", handlePointerDown, false); _ElementUtilities._addEventListener(this._contentDiv, "pointermove", handleShowButtons, false); _ElementUtilities._addEventListener(this._contentDiv, "pointerup", handlePointerUp, false); } this._panningDivContainer.addEventListener("scroll", function () { that._scrollPosChanged(); }, false); this._panningDiv.addEventListener("blur", function () { if (!that._touchInteraction) { that._fadeOutButtons(); } }, true); // Scroll position isn't maintained when an element is added/removed from // the DOM so every time we are placed back in, let the PageManager // fix the scroll position. var initiallyParented = _Global.document.body.contains(this._flipviewDiv); _ElementUtilities._addInsertedNotifier(this._flipviewDiv); this._flipviewDiv.addEventListener("WinJSNodeInserted", function (event) { // WinJSNodeInserted fires even if the element is already in the DOM if (initiallyParented) { initiallyParented = false; return; } that._pageManager.resized(); }, false); this._flipviewDiv.addEventListener("keydown", function (event) { var cancelBubbleIfHandled = true; if (!that._isInteractive(event.target)) { var Key = _ElementUtilities.Key, handled = false; if (that._isHorizontal) { switch (event.keyCode) { case Key.leftArrow: (that._rtl ? that.next() : that.previous()); handled = true; break; case Key.pageUp: that.previous(); handled = true; break; case Key.rightArrow: (that._rtl ? that.previous() : that.next()); handled = true; break; case Key.pageDown: that.next(); handled = true; break; // Prevent scrolling pixel by pixel, but let the event bubble up case Key.upArrow: case Key.downArrow: handled = true; cancelBubbleIfHandled = false; break; } } else { switch (event.keyCode) { case Key.upArrow: case Key.pageUp: that.previous(); handled = true; break; case Key.downArrow: case Key.pageDown: that.next(); handled = true; break; case Key.space: handled = true; break; } } switch (event.keyCode) { case Key.home: that.currentPage = 0; handled = true; break; case Key.end: if (that._pageManager._cachedSize > 0) { that.currentPage = that._pageManager._cachedSize - 1; } handled = true; break; } if (handled) { event.preventDefault(); if (cancelBubbleIfHandled) { event.stopPropagation(); } return true; } } }, false); flipViewInitialized = true; }, _windowWheelHandler: function FlipView_windowWheelHandler(ev) { // When you are using the mouse wheel to scroll a horizontal area such as a WinJS.UI.Hub and one of the sections // has a WinJS.UI.FlipView you may get stuck on that item. This logic is to allow a scroll event to skip the flipview's // overflow scroll div and instead go to the parent scroller. We only skip the scroll wheel event for a fixed amount of time ev = ev.detail.originalEvent; var wheelWithinFlipper = ev.target && (this._flipviewDiv.contains(ev.target) || this._flipviewDiv === ev.target); var that = this; var now = _BaseUtils._now(); var withinAvoidTime = this._avoidTrappingTime > now; if (!wheelWithinFlipper || withinAvoidTime) { this._avoidTrappingTime = now + avoidTrapDelay; } if (wheelWithinFlipper && withinAvoidTime) { this._panningDivContainer.style["overflowX"] = "hidden"; this._panningDivContainer.style["overflowY"] = "hidden"; _BaseUtils._yieldForDomModification(function () { // Avoid being stuck between items that._pageManager._ensureCentered(); if (that._isHorizontal) { that._panningDivContainer.style["overflowX"] = (that._environmentSupportsTouch ? "scroll" : "hidden"); that._panningDivContainer.style["overflowY"] = "hidden"; } else { that._panningDivContainer.style["overflowY"] = (that._environmentSupportsTouch ? "scroll" : "hidden"); that._panningDivContainer.style["overflowX"] = "hidden"; } }); } else if (wheelWithinFlipper) { this._pageManager.simulateMouseWheelScroll(ev); } }, _isInteractive: function FlipView_isInteractive(element) { if (element.parentNode) { var matches = element.parentNode.querySelectorAll(".win-interactive, .win-interactive *"); for (var i = 0, len = matches.length; i < len; i++) { if (matches[i] === element) { return true; } } } return false; }, _refreshHandler: function FlipView_refreshHandler() { var dataSource = this._dataSourceAfterRefresh || this._dataSource, renderer = this._itemRendererAfterRefresh || this._itemRenderer, initialIndex = this._indexAfterRefresh || 0; this._setDatasource(dataSource, renderer, initialIndex); this._dataSourceAfterRefresh = null; this._itemRendererAfterRefresh = null; this._indexAfterRefresh = 0; this._refreshTimer = false; }, _refresh: function FlipView_refresh() { if (!this._refreshTimer) { var that = this; this._refreshTimer = true; // Batch calls to _refresh Scheduler.schedule(function FlipView_refreshHandler() { if (that._refreshTimer && !that._disposed) { that._refreshHandler(); } }, Scheduler.Priority.high, null, "WinJS.UI.FlipView._refreshHandler"); } }, _getItemRenderer: function FlipView_getItemRenderer(itemTemplate) { var itemRenderer = null; if (typeof itemTemplate === "function") { var itemPromise = new Promise(function () { }); var itemTemplateResult = itemTemplate(itemPromise); if (itemTemplateResult.element) { if (typeof itemTemplateResult.element === "object" && typeof itemTemplateResult.element.then === "function") { // This renderer returns a promise to an element itemRenderer = function (itemPromise) { var elementRoot = _Global.document.createElement("div"); elementRoot.className = "win-template"; _Dispose.markDisposable(elementRoot); return { element: elementRoot, renderComplete: itemTemplate(itemPromise).element.then(function (element) { elementRoot.appendChild(element); }) }; }; } else { // This renderer already returns a placeholder itemRenderer = itemTemplate; } } else { // Return a renderer that has return a placeholder itemRenderer = function (itemPromise) { var elementRoot = _Global.document.createElement("div"); elementRoot.className = "win-template"; _Dispose.markDisposable(elementRoot); // The pagecompleted event relies on this elementRoot // to ensure that we are still looking at the same // item after the render completes. return { element: elementRoot, renderComplete: itemPromise.then(function () { return Promise.as(itemTemplate(itemPromise)).then(function (element) { elementRoot.appendChild(element); }); }) }; }; } } else if (typeof itemTemplate === "object") { itemRenderer = itemTemplate.renderItem; } return itemRenderer; }, _navigate: function FlipView_navigate(goForward, cancelAnimationCallback) { if (_BaseUtils.validation && this._refreshTimer) { throw new _ErrorFromName("WinJS.UI.FlipView.NavigationDuringStateChange", strings.navigationDuringStateChange); } if (!this._animating) { this._animatingForward = goForward; } this._goForward = goForward; if (this._animating && !this._cancelAnimation()) { return false; } var that = this; var customAnimation = (goForward ? this._nextAnimation : this._prevAnimation), animation = (customAnimation ? customAnimation : this._defaultAnimation.bind(this)), completionCallback = function (goForward) { that._completeNavigation(goForward); }, elements = this._pageManager.startAnimatedNavigation(goForward, cancelAnimationCallback, completionCallback); if (elements) { this._animationsStarted(); var outgoingElement = elements.outgoing.pageRoot, incomingElement = elements.incoming.pageRoot; this._contentDiv.appendChild(outgoingElement); this._contentDiv.appendChild(incomingElement); this._completeNavigationPending = true; animation(outgoingElement, incomingElement).then(function () { if (that._completeNavigationPending) { completionCallback(that._goForward); } }).done(); return true; } else { return false; } }, _cancelDefaultAnimation: function FlipView_cancelDefaultAnimation(outgoingElement, incomingElement) { // Cancel the fadeOut animation outgoingElement.style.opacity = 0; // Cancel the enterContent animation incomingElement.style.animationName = ""; incomingElement.style.opacity = 1; }, _cancelAnimation: function FlipView_cancelAnimation() { if (this._pageManager._navigationAnimationRecord && this._pageManager._navigationAnimationRecord.completionCallback) { var cancelCallback = this._pageManager._navigationAnimationRecord.cancelAnimationCallback; if (cancelCallback) { cancelCallback = cancelCallback.bind(this); } if (this._pageManager._navigationAnimationRecord && this._pageManager._navigationAnimationRecord.elementContainers) { var outgoingPage = this._pageManager._navigationAnimationRecord.elementContainers[0], incomingPage = this._pageManager._navigationAnimationRecord.elementContainers[1], outgoingElement = outgoingPage.pageRoot, incomingElement = incomingPage.pageRoot; // Invoke the function that will cancel the animation if (cancelCallback) { cancelCallback(outgoingElement, incomingElement); } // Invoke the completion function after cancelling the animation this._pageManager._navigationAnimationRecord.completionCallback(this._animatingForward); return true; } } return false; }, _completeNavigation: function FlipView_completeNavigation(goForward) { if (this._disposed) { return; } this._pageManager._resizing = false; if (this._pageManager._navigationAnimationRecord && this._pageManager._navigationAnimationRecord.elementContainers) { var outgoingPage = this._pageManager._navigationAnimationRecord.elementContainers[0], incomingPage = this._pageManager._navigationAnimationRecord.elementContainers[1], outgoingElement = outgoingPage.pageRoot, incomingElement = incomingPage.pageRoot; if (outgoingElement.parentNode) { outgoingElement.parentNode.removeChild(outgoingElement); } if (incomingElement.parentNode) { incomingElement.parentNode.removeChild(incomingElement); } this._pageManager.endAnimatedNavigation(goForward, outgoingPage, incomingPage); this._fadeOutButtons(); this._scrollPosChanged(); this._pageManager._ensureCentered(true); this._animationsFinished(); } this._completeNavigationPending = false; }, _completeJump: function FlipView_completeJump() { if (this._disposed) { return; } this._pageManager._resizing = false; if (this._pageManager._navigationAnimationRecord && this._pageManager._navigationAnimationRecord.elementContainers) { var outgoingPage = this._pageManager._navigationAnimationRecord.elementContainers[0], incomingPage = this._pageManager._navigationAnimationRecord.elementContainers[1], outgoingElement = outgoingPage.pageRoot, incomingElement = incomingPage.pageRoot; if (outgoingElement.parentNode) { outgoingElement.parentNode.removeChild(outgoingElement); } if (incomingElement.parentNode) { incomingElement.parentNode.removeChild(incomingElement); } this._pageManager.endAnimatedJump(outgoingPage, incomingPage); this._animationsFinished(); } this._completeJumpPending = false; }, _resize: function FlipView_resize() { this._pageManager.resized(); }, _setCurrentIndex: function FlipView_setCurrentIndex(index) { return this._pageManager.jumpToIndex(index); }, _getCurrentIndex: function FlipView_getCurrentIndex() { return this._pageManager.currentIndex(); }, _setDatasource: function FlipView_setDatasource(source, template, index) { if (this._animating) { this._cancelAnimation(); } var initialIndex = 0; if (index !== undefined) { initialIndex = index; } this._dataSource = source; this._itemRenderer = template; var oldItemsManager = this._itemsManager; this._itemsManager = _ItemsManager._createItemsManager(this._dataSource, this._itemRenderer, this._itemsManagerCallback, { ownerElement: this._flipviewDiv }); this._dataSource = this._itemsManager.dataSource; var that = this; this._dataSource.getCount().then(function (count) { that._pageManager._cachedSize = count; }); this._pageManager.setNewItemsManager(this._itemsManager, initialIndex); oldItemsManager && oldItemsManager.release(); }, _fireDatasourceCountChangedEvent: function FlipView_fireDatasourceCountChangedEvent() { var that = this; Scheduler.schedule(function FlipView_dispatchDataSourceCountChangedEvent() { var event = _Global.document.createEvent("Event"); event.initEvent(FlipView.datasourceCountChangedEvent, true, true); _WriteProfilerMark("WinJS.UI.FlipView:dataSourceCountChangedEvent,info"); that._flipviewDiv.dispatchEvent(event); }, Scheduler.Priority.normal, null, "WinJS.UI.FlipView._dispatchDataSourceCountChangedEvent"); }, _scrollPosChanged: function FlipView_scrollPosChanged() { this._pageManager.scrollPosChanged(); }, _axisAsString: function FlipView_axisAsString() { return (this._isHorizontal ? "horizontal" : "vertical"); }, _setupOrientation: function FlipView_setupOrientation() { if (this._isHorizontal) { this._panningDivContainer.style["overflowX"] = (this._environmentSupportsTouch ? "scroll" : "hidden"); this._panningDivContainer.style["overflowY"] = "hidden"; var rtl = _Global.getComputedStyle(this._flipviewDiv, null).direction === "rtl"; this._rtl = rtl; if (rtl) { this._prevButton.className = navButtonClass + " " + navButtonRightClass; this._nextButton.className = navButtonClass + " " + navButtonLeftClass; } else { this._prevButton.className = navButtonClass + " " + navButtonLeftClass; this._nextButton.className = navButtonClass + " " + navButtonRightClass; } this._prevButton.innerHTML = (rtl ? rightArrowGlyph : leftArrowGlyph); this._nextButton.innerHTML = (rtl ? leftArrowGlyph : rightArrowGlyph); } else { this._panningDivContainer.style["overflowY"] = (this._environmentSupportsTouch ? "scroll" : "hidden"); this._panningDivContainer.style["overflowX"] = "hidden"; this._prevButton.className = navButtonClass + " " + navButtonTopClass; this._nextButton.className = navButtonClass + " " + navButtonBottomClass; this._prevButton.innerHTML = topArrowGlyph; this._nextButton.innerHTML = bottomArrowGlyph; } this._panningDivContainer.style["msOverflowStyle"] = "none"; }, _fadeInButton: function FlipView_fadeInButton(button, forceShow) { if (this._mouseInViewport || forceShow || !this._environmentSupportsTouch) { if (button === "next" && this._hasNextContent) { if (this._nextButtonAnimation) { this._nextButtonAnimation.cancel(); this._nextButtonAnimation = null; } this._nextButton.style.visibility = "visible"; this._nextButtonAnimation = this._fadeInFromCurrentValue(this._nextButton); } else if (button === "prev" && this._hasPrevContent) { if (this._prevButtonAnimation) { this._prevButtonAnimation.cancel(); this._prevButtonAnimation = null; } this._prevButton.style.visibility = "visible"; this._prevButtonAnimation = this._fadeInFromCurrentValue(this._prevButton); } } }, _fadeOutButton: function FlipView_fadeOutButton(button) { var that = this; if (button === "next") { if (this._nextButtonAnimation) { this._nextButtonAnimation.cancel(); this._nextButtonAnimation = null; } this._nextButtonAnimation = Animations.fadeOut(this._nextButton). then(function () { that._nextButton.style.visibility = "hidden"; }); return this._nextButtonAnimation; } else { if (this._prevButtonAnimation) { this._prevButtonAnimation.cancel(); this._prevButtonAnimation = null; } this._prevButtonAnimation = Animations.fadeOut(this._prevButton). then(function () { that._prevButton.style.visibility = "hidden"; }); return this._prevButtonAnimation; } }, _fadeOutButtons: function FlipView_fadeOutButtons(immediately) { if (!this._environmentSupportsTouch) { return; } if (this._buttonFadePromise) { this._buttonFadePromise.cancel(); this._buttonFadePromise = null; } var that = this; this._buttonFadePromise = (immediately ? Promise.wrap() : Promise.timeout(_TransitionAnimation._animationTimeAdjustment(buttonFadeDelay))).then(function () { that._fadeOutButton("prev"); that._fadeOutButton("next"); that._buttonFadePromise = null; }); }, _animationsStarted: function FlipView_animationsStarted() { this._animating = true; }, _animationsFinished: function FlipView_animationsFinished() { this._animating = false; }, _defaultAnimation: function FlipView_defaultAnimation(curr, next) { var incomingPageMove = {}; next.style.left = "0px"; next.style.top = "0px"; next.style.opacity = 0.0; var pageDirection = ((curr.itemIndex > next.itemIndex) ? -animationMoveDelta : animationMoveDelta); incomingPageMove.left = (this._isHorizontal ? (this._rtl ? -pageDirection : pageDirection) : 0) + "px"; incomingPageMove.top = (this._isHorizontal ? 0 : pageDirection) + "px"; var fadeOutPromise = Animations.fadeOut(curr), enterContentPromise = Animations.enterContent(next, [incomingPageMove], { mechanism: "transition" }); return Promise.join([fadeOutPromise, enterContentPromise]); }, _fadeInFromCurrentValue: function FlipView_fadeInFromCurrentValue(shown) { // Intentionally not using the PVL fadeIn animation because we don't want // to start always from 0 in some cases return _TransitionAnimation.executeTransition( shown, { property: "opacity", delay: 0, duration: 167, timing: "linear", to: 1 }); } }, _Constants); _Base.Class.mix(FlipView, _Events.createEventProperties( FlipView.datasourceCountChangedEvent, FlipView.pageVisibilityChangedEvent, FlipView.pageSelectedEvent, FlipView.pageCompletedEvent)); _Base.Class.mix(FlipView, _Control.DOMEventMixin); return FlipView; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/ItemContainer',[ 'exports', '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Events', '../Core/_Log', '../Core/_Resources', '../Core/_WriteProfilerMark', '../Promise', '../Scheduler', '../Utilities/_Control', '../Utilities/_Dispose', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', '../Utilities/_KeyboardBehavior', '../Utilities/_UI', './ItemContainer/_Constants', './ItemContainer/_ItemEventsHandler' ], function itemContainerInit(exports, _Global, _Base, _BaseUtils, _ErrorFromName, _Events, _Log, _Resources, _WriteProfilerMark, Promise, Scheduler, _Control, _Dispose, _ElementUtilities, _Hoverable, _KeyboardBehavior, _UI, _Constants, _ItemEventsHandler) { "use strict"; var createEvent = _Events._createEventProperty; var eventNames = { invoked: "invoked", selectionchanging: "selectionchanging", selectionchanged: "selectionchanged" }; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { /// /// /// Defines an item that can be pressed, swiped, and dragged. /// /// /// /// /// HTML content /// ]]> /// Raised when the user taps or clicks the item. /// Raised before the item is selected or deselected. /// Raised after the item is selected or deselected. /// Main container for the selection item control. /// The background of a selection checkmark. /// A selection checkmark. /// Used to display an outline when the main container has keyboard focus. /// /// /// ItemContainer: _Base.Namespace._lazy(function () { var strings = { get duplicateConstruction() { return "Invalid argument: Controls may only be instantiated one time for each DOM element"; } }; var ItemContainer = _Base.Class.define(function ItemContainer_ctor(element, options) { /// /// /// Creates a new ItemContainer control. /// /// /// The DOM element that hosts the ItemContainer control. /// /// /// An object that contains one or more property/value pairs to apply to the new control. /// Each property of the options object corresponds to one of the control's properties or events. /// Event names must begin with "on". For example, to provide a handler for the selectionchanging event, /// add a property named "onselectionchanging" to the options object and set its value to the event handler. /// /// /// The new ItemContainer control. /// /// element = element || _Global.document.createElement("DIV"); this._id = element.id || _ElementUtilities._uniqueID(element); this._writeProfilerMark("constructor,StartTM"); options = options || {}; if (element.winControl) { throw new _ErrorFromName("WinJS.UI.ItemContainer.DuplicateConstruction", strings.duplicateConstruction); } // Attaching JS control to DOM element element.winControl = this; this._element = element; _ElementUtilities.addClass(element, "win-disposable"); this._selectionMode = _UI.SelectionMode.single; this._draggable = false; this._pressedEntity = { type: _UI.ObjectType.item, index: _Constants._INVALID_INDEX }; this.tapBehavior = _UI.TapBehavior.invokeOnly; this.swipeOrientation = _UI.Orientation.vertical; this.swipeBehavior = _UI.SwipeBehavior.select; _ElementUtilities.addClass(this.element, ItemContainer._ClassName.itemContainer + " " + _Constants._containerClass); this._setupInternalTree(); this._selection = new exports._SingleItemSelectionManager(element, this._itemBox); this._setTabIndex(); _Control.setOptions(this, options); this._mutationObserver = new _ElementUtilities._MutationObserver(this._itemPropertyChange.bind(this)); this._mutationObserver.observe(element, { attributes: true, attributeFilter: ["aria-selected"] }); this._setAriaRole(); var that = this; if (!this.selectionDisabled) { Scheduler.schedule(function ItemContainer_async_initialize() { that._setDirectionClass(); }, Scheduler.Priority.normal, null, "WinJS.UI.ItemContainer_async_initialize"); } this._itemEventsHandler = new _ItemEventsHandler._ItemEventsHandler(Object.create({ containerFromElement: function () { return that.element; }, indexForItemElement: function () { return 1; }, indexForHeaderElement: function () { return _Constants._INVALID_INDEX; }, itemBoxAtIndex: function () { return that._itemBox; }, itemAtIndex: function () { return that.element; }, headerAtIndex: function () { return null; }, containerAtIndex: function () { return that.element; }, isZombie: function () { return this._disposed; }, getItemPosition: function () { return that._getItemPosition(); }, rtl: function () { return that._rtl(); }, fireInvokeEvent: function () { that._fireInvokeEvent(); }, verifySelectionAllowed: function () { return that._verifySelectionAllowed(); }, changeFocus: function () { }, selectRange: function (firstIndex, lastIndex) { return that._selection.set({ firstIndex: firstIndex, lastIndex: lastIndex }); } }, { pressedEntity: { get: function () { return that._pressedEntity; }, set: function (value) { that._pressedEntity = value; } }, pressedElement: { enumerable: true, set: function (value) { that._pressedElement = value; } }, eventHandlerRoot: { enumerable: true, get: function () { return that.element; } }, swipeBehavior: { enumerable: true, get: function () { return that._swipeBehavior; } }, selectionMode: { enumerable: true, get: function () { return that._selectionMode; } }, accessibleItemClass: { enumerable: true, get: function () { // CSS class of the element with the aria role return _Constants._containerClass; } }, canvasProxy: { enumerable: true, get: function () { return that._captureProxy; } }, tapBehavior: { enumerable: true, get: function () { return that._tapBehavior; } }, draggable: { enumerable: true, get: function () { return that._draggable; } }, selection: { enumerable: true, get: function () { return that._selection; } }, horizontal: { enumerable: true, get: function () { return that._swipeOrientation === _UI.Orientation.vertical; } }, customFootprintParent: { enumerable: true, get: function () { // Use the main container as the footprint return null; } }, skipPreventDefaultOnPointerDown: { enumerable: true, get: function () { return true; } } })); function eventHandler(eventName, caseSensitive, capture) { return { name: (caseSensitive ? eventName : eventName.toLowerCase()), handler: function (eventObject) { that["_on" + eventName](eventObject); }, capture: capture }; } var events = [ eventHandler("MSManipulationStateChanged", true, true), eventHandler("PointerDown"), eventHandler("Click"), eventHandler("PointerUp"), eventHandler("PointerCancel"), eventHandler("LostPointerCapture"), eventHandler("ContextMenu"), eventHandler("MSHoldVisual", true), eventHandler("FocusIn"), eventHandler("FocusOut"), eventHandler("DragStart"), eventHandler("DragEnd"), eventHandler("KeyDown") ]; events.forEach(function (eventHandler) { _ElementUtilities._addEventListener(that.element, eventHandler.name, eventHandler.handler, !!eventHandler.capture); }); this._writeProfilerMark("constructor,StopTM"); }, { /// element: { get: function () { return this._element; } }, /// /// Gets or sets a value that specifies whether the item can be dragged. The default value is false. /// /// draggable: { get: function () { return this._draggable; }, set: function (value) { if (_BaseUtils.isPhone) { return; } if (this._draggable !== value) { this._draggable = value; this._updateDraggableAttribute(); } } }, /// /// Gets or sets a value that specifies whether the item is selected. /// selected: { get: function () { return this._selection.selected; }, set: function (value) { if (this._selection.selected !== value) { this._selection.selected = value; } } }, /// /// Gets or sets the swipe orientation of the ItemContainer control. /// The default value is "vertical". /// swipeOrientation: { get: function () { return this._swipeOrientation; }, set: function (value) { if (value === _UI.Orientation.vertical) { _ElementUtilities.removeClass(this.element, ItemContainer._ClassName.horizontal); _ElementUtilities.addClass(this.element, ItemContainer._ClassName.vertical); } else { value = _UI.Orientation.horizontal; _ElementUtilities.removeClass(this.element, ItemContainer._ClassName.vertical); _ElementUtilities.addClass(this.element, ItemContainer._ClassName.horizontal); } this._swipeOrientation = value; } }, /// /// Gets or sets how the ItemContainer control reacts when the user taps or clicks an item. /// The tap or click can invoke the item, select it and invoke it, or have no effect. /// Possible values: "toggleSelect", "invokeOnly", and "none". The default value is "invokeOnly". /// tapBehavior: { get: function () { return this._tapBehavior; }, set: function (value) { if (_BaseUtils.isPhone && value === _UI.TapBehavior.directSelect) { return; } this._tapBehavior = value; this._setAriaRole(); } }, /// /// Gets or sets how the ItemContainer control reacts to the swipe interaction. /// The swipe gesture can select the item or it can have no effect on the current selection. /// Possible values: "select", "none". The default value is: "select". /// /// swipeBehavior: { get: function () { return this._swipeBehavior; }, set: function (value) { this._swipeBehavior = value; this._setSwipeClass(); } }, /// /// Gets or sets whether the item selection is disabled. The default value is false. /// selectionDisabled: { get: function () { return this._selectionMode === _UI.SelectionMode.none; }, set: function (value) { if (value) { this._selectionMode = _UI.SelectionMode.none; } else { this._setDirectionClass(); this._selectionMode = _UI.SelectionMode.single; } this._setSwipeClass(); this._setAriaRole(); } }, /// /// Raised when the item is invoked. You can use the tapBehavior property to specify whether taps and clicks invoke the item. /// oninvoked: createEvent(eventNames.invoked), /// /// Raised just before the item is selected or deselected. /// onselectionchanging: createEvent(eventNames.selectionchanging), /// /// Raised after the item is selected or deselected. /// onselectionchanged: createEvent(eventNames.selectionchanged), forceLayout: function () { /// /// /// Forces the ItemContainer control to update its layout. /// Use this function when the reading direction of the app changes after the control has been initialized. /// /// this._forceLayout(); }, dispose: function () { /// /// /// Disposes this control. /// /// if (this._disposed) { return; } this._disposed = true; this._itemEventsHandler.dispose(); _Dispose.disposeSubTree(this.element); }, _onMSManipulationStateChanged: function ItemContainer_onMSManipulationStateChanged(eventObject) { this._itemEventsHandler.onMSManipulationStateChanged(eventObject); }, _onPointerDown: function ItemContainer_onPointerDown(eventObject) { this._itemEventsHandler.onPointerDown(eventObject); }, _onClick: function ItemContainer_onClick(eventObject) { this._itemEventsHandler.onClick(eventObject); }, _onPointerUp: function ItemContainer_onPointerUp(eventObject) { if (_ElementUtilities.hasClass(this._itemBox, _Constants._itemFocusClass)) { this._onFocusOut(eventObject); } this._itemEventsHandler.onPointerUp(eventObject); }, _onPointerCancel: function ItemContainer_onPointerCancel(eventObject) { this._itemEventsHandler.onPointerCancel(eventObject); }, _onLostPointerCapture: function ItemContainer_onLostPointerCapture(eventObject) { this._itemEventsHandler.onLostPointerCapture(eventObject); }, _onContextMenu: function ItemContainer_onContextMenu(eventObject) { this._itemEventsHandler.onContextMenu(eventObject); }, _onMSHoldVisual: function ItemContainer_onMSHoldVisual(eventObject) { this._itemEventsHandler.onMSHoldVisual(eventObject); }, _onFocusIn: function ItemContainer_onFocusIn() { if (this._itemBox.querySelector("." + _Constants._itemFocusOutlineClass) || !_KeyboardBehavior._keyboardSeenLast) { return; } _ElementUtilities.addClass(this._itemBox, _Constants._itemFocusClass); var outline = _Global.document.createElement("div"); outline.className = _Constants._itemFocusOutlineClass; this._itemBox.appendChild(outline); }, _onFocusOut: function ItemContainer_onFocusOut() { _ElementUtilities.removeClass(this._itemBox, _Constants._itemFocusClass); var outline = this._itemBox.querySelector("." + _Constants._itemFocusOutlineClass); if (outline) { outline.parentNode.removeChild(outline); } }, _onDragStart: function ItemContainer_onDragStart(eventObject) { // Drag shouldn't be initiated when the user holds down the mouse on a win-interactive element and moves. // The problem is that the dragstart event's srcElement+target will both be an itembox (which has draggable=true), so we can't check for win-interactive in the dragstart event handler. // The itemEventsHandler sets our _pressedElement field on PointerDown, so we use that instead when checking for interactive. if (this._pressedElement && this._itemEventsHandler._isInteractive(this._pressedElement)) { eventObject.preventDefault(); } else { this._dragging = true; var that = this; // Firefox requires setData to be called on the dataTransfer object in order for DnD to continue. // Firefox also has an issue rendering the item's itemBox+element, so we need to use setDragImage, using the item's container, to get it to render. eventObject.dataTransfer.setData("text", ""); if (eventObject.dataTransfer.setDragImage) { var rect = this.element.getBoundingClientRect(); eventObject.dataTransfer.setDragImage(this.element, eventObject.clientX - rect.left, eventObject.clientY - rect.top); } // We delay setting the win-dragsource CSS class so that IE has time to create a thumbnail before me make it opaque _BaseUtils._yieldForDomModification(function () { if (that._dragging) { _ElementUtilities.addClass(that._itemBox, _Constants._dragSourceClass); } }); } }, _onDragEnd: function ItemContainer_onDragEnd() { this._dragging = false; _ElementUtilities.removeClass(this._itemBox, _Constants._dragSourceClass); this._itemEventsHandler.resetPointerDownState(); }, _onKeyDown: function ItemContainer_onKeyDown(eventObject) { if (!this._itemEventsHandler._isInteractive(eventObject.target)) { var Key = _ElementUtilities.Key, keyCode = eventObject.keyCode, swipeEnabled = this._swipeBehavior === _UI.SwipeBehavior.select; var handled = false; if (!eventObject.ctrlKey && keyCode === Key.enter) { var allowed = this._verifySelectionAllowed(); if (allowed.canTapSelect) { this.selected = !this.selected; } this._fireInvokeEvent(); handled = true; } else if (eventObject.ctrlKey && keyCode === Key.enter || (swipeEnabled && eventObject.shiftKey && keyCode === Key.F10) || (swipeEnabled && keyCode === Key.menu) || keyCode === Key.space) { if (!this.selectionDisabled) { this.selected = !this.selected; handled = _ElementUtilities._setActive(this.element); } } else if (keyCode === Key.escape && this.selected) { this.selected = false; handled = true; } if (handled) { eventObject.stopPropagation(); eventObject.preventDefault(); } } }, _setTabIndex: function ItemContainer_setTabIndex() { var currentTabIndex = this.element.getAttribute("tabindex"); if (!currentTabIndex) { // Set the tabindex to 0 only if the application did not already // provide a tabindex this.element.setAttribute("tabindex", "0"); } }, _rtl: function ItemContainer_rtl() { if (typeof this._cachedRTL !== "boolean") { this._cachedRTL = _Global.getComputedStyle(this.element, null).direction === "rtl"; } return this._cachedRTL; }, _setDirectionClass: function ItemContainer_setDirectionClass() { _ElementUtilities[this._rtl() ? "addClass" : "removeClass"](this.element, _Constants._rtlListViewClass); }, _forceLayout: function ItemContainer_forceLayout() { this._cachedRTL = _Global.getComputedStyle(this.element, null).direction === "rtl"; this._setDirectionClass(); }, _getItemPosition: function ItemContainer_getItemPosition() { var container = this.element; if (container) { return Promise.wrap({ left: (this._rtl() ? container.offsetParent.offsetWidth - container.offsetLeft - container.offsetWidth : container.offsetLeft), top: container.offsetTop, totalWidth: _ElementUtilities.getTotalWidth(container), totalHeight: _ElementUtilities.getTotalHeight(container), contentWidth: _ElementUtilities.getContentWidth(container), contentHeight: _ElementUtilities.getContentHeight(container) }); } else { return Promise.cancel; } }, _itemPropertyChange: function ItemContainer_itemPropertyChange(list) { if (this._disposed) { return; } var container = list[0].target; var ariaSelected = container.getAttribute("aria-selected") === "true"; // Only respond to aria-selected changes coming from UIA. This check // relies on the fact that, in renderSelection, we update the selection // visual before aria-selected. if (ariaSelected !== _ElementUtilities._isSelectionRendered(this._itemBox)) { if (this.selectionDisabled) { // Revert the change made by UIA since the control has selection disabled _ElementUtilities._setAttribute(container, "aria-selected", !ariaSelected); } else { this.selected = ariaSelected; // Revert the change because the update was prevented on the selectionchanging event if (ariaSelected !== this.selected) { _ElementUtilities._setAttribute(container, "aria-selected", !ariaSelected); } } } }, _setSwipeClass: function ItemContainer_setSwipeClass() { if (_BaseUtils.isPhone) { // Cross-slide is disabled on phone return; } // We apply an -ms-touch-action style to block panning and swiping from occurring at the same time. if ((this._swipeBehavior === _UI.SwipeBehavior.select && this._selectionMode !== _UI.SelectionMode.none) || this._draggable) { _ElementUtilities.addClass(this._element, _Constants._swipeableClass); } else { _ElementUtilities.removeClass(this._element, _Constants._swipeableClass); } }, _updateDraggableAttribute: function ItemContainer_updateDraggableAttribute() { this._setSwipeClass(); this._itemBox.setAttribute("draggable", this._draggable); }, _verifySelectionAllowed: function ItemContainer_verifySelectionAllowed() { if (this._selectionMode !== _UI.SelectionMode.none && (this._tapBehavior === _UI.TapBehavior.toggleSelect || this._swipeBehavior === _UI.SwipeBehavior.select)) { var canSelect = this._selection.fireSelectionChanging(); return { canSelect: canSelect, canTapSelect: canSelect && this._tapBehavior === _UI.TapBehavior.toggleSelect }; } else { return { canSelect: false, canTapSelect: false }; } }, _setupInternalTree: function ItemContainer_setupInternalTree() { var item = _Global.document.createElement("div"); item.className = _Constants._itemClass; this._captureProxy = _Global.document.createElement("div"); this._itemBox = _Global.document.createElement("div"); this._itemBox.className = _Constants._itemBoxClass; var child = this.element.firstChild; while (child) { var sibling = child.nextSibling; item.appendChild(child); child = sibling; } this.element.appendChild(this._itemBox); this._itemBox.appendChild(item); this.element.appendChild(this._captureProxy); }, _fireInvokeEvent: function ItemContainer_fireInvokeEvent() { if (this.tapBehavior !== _UI.TapBehavior.none) { var eventObject = _Global.document.createEvent("CustomEvent"); eventObject.initCustomEvent(eventNames.invoked, true, false, {}); this.element.dispatchEvent(eventObject); } }, _setAriaRole: function ItemContainer_setAriaRole() { if (!this.element.getAttribute("role") || this._usingDefaultItemRole) { this._usingDefaultItemRole = true; var defaultItemRole; if (this.tapBehavior === _UI.TapBehavior.none && this.selectionDisabled) { defaultItemRole = "listitem"; } else { defaultItemRole = "option"; } _ElementUtilities._setAttribute(this.element, "role", defaultItemRole); } }, _writeProfilerMark: function ItemContainer_writeProfilerMark(text) { var message = "WinJS.UI.ItemContainer:" + this._id + ":" + text; _WriteProfilerMark(message); _Log.log && _Log.log(message, null, "itemcontainerprofiler"); } }, { // Names of classes used by the ItemContainer. _ClassName: { itemContainer: "win-itemcontainer", vertical: "win-vertical", horizontal: "win-horizontal", } }); _Base.Class.mix(ItemContainer, _Control.DOMEventMixin); return ItemContainer; }), _SingleItemSelectionManager: _Base.Namespace._lazy(function () { return _Base.Class.define(function SingleItemSelectionManager_ctor(element, itemBox) { this._selected = false; this._element = element; this._itemBox = itemBox; }, { selected: { get: function () { return this._selected; }, set: function (value) { value = !!value; if (this._selected !== value) { if (this.fireSelectionChanging()) { this._selected = value; _ItemEventsHandler._ItemEventsHandler.renderSelection(this._itemBox, this._element, value, true, this._element); this.fireSelectionChanged(); } } } }, count: function SingleItemSelectionManager_count() { return this._selected ? 1 : 0; }, getIndices: function SingleItemSelectionManager_getIndices() { // not used }, getItems: function SingleItemSelectionManager_getItems() { // not used }, getRanges: function SingleItemSelectionManager_getRanges() { // not used }, isEverything: function SingleItemSelectionManager_isEverything() { return false; }, set: function SingleItemSelectionManager_set() { this.selected = true; }, clear: function SingleItemSelectionManager_clear() { this.selected = false; }, add: function SingleItemSelectionManager_add() { this.selected = true; }, remove: function SingleItemSelectionManager_remove() { this.selected = false; }, selectAll: function SingleItemSelectionManager_selectAll() { // not used }, fireSelectionChanging: function SingleItemSelectionManager_fireSelectionChanging() { var eventObject = _Global.document.createEvent("CustomEvent"); eventObject.initCustomEvent(eventNames.selectionchanging, true, true, {}); return this._element.dispatchEvent(eventObject); }, fireSelectionChanged: function ItemContainer_fireSelectionChanged() { var eventObject = _Global.document.createEvent("CustomEvent"); eventObject.initCustomEvent(eventNames.selectionchanged, true, false, {}); this._element.dispatchEvent(eventObject); }, _isIncluded: function SingleItemSelectionManager_isIncluded() { return this._selected; }, _getFocused: function SingleItemSelectionManager_getFocused() { return { type: _UI.ObjectType.item, index: _Constants._INVALID_INDEX }; } }); }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/Repeater',[ 'exports', '../Core/_Global', '../Core/_Base', '../Core/_ErrorFromName', '../Core/_Events', '../Core/_Resources', '../Core/_WriteProfilerMark', '../BindingList', '../BindingTemplate', '../Promise', '../Utilities/_Control', '../Utilities/_Dispose', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', ], function repeaterInit(exports, _Global, _Base, _ErrorFromName, _Events, _Resources, _WriteProfilerMark, BindingList, BindingTemplate, Promise, _Control, _Dispose, _ElementUtilities, _Hoverable) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { /// /// /// Uses templates to generate HTML from a set of data. /// /// /// /// /// ]]> /// The Repeater control itself /// /// /// Repeater: _Base.Namespace._lazy(function () { // Constants var ITEMSLOADED = "itemsloaded", ITEMCHANGING = "itemchanging", ITEMCHANGED = "itemchanged", ITEMINSERTING = "iteminserting", ITEMINSERTED = "iteminserted", ITEMMOVING = "itemmoving", ITEMMOVED = "itemmoved", ITEMREMOVING = "itemremoving", ITEMREMOVED = "itemremoved", ITEMSRELOADING = "itemsreloading", ITEMSRELOADED = "itemsreloaded"; var createEvent = _Events._createEventProperty; function stringifyItem(dataItem) { // Repeater uses this as its default renderer when no template is provided. var itemElement = _Global.document.createElement("div"); itemElement.textContent = JSON.stringify(dataItem); return itemElement; } // Statics var strings = { get duplicateConstruction() { return "Invalid argument: Controls may only be instantiated one time for each DOM element"; }, get asynchronousRender() { return "Top level items must render synchronously"; }, get repeaterReentrancy() { return "Cannot modify Repeater data until Repeater has commited previous modification."; }, }; var Repeater = _Base.Class.define(function Repeater_ctor(element, options) { /// /// /// Creates a new Repeater control. /// /// /// The DOM element that will host the new control. The Repeater will create an element if this value is null. /// /// /// An object that contains one or more property/value pairs to apply to the /// new Repeater. Each property of the options object corresponds to one of the /// object's properties or events. Event names must begin with "on". /// /// /// The new Repeater control. /// /// // Check to make sure we weren't duplicated if (element && element.winControl) { throw new _ErrorFromName("WinJS.UI.Repeater.DuplicateConstruction", strings.duplicateConstruction); } this._element = element || _Global.document.createElement("div"); this._id = this._element.id || _ElementUtilities._uniqueID(this._element); this._writeProfilerMark("constructor,StartTM"); options = options || {}; _ElementUtilities.addClass(this._element, "win-repeater win-disposable"); this._render = null; this._modifying = false; this._disposed = false; this._element.winControl = this; this._dataListeners = { itemchanged: this._dataItemChangedHandler.bind(this), iteminserted: this._dataItemInsertedHandler.bind(this), itemmoved: this._dataItemMovedHandler.bind(this), itemremoved: this._dataItemRemovedHandler.bind(this), reload: this._dataReloadHandler.bind(this), }; // Consume Repeater innerHTML and return a template. var inlineTemplate = this._extractInlineTemplate(); this._initializing = true; // Use the inlinetemplate if a parameter was not given. // Either way, Repeater's innerHTML has now been consumed. this.template = options.template || inlineTemplate; this.data = options.data; this._initializing = false; _Control._setOptions(this, options, true); // Events only this._repeatedDOM = []; this._renderAllItems(); this.dispatchEvent(ITEMSLOADED, {}); this._writeProfilerMark("constructor,StopTM"); }, { /// element: { get: function () { return this._element; } }, /// /// Gets or sets the WinJS.Binding.List that provides the Repeater control with items to display. /// data: { get: function () { return this._data; }, set: function (data) { this._writeProfilerMark("data.set,StartTM"); if (this._data) { this._removeDataListeners(); } this._data = data || new BindingList.List(); this._addDataListeners(); if (!this._initializing) { this._reloadRepeater(true); this.dispatchEvent(ITEMSLOADED, {}); } this._writeProfilerMark("data.set,StopTM"); } }, /// /// Gets or sets a Template or custom rendering function that defines the HTML of each item within the Repeater. /// template: { get: function () { return this._template; }, set: function (template) { this._writeProfilerMark("template.set,StartTM"); this._template = (template || stringifyItem); this._render = _ElementUtilities._syncRenderer(this._template, this.element.tagName); if (!this._initializing) { this._reloadRepeater(true); this.dispatchEvent(ITEMSLOADED, {}); } this._writeProfilerMark("template.set,StopTM"); } }, /// length: { get: function () { return this._repeatedDOM.length; }, }, elementFromIndex: function Repeater_elementFromIndex(index) { /// /// /// Returns the HTML element for the item with the specified index. /// /// /// The index of the item. /// /// /// The DOM element for the specified item. /// /// return this._repeatedDOM[index]; }, dispose: function Repeater_dispose() { /// /// /// Prepare this Repeater for garbage collection. /// /// if (this._disposed) { return; } this._disposed = true; // Mark this control as disposed. this._removeDataListeners(); this._data = null; this._template = null; for (var i = 0, len = this._repeatedDOM.length; i < len; i++) { _Dispose._disposeElement(this._repeatedDOM[i]); } }, /// /// Raised when the Repeater has finished loading a new set of data. This event is only fired on construction /// or when the Repeater control's data source or template is replaced. /// onitemsloaded: createEvent(ITEMSLOADED), /// /// Raised after an item in the Repeater control's data source changes but before the corresponding DOM element has been updated. /// onitemchanging: createEvent(ITEMCHANGING), /// /// Raised after an item in the Repeater control's data source changes and after the corresponding DOM element has been updated. /// onitemchanged: createEvent(ITEMCHANGED), /// /// Raised after an item has been added to the Repeater control's data source but before the corresponding DOM element has been added. /// oniteminserting: createEvent(ITEMINSERTING), /// /// Raised after an item has been added to the Repeater control's data source and after the corresponding DOM element has been added. /// oniteminserted: createEvent(ITEMINSERTED), /// /// Raised after an item has been moved from one index to another in the Repeater control's data source but before the corresponding DOM element has been moved. /// onitemmoving: createEvent(ITEMMOVING), /// /// Raised after an item has been moved from one index to another in the Repeater control's data source and after the corresponding DOM element has been moved. /// onitemmoved: createEvent(ITEMMOVED), /// /// Raised after an item has been removed from the Repeater control's data source but before the corresponding DOM element has been removed. /// onitemremoving: createEvent(ITEMREMOVING), /// /// Raised after an item has been removed from one index to another in the Repeater control's data source and after the corresponding DOM element has been removed. /// onitemremoved: createEvent(ITEMREMOVED), /// /// The list has been refreshed and any references to data in the list may be incorrect. /// Raised after the Repeater control's underlying data has been updated but before the updated HTML has been reloaded. /// onitemsreloading: createEvent(ITEMSRELOADING), /// /// Raised after the Repeater control's underlying data has been updated and after the updated HTML has been reloaded. /// onitemsreloaded: createEvent(ITEMSRELOADED), _extractInlineTemplate: function Repeater_extractInlineTemplate() { // Creates and returns a WinJS.BindingTemplate from the Repeater innerHTML. if (this._element.firstElementChild) { var templateElement = _Global.document.createElement(this._element.tagName); while (this._element.firstElementChild) { // Move each child element from the Repeater to the Template Element templateElement.appendChild(this._element.firstElementChild); } return new BindingTemplate.Template(templateElement, { extractChild: true }); } }, _renderAllItems: function Repeater_renderAllItems() { var fragment = _Global.document.createDocumentFragment(); for (var i = 0, len = this._data.length; i < len; i++) { var renderedItem = this._render(this._data.getAt(i)); if (!renderedItem) { throw new _ErrorFromName("WinJS.UI.Repeater.AsynchronousRender", strings.asynchronousRender); } fragment.appendChild(renderedItem); this._repeatedDOM.push(renderedItem); } this._element.appendChild(fragment); }, _reloadRepeater: function Repeater_reloadRepeater(shouldDisposeElements) { this._unloadRepeatedDOM(shouldDisposeElements); this._repeatedDOM = []; this._renderAllItems(); }, _unloadRepeatedDOM: function Repeater_unloadRepeatedDOM(shouldDisposeElements) { for (var i = 0, len = this._repeatedDOM.length; i < len; i++) { var element = this._repeatedDOM[i]; if (!!shouldDisposeElements) { // this_dataReloadHandler uses this to defer disposal until after animations have completed, // at which point it manually disposes each element. _Dispose._disposeElement(element); } if (element.parentElement === this._element) { this._element.removeChild(element); } } }, _addDataListeners: function Repeater_addDataListeners() { Object.keys(this._dataListeners).forEach(function (eventName) { this._data.addEventListener(eventName, this._dataListeners[eventName], false); }.bind(this)); }, _beginModification: function Repeater_beginModification() { if (this._modifying) { throw new _ErrorFromName("WinJS.UI.Repeater.RepeaterModificationReentrancy", strings.repeaterReentrancy); } this._modifying = true; }, _endModification: function Repeater_endModification() { this._modifying = false; }, _removeDataListeners: function Repeater_removeDataListeners() { Object.keys(this._dataListeners).forEach(function (eventName) { this._data.removeEventListener(eventName, this._dataListeners[eventName], false); }.bind(this)); }, _dataItemChangedHandler: function Repeater_dataItemChangedHandler(eventInfo) { // Handles the 'itemchanged' event fired by WinJS.Binding.List this._beginModification(); var animationPromise; var root = this._element; var index = eventInfo.detail.index; var renderedItem = this._render(eventInfo.detail.newValue); if (!renderedItem) { throw new _ErrorFromName("WinJS.UI.Repeater.AsynchronousRender", strings.asynchronousRender); } // Append to the event object if (this._repeatedDOM[index]) { eventInfo.detail.oldElement = this._repeatedDOM[index]; } eventInfo.detail.newElement = renderedItem; eventInfo.detail.setPromise = function setPromise(delayPromise) { animationPromise = delayPromise; }; this._writeProfilerMark(ITEMCHANGING + ",info"); this.dispatchEvent(ITEMCHANGING, eventInfo.detail); // Make the change var oldItem = null; if (index < this._repeatedDOM.length) { oldItem = this._repeatedDOM[index]; root.replaceChild(renderedItem, oldItem); this._repeatedDOM[index] = renderedItem; } else { root.appendChild(renderedItem); this._repeatedDOM.push(renderedItem); } this._endModification(); this._writeProfilerMark(ITEMCHANGED + ",info"); this.dispatchEvent(ITEMCHANGED, eventInfo.detail); if (oldItem) { // Give the option to delay element disposal. Promise.as(animationPromise).done(function () { _Dispose._disposeElement(oldItem); }.bind(this)); } }, _dataItemInsertedHandler: function Repeater_dataItemInsertedHandler(eventInfo) { // Handles the 'iteminserted' event fired by WinJS.Binding.List this._beginModification(); var index = eventInfo.detail.index; var renderedItem = this._render(eventInfo.detail.value); if (!renderedItem) { throw new _ErrorFromName("WinJS.UI.Repeater.AsynchronousRender", strings.asynchronousRender); } var root = this._element; eventInfo.detail.affectedElement = renderedItem; this._writeProfilerMark(ITEMINSERTING + ",info"); this.dispatchEvent(ITEMINSERTING, eventInfo.detail); if (index < this._repeatedDOM.length) { var nextSibling = this._repeatedDOM[index]; root.insertBefore(renderedItem, nextSibling); } else { root.appendChild(renderedItem); } // Update collection of rendered elements this._repeatedDOM.splice(index, 0, renderedItem); this._endModification(); this._writeProfilerMark(ITEMINSERTED + ",info"); this.dispatchEvent(ITEMINSERTED, eventInfo.detail); }, _dataItemMovedHandler: function Repeater_dataItemMovedHandler(eventInfo) { // Handles the 'itemmoved' event fired by WinJS.Binding.List this._beginModification(); var movingItem = this._repeatedDOM[eventInfo.detail.oldIndex]; // Fire the event before we start the move. eventInfo.detail.affectedElement = movingItem; this._writeProfilerMark(ITEMMOVING + ",info"); this.dispatchEvent(ITEMMOVING, eventInfo.detail); // Remove this._repeatedDOM.splice(eventInfo.detail.oldIndex, 1)[0]; movingItem.parentNode.removeChild(movingItem); // Insert if (eventInfo.detail.newIndex < (this._data.length) - 1) { var nextSibling = this._repeatedDOM[eventInfo.detail.newIndex]; this._element.insertBefore(movingItem, nextSibling); this._repeatedDOM.splice(eventInfo.detail.newIndex, 0, movingItem); } else { this._repeatedDOM.push(movingItem); this._element.appendChild(movingItem); } this._endModification(); this._writeProfilerMark(ITEMMOVED + ",info"); this.dispatchEvent(ITEMMOVED, eventInfo.detail); }, _dataItemRemovedHandler: function Repeater_dataItemRemoveHandler(eventInfo) { // Handles the 'itemremoved' event fired by WinJS.Binding.List this._beginModification(); var animationPromise; var oldItem = this._repeatedDOM[eventInfo.detail.index]; // Trim 'value' and 'key' from the eventInfo.details that Binding.List gave for the removal case, // since both of those properties already exist inside of eventInfo.details.item. var eventDetail = { affectedElement: oldItem, index: eventInfo.detail.index, item: eventInfo.detail.item }; eventDetail.setPromise = function setPromise(delayPromise) { animationPromise = delayPromise; }; this._writeProfilerMark(ITEMREMOVING + ",info"); this.dispatchEvent(ITEMREMOVING, eventDetail); oldItem.parentNode.removeChild(oldItem); this._repeatedDOM.splice(eventInfo.detail.index, 1); this._endModification(); this._writeProfilerMark(ITEMREMOVED + ",info"); this.dispatchEvent(ITEMREMOVED, eventDetail); Promise.as(animationPromise).done(function () { _Dispose._disposeElement(oldItem); }.bind(this)); }, _dataReloadHandler: function Repeater_dataReloadHandler() { // Handles the 'reload' event fired by WinJS.Binding.List whenever it performs operations such as reverse() or sort() this._beginModification(); var animationPromise; var shallowCopyBefore = this._repeatedDOM.slice(0); var eventDetail = { affectedElements: shallowCopyBefore }; eventDetail.setPromise = function (delayPromise) { animationPromise = delayPromise; }; this._writeProfilerMark(ITEMSRELOADING + ",info"); this.dispatchEvent(ITEMSRELOADING, eventDetail); this._reloadRepeater(false /*shouldDisposeElements */); var shallowCopyAfter = this._repeatedDOM.slice(0); this._endModification(); this._writeProfilerMark(ITEMSRELOADED + ",info"); this.dispatchEvent(ITEMSRELOADED, { affectedElements: shallowCopyAfter }); Promise.as(animationPromise).done(function () { // Gives the option to defer disposal. for (var i = 0, len = shallowCopyBefore.length; i < len; i++) { _Dispose._disposeElement(shallowCopyBefore[i]); } }.bind(this)); }, _writeProfilerMark: function Repeater_writeProfilerMark(text) { _WriteProfilerMark("WinJS.UI.Repeater:" + this._id + ":" + text); } }, { isDeclarativeControlContainer: true, }); _Base.Class.mix(Repeater, _Control.DOMEventMixin); return Repeater; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/DatePicker',[ '../Core/_Global', '../Core/_WinRT', '../Core/_Base', '../Core/_BaseUtils', '../Core/_Events', '../Core/_Resources', '../Utilities/_Control', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', '../Utilities/_Select', 'require-style!less/desktop/controls', 'require-style!less/phone/controls' ], function datePickerInit(_Global, _WinRT, _Base, _BaseUtils, _Events, _Resources, _Control, _ElementUtilities, _Hoverable, _Select) { "use strict"; _Base.Namespace.define("WinJS.UI", { /// /// Allows users to pick a date value. /// /// /// Date Picker /// /// /// ]]> /// Occurs when the current date changes. /// /// /// DatePicker: _Base.Namespace._lazy(function () { // Constants definition var DEFAULT_DAY_PATTERN = 'day', DEFAULT_MONTH_PATTERN = '{month.full}', DEFAULT_YEAR_PATTERN = 'year.full'; var strings = { get ariaLabel() { return _Resources._getWinJSString("ui/datePicker").value; }, get selectDay() { return _Resources._getWinJSString("ui/selectDay").value; }, get selectMonth() { return _Resources._getWinJSString("ui/selectMonth").value; }, get selectYear() { return _Resources._getWinJSString("ui/selectYear").value; }, }; var yearFormatCache = {}; function newFormatter(pattern, calendar, defaultPattern) { var dtf = _WinRT.Windows.Globalization.DateTimeFormatting; pattern = !pattern ? defaultPattern : pattern; var c = new dtf.DateTimeFormatter(pattern); if (calendar) { return new dtf.DateTimeFormatter(pattern, c.languages, c.geographicRegion, calendar, c.clock); } return c; } function formatCacheLookup(pattern, calendar, defaultPattern) { var pat = yearFormatCache[pattern]; if (!pat) { pat = yearFormatCache[pattern] = {}; } var cal = pat[calendar]; if (!cal) { cal = pat[calendar] = {}; } var def = cal[defaultPattern]; if (!def) { def = cal[defaultPattern] = {}; def.formatter = newFormatter(pattern, calendar, defaultPattern); def.years = {}; } return def; } function formatYear(pattern, calendar, defaultPattern, datePatterns, order, cal) { var cache = formatCacheLookup(pattern, calendar, defaultPattern); var y = cache.years[cal.year + "-" + cal.era]; if (!y) { y = cache.formatter.format(cal.getDateTime()); cache.years[cal.year + "-" + cal.era] = y; } return y; } function formatMonth(pattern, calendar, defaultPattern, cal) { var cache = formatCacheLookup(pattern, calendar, defaultPattern); // can't cache actual month names because the hebrew calendar varies // the month name depending on religious holidays and leap months. // return cache.formatter.format(cal.getDateTime()); } function formatDay(pattern, calendar, defaultPattern, cal) { var cache = formatCacheLookup(pattern, calendar, defaultPattern); // can't cache actual day names because the format may include the day of the week, // which, of course, varies from month to month. // return cache.formatter.format(cal.getDateTime()); } function newCal(calendar) { var glob = _WinRT.Windows.Globalization; var c = new glob.Calendar(); if (calendar) { return new glob.Calendar(c.languages, calendar, c.getClock()); } return c; } function yearDiff(start, end) { var yearCount = 0; if (start.era === end.era) { yearCount = end.year - start.year; } else { while (start.era !== end.era || start.year !== end.year) { yearCount++; start.addYears(1); } } return yearCount; } var DatePicker = _Base.Class.define(function DatePicker_ctor(element, options) { /// /// Creates a new DatePicker control. /// /// The DOM element that will host the DatePicker control. /// /// /// An object that contains one or more property/value pairs to apply to the new control. Each property of the options object corresponds /// to one of the control's properties or events. /// /// A constructed DatePicker control. /// /// // Default to current date this._currentDate = new Date(); // Default to +/- 100 years this._minYear = this._currentDate.getFullYear() - 100; this._maxYear = this._currentDate.getFullYear() + 100; this._datePatterns = { date: null, month: null, year: null }; element = element || _Global.document.createElement("div"); _ElementUtilities.addClass(element, "win-disposable"); element.winControl = this; var label = element.getAttribute("aria-label"); if (!label) { element.setAttribute("aria-label", strings.ariaLabel); } // Options should be set after the element is initialized which is // the same order of operation as imperatively setting options. this._init(element); _Control.setOptions(this, options); }, { _information: null, _currentDate: null, _calendar: null, _disabled: false, _dateElement: null, _dateControl: null, _monthElement: null, _monthControl: null, _minYear: null, _maxYear: null, _yearElement: null, _yearControl: null, _datePatterns: { date: null, month: null, year: null }, _addAccessibilityAttributes: function () { //see http://www.w3.org/TR/wai-aria/rdf_model.png for details this._domElement.setAttribute("role", "group"); this._dateElement.setAttribute("aria-label", strings.selectDay); this._monthElement.setAttribute("aria-label", strings.selectMonth); this._yearElement.setAttribute("aria-label", strings.selectYear); }, _addControlsInOrder: function () { var e = this._domElement; var that = this; var orderIndex = 0; // don't use forEach's index, because "era" is in the list that._information.order.forEach(function (s) { switch (s) { case "month": e.appendChild(that._monthElement); _ElementUtilities.addClass(that._monthElement, "win-order" + (orderIndex++)); break; case "date": e.appendChild(that._dateElement); _ElementUtilities.addClass(that._dateElement, "win-order" + (orderIndex++)); break; case "year": e.appendChild(that._yearElement); _ElementUtilities.addClass(that._yearElement, "win-order" + (orderIndex++)); break; } }); }, _createControlElements: function () { this._monthElement = _Global.document.createElement("select"); this._monthElement.className = "win-datepicker-month"; this._dateElement = _Global.document.createElement("select"); this._dateElement.className = "win-datepicker-date"; this._yearElement = _Global.document.createElement("select"); this._yearElement.className = "win-datepicker-year"; }, _createControls: function () { var info = this._information; var index = info.getIndex(this.current); if (info.forceLanguage) { this._domElement.setAttribute("lang", info.forceLanguage); this._domElement.setAttribute("dir", info.isRTL ? "rtl" : "ltr"); } this._yearControl = new _Select._Select(this._yearElement, { dataSource: this._information.years, disabled: this.disabled, index: index.year }); this._monthControl = new _Select._Select(this._monthElement, { dataSource: this._information.months(index.year), disabled: this.disabled, index: index.month }); this._dateControl = new _Select._Select(this._dateElement, { dataSource: this._information.dates(index.year, index.month), disabled: this.disabled, index: index.date }); this._wireupEvents(); }, dispose: function () { /// /// /// Disposes this control. /// /// /// }, /// /// Gets or sets the calendar to use. /// /// calendar: { get: function () { return this._calendar; }, set: function (value) { this._calendar = value; this._setElement(this._domElement); } }, /// /// Gets or sets the current date of the DatePicker. /// /// current: { get: function () { var d = this._currentDate; var y = d.getFullYear(); return new Date(Math.max(Math.min(this.maxYear, y), this.minYear), d.getMonth(), d.getDate(), 12, 0, 0, 0); }, set: function (value) { var newDate; if (typeof (value) === "string") { newDate = new Date(Date.parse(value)); newDate.setHours(12, 0, 0, 0); } else { newDate = value; } var oldDate = this._currentDate; if (oldDate !== newDate) { this._currentDate = newDate; this._updateDisplay(); } } }, /// /// Gets or sets a value that specifies whether the DatePicker is disabled. A value of true indicates that the DatePicker is disabled. /// /// disabled: { get: function () { return this._disabled; }, set: function (value) { if (this._disabled !== value) { this._disabled = value; // all controls get populated at the same time, so any check is OK // if (this._yearControl) { this._monthControl.setDisabled(value); this._dateControl.setDisabled(value); this._yearControl.setDisabled(value); } } } }, /// /// Gets or sets the display pattern for the date. /// /// datePattern: { get: function () { return this._datePatterns.date; }, set: function (value) { if (this._datePatterns.date !== value) { this._datePatterns.date = value; this._init(); } } }, /// element: { get: function () { return this._domElement; } }, _setElement: function (element) { this._domElement = this._domElement || element; if (!this._domElement) { return; } _ElementUtilities.empty(this._domElement); _ElementUtilities.addClass(this._domElement, "win-datepicker"); this._updateInformation(); this._createControlElements(); this._addControlsInOrder(); this._createControls(); this._addAccessibilityAttributes(); }, /// /// Gets or sets the minimum Gregorian year available for picking. /// /// minYear: { get: function () { return this._information.getDate({ year: 0, month: 0, date: 0 }).getFullYear(); }, set: function (value) { if (this._minYear !== value) { this._minYear = value; if (value > this._maxYear) { this._maxYear = value; } this._updateInformation(); if (this._yearControl) { this._yearControl.dataSource = this._information.years; } this._updateDisplay(); } } }, /// /// Gets or sets the maximum Gregorian year available for picking. /// /// maxYear: { get: function () { var index = { year: this._information.years.getLength() - 1 }; index.month = this._information.months(index.year).getLength() - 1; index.date = this._information.dates(index.year, index.month).getLength() - 1; return this._information.getDate(index).getFullYear(); }, set: function (value) { if (this._maxYear !== value) { this._maxYear = value; if (value < this._minYear) { this._minYear = value; } this._updateInformation(); if (this._yearControl) { this._yearControl.dataSource = this._information.years; } this._updateDisplay(); } } }, /// /// Gets or sets the display pattern for the month. /// /// monthPattern: { get: function () { return this._datePatterns.month; }, set: function (value) { if (this._datePatterns.month !== value) { this._datePatterns.month = value; this._init(); } } }, _updateInformation: function () { // since "year" in the date ctor can be two digit (85 == 1985), we need // to force "full year" to capture dates < 100 a.d. // var min = new Date(this._minYear, 0, 1, 12, 0, 0); var max = new Date(this._maxYear, 11, 31, 12, 0, 0); min.setFullYear(this._minYear); max.setFullYear(this._maxYear); this._information = DatePicker.getInformation(min, max, this._calendar, this._datePatterns); }, _init: function (element) { this._setElement(element); }, _updateDisplay: function () { if (!this._domElement) { return; } // all controls get populated at the same time, so any check is OK // if (this._yearControl) { //Render display index based on constraints (minYear and maxYear constraints) //Will not modify current date var index = this._information.getIndex(this.current); this._yearControl.index = index.year; this._monthControl.dataSource = this._information.months(index.year); this._monthControl.index = index.month; this._dateControl.dataSource = this._information.dates(index.year, index.month); this._dateControl.index = index.date; } }, _wireupEvents: function () { var that = this; function changed() { that._currentDate = that._information.getDate({ year: that._yearControl.index, month: that._monthControl.index, date: that._dateControl.index }, that._currentDate); var index = that._information.getIndex(that._currentDate); // Changing the month (or year, if the current date is 2/29) changes the day range, and could have made the day selection invalid that._monthControl.dataSource = that._information.months(index.year); that._monthControl.index = index.month; that._dateControl.dataSource = that._information.dates(index.year, index.month); that._dateControl.index = index.date; } this._dateElement.addEventListener("change", changed, false); this._monthElement.addEventListener("change", changed, false); this._yearElement.addEventListener("change", changed, false); }, /// /// Gets or sets the display pattern for year. /// /// yearPattern: { get: function () { return this._datePatterns.year; }, set: function (value) { if (this._datePatterns.year !== value) { this._datePatterns.year = value; this._init(); } } }, }, { _getInformationWinRT: function (startDate, endDate, calendar, datePatterns) { datePatterns = datePatterns || { date: DEFAULT_DAY_PATTERN, month: DEFAULT_MONTH_PATTERN, year: DEFAULT_YEAR_PATTERN }; var tempCal = newCal(calendar); var monthCal = newCal(calendar); var dayCal = newCal(calendar); tempCal.setToMin(); var minDateTime = tempCal.getDateTime(); tempCal.setToMax(); var maxDateTime = tempCal.getDateTime(); function clamp(date) { return new Date(Math.min(new Date(Math.max(minDateTime, date)), maxDateTime)); } tempCal.hour = 12; startDate = clamp(startDate); endDate = clamp(endDate); tempCal.setDateTime(endDate); var end = { year: tempCal.year, era: tempCal.era }; tempCal.setDateTime(startDate); var yearLen = 0; yearLen = yearDiff(tempCal, end) + 1; // Explicity use a template that's equivalent to a longdate template // as longdate/shortdate can be overriden by the user var dateformat = formatCacheLookup("day month.full year", calendar).formatter; var localdatepattern = dateformat.patterns[0]; var isRTL = localdatepattern.charCodeAt(0) === 8207; var order = ["date", "month", "year"]; var indexes = { month: localdatepattern.indexOf("{month"), date: localdatepattern.indexOf("{day"), year: localdatepattern.indexOf("{year") }; order.sort(function (a, b) { if (indexes[a] < indexes[b]) { return -1; } else if (indexes[a] > indexes[b]) { return 1; } else { return 0; } }); var yearSource = (function () { return { getLength: function () { return yearLen; }, getValue: function (index) { tempCal.setDateTime(startDate); tempCal.addYears(index); return formatYear(datePatterns.year, calendar, DEFAULT_YEAR_PATTERN, datePatterns, order, tempCal); } }; })(); var monthSource = function (yearIndex) { monthCal.setDateTime(startDate); monthCal.addYears(yearIndex); return { getLength: function () { return monthCal.numberOfMonthsInThisYear; }, getValue: function (index) { monthCal.month = monthCal.firstMonthInThisYear; monthCal.addMonths(index); return formatMonth(datePatterns.month, calendar, DEFAULT_MONTH_PATTERN, monthCal); } }; }; var dateSource = function (yearIndex, monthIndex) { dayCal.setDateTime(startDate); dayCal.addYears(yearIndex); dayCal.month = dayCal.firstMonthInThisYear; dayCal.addMonths(monthIndex); dayCal.day = dayCal.firstDayInThisMonth; return { getLength: function () { return dayCal.numberOfDaysInThisMonth; }, getValue: function (index) { dayCal.day = dayCal.firstDayInThisMonth; dayCal.addDays(index); return formatDay(datePatterns.date, calendar, DEFAULT_DAY_PATTERN, dayCal); } }; }; return { isRTL: isRTL, forceLanguage: dateformat.resolvedLanguage, order: order, getDate: function (index, lastDate) { var lastCal; if (lastDate) { tempCal.setDateTime(lastDate); lastCal = { year: tempCal.year, month: tempCal.month, day: tempCal.day }; } var c = tempCal; c.setDateTime(startDate); c.addYears(index.year); var guessMonth; if (c.firstMonthInThisYear > c.lastMonthInThisYear) { if (index.month + c.firstMonthInThisYear > c.numberOfMonthsInThisYear) { guessMonth = index.month + c.firstMonthInThisYear - c.numberOfMonthsInThisYear; } else { guessMonth = index.month + c.firstMonthInThisYear; } if (lastCal && lastCal.year !== c.year) { // Year has changed in some transitions in Thai Calendar, this will change the first month, and last month indices of the year. guessMonth = Math.max(Math.min(lastCal.month, c.numberOfMonthsInThisYear), 1); } } else { if (lastCal && lastCal.year !== c.year) { // Year has changed in some transitions in Thai Calendar, this will change the first month, and last month indices of the year. guessMonth = Math.max(Math.min(lastCal.month, c.firstMonthInThisYear + c.numberOfMonthsInThisYear - 1), c.firstMonthInThisYear); } else { guessMonth = Math.max(Math.min(index.month + c.firstMonthInThisYear, c.firstMonthInThisYear + c.numberOfMonthsInThisYear - 1), c.firstMonthInThisYear); } } c.month = guessMonth; var guessDay = Math.max(Math.min(index.date + c.firstDayInThisMonth, c.firstDayInThisMonth + c.numberOfDaysInThisMonth - 1), c.firstDayInThisMonth); if (lastCal && (lastCal.year !== c.year || lastCal.month !== c.month)) { guessDay = Math.max(Math.min(lastCal.day, c.firstDayInThisMonth + c.numberOfDaysInThisMonth - 1), c.firstDayInThisMonth); } c.day = c.firstDayInThisMonth; c.addDays(guessDay - c.firstDayInThisMonth); return c.getDateTime(); }, getIndex: function (date) { var curDate = clamp(date); tempCal.setDateTime(curDate); var cur = { year: tempCal.year, era: tempCal.era }; var yearIndex = 0; tempCal.setDateTime(startDate); tempCal.month = 1; yearIndex = yearDiff(tempCal, cur); tempCal.setDateTime(curDate); var monthIndex = tempCal.month - tempCal.firstMonthInThisYear; if (monthIndex < 0) { // A special case is in some ThaiCalendar years first month // of the year is April, last month is March and month flow is wrap-around // style; April, March .... November, December, January, February, March. So the first index // will be 4 and last index will be 3. We are handling the case to convert this wraparound behavior // into selected index. monthIndex = tempCal.month - tempCal.firstMonthInThisYear + tempCal.numberOfMonthsInThisYear; } var dateIndex = tempCal.day - tempCal.firstDayInThisMonth; var index = { year: yearIndex, month: monthIndex, date: dateIndex }; return index; }, years: yearSource, months: monthSource, dates: dateSource }; }, _getInformationJS: function (startDate, endDate) { var minYear = startDate.getFullYear(); var maxYear = endDate.getFullYear(); var yearSource = { getLength: function () { return Math.max(0, maxYear - minYear + 1); }, getValue: function (index) { return minYear + index; } }; var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; var monthSource = function () { return { getLength: function () { return months.length; }, getValue: function (index) { return months[index]; }, getMonthNumber: function (index) { return Math.min(index, months.length - 1); } }; }; var dateSource = function (yearIndex, monthIndex) { var temp = new Date(); var year = yearSource.getValue(yearIndex); // The +1 is needed to make using a day of 0 work correctly var month = monthIndex + 1; // index is always correct, unlike getMonth which changes when the date is invalid temp.setFullYear(year, month, 0); var maxValue = temp.getDate(); return { getLength: function () { return maxValue; }, getValue: function (index) { return "" + (index + 1); }, getDateNumber: function (index) { return Math.min(index + 1, maxValue); } }; }; return { order: ["month", "date", "year"], getDate: function (index) { return new Date( yearSource.getValue(index.year), monthSource(index.year).getMonthNumber(index.month), dateSource(index.year, index.month).getDateNumber(index.date), 12, 0 ); }, getIndex: function (date) { var yearIndex = 0; var year = date.getFullYear(); if (year < minYear) { yearIndex = 0; } else if (year > this.maxYear) { yearIndex = yearSource.getLength() - 1; } else { yearIndex = date.getFullYear() - minYear; } var monthIndex = Math.min(date.getMonth(), monthSource(yearIndex).getLength()); var dateIndex = Math.min(date.getDate() - 1, dateSource(yearIndex, monthIndex).getLength()); return { year: yearIndex, month: monthIndex, date: dateIndex }; }, years: yearSource, months: monthSource, dates: dateSource }; } }); if (_WinRT.Windows.Globalization.Calendar && _WinRT.Windows.Globalization.DateTimeFormatting) { DatePicker.getInformation = DatePicker._getInformationWinRT; } else { DatePicker.getInformation = DatePicker._getInformationJS; } _Base.Class.mix(DatePicker, _Events.createEventProperties("change")); _Base.Class.mix(DatePicker, _Control.DOMEventMixin); return DatePicker; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/TimePicker',[ '../Core/_Global', '../Core/_WinRT', '../Core/_Base', '../Core/_BaseUtils', '../Core/_Events', '../Core/_Resources', '../Utilities/_Control', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', '../Utilities/_Select', 'require-style!less/desktop/controls', 'require-style!less/phone/controls' ], function timePickerInit(_Global, _WinRT, _Base, _BaseUtils, _Events, _Resources, _Control, _ElementUtilities, _Hoverable, _Select) { "use strict"; _Base.Namespace.define("WinJS.UI", { /// /// Allows users to select time values. /// /// /// Time Picker /// /// /// ]]> /// Occurs when the time changes. /// /// /// TimePicker: _Base.Namespace._lazy(function () { // Constants definition var DEFAULT_MINUTE_PATTERN = "{minute.integer(2)}", DEFAULT_HOUR_PATTERN = "{hour.integer(1)}", DEFAULT_PERIOD_PATTERN = "{period.abbreviated(2)}"; var strings = { get ariaLabel() { return _Resources._getWinJSString("ui/timePicker").value; }, get selectHour() { return _Resources._getWinJSString("ui/selectHour").value; }, get selectMinute() { return _Resources._getWinJSString("ui/selectMinute").value; }, get selectAMPM() { return _Resources._getWinJSString("ui/selectAMPM").value; }, }; // date1 and date2 must be Date objects with their date portions set to the // sentinel date. var areTimesEqual = function (date1, date2) { return date1.getHours() === date2.getHours() && date1.getMinutes() === date2.getMinutes(); }; var TimePicker = _Base.Class.define(function TimePicker_ctor(element, options) { /// /// Initializes a new instance of the TimePicker control /// /// The DOM element associated with the TimePicker control. /// /// /// The set of options to be applied initially to the TimePicker control. /// /// A constructed TimePicker control. /// /// // Default to current time this._currentTime = TimePicker._sentinelDate(); element = element || _Global.document.createElement("div"); _ElementUtilities.addClass(element, "win-disposable"); element.winControl = this; var label = element.getAttribute("aria-label"); if (!label) { element.setAttribute("aria-label", strings.ariaLabel); } this._timePatterns = { minute: null, hour: null, period: null }; // Options should be set after the element is initialized which is // the same order of operation as imperatively setting options. this._init(element); _Control.setOptions(this, options); }, { _currentTime: null, _clock: null, _disabled: false, _hourElement: null, _hourControl: null, _minuteElement: null, _minuteControl: null, _ampmElement: null, _ampmControl: null, _minuteIncrement: 1, _timePatterns: { minute: null, hour: null, period: null }, _information: null, _addAccessibilityAttributes: function () { //see http://www.w3.org/TR/wai-aria/rdf_model.png for details this._domElement.setAttribute("role", "group"); this._hourElement.setAttribute("aria-label", strings.selectHour); this._minuteElement.setAttribute("aria-label", strings.selectMinute); if (this._ampmElement) { this._ampmElement.setAttribute("aria-label", strings.selectAMPM); } }, _addControlsInOrder: function (info) { var that = this; info.order.forEach(function (s, index) { switch (s) { case "hour": that._domElement.appendChild(that._hourElement); _ElementUtilities.addClass(that._hourElement, "win-order" + index); break; case "minute": that._domElement.appendChild(that._minuteElement); _ElementUtilities.addClass(that._minuteElement, "win-order" + index); break; case "period": if (that._ampmElement) { that._domElement.appendChild(that._ampmElement); _ElementUtilities.addClass(that._ampmElement, "win-order" + index); } break; } }); }, dispose: function () { /// /// /// Disposes this TimePicker. /// /// /// }, /// /// Gets or sets the type of clock to display (12HourClock or 24HourClock). It defaults to the user setting. /// /// clock: { get: function () { return this._clock; }, set: function (value) { if (this._clock !== value) { this._clock = value; this._init(); } } }, /// /// Gets or sets the current date (and time) of the TimePicker. /// /// current: { get: function () { var cur = this._currentTime; if (cur) { var time = TimePicker._sentinelDate(); time.setHours(cur.getHours()); // accounts for AM/PM time.setMinutes(this._getMinutesIndex(cur) * this.minuteIncrement); time.setSeconds(0); time.setMilliseconds(0); return time; } else { return cur; } }, set: function (value) { var newTime; if (typeof (value) === "string") { newTime = TimePicker._sentinelDate(); newTime.setTime(Date.parse(newTime.toDateString() + " " + value)); } else { newTime = TimePicker._sentinelDate(); newTime.setHours(value.getHours()); newTime.setMinutes(value.getMinutes()); } var oldTime = this._currentTime; if (!areTimesEqual(oldTime, newTime)) { this._currentTime = newTime; this._updateDisplay(); } } }, /// /// Specifies whether the TimePicker is disabled. /// /// disabled: { get: function () { return this._disabled; }, set: function (value) { if (this._disabled !== value) { this._disabled = value; if (this._hourControl) { this._hourControl.setDisabled(value); this._minuteControl.setDisabled(value); } if (this._ampmControl) { this._ampmControl.setDisabled(value); } } } }, /// element: { get: function () { return this._domElement; } }, _init: function (element) { this._setElement(element); this._updateDisplay(); }, /// /// Gets or sets the display pattern for the hour. /// /// hourPattern: { get: function () { return this._timePatterns.hour.pattern; }, set: function (value) { if (this._timePatterns.hour !== value) { this._timePatterns.hour = value; this._init(); } } }, _getHoursAmpm: function (time) { var hours24 = time.getHours(); if (this._ampmElement) { if (hours24 === 0) { return { hours: 12, ampm: 0 }; } else if (hours24 < 12) { return { hours: hours24, ampm: 0 }; } return { hours: hours24 - 12, ampm: 1 }; } return { hours: hours24 }; }, _getHoursIndex: function (hours) { if (this._ampmElement && hours === 12) { return 0; } return hours; }, _getMinutesIndex: function (time) { return parseInt(time.getMinutes() / this.minuteIncrement); }, /// /// Gets or sets the minute increment. For example, "15" specifies that the TimePicker minute control should display only the choices 00, 15, 30, 45. /// /// minuteIncrement: { //prevent divide by 0, and leave user's input intact get: function () { return Math.max(1, Math.abs(this._minuteIncrement | 0) % 60); }, set: function (value) { if (this._minuteIncrement !== value) { this._minuteIncrement = value; this._init(); } } }, /// /// Gets or sets the display pattern for the minute. /// /// minutePattern: { get: function () { return this._timePatterns.minute.pattern; }, set: function (value) { if (this._timePatterns.minute !== value) { this._timePatterns.minute = value; this._init(); } } }, /// /// Gets or sets the display pattern for the period. /// /// periodPattern: { get: function () { return this._timePatterns.period.pattern; }, set: function (value) { if (this._timePatterns.period !== value) { this._timePatterns.period = value; this._init(); } } }, _setElement: function (element) { this._domElement = this._domElement || element; if (!this._domElement) { return; } var info = TimePicker.getInformation(this.clock, this.minuteIncrement, this._timePatterns); this._information = info; if (info.forceLanguage) { this._domElement.setAttribute("lang", info.forceLanguage); this._domElement.setAttribute("dir", info.isRTL ? "rtl" : "ltr"); } _ElementUtilities.empty(this._domElement); _ElementUtilities.addClass(this._domElement, "win-timepicker"); this._hourElement = _Global.document.createElement("select"); _ElementUtilities.addClass(this._hourElement, "win-timepicker-hour"); this._minuteElement = _Global.document.createElement("select"); _ElementUtilities.addClass(this._minuteElement, "win-timepicker-minute"); this._ampmElement = null; if (info.clock === "12HourClock") { this._ampmElement = _Global.document.createElement("select"); _ElementUtilities.addClass(this._ampmElement, "win-timepicker-period"); } this._addControlsInOrder(info); var hoursAmpm = this._getHoursAmpm(this.current); this._hourControl = new _Select._Select(this._hourElement, { dataSource: this._getInfoHours(), disabled: this.disabled, index: this._getHoursIndex(hoursAmpm.hours) }); this._minuteControl = new _Select._Select(this._minuteElement, { dataSource: info.minutes, disabled: this.disabled, index: this._getMinutesIndex(this.current) }); this._ampmControl = null; if (this._ampmElement) { this._ampmControl = new _Select._Select(this._ampmElement, { dataSource: info.periods, disabled: this.disabled, index: hoursAmpm.ampm }); } this._wireupEvents(); this._updateValues(); this._addAccessibilityAttributes(); }, _getInfoHours: function () { return this._information.hours; }, _updateLayout: function () { if (!this._domElement) { return; } this._updateValues(); }, _updateValues: function () { if (this._hourControl) { var hoursAmpm = this._getHoursAmpm(this.current); if (this._ampmControl) { this._ampmControl.index = hoursAmpm.ampm; } this._hourControl.index = this._getHoursIndex(hoursAmpm.hours); this._minuteControl.index = this._getMinutesIndex(this.current); } }, _updateDisplay: function () { //Render display index based on constraints (minuteIncrement) //Will not modify current time var hoursAmpm = this._getHoursAmpm(this.current); if (this._ampmControl) { this._ampmControl.index = hoursAmpm.ampm; } if (this._hourControl) { this._hourControl.index = this._getHoursIndex(hoursAmpm.hours); this._minuteControl.index = this._getMinutesIndex(this.current); } }, _wireupEvents: function () { var that = this; var fixupHour = function () { var hour = that._hourControl.index; if (that._ampmElement) { if (that._ampmControl.index === 1) { if (hour !== 12) { hour += 12; } } } return hour; }; var changed = function () { var hour = fixupHour(); that._currentTime.setHours(hour); that._currentTime.setMinutes(that._minuteControl.index * that.minuteIncrement); }; this._hourElement.addEventListener("change", changed, false); this._minuteElement.addEventListener("change", changed, false); if (this._ampmElement) { this._ampmElement.addEventListener("change", changed, false); } } }, { _sentinelDate: function () { // This is July 15th, 2011 as our sentinel date. There are no known // daylight savings transitions that happened on that date. var current = new Date(); return new Date(2011, 6, 15, current.getHours(), current.getMinutes()); }, _getInformationWinRT: function (clock, minuteIncrement, timePatterns) { var newFormatter = function (pattern, defaultPattern) { var dtf = _WinRT.Windows.Globalization.DateTimeFormatting; pattern = !pattern ? defaultPattern : pattern; var formatter = new dtf.DateTimeFormatter(pattern); if (clock) { formatter = dtf.DateTimeFormatter(pattern, formatter.languages, formatter.geographicRegion, formatter.calendar, clock); } return formatter; }; var glob = _WinRT.Windows.Globalization; var calendar = new glob.Calendar(); if (clock) { calendar = new glob.Calendar(calendar.languages, calendar.getCalendarSystem(), clock); } calendar.setDateTime(TimePicker._sentinelDate()); var computedClock = calendar.getClock(); var numberOfHours = 24; numberOfHours = calendar.numberOfHoursInThisPeriod; var periods = (function () { var periodFormatter = newFormatter(timePatterns.period, DEFAULT_PERIOD_PATTERN); return { getLength: function () { return 2; }, getValue: function (index) { var date = TimePicker._sentinelDate(); if (index === 0) { date.setHours(1); var am = periodFormatter.format(date); return am; } if (index === 1) { date.setHours(13); var pm = periodFormatter.format(date); return pm; } return null; } }; })(); // Determine minute format from the DateTimeFormatter var minutes = (function () { var minuteFormatter = newFormatter(timePatterns.minute, DEFAULT_MINUTE_PATTERN); var now = TimePicker._sentinelDate(); return { getLength: function () { return 60 / minuteIncrement; }, getValue: function (index) { var display = index * minuteIncrement; now.setMinutes(display); return minuteFormatter.format(now); } }; })(); // Determine hour format from the DateTimeFormatter var hours = (function () { var hourFormatter = newFormatter(timePatterns.hour, DEFAULT_HOUR_PATTERN); var now = TimePicker._sentinelDate(); return { getLength: function () { return numberOfHours; }, getValue: function (index) { now.setHours(index); return hourFormatter.format(now); } }; })(); // Determine the order of the items from the DateTimeFormatter. // "hour minute" also returns the period (if needed). // var hourMinuteFormatter = newFormatter("hour minute"); var pattern = hourMinuteFormatter.patterns[0]; var order = ["hour", "minute"]; var indexes = { period: pattern.indexOf("{period"), hour: pattern.indexOf("{hour"), minute: pattern.indexOf("{minute") }; if (indexes.period > -1) { order.push("period"); } var DateTimeFormatter = _WinRT.Windows.Globalization.DateTimeFormatting.DateTimeFormatter; var dtf = new DateTimeFormatter("month.full", _WinRT.Windows.Globalization.ApplicationLanguages.languages, "ZZ", "GregorianCalendar", "24HourClock"); var pat = dtf.patterns[0]; var isRTL = pat.charCodeAt(0) === 8207; if (isRTL) { var temp = indexes.hour; indexes.hour = indexes.minute; indexes.minute = temp; } order.sort(function (a, b) { if (indexes[a] < indexes[b]) { return -1; } else if (indexes[a] > indexes[b]) { return 1; } else { return 0; } }); return { minutes: minutes, hours: hours, clock: computedClock, periods: periods, order: order, forceLanguage: hourMinuteFormatter.resolvedLanguage, isRTL: isRTL }; }, _getInformationJS: function (clock, minuteIncrement) { var hours = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; var minutes = {}; minutes.getLength = function () { return 60 / minuteIncrement; }; minutes.getValue = function (index) { var display = index * minuteIncrement; if (display < 10) { return "0" + display.toString(); } else { return display.toString(); } }; var order = ["hour", "minute", "period"]; if (clock === "24HourClock") { hours = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]; order = ["hour", "minute"]; } return { minutes: minutes, hours: hours, clock: clock || "12HourClock", periods: ["AM", "PM"], order: order }; } }); if (_WinRT.Windows.Globalization.DateTimeFormatting && _WinRT.Windows.Globalization.Calendar && _WinRT.Windows.Globalization.ApplicationLanguages) { TimePicker.getInformation = TimePicker._getInformationWinRT; } else { TimePicker.getInformation = TimePicker._getInformationJS; } _Base.Class.mix(TimePicker, _Events.createEventProperties("change")); _Base.Class.mix(TimePicker, _Control.DOMEventMixin); return TimePicker; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Back Button define('WinJS/Controls/BackButton',[ '../Core/_Global', '../Core/_Base', '../Core/_ErrorFromName', '../Core/_Resources', '../Navigation', '../Utilities/_Control', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', 'require-style!less/desktop/controls', 'require-style!less/phone/controls' ], function backButtonInit(_Global, _Base, _ErrorFromName, _Resources, Navigation, _Control, _ElementUtilities, _Hoverable) { "use strict"; var Key = _ElementUtilities.Key; // Class Names var navigationBackButtonClass = 'win-navigation-backbutton'; var glyphClass = "win-back"; // CONSTANTS var MOUSE_BACK_BUTTON = 3; // Create Singleton for global event registering/unregistering. This Singleton should only be created once. // Here the function 'returnBackButtonSingelton' is called immediateley and its result is the singleton object. var singleton = (function createBackButtonSingleton() { /* Step 1: Build JavaScript closure */ function hookUpBackButtonGlobalEventHandlers() { // Subscribes to global events on the window object _Global.addEventListener('keyup', backButtonGlobalKeyUpHandler, false); _ElementUtilities._addEventListener(_Global, 'pointerup', backButtonGlobalMSPointerUpHandler, false); } function unHookBackButtonGlobalEventHandlers() { // Unsubscribes from global events on the window object _Global.removeEventListener('keyup', backButtonGlobalKeyUpHandler, false); _ElementUtilities._removeEventListener(_Global, 'pointerup', backButtonGlobalMSPointerUpHandler, false); } function backButtonGlobalKeyUpHandler(event) { // Navigates back when (alt + left) or BrowserBack keys are released. if ((event.keyCode === Key.leftArrow && event.altKey && !event.shiftKey && !event.ctrlKey) || (event.keyCode === Key.browserBack)) { Navigation.back(); } } function backButtonGlobalMSPointerUpHandler(event) { // Responds to clicks to enable navigation using 'back' mouse buttons. if (event.button === MOUSE_BACK_BUTTON) { Navigation.back(); } } // Singleton reference count for registering and unregistering global event handlers. var backButtonReferenceCount = 0; // /* Step 2: Return Singleton object literal */ return { addRef: function () { if (backButtonReferenceCount === 0) { hookUpBackButtonGlobalEventHandlers(); } backButtonReferenceCount++; }, release: function () { if (backButtonReferenceCount > 0) { // Ensure count won't become negative. backButtonReferenceCount--; if (backButtonReferenceCount === 0) { unHookBackButtonGlobalEventHandlers(); } } }, getCount: function () { // Return the value of the reference count. Useful for unit testing. return backButtonReferenceCount; } }; }()); // Immediate invoke creates and returns the Singleton _Base.Namespace.define("WinJS.UI", { /// /// /// Provides backwards navigation functionality. /// /// /// /// /// /// ]]> /// The BackButton control itself /// The Back Arrow glyph /// /// /// BackButton: _Base.Namespace._lazy(function () { // Statics var strings = { get ariaLabel() { return _Resources._getWinJSString("ui/backbuttonarialabel").value; }, get duplicateConstruction() { return "Invalid argument: Controls may only be instantiated one time for each DOM element"; }, get badButtonElement() { return "Invalid argument: For a button, toggle, or flyout command, the element must be null or a button element"; } }; var BackButton = _Base.Class.define(function BackButton_ctor(element, options) { /// /// /// Creates a new BackButton control /// /// /// The DOM element that will host the control. If this parameter is null, this constructor creates one for you. /// /// /// An object that contains one or more property/value pairs to apply to the new control. Each property of the options object corresponds to /// one of the control's properties or events. /// /// /// A BackButton control. /// /// /// // Check to make sure we weren't duplicated if (element && element.winControl) { throw new _ErrorFromName("WinJS.UI.BackButton.DuplicateConstruction", strings.duplicateConstruction); } this._element = element || _Global.document.createElement("button"); options = options || {}; this._initializeButton(); // This will also set the aria-label and tooltip this._disposed = false; // Remember ourselves this._element.winControl = this; _Control.setOptions(this, options); // Add event handlers for this back button instance this._buttonClickHandler = this._handleBackButtonClick.bind(this); this._element.addEventListener('click', this._buttonClickHandler, false); this._navigatedHandler = this._handleNavigatedEvent.bind(this); Navigation.addEventListener('navigated', this._navigatedHandler, false); // Increment reference count / manage add global event handlers singleton.addRef(); }, { /// element: { get: function () { return this._element; } }, dispose: function () { /// /// /// Disposes this control. /// /// /// if (this._disposed) { return; } this._disposed = true; // Mark this control as disposed. // Remove 'navigated' eventhandler for this BackButton Navigation.removeEventListener('navigated', this._navigatedHandler, false); singleton.release(); // Decrement reference count. }, refresh: function () { /// /// /// Sets the 'disabled' attribute to correct the value based on the current navigation history stack. /// /// /// if (Navigation.canGoBack) { this._element.disabled = false; } else { this._element.disabled = true; } }, _initializeButton: function () { //Final EN-US HTML should be: // //Button will automatically be disabled if WinJS.Navigation.history.canGoBack is false. // Verify the HTML is a button if (this._element.tagName !== "BUTTON") { throw new _ErrorFromName("WinJS.UI.BackButton.BadButtonElement", strings.badButtonElement); } // Attach our css classes _ElementUtilities.addClass(this._element, navigationBackButtonClass); // Attach disposable class. _ElementUtilities.addClass(this._element, "win-disposable"); // Create inner glyph element this._element.innerHTML = ''; // Set the 'disabled' property to the correct value based on the current navigation history stack. this.refresh(); // Set Aria-label and native tooltip to the same localized string equivalent of "Back" this._element.setAttribute("aria-label", strings.ariaLabel); this._element.setAttribute("title", strings.ariaLabel); // Explicitly set type attribute to avoid the default '; this._headerTabStopElement = this._headerElement.firstElementChild; // The purpose of headerWrapperElement is to lay out its children in a flexbox. Ideally, this flexbox would // be on headerTabStopElement. However, firefox lays out flexboxes with display:flex differently. // Firefox bug 1014285 (Button with display:inline-flex doesn't layout properly) // https://bugzilla.mozilla.org/show_bug.cgi?id=1014285 this._headerWrapperElement = this._headerTabStopElement.firstElementChild; this._headerContentElement = this._headerWrapperElement.firstElementChild; this._headerChevronElement = this._headerWrapperElement.lastElementChild; element.appendChild(this._headerElement); this._winKeyboard = new _KeyboardBehavior._WinKeyboard(this._headerElement); this._contentElement = _Global.document.createElement("DIV"); this._contentElement.className = HubSection._ClassName.hubSectionContent; this._contentElement.style.visibility = "hidden"; element.appendChild(this._contentElement); // Reparent any existing elements inside the new hub section content element. var elementToMove = this.element.firstChild; while (elementToMove !== this._headerElement) { var nextElement = elementToMove.nextSibling; this._contentElement.appendChild(elementToMove); elementToMove = nextElement; } this._processors = [ControlProcessor.processAll]; _Control.setOptions(this, options); }, { /// element: { get: function () { return this._element; } }, /// /// Gets or sets a value that specifies whether the header is static. Set this value to true to disable clicks and other interactions. /// /// isHeaderStatic: { get: function () { return this._isHeaderStatic; }, set: function (value) { this._isHeaderStatic = value; if (!this._isHeaderStatic) { this._headerTabStopElement.setAttribute("role", "link"); _ElementUtilities.addClass(this._headerTabStopElement, HubSection._ClassName.hubSectionInteractive); } else { this._headerTabStopElement.setAttribute("role", "heading"); _ElementUtilities.removeClass(this._headerTabStopElement, HubSection._ClassName.hubSectionInteractive); } } }, /// /// Gets the DOM element that hosts the HubSection's content. /// /// contentElement: { get: function () { return this._contentElement; } }, /// /// Get or set the HubSection's header. After you set this property, the Hub renders the header again. /// /// header: { get: function () { return this._header; }, set: function (value) { // Render again even if it is equal to itself. this._header = value; this._renderHeader(); } }, _setHeaderTemplate: function HubSection_setHeaderTemplate(template) { this._template = _ElementUtilities._syncRenderer(template); this._renderHeader(); }, _renderHeader: function HubSection_renderHeader() { if (this._template) { _Dispose._disposeElement(this._headerContentElement); _ElementUtilities.empty(this._headerContentElement); this._template(this, this._headerContentElement); } }, _process: function HubSection_process() { var that = this; this._processed = (this._processors || []).reduce(function (promise, processor) { return promise.then(function () { return processor(that.contentElement); }); }, this._processed || Promise.as()); this._processors = null; return this._processed; }, dispose: function HubSection_dispose() { /// /// /// Disposes this control. /// /// /// if (this._disposed) { return; } this._disposed = true; this._processors = null; _Dispose._disposeElement(this._headerContentElement); _Dispose.disposeSubTree(this.contentElement); } }, { // Names of classes used by the HubSection. _ClassName: { hubSection: "win-hub-section", hubSectionHeader: "win-hub-section-header", hubSectionHeaderTabStop: "win-hub-section-header-tabstop", hubSectionHeaderWrapper: "win-hub-section-header-wrapper", hubSectionInteractive: "win-hub-section-header-interactive", hubSectionHeaderContent: "win-hub-section-header-content", hubSectionHeaderChevron: "win-hub-section-header-chevron", hubSectionContent: "win-hub-section-content" }, _Constants: { ellipsisTypeClassName: "win-type-ellipsis", xLargeTypeClassName: "win-type-x-large" }, isDeclarativeControlContainer: _BaseUtils.markSupportedForProcessing(function (section, callback) { if (callback === ControlProcessor.processAll) { return; } section._processors = section._processors || []; section._processors.push(callback); // Once processed the first time synchronously queue up new processors as they come in if (section._processed) { section._process(); } }) }); return HubSection; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/Hub',[ '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Events', '../Core/_Log', '../Core/_Resources', '../Core/_WriteProfilerMark', '../Animations', '../Animations/_TransitionAnimation', '../BindingList', '../ControlProcessor', '../Promise', '../_Signal', '../Scheduler', '../Utilities/_Control', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', '../Utilities/_UI', './Hub/_Section', 'require-style!less/desktop/controls', 'require-style!less/phone/controls' ], function hubInit(_Global, _Base, _BaseUtils, _ErrorFromName, _Events, _Log, _Resources, _WriteProfilerMark, Animations, _TransitionAnimation, BindingList, ControlProcessor, Promise, _Signal, Scheduler, _Control, _ElementUtilities, _Hoverable, _UI, _Section) { "use strict"; _Base.Namespace.define("WinJS.UI", { /// /// /// Displays sections of content. /// /// /// /// /// /// ///
HubSection Content
/// ]]>
/// Raised when the Hub is about to play an entrance or a transition animation. /// Raised when a header is invoked. /// Raised when the loading state changes. /// The entire Hub control. /// The progress indicator for the Hub. /// The viewport of the Hub. /// The scrollable region of the Hub. /// /// /// Hub: _Base.Namespace._lazy(function () { var Key = _ElementUtilities.Key; function hubDefaultHeaderTemplate(section) { var element = _Global.document.createTextNode(typeof section.header === "object" ? JSON.stringify(section.header) : ('' + section.header)); return element; } var createEvent = _Events._createEventProperty; var eventNames = { contentAnimating: "contentanimating", headerInvoked: "headerinvoked", loadingStateChanged: "loadingstatechanged" }; // Delay time before progress dots are shown when loading hub section(s) on screen. var progressDelay = 500; var verticalNames = { scrollPos: "scrollTop", scrollSize: "scrollHeight", offsetPos: "offsetTop", offsetSize: "offsetHeight", oppositeOffsetSize: "offsetWidth", marginStart: "marginTop", marginEnd: "marginBottom", borderStart: "borderTopWidth", borderEnd: "borderBottomWidth", paddingStart: "paddingTop", paddingEnd: "paddingBottom" }; var rtlHorizontalNames = { scrollPos: "scrollLeft", scrollSize: "scrollWidth", offsetPos: "offsetLeft", offsetSize: "offsetWidth", oppositeOffsetSize: "offsetHeight", marginStart: "marginRight", marginEnd: "marginLeft", borderStart: "borderRightWidth", borderEnd: "borderLeftWidth", paddingStart: "paddingRight", paddingEnd: "paddingLeft" }; var ltrHorizontalNames = { scrollPos: "scrollLeft", scrollSize: "scrollWidth", offsetPos: "offsetLeft", offsetSize: "offsetWidth", oppositeOffsetSize: "offsetHeight", marginStart: "marginLeft", marginEnd: "marginRight", borderStart: "borderLeftWidth", borderEnd: "borderRightWidth", paddingStart: "paddingLeft", paddingEnd: "paddingRight" }; var Hub = _Base.Class.define(function Hub_ctor(element, options) { /// /// /// Creates a new Hub control. /// /// /// The DOM element that hosts the Hub control. /// /// /// An object that contains one or more property/value pairs to apply to the new control. /// Each property of the options object corresponds to one of the control's properties or events. /// Event names must begin with "on". For example, to provide a handler for the contentanimating event, /// add a property named "oncontentanimating" to the options object and set its value to the event handler. /// /// /// The new Hub. /// /// /// element = element || _Global.document.createElement("DIV"); options = options || {}; if (element.winControl) { throw new _ErrorFromName("WinJS.UI.Hub.DuplicateConstruction", strings.duplicateConstruction); } this._id = element.id || _ElementUtilities._uniqueID(element); this._writeProfilerMark("constructor,StartTM"); this._windowKeyDownHandlerBound = this._windowKeyDownHandler.bind(this); _Global.addEventListener('keydown', this._windowKeyDownHandlerBound); // Attaching JS control to DOM element element.winControl = this; this._element = element; _ElementUtilities.addClass(this.element, Hub._ClassName.hub); _ElementUtilities.addClass(this.element, "win-disposable"); this._viewportElement = _Global.document.createElement("DIV"); this._viewportElement.className = Hub._ClassName.hubViewport; this._element.appendChild(this._viewportElement); this._viewportElement.setAttribute("role", "group"); this._viewportElement.setAttribute("aria-label", strings.hubViewportAriaLabel); this._surfaceElement = _Global.document.createElement("DIV"); this._surfaceElement.className = Hub._ClassName.hubSurface; this._viewportElement.appendChild(this._surfaceElement); // Start invisible so that you do not see the content loading until the sections are ready. this._visible = false; this._viewportElement.style.opacity = 0; if (!options.orientation) { this._orientation = _UI.Orientation.horizontal; _ElementUtilities.addClass(this.element, Hub._ClassName.hubHorizontal); } this._fireEntrance = true; this._animateEntrance = true; this._loadId = 0; this.runningAnimations = new Promise.wrap(); this._currentIndexForSezo = 0; // This internally assigns this.sections which causes section to be used (even from options) before // scrollPosition or sectionOnScreen. this._parse(); _Control.setOptions(this, options); _ElementUtilities._addEventListener(this.element, "focusin", this._focusin.bind(this), false); this.element.addEventListener("keydown", this._keyDownHandler.bind(this)); this.element.addEventListener("click", this._clickHandler.bind(this)); this._resizeHandlerBound = this._resizeHandler.bind(this); this.element.addEventListener("mselementresize", this._resizeHandlerBound); _ElementUtilities._resizeNotifier.subscribe(this.element, this._resizeHandlerBound); this._viewportElement.addEventListener("scroll", this._scrollHandler.bind(this)); this._surfaceElement.addEventListener("mselementresize", this._contentResizeHandler.bind(this)); this._handleSectionChangedBind = this._handleSectionChanged.bind(this); this._handleSectionInsertedBind = this._handleSectionInserted.bind(this); this._handleSectionMovedBind = this._handleSectionMoved.bind(this); this._handleSectionRemovedBind = this._handleSectionRemoved.bind(this); this._handleSectionReloadBind = this._handleSectionReload.bind(this); this._refresh(); this._writeProfilerMark("constructor,StopTM"); }, { /// element: { get: function () { return this._element; } }, /// /// Gets or sets the orientation of sections within the Hub. /// /// orientation: { get: function () { return this._orientation; }, set: function (value) { if (value === this._orientation) { return; } this._measured = false; // clear existing scroll before we switch orientation if (this._names) { // handle setting orientation before we measure var newScrollPos = {}; newScrollPos[this._names.scrollPos] = 0; _ElementUtilities.setScrollPosition(this._viewportElement, newScrollPos); } if (value === _UI.Orientation.vertical) { _ElementUtilities.removeClass(this.element, Hub._ClassName.hubHorizontal); _ElementUtilities.addClass(this.element, Hub._ClassName.hubVertical); } else { value = _UI.Orientation.horizontal; _ElementUtilities.removeClass(this.element, Hub._ClassName.hubVertical); _ElementUtilities.addClass(this.element, Hub._ClassName.hubHorizontal); } this._orientation = value; Scheduler.schedule(this._updateSnapList.bind(this), Scheduler.Priority.idle); } }, /// /// Gets or sets the WinJS.Binding.List of HubSection objects that belong to this Hub. /// /// sections: { get: function () { if (this._pendingSections) { return this._pendingSections; } return this._sections; }, set: function (value) { var resetScrollPosition = !this._pendingSections; this._pendingSections = value; this._refresh(); if (resetScrollPosition) { this.scrollPosition = 0; } } }, /// /// Gets or sets the WinJS.Binding.Template or template function that creates the DOM elements /// which represent the header for each HubSection. Each header can /// contain multiple DOM elements, but we recommend that it have a single /// root element. /// /// headerTemplate: { get: function () { if (this._pendingHeaderTemplate) { return this._pendingHeaderTemplate; } if (!this._headerTemplate) { this._headerTemplate = hubDefaultHeaderTemplate; } return this._headerTemplate; }, set: function (value) { this._pendingHeaderTemplate = value || hubDefaultHeaderTemplate; this._refresh(); } }, /// /// Gets or sets the position of the Hub's scrollbar. /// /// scrollPosition: { get: function () { if (+this._pendingScrollLocation === this._pendingScrollLocation) { return this._pendingScrollLocation; } this._measure(); return this._scrollPosition; }, set: function (value) { value = Math.max(0, value); if (this._pendingRefresh) { // Unable to constrain length because sections may have changed. this._pendingScrollLocation = value; this._pendingSectionOnScreen = null; } else { this._measure(); var targetScrollPos = Math.max(0, Math.min(this._scrollLength - this._viewportSize, value)); this._scrollPosition = targetScrollPos; var newScrollPos = {}; newScrollPos[this._names.scrollPos] = targetScrollPos; _ElementUtilities.setScrollPosition(this._viewportElement, newScrollPos); } } }, /// /// Gets or sets the index of first section in view. This property is useful for restoring a previous view when your app launches or resumes. /// /// sectionOnScreen: { get: function () { if (+this._pendingSectionOnScreen === this._pendingSectionOnScreen) { return this._pendingSectionOnScreen; } this._measure(); for (var i = 0; i < this._sectionSizes.length; i++) { var sectionSize = this._sectionSizes[i]; if ((sectionSize.offset + sectionSize.size - sectionSize.borderEnd - sectionSize.paddingEnd) > (this._scrollPosition + this._startSpacer + sectionSize.borderStart + sectionSize.paddingStart)) { return i; } } return -1; }, set: function (value) { value = Math.max(0, value); if (this._pendingRefresh) { this._pendingSectionOnScreen = value; this._pendingScrollLocation = null; } else { this._measure(); if (value >= 0 && value < this._sectionSizes.length) { this._scrollToSection(value); } } } }, /// /// Gets or sets the index of first section at least partially in view. Use for animations. /// /// indexOfFirstVisible: { get: function () { this._measure(); for (var i = 0; i < this._sectionSizes.length; i++) { var sectionSize = this._sectionSizes[i]; if ((sectionSize.offset + sectionSize.size - sectionSize.borderEnd - sectionSize.paddingEnd) > this._scrollPosition) { return i; } } return -1; } }, /// /// Gets or sets the index of last section at least partially in view. Use for animations. /// /// indexOfLastVisible: { get: function () { this._measure(); for (var i = this._sectionSizes.length - 1; i >= 0; i--) { var sectionSize = this._sectionSizes[i]; if ((sectionSize.offset + sectionSize.paddingStart + sectionSize.borderStart) < (this._scrollPosition + this._viewportSize)) { return i; } } return -1; } }, /// /// Raised when the user clicks on an interactive header. /// /// onheaderinvoked: createEvent(eventNames.headerInvoked), /// /// Raised when the loadingState of the Hub changes. /// /// onloadingstatechanged: createEvent(eventNames.loadingStateChanged), /// /// Raised when Hub is about to play entrance, contentTransition, insert, or remove animations. /// /// oncontentanimating: createEvent(eventNames.contentAnimating), _refresh: function hub_refresh() { if (this._pendingRefresh) { return; } this._loadId++; this._setState(Hub.LoadingState.loading); // This is to coalesce property setting operations such as sections and scrollPosition. this._pendingRefresh = true; Scheduler.schedule(this._refreshImpl.bind(this), Scheduler.Priority.high); }, _refreshImpl: function hub_refreshImpl() { if (this._disposed) { return; } var fadeOutAnimation = Promise.wrap(); if (this._pendingSections) { this._animateEntrance = true; this._fireEntrance = !this._visible; if (!this._fireEntrance) { this._visible = false; this._viewportElement.style.opacity = 0; if (_TransitionAnimation.isAnimationEnabled()) { var animateTransition = this._fireEvent(Hub._EventName.contentAnimating, { type: Hub.AnimationType.contentTransition }); if (animateTransition) { this._viewportElement.style["-ms-overflow-style"] = "none"; fadeOutAnimation = Animations.fadeOut(this._viewportElement).then(function () { this._viewportElement.style["-ms-overflow-style"] = ""; }.bind(this)); } this._animateEntrance = animateTransition; } } } fadeOutAnimation.done(this._applyProperties.bind(this)); }, _applyProperties: function hub_applyProperties() { if (this._disposed) { return; } this._pendingRefresh = false; var needsToLoadSections = false; if (this._pendingSections) { needsToLoadSections = true; this._updateEvents(this._sections, this._pendingSections); this._sections = this._pendingSections; this._pendingSections = null; // Remove any declaratively specified hub sections before attachSections. while (this.element.firstElementChild !== this._viewportElement) { var toRemove = this.element.firstElementChild; toRemove.parentNode.removeChild(toRemove); } _ElementUtilities.empty(this._surfaceElement); } if (this._pendingHeaderTemplate) { this._headerTemplate = this._pendingHeaderTemplate; this._pendingHeaderTemplate = null; } this._assignHeaderTemplate(); if (needsToLoadSections) { this._attachSections(); } // Scroll after headers are rendered and sections are attached so the scroll thumb is correct. if (+this._pendingSectionOnScreen === this._pendingSectionOnScreen) { // If there are both pending section on screen and scroll location use section on screen. this.sectionOnScreen = this._pendingSectionOnScreen; } else if (+this._pendingScrollLocation === this._pendingScrollLocation) { this.scrollPosition = this._pendingScrollLocation; } else { // Sections reset without sectionOnScreen or scrollPosition APIs. this.scrollPosition = 0; } this._pendingSectionOnScreen = null; this._pendingScrollLocation = null; // Using current (or new) scroll location load the sections this._setState(Hub.LoadingState.loading); this._loadSections(); }, _handleSectionChanged: function hub_handleSectionChanged(ev) { // Change is triggered by binding list setAt() API. if (this._pendingSections) { return; } var newSection = ev.detail.newValue; var oldSection = ev.detail.oldValue; newSection._setHeaderTemplate(this.headerTemplate); if (newSection.element !== oldSection.element) { if (newSection.element.parentNode === this._surfaceElement) { throw new _ErrorFromName("WinJS.UI.Hub.DuplicateSection", strings.duplicateSection); } this._surfaceElement.insertBefore(newSection.element, oldSection.element); this._surfaceElement.removeChild(oldSection.element); this._measured = false; this._setState(Hub.LoadingState.loading); this._loadSections(); } }, _handleSectionInserted: function hub_handleSectionInserted(ev) { // Insert is triggered by binding list insert APIs such as splice(), push(), and unshift(). if (this._pendingSections) { return; } var index = ev.detail.index; var section = ev.detail.value; if (section._animation) { section._animation.cancel(); } var animation; var result = this._fireEvent(Hub._EventName.contentAnimating, { type: Hub.AnimationType.insert, index: index, section: section }); if (result) { var affectedElements = []; for (var i = index + 1; i < this.sections.length; i++) { affectedElements.push(this.sections.getAt(i).element); } animation = new Animations._createUpdateListAnimation([section.element], [], affectedElements); } if (section.element.parentNode === this._surfaceElement) { throw new _ErrorFromName("WinJS.UI.Hub.DuplicateSection", strings.duplicateSection); } section._setHeaderTemplate(this.headerTemplate); if (index < this.sections.length - 1) { this._surfaceElement.insertBefore(section.element, this.sections.getAt(index + 1).element); } else { this._surfaceElement.appendChild(section.element); } this._measured = false; if (animation) { var insertAnimation = animation.execute(); this.runningAnimations = Promise.join([this.runningAnimations, insertAnimation]); } this._setState(Hub.LoadingState.loading); this._loadSections(); }, _handleSectionMoved: function hub_handleSectionMoved(ev) { // Move is triggered by binding list move() API. if (this._pendingSections) { return; } var newIndex = ev.detail.newIndex; var section = ev.detail.value; if (newIndex < this.sections.length - 1) { this._surfaceElement.insertBefore(section.element, this.sections.getAt(newIndex + 1).element); } else { this._surfaceElement.appendChild(section.element); } this._measured = false; this._setState(Hub.LoadingState.loading); this._loadSections(); }, _handleSectionRemoved: function hub_handleSectionRemoved(ev) { // Removed is triggered by binding list removal APIs such as splice(), pop(), and shift(). if (this._pendingSections) { return; } var section = ev.detail.value; var index = ev.detail.index; var animationPromise = Promise.wrap(); var result = this._fireEvent(Hub._EventName.contentAnimating, { type: Hub.AnimationType.remove, index: index, section: section }); if (result) { var affectedElements = []; for (var i = index; i < this.sections.length; i++) { affectedElements.push(this.sections.getAt(i).element); } var animation = new Animations._createUpdateListAnimation([], [section.element], affectedElements); this._measure(); var offsetTop = section.element.offsetTop; var offsetLeft = section.element.offsetLeft; section.element.style.position = "absolute"; section.element.style.top = offsetTop; section.element.style.left = offsetLeft; section.element.style.opacity = 0; this._measured = false; animationPromise = animation.execute().then(function () { section.element.style.position = ""; section.element.style.top = ""; section.element.style.left = ""; section.element.style.opacity = 1; }.bind(this)); } animationPromise.done(function () { if (!this._disposed) { this._surfaceElement.removeChild(section.element); this._measured = false; } }.bind(this)); // Store animation promise in case it is inserted before remove animation finishes. section._animation = animationPromise; this.runningAnimations = Promise.join([this.runningAnimations, animationPromise]); this._setState(Hub.LoadingState.loading); this._loadSections(); }, _handleSectionReload: function hub_handleSectionReload() { // Reload is triggered by large operations on the binding list such as reverse(). This causes // _pendingSections to be true which ignores future insert/remove/modified/moved events until the new // sections list is applied. this.sections = this.sections; }, _updateEvents: function hub_updateEvents(oldSections, newSections) { if (oldSections) { oldSections.removeEventListener("itemchanged", this._handleSectionChangedBind); oldSections.removeEventListener("iteminserted", this._handleSectionInsertedBind); oldSections.removeEventListener("itemmoved", this._handleSectionMovedBind); oldSections.removeEventListener("itemremoved", this._handleSectionRemovedBind); oldSections.removeEventListener("reload", this._handleSectionReloadBind); } if (newSections) { newSections.addEventListener("itemchanged", this._handleSectionChangedBind); newSections.addEventListener("iteminserted", this._handleSectionInsertedBind); newSections.addEventListener("itemmoved", this._handleSectionMovedBind); newSections.addEventListener("itemremoved", this._handleSectionRemovedBind); newSections.addEventListener("reload", this._handleSectionReloadBind); } }, _attachSections: function hub_attachSections() { this._measured = false; for (var i = 0; i < this.sections.length; i++) { var section = this._sections.getAt(i); if (section._animation) { section._animation.cancel(); } if (section.element.parentNode === this._surfaceElement) { throw new _ErrorFromName("WinJS.UI.Hub.DuplicateSection", strings.duplicateSection); } this._surfaceElement.appendChild(section.element); } }, _assignHeaderTemplate: function hub_assignHeaderTemplate() { this._measured = false; for (var i = 0; i < this.sections.length; i++) { var section = this._sections.getAt(i); section._setHeaderTemplate(this.headerTemplate); } }, _loadSection: function hub_loadSection(index) { var section = this._sections.getAt(index); return section._process().then(function resetVisibility() { var style = section.contentElement.style; if (style.visibility !== "") { style.visibility = ""; } }); }, _loadSections: function hub_loadSections() { // Used to know if another load has interrupted this one. this._loadId++; var loadId = this._loadId; var that = this; var onScreenItemsAnimatedPromise = Promise.wrap(); var sectionIndicesToLoad = []; var allSectionsLoadedPromise = Promise.wrap(); function loadNextSectionAfterPromise(promise) { promise.then(function () { Scheduler.schedule(loadNextSection, Scheduler.Priority.idle); }); } function loadNextSection() { if (loadId === that._loadId && !that._disposed) { if (sectionIndicesToLoad.length) { var index = sectionIndicesToLoad.shift(); var loadedPromise = that._loadSection(index); loadNextSectionAfterPromise(loadedPromise); } else { allSectionsLoadedSignal.complete(); } } } if (!this._showProgressPromise) { this._showProgressPromise = Promise.timeout(progressDelay).then(function () { if (this._disposed) { return; } if (!this._progressBar) { this._progressBar = _Global.document.createElement("progress"); _ElementUtilities.addClass(this._progressBar, Hub._ClassName.hubProgress); this._progressBar.max = 100; } if (!this._progressBar.parentNode) { this.element.insertBefore(this._progressBar, this._viewportElement); } this._showProgressPromise = null; }.bind(this), function () { this._showProgressPromise = null; }.bind(this)); } if (this.sections.length) { var allSectionsLoadedSignal = new _Signal(); allSectionsLoadedPromise = allSectionsLoadedSignal.promise; // Synchronously load the sections on screen. var synchronousProcessPromises = []; var start = Math.max(0, this.indexOfFirstVisible); var end = Math.max(0, this.indexOfLastVisible); for (var i = start; i <= end; i++) { synchronousProcessPromises.push(this._loadSection(i)); } // Determine the order to load the rest of the sections. start--; end++; while (start >= 0 || end < this.sections.length) { if (end < this.sections.length) { sectionIndicesToLoad.push(end); end++; } if (start >= 0) { sectionIndicesToLoad.push(start); start--; } } var onScreenSectionsLoadedPromise = Promise.join(synchronousProcessPromises); // In case there are overlapping load calls onScreenSectionsLoadedPromise.done(function () { if (loadId === this._loadId && !that._disposed) { if (this._showProgressPromise) { this._showProgressPromise.cancel(); } if (this._progressBar && this._progressBar.parentNode) { this._progressBar.parentNode.removeChild(this._progressBar); } Scheduler.schedule(function Hub_entranceAnimation() { if (loadId === this._loadId && !that._disposed) { if (!this._visible) { this._visible = true; this._viewportElement.style.opacity = 1; if (this._animateEntrance && _TransitionAnimation.isAnimationEnabled()) { var eventDetail = { type: Hub.AnimationType.entrance }; if (!this._fireEntrance || this._fireEvent(Hub._EventName.contentAnimating, eventDetail)) { this._viewportElement.style["-ms-overflow-style"] = "none"; onScreenItemsAnimatedPromise = Animations.enterContent(this._viewportElement, [{ left: this._fireEntrance ? "100px" : "40px", top: "0px", rtlflip: true }], { mechanism: "transition" }).then(function () { this._viewportElement.style["-ms-overflow-style"] = ""; }.bind(this)); } } if (this._element === _Global.document.activeElement) { this._moveFocusIn(this.sectionOnScreen); } } } }, Scheduler.Priority.high, this, "WinJS.UI.Hub.entranceAnimation"); } }.bind(this)); loadNextSectionAfterPromise(onScreenSectionsLoadedPromise); } else { if (this._showProgressPromise) { this._showProgressPromise.cancel(); } if (this._progressBar && this._progressBar.parentNode) { this._progressBar.parentNode.removeChild(this._progressBar); } } Promise.join([this.runningAnimations, onScreenItemsAnimatedPromise, allSectionsLoadedPromise]).done(function () { if (loadId === this._loadId && !that._disposed) { this.runningAnimations = Promise.wrap(); this._setState(Hub.LoadingState.complete); Scheduler.schedule(this._updateSnapList.bind(this), Scheduler.Priority.idle); } }.bind(this)); }, /// loadingState: { get: function () { return this._loadingState; } }, _setState: function Hub_setState(state) { if (state !== this._loadingState) { this._writeProfilerMark("loadingStateChanged:" + state + ",info"); this._loadingState = state; var eventObject = _Global.document.createEvent("CustomEvent"); eventObject.initCustomEvent(Hub._EventName.loadingStateChanged, true, false, { loadingState: state }); this._element.dispatchEvent(eventObject); } }, _parse: function hub_parse() { var hubSections = []; var hubSectionEl = this.element.firstElementChild; while (hubSectionEl !== this._viewportElement) { ControlProcessor.processAll(hubSectionEl); var hubSectionContent = hubSectionEl.winControl; if (hubSectionContent) { hubSections.push(hubSectionContent); } else { throw new _ErrorFromName("WinJS.UI.Hub.InvalidContent", strings.invalidContent); } var nextSectionEl = hubSectionEl.nextElementSibling; hubSectionEl = nextSectionEl; } this.sections = new BindingList.List(hubSections); }, _fireEvent: function hub_fireEvent(type, detail) { // Returns true if ev.preventDefault() was not called var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent(type, true, true, detail); return this.element.dispatchEvent(event); }, _findHeaderTabStop: function hub_findHeaderTabStop(element) { if (element.parentNode) { if (_ElementUtilities._matchesSelector(element, ".win-hub-section-header-tabstop, .win-hub-section-header-tabstop *")) { while (!_ElementUtilities.hasClass(element, "win-hub-section-header-tabstop")) { element = element.parentElement; } return element; } } return null; }, _isHeaderInteractive: function hub_isHeaderInteractive(element) { // Helper method to skip keyboarding and clicks with a header's sub interactive content if (element.parentNode) { return _ElementUtilities._matchesSelector(element, ".win-interactive, .win-interactive *"); } return false; }, _clickHandler: function hub_clickHandler(ev) { var headerTabStopElement = this._findHeaderTabStop(ev.target); if (headerTabStopElement && !this._isHeaderInteractive(ev.target)) { var section = headerTabStopElement.parentElement.parentElement.winControl; if (!section.isHeaderStatic) { var sectionIndex = this.sections.indexOf(section); this._fireEvent(Hub._EventName.headerInvoked, { index: sectionIndex, section: section }); } } }, _resizeHandler: function hub_resizeHandler() { // Viewport needs to be measured this._measured = false; Scheduler.schedule(this._updateSnapList.bind(this), Scheduler.Priority.idle); }, _contentResizeHandler: function hub_contentResizeHandler() { // Sections and scroll length need to be measured this._measured = false; Scheduler.schedule(this._updateSnapList.bind(this), Scheduler.Priority.idle); }, _scrollHandler: function hub_scrollHandler() { // Scroll location needs to be retrieved this._measured = false; if (this._pendingSections) { return; } // Scroll events caused by users overwrite pending API modifications to scrollposition. this._pendingScrollLocation = null; this._pendingSectionOnScreen = null; if (!this._pendingScrollHandler) { this._pendingScrollHandler = _BaseUtils._requestAnimationFrame(function () { this._pendingScrollHandler = null; if (this._pendingSections) { return; } if (this.loadingState !== Hub.LoadingState.complete) { this._loadSections(); } }.bind(this)); } }, _measure: function hub_measure() { // Any time a size changes (section growing, window resizing, etc) cachedSizes should be set to false // and any time the variables need to be read again we should measure the variables. To avoid a lot of // seperate layouts we measure the variables in a single batch. if (!this._measured || this._scrollLength === 0) { this._writeProfilerMark("measure,StartTM"); this._measured = true; this._rtl = _Global.getComputedStyle(this._element, null).direction === "rtl"; if (this.orientation === _UI.Orientation.vertical) { this._names = verticalNames; } else { if (this._rtl) { this._names = rtlHorizontalNames; } else { this._names = ltrHorizontalNames; } } this._viewportSize = this._viewportElement[this._names.offsetSize]; this._viewportOppositeSize = this._viewportElement[this._names.oppositeOffsetSize]; this._scrollPosition = _ElementUtilities.getScrollPosition(this._viewportElement)[this._names.scrollPos]; this._scrollLength = this._viewportElement[this._names.scrollSize]; var surfaceElementComputedStyle = _Global.getComputedStyle(this._surfaceElement); this._startSpacer = parseFloat(surfaceElementComputedStyle[this._names.marginStart]) + parseFloat(surfaceElementComputedStyle[this._names.borderStart]) + parseFloat(surfaceElementComputedStyle[this._names.paddingStart]); this._endSpacer = parseFloat(surfaceElementComputedStyle[this._names.marginEnd]) + parseFloat(surfaceElementComputedStyle[this._names.borderEnd]) + parseFloat(surfaceElementComputedStyle[this._names.paddingEnd]); this._sectionSizes = []; for (var i = 0; i < this.sections.length; i++) { var section = this.sections.getAt(i); var computedSectionStyle = _Global.getComputedStyle(section.element); this._sectionSizes[i] = { offset: section.element[this._names.offsetPos], // Reminder: offsetWidth doesn't include margins and also rounds. size: section.element[this._names.offsetSize], marginStart: parseFloat(computedSectionStyle[this._names.marginStart]), marginEnd: parseFloat(computedSectionStyle[this._names.marginEnd]), borderStart: parseFloat(computedSectionStyle[this._names.borderStart]), borderEnd: parseFloat(computedSectionStyle[this._names.borderEnd]), paddingStart: parseFloat(computedSectionStyle[this._names.paddingStart]), paddingEnd: parseFloat(computedSectionStyle[this._names.paddingEnd]) }; if (this._rtl && this.orientation === _UI.Orientation.horizontal) { this._sectionSizes[i].offset = this._viewportSize - (this._sectionSizes[i].offset + this._sectionSizes[i].size); } } this._writeProfilerMark("measure,StopTM"); } }, _updateSnapList: function hub_updateSnapList() { this._writeProfilerMark("updateSnapList,StartTM"); this._measure(); var snapList = "snapList("; for (var i = 0; i < this._sectionSizes.length; i++) { if (i > 0) { snapList += ","; } var sectionSize = this._sectionSizes[i]; snapList += (sectionSize.offset - sectionSize.marginStart - this._startSpacer) + "px"; } snapList += ")"; var snapListY = ""; var snapListX = ""; if (this.orientation === _UI.Orientation.vertical) { snapListY = snapList; } else { snapListX = snapList; } if (this._lastSnapPointY !== snapListY) { this._lastSnapPointY = snapListY; this._viewportElement.style['-ms-scroll-snap-points-y'] = snapListY; } if (this._lastSnapPointX !== snapListX) { this._lastSnapPointX = snapListX; this._viewportElement.style['-ms-scroll-snap-points-x'] = snapListX; } this._writeProfilerMark("updateSnapList,StopTM"); }, _scrollToSection: function Hub_scrollToSection(index, withAnimation) { this._measure(); var sectionSize = this._sectionSizes[index]; var scrollPositionToShowStartMargin = Math.min(this._scrollLength - this._viewportSize, sectionSize.offset - sectionSize.marginStart - this._startSpacer); this._scrollTo(scrollPositionToShowStartMargin, withAnimation); }, _ensureVisible: function hub_ensureVisible(index, withAnimation) { this._measure(); var targetScrollPos = this._ensureVisibleMath(index, this._scrollPosition); this._scrollTo(targetScrollPos, withAnimation); }, _ensureVisibleMath: function hub_ensureVisibleMath(index, targetScrollPos) { this._measure(); var sectionSize = this._sectionSizes[index]; var scrollPositionToShowStartMargin = Math.min(this._scrollLength - this._viewportSize, sectionSize.offset - sectionSize.marginStart - this._startSpacer); var scrollPositionToShowEndMargin = Math.max(0, sectionSize.offset + sectionSize.size + sectionSize.marginEnd + this._endSpacer - this._viewportSize + 1); if (targetScrollPos > scrollPositionToShowStartMargin) { targetScrollPos = scrollPositionToShowStartMargin; } else if (targetScrollPos < scrollPositionToShowEndMargin) { targetScrollPos = Math.min(scrollPositionToShowStartMargin, scrollPositionToShowEndMargin); } return targetScrollPos; }, _scrollTo: function hub_scrollTo(scrollPos, withAnimation) { this._scrollPosition = scrollPos; if (withAnimation) { if (this.orientation === _UI.Orientation.vertical) { _ElementUtilities._zoomTo(this._viewportElement, { contentX: 0, contentY: this._scrollPosition, viewportX: 0, viewportY: 0 }); } else { _ElementUtilities._zoomTo(this._viewportElement, { contentX: this._scrollPosition, contentY: 0, viewportX: 0, viewportY: 0 }); } } else { var newScrollPos = {}; newScrollPos[this._names.scrollPos] = this._scrollPosition; _ElementUtilities.setScrollPosition(this._viewportElement, newScrollPos); } }, _windowKeyDownHandler: function hub_windowKeyDownHandler(ev) { // Include tab and shift tab. Note: Alt Key + Tab and Windows Key + Tab do not fire keydown with ev.key === "Tab". if (ev.keyCode === Key.tab) { this._tabSeenLast = true; var that = this; _BaseUtils._yieldForEvents(function () { that._tabSeenLast = false; }); } }, _focusin: function hub_focusin(ev) { // On focus we call ensureVisible to handle the tab or shift/tab to header. However if the // focus was caused by a pointer down event we skip the focus. if (this._tabSeenLast) { var headerTabStopElement = this._findHeaderTabStop(ev.target); if (headerTabStopElement && !this._isHeaderInteractive(ev.target)) { var sectionIndex = this.sections.indexOf(headerTabStopElement.parentElement.parentElement.winControl); if (sectionIndex > -1) { this._ensureVisible(sectionIndex, true); } } } // Always remember the focused section for SemanticZoom. var sectionElement = ev.target; while (sectionElement && !_ElementUtilities.hasClass(sectionElement, _Section.HubSection._ClassName.hubSection)) { sectionElement = sectionElement.parentElement; } if (sectionElement) { var sectionIndex = this.sections.indexOf(sectionElement.winControl); if (sectionIndex > -1) { this._currentIndexForSezo = sectionIndex; } } if (ev.target === this.element) { var indexToFocus; if (+this._sectionToFocus === this._sectionToFocus && this._sectionToFocus >= 0 && this._sectionToFocus < this.sections.length) { indexToFocus = this._sectionToFocus; this._sectionToFocus = null; } else { indexToFocus = this.sectionOnScreen; } this._moveFocusIn(indexToFocus); } }, _moveFocusIn: function hub_moveFocusIn(indexToFocus) { if (indexToFocus >= 0) { for (var i = indexToFocus; i < this.sections.length; i++) { var section = this.sections.getAt(i); var focusAttempt = _ElementUtilities._trySetActive(section._headerTabStopElement, this._viewportElement); if (focusAttempt) { return; } if (_ElementUtilities._setActiveFirstFocusableElement(section.contentElement, this._viewportElement)) { return; } } for (var i = indexToFocus - 1; i >= 0; i--) { var section = this.sections.getAt(i); if (_ElementUtilities._setActiveFirstFocusableElement(section.contentElement, this._viewportElement)) { return; } var focusAttempt = _ElementUtilities._trySetActive(section._headerTabStopElement, this._viewportElement); if (focusAttempt) { return; } } } }, _keyDownHandler: function hub_keyDownHandler(ev) { var leftKey = this._rtl ? Key.rightArrow : Key.leftArrow; var rightKey = this._rtl ? Key.leftArrow : Key.rightArrow; if (ev.keyCode === Key.upArrow || ev.keyCode === Key.downArrow || ev.keyCode === Key.leftArrow || ev.keyCode === Key.rightArrow || ev.keyCode === Key.pageUp || ev.keyCode === Key.pageDown) { var headerTabStopElement = this._findHeaderTabStop(ev.target); if (headerTabStopElement && !this._isHeaderInteractive(ev.target)) { var currentSection = this.sections.indexOf(headerTabStopElement.parentElement.parentElement.winControl); var targetSectionIndex; var useEnsureVisible = false; // Page up/down go to the next/previous header and line it up with the app header. Up/Right/Down/Left // move focus to the next/previous header and move it on screen (app header distance from either edge). if (ev.keyCode === Key.pageDown || (this.orientation === _UI.Orientation.horizontal && ev.keyCode === rightKey) || (this.orientation === _UI.Orientation.vertical && ev.keyCode === Key.downArrow)) { // Do not include hidden headers. for (var i = currentSection + 1; i < this.sections.length; i++) { if (this._tryFocus(i)) { targetSectionIndex = i; break; } } } else if (ev.keyCode === Key.pageUp || (this.orientation === _UI.Orientation.horizontal && ev.keyCode === leftKey) || (this.orientation === _UI.Orientation.vertical && ev.keyCode === Key.upArrow)) { // Do not include hidden headers. for (var i = currentSection - 1; i >= 0; i--) { if (this._tryFocus(i)) { targetSectionIndex = i; break; } } } if (ev.keyCode === Key.upArrow || ev.keyCode === Key.downArrow || ev.keyCode === Key.leftArrow || ev.keyCode === Key.rightArrow) { useEnsureVisible = true; } if (+targetSectionIndex === targetSectionIndex) { if (useEnsureVisible) { this._ensureVisible(targetSectionIndex, true); } else { this._scrollToSection(targetSectionIndex, true); } } ev.preventDefault(); } } else if (ev.keyCode === Key.home || ev.keyCode === Key.end) { // Home/End scroll to start/end and leave focus where it is. this._measure(); var maxScrollPos = Math.max(0, this._scrollLength - this._viewportSize); this._scrollTo(ev.keyCode === Key.home ? 0 : maxScrollPos, true); ev.preventDefault(); } }, _tryFocus: function hub_tryFocus(index) { var targetSection = this.sections.getAt(index); _ElementUtilities._setActive(targetSection._headerTabStopElement, this._viewportElement); return _Global.document.activeElement === targetSection._headerTabStopElement; }, /// /// Gets a ZoomableView. This API supports the SemanticZoom infrastructure /// and is not intended to be used directly from your code. /// /// zoomableView: { get: function zoomableView_get() { if (!this._zoomableView) { this._zoomableView = new ZoomableView(this); } return this._zoomableView; } }, _getPanAxis: function hub_getPanAxis() { return this.orientation === _UI.Orientation.horizontal ? "horizontal" : "vertical"; }, _configureForZoom: function hub_configureForZoom() { // Nothing to configure. }, _setCurrentItem: function hub_setCurrentItem(x, y) { var offset; if (this.orientation === _UI.Orientation.horizontal) { offset = x; } else { offset = y; } this._measure(); offset = offset + this._scrollPosition; this._currentIndexForSezo = this._sectionSizes.length - 1; for (var i = 1; i < this._sectionSizes.length; i++) { var sectionSize = this._sectionSizes[i]; if (sectionSize.offset - sectionSize.marginStart > offset) { this._currentIndexForSezo = i - 1; break; } } }, _getCurrentItem: function hub_getCurrentItem() { var itemPosition; if (this._sectionSizes.length > 0) { this._measure(); var index = Math.max(0, Math.min(this._currentIndexForSezo, this._sectionSizes.length)); var sectionSize = this._sectionSizes[index]; if (this.orientation === _UI.Orientation.horizontal) { itemPosition = { left: Math.max(0, sectionSize.offset - sectionSize.marginStart - this._scrollPosition), top: 0, width: sectionSize.size, height: this._viewportOppositeSize }; } else { itemPosition = { left: 0, top: Math.max(0, sectionSize.offset - sectionSize.marginStart - this._scrollPosition), width: this._viewportOppositeSize, height: sectionSize.size, }; } var section = this.sections.getAt(index); // BUGBUG: 53301 ListView and Hub should document what they expect to be returned from the // getCurrentItem so that positionItem apis line up. ListView zoomed out expects an object with // groupIndexHint, groupKey, or groupDescription. Hub expects an object with index. return Promise.wrap({ item: { data: section, index: index, groupIndexHint: index }, position: itemPosition }); } }, _beginZoom: function hub_beginZoom() { // Hide scroll thumb. this._viewportElement.style["-ms-overflow-style"] = "none"; }, _positionItem: function hub_positionItem(item, position) { if (item.index >= 0 && item.index < this._sectionSizes.length) { this._measure(); var sectionSize = this._sectionSizes[item.index]; var offsetFromViewport; if (this.orientation === _UI.Orientation.horizontal) { offsetFromViewport = position.left; } else { offsetFromViewport = position.top; } this._sectionToFocus = item.index; var targetScrollPosition = sectionSize.offset - offsetFromViewport; // clamp section: var targetScrollPosition = this._ensureVisibleMath(item.index, targetScrollPosition); this._scrollPosition = targetScrollPosition; var newScrollPos = {}; newScrollPos[this._names.scrollPos] = this._scrollPosition; _ElementUtilities.setScrollPosition(this._viewportElement, newScrollPos); } }, _endZoom: function hub_endZoom() { // Show scroll thumb. this._viewportElement.style["-ms-overflow-style"] = ""; }, _writeProfilerMark: function hub_writeProfilerMark(text) { var message = "WinJS.UI.Hub:" + this._id + ":" + text; _WriteProfilerMark(message); _Log.log && _Log.log(message, null, "hubprofiler"); }, dispose: function hub_dispose() { /// /// /// Disposes this control. /// /// /// if (this._disposed) { return; } this._disposed = true; _Global.removeEventListener('keydown', this._windowKeyDownHandlerBound); _ElementUtilities._resizeNotifier.unsubscribe(this.element, this._resizeHandlerBound); this._updateEvents(this._sections); for (var i = 0; i < this.sections.length; i++) { this.sections.getAt(i).dispose(); } } }, { /// /// Specifies whether the Hub animation is an entrance animation or a transition animation. /// /// AnimationType: { /// /// The animation plays when the Hub is first displayed. /// /// entrance: "entrance", /// /// The animation plays when the Hub is changing its content. /// /// contentTransition: "contentTransition", /// /// The animation plays when a section is inserted into the Hub. /// /// insert: "insert", /// /// The animation plays when a section is removed into the Hub. /// /// remove: "remove", }, /// /// Gets the current loading state of the Hub. /// /// LoadingState: { /// /// The Hub is loading sections. /// /// loading: "loading", /// /// All sections are loaded and animations are complete. /// /// complete: "complete" }, // Names of classes used by the Hub. _ClassName: { hub: "win-hub", hubSurface: "win-hub-surface", hubProgress: "win-hub-progress", hubViewport: "win-hub-viewport", hubVertical: "win-hub-vertical", hubHorizontal: "win-hub-horizontal", }, // Names of events fired by the Hub. _EventName: { contentAnimating: eventNames.contentAnimating, headerInvoked: eventNames.headerInvoked, loadingStateChanged: eventNames.loadingStateChanged }, }); _Base.Class.mix(Hub, _Control.DOMEventMixin); var ZoomableView = _Base.Class.define(function ZoomableView_ctor(hub) { this._hub = hub; }, { getPanAxis: function () { return this._hub._getPanAxis(); }, configureForZoom: function (isZoomedOut, isCurrentView, triggerZoom, prefetchedPages) { this._hub._configureForZoom(isZoomedOut, isCurrentView, triggerZoom, prefetchedPages); }, setCurrentItem: function (x, y) { this._hub._setCurrentItem(x, y); }, getCurrentItem: function () { return this._hub._getCurrentItem(); }, beginZoom: function () { this._hub._beginZoom(); }, positionItem: function (item, position) { return this._hub._positionItem(item, position); }, endZoom: function (isCurrentView) { this._hub._endZoom(isCurrentView); } }); var strings = { get duplicateConstruction() { return "Invalid argument: Controls may only be instantiated one time for each DOM element"; }, get duplicateSection() { return "Hub duplicate sections: Each HubSection must be unique"; }, get invalidContent() { return "Invalid content: Hub content must be made up of HubSections."; }, get hubViewportAriaLabel() { return _Resources._getWinJSString("ui/hubViewportAriaLabel").value; } }; return Hub; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/AppBar/_Constants',[ 'exports', '../../Core/_Base', ], function appBarConstantsInit(exports, _Base) { "use strict"; _Base.Namespace._moduleDefine(exports, null, { // AppBar class names. appBarClass: "win-appbar", firstDivClass: "win-firstdiv", finalDivClass: "win-finaldiv", invokeButtonClass: "win-appbar-invokebutton", ellipsisClass: "win-appbar-ellipsis", primaryCommandsClass: "win-primarygroup", secondaryCommandsClass: "win-secondarygroup", reducedClass: "win-reduced", commandLayoutClass: "win-commandlayout", topClass: "win-top", bottomClass: "win-bottom", showingClass : "win-appbar-showing", shownClass : "win-appbar-shown", hidingClass : "win-appbar-hiding", hiddenClass: "win-appbar-hidden", minimalClass: "win-appbar-minimal", // Constants for AppBar placement appBarPlacementTop: "top", appBarPlacementBottom: "bottom", // Constants for AppBar layouts appBarLayoutCustom: "custom", appBarLayoutCommands: "commands", // Constant for AppBar invokebutton width appBarInvokeButtonWidth: 60, // Constants for Commands typeSeparator: "separator", typeContent: "content", typeButton: "button", typeToggle: "toggle", typeFlyout: "flyout", menuCommandClass: "win-command", appBarCommandClass: "win-command", appBarCommandGlobalClass: "win-global", appBarCommandSelectionClass: "win-selection", sectionSelection: "selection", sectionGlobal: "global", // Prevents the element from showing a focus rect hideFocusClass: "win-hidefocus", // Other class names overlayClass: "win-overlay", flyoutClass: "win-flyout", flyoutLightClass: "win-ui-light", menuClass: "win-menu", menuToggleClass: "win-menu-toggle", settingsFlyoutClass: "win-settingsflyout", scrollsClass: "win-scrolls", // Constants for AppBarCommand full-size widths. separatorWidth: 60, buttonWidth: 100, narrowClass: "win-narrow", wideClass: "win-wide", _clickEatingAppBarClass: "win-appbarclickeater", _clickEatingFlyoutClass: "win-flyoutmenuclickeater", _visualViewportClass: "win-visualviewport-space", }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. /// animatable,appbar,appbars,divs,Flyout,Flyouts,iframe,Statics,unfocus,unselectable define('WinJS/Controls/Flyout/_Overlay',[ 'exports', '../../Core/_Global', '../../Core/_WinRT', '../../Core/_Base', '../../Core/_BaseUtils', '../../Core/_ErrorFromName', '../../Core/_Events', '../../Core/_Resources', '../../Core/_WriteProfilerMark', '../../Animations', '../../ControlProcessor', '../../Promise', '../../Scheduler', '../../Utilities/_Control', '../../Utilities/_ElementUtilities', '../AppBar/_Constants' ], function overlayInit(exports, _Global, _WinRT, _Base, _BaseUtils, _ErrorFromName, _Events, _Resources, _WriteProfilerMark, Animations, ControlProcessor, Promise, Scheduler, _Control, _ElementUtilities, _Constants) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _Overlay: _Base.Namespace._lazy(function () { var createEvent = _Events._createEventProperty; // Event Names var BEFORESHOW = "beforeshow"; var AFTERSHOW = "aftershow"; var BEFOREHIDE = "beforehide"; var AFTERHIDE = "afterhide"; // Helper to get DOM elements from input single object or array or IDs/toolkit/dom elements function _resolveElements(elements) { // No input is just an empty array if (!elements) { return []; } // Make sure it's in array form. if (typeof elements === "string" || !elements || !elements.length) { elements = [elements]; } // Make sure we have a DOM element for each one, (could be string id name or toolkit object) var i, realElements = []; for (i = 0; i < elements.length; i++) { if (elements[i]) { if (typeof elements[i] === "string") { var element = _Global.document.getElementById(elements[i]); if (element) { realElements.push(element); } } else if (elements[i].element) { realElements.push(elements[i].element); } else { realElements.push(elements[i]); } } } return realElements; } // Helpers for keyboard showing related events function _allOverlaysCallback(event, command) { var elements = _Global.document.querySelectorAll("." + _Constants.overlayClass); if (elements) { var len = elements.length; for (var i = 0; i < len; i++) { var element = elements[i]; var control = element.winControl; if (!control._disposed) { if (control) { control[command](event); } } } } } function _edgyMayHideFlyouts() { // Flyouts and SettingsFlyouts should not light dismiss when they are the target of a right click. if (!_Overlay._rightMouseMightEdgy) { _Overlay._hideAllFlyouts(); } } var strings = { get duplicateConstruction() { return "Invalid argument: Controls may only be instantiated one time for each DOM element"; }, get mustContainCommands() { return "Invalid HTML: AppBars/Menus must contain only AppBarCommands/MenuCommands"; }, get closeOverlay() { return _Resources._getWinJSString("ui/closeOverlay").value; }, }; var _Overlay = _Base.Class.define(function _Overlay_ctor(element, options) { /// /// /// Constructs the Overlay control and associates it with the underlying DOM element. /// /// /// The DOM element to be associated with the Overlay control. /// /// /// The set of options to be applied initially to the Overlay control. /// /// A fully constructed Overlay control. /// this._baseOverlayConstructor(element, options); }, { // Functions/properties _baseOverlayConstructor: function _Overlay_baseOverlayConstructor(element, options) { this._disposed = false; // Make sure there's an input element if (!element) { element = _Global.document.createElement("div"); } // Check to make sure we weren't duplicated var overlay = element.winControl; if (overlay) { throw new _ErrorFromName("WinJS.UI._Overlay.DuplicateConstruction", strings.duplicateConstruction); } if (!this._element) { this._element = element; } this._sticky = false; this._doNext = ""; this._element.style.visibility = "hidden"; this._element.style.opacity = 0; // Remember ourselves element.winControl = this; // Attach our css class _ElementUtilities.addClass(this._element, _Constants.overlayClass); _ElementUtilities.addClass(this._element, "win-disposable"); // We don't want to be selectable, set UNSELECTABLE var unselectable = this._element.getAttribute("unselectable"); if (unselectable === null || unselectable === undefined) { this._element.setAttribute("unselectable", "on"); } // Base animation is popIn/popOut this._currentAnimateIn = this._baseAnimateIn; this._currentAnimateOut = this._baseAnimateOut; this._animationPromise = Promise.as(); // Command Animations to Queue this._queuedToShow = []; this._queuedToHide = []; this._queuedCommandAnimation = false; if (options) { _Control.setOptions(this, options); } }, /// element: { get: function () { return this._element; } }, /// Disable an Overlay, setting or getting the HTML disabled attribute. When disabled the Overlay will no longer display with show(), and will hide if currently visible. disabled: { get: function () { // Ensure it's a boolean because we're using the DOM element to keep in-sync return !!this._element.disabled; }, set: function (value) { // Force this check into a boolean because our current state could be a bit confused since we tie to the DOM element value = !!value; var oldValue = !!this._element.disabled; if (oldValue !== value) { this._element.disabled = value; if (!this.hidden && this._element.disabled) { this._hideOrDismiss(); } } } }, /// /// Occurs immediately before the control is shown. /// onbeforeshow: createEvent(BEFORESHOW), /// /// Occurs immediately after the control is shown. /// onaftershow: createEvent(AFTERSHOW), /// /// Occurs immediately before the control is hidden. /// onbeforehide: createEvent(BEFOREHIDE), /// /// Occurs immediately after the control is hidden. /// onafterhide: createEvent(AFTERHIDE), dispose: function () { /// /// /// Disposes this Overlay. /// /// if (this._disposed) { return; } this._disposed = true; this._dispose(); }, _dispose: function _Overlay_dispose() { // To be overridden by subclasses }, show: function () { /// /// /// Shows the Overlay, if hidden, regardless of other state /// /// // call private show to distinguish it from public version this._show(); }, _show: function _Overlay_show() { // We call our base _baseShow because AppBar may need to override show this._baseShow(); }, hide: function () { /// /// /// Hides the Overlay, if visible, regardless of other state /// /// // call private hide to distinguish it from public version this._hide(); }, _hide: function _Overlay_hide() { // We call our base _baseHide because AppBar may need to override hide this._baseHide(); }, // Is the overlay "hidden"? /// hidden: { get: function () { return (this._element.style.visibility === "hidden" || this._element.winAnimating === "hiding" || this._doNext === "hide"); } }, addEventListener: function (type, listener, useCapture) { /// /// /// Add an event listener to the DOM element for this Overlay /// /// Required. Event type to add, "beforehide", "afterhide", "beforeshow", or "aftershow" /// Required. The event handler function to associate with this event. /// Optional. True, register for the event capturing phase. False for the event bubbling phase. /// return this._element.addEventListener(type, listener, useCapture); }, removeEventListener: function (type, listener, useCapture) { /// /// /// Remove an event listener to the DOM element for this Overlay /// /// Required. Event type to remove, "beforehide", "afterhide", "beforeshow", or "aftershow" /// Required. The event handler function to associate with this event. /// Optional. True, register for the event capturing phase. False for the event bubbling phase. /// return this._element.removeEventListener(type, listener, useCapture); }, _baseShow: function _Overlay_baseShow() { // If we are already animating, just remember this for later if (this._animating || this._needToHandleShowingKeyboard || this._needToHandleHidingKeyboard) { this._doNext = "show"; return false; } // Each overlay tracks the size of the element for triggering light-dismiss in the window resize handler. this._cachedDocumentSize = this._cachedDocumentSize || _Overlay._sizeOfDocument(); // "hiding" would need to cancel. if (this._element.style.visibility !== "visible") { // Let us know we're showing. this._element.winAnimating = "showing"; // Hiding, but not none this._element.style.display = ""; this._element.style.visibility = "hidden"; // In case their event is going to manipulate commands, see if there are // any queued command animations we can handle while we're still hidden. if (this._queuedCommandAnimation) { this._showAndHideFast(this._queuedToShow, this._queuedToHide); this._queuedToShow = []; this._queuedToHide = []; } // Send our "beforeShow" event this._sendEvent(_Overlay.beforeShow); // Need to measure this._findPosition(); // Make sure it's visible, and fully opaque. // Do the popup thing, sending event afterward. var that = this; this._animationPromise = this._currentAnimateIn(). then(function () { that._baseEndShow(); }, function () { that._baseEndShow(); }); return true; } return false; }, // Flyout in particular will need to measure our positioning. _findPosition: function _Overlay_findPosition() { }, _baseEndShow: function _Overlay_baseEndShow() { if (this._disposed) { return; } // Make sure it's visible after showing this._element.setAttribute("aria-hidden", "false"); this._element.winAnimating = ""; // Do our derived classes show stuff this._endShow(); // We're shown now if (this._doNext === "show") { this._doNext = ""; } // After showing, send the after showing event this._sendEvent(_Overlay.afterShow); this._writeProfilerMark("show,StopTM"); // Overlay writes the stop profiler mark for all of its derived classes. // If we had something queued, do that Scheduler.schedule(this._checkDoNext, Scheduler.Priority.normal, this, "WinJS.UI._Overlay._checkDoNext"); }, _endShow: function _Overlay_endShow() { // Nothing by default }, _baseHide: function _Overlay_baseHide() { // If we are already animating, just remember this for later if (this._animating || this._needToHandleShowingKeyboard) { this._doNext = "hide"; return false; } // In the unlikely event we're between the hiding keyboard and the resize events, just snap it away: if (this._needToHandleHidingKeyboard) { // use the "uninitialized" flag this._element.style.visibility = ""; } // "showing" would need to queue up. if (this._element.style.visibility !== "hidden") { // Let us know we're hiding, accessibility as well. this._element.winAnimating = "hiding"; this._element.setAttribute("aria-hidden", "true"); // Send our "beforeHide" event this._sendEvent(_Overlay.beforeHide); // If our visibility is empty, then this is the first time, just hide it if (this._element.style.visibility === "") { // Initial hiding, just hide it this._element.style.opacity = 0; this._baseEndHide(); } else { // Make sure it's hidden, and fully transparent. var that = this; this._animationPromise = this._currentAnimateOut(). then(function () { that._baseEndHide(); }, function () { that._baseEndHide(); }); } return true; } return false; }, _baseEndHide: function _Overlay_baseEndHide() { if (this._disposed) { return; } // Make sure animation is finished. this._element.style.visibility = "hidden"; this._element.style.display = "none"; this._element.winAnimating = ""; // In case their event is going to manipulate commands, see if there // are any queued command animations we can handle now we're hidden. if (this._queuedCommandAnimation) { this._showAndHideFast(this._queuedToShow, this._queuedToHide); this._queuedToShow = []; this._queuedToHide = []; } // We're hidden now if (this._doNext === "hide") { this._doNext = ""; } // After hiding, send our "afterHide" event this._sendEvent(_Overlay.afterHide); this._writeProfilerMark("hide,StopTM"); // Overlay writes the stop profiler mark for all of its derived classes. // If we had something queued, do that. This has to be after // the afterHide event in case it triggers a show() and they // have something to do in beforeShow that requires afterHide first. Scheduler.schedule(this._checkDoNext, Scheduler.Priority.normal, this, "WinJS.UI._Overlay._checkDoNext"); }, _checkDoNext: function _Overlay_checkDoNext() { // Do nothing if we're still animating if (this._animating || this._needToHandleShowingKeyboard || this._needToHandleHidingKeyboard || this._disposed) { return; } if (this._doNext === "hide") { // Do hide first because animating commands would be easier this._hide(); this._doNext = ""; } else if (this._queuedCommandAnimation) { // Do queued commands before showing if possible this._showAndHideQueue(); } else if (this._doNext === "show") { // Show last so that we don't unnecessarily animate commands this._show(); this._doNext = ""; } }, // Default animations _baseAnimateIn: function _Overlay_baseAnimateIn() { this._element.style.opacity = 0; this._element.style.visibility = "visible"; // touch opacity so that IE fades from the 0 we just set to 1 _Global.getComputedStyle(this._element, null).opacity; return Animations.fadeIn(this._element); }, _baseAnimateOut: function _Overlay_baseAnimateOut() { this._element.style.opacity = 1; // touch opacity so that IE fades from the 1 we just set to 0 _Global.getComputedStyle(this._element, null).opacity; return Animations.fadeOut(this._element); }, _animating: { get: function _Overlay_animating_get() { // Ensure it's a boolean because we're using the DOM element to keep in-sync return !!this._element.winAnimating; } }, // Send one of our events _sendEvent: function _Overlay_sendEvent(eventName, detail) { if (this._disposed) { return; } var event = _Global.document.createEvent("CustomEvent"); event.initEvent(eventName, true, true, (detail || {})); this._element.dispatchEvent(event); }, // Show commands _showCommands: function _Overlay_showCommands(commands, immediate) { var showHide = this._resolveCommands(commands); this._showAndHideCommands(showHide.commands, [], immediate); }, // Hide commands _hideCommands: function _Overlay_hideCommands(commands, immediate) { var showHide = this._resolveCommands(commands); this._showAndHideCommands([], showHide.commands, immediate); }, // Hide commands _showOnlyCommands: function _Overlay_showOnlyCommands(commands, immediate) { var showHide = this._resolveCommands(commands); this._showAndHideCommands(showHide.commands, showHide.others, immediate); }, _showAndHideCommands: function _Overlay_showAndHideCommands(showCommands, hideCommands, immediate) { // Immediate is "easy" if (immediate || (this.hidden && !this._animating)) { // Immediate mode (not animated) this._showAndHideFast(showCommands, hideCommands); // Need to remove them from queues, but others could be queued this._removeFromQueue(showCommands, this._queuedToShow); this._removeFromQueue(hideCommands, this._queuedToHide); } else { // Queue Commands this._updateAnimateQueue(showCommands, this._queuedToShow, this._queuedToHide); this._updateAnimateQueue(hideCommands, this._queuedToHide, this._queuedToShow); } }, _removeFromQueue: function _Overlay_removeFromQueue(commands, queue) { // remove commands from queue. var count; for (count = 0; count < commands.length; count++) { // Remove if it was in queue var countQ; for (countQ = 0; countQ < queue.length; countQ++) { if (queue[countQ] === commands[count]) { queue.splice(countQ, 1); break; } } } }, _updateAnimateQueue: function _Overlay_updateAnimateQueue(addCommands, toQueue, fromQueue) { if (this._disposed) { return; } // Add addCommands to toQueue and remove addCommands from fromQueue. var count; for (count = 0; count < addCommands.length; count++) { // See if it's already in toQueue var countQ; for (countQ = 0; countQ < toQueue.length; countQ++) { if (toQueue[countQ] === addCommands[count]) { break; } } if (countQ === toQueue.length) { // Not found, add it toQueue[countQ] = addCommands[count]; } // Remove if it was in fromQueue for (countQ = 0; countQ < fromQueue.length; countQ++) { if (fromQueue[countQ] === addCommands[count]) { fromQueue.splice(countQ, 1); break; } } } // If we haven't queued the actual animation if (!this._queuedCommandAnimation) { // If not already animating, we'll need to call _checkDoNext if (!this._animating) { Scheduler.schedule(this._checkDoNext, Scheduler.Priority.normal, this, "WinJS.UI._Overlay._checkDoNext"); } this._queuedCommandAnimation = true; } }, // show/hide commands without doing any animation. _showAndHideFast: function _Overlay_showAndHideFast(showCommands, hideCommands) { var count; var command; for (count = 0; count < showCommands.length; count++) { command = showCommands[count]; if (command && command.style) { command.style.visibility = ""; command.style.display = ""; } } for (count = 0; count < hideCommands.length; count++) { command = hideCommands[count]; if (command && command.style) { command.style.visibility = "hidden"; command.style.display = "none"; } } this._commandsUpdated(); }, // show and hide the queued commands, perhaps animating if overlay isn't hidden. _showAndHideQueue: function _Overlay_showAndHideQueue() { // Only called if not currently animating. // We'll be done with the queued stuff when we return. this._queuedCommandAnimation = false; // Shortcut if hidden if (this.hidden) { this._showAndHideFast(this._queuedToShow, this._queuedToHide); // Might be something else to do Scheduler.schedule(this._checkDoNext, Scheduler.Priority.normal, this, "WinJS.UI._Overlay._checkDoNext"); } else { // Animation has 3 parts: "hiding", "showing", and "moving" // PVL has "addToList" and "deleteFromList", both of which allow moving parts. // So we'll set up "add" for showing, and use "delete" for "hiding" + moving, // then trigger both at the same time. var showCommands = this._queuedToShow; var hideCommands = this._queuedToHide; var siblings = this._findSiblings(showCommands.concat(hideCommands)); // Filter out the commands queued for animation that don't need to be animated. var count; for (count = 0; count < showCommands.length; count++) { // If this one's not real or not attached, skip it if (!showCommands[count] || !showCommands[count].style || !_Global.document.body.contains(showCommands[count])) { // Not real, skip it showCommands.splice(count, 1); count--; } else if (showCommands[count].style.visibility !== "hidden" && showCommands[count].style.opacity !== "0") { // Don't need to animate showing this one, already visible, so now it's a sibling siblings.push(showCommands[count]); showCommands.splice(count, 1); count--; } } for (count = 0; count < hideCommands.length; count++) { // If this one's not real or not attached, skip it if (!hideCommands[count] || !hideCommands[count].style || !_Global.document.body.contains(hideCommands[count]) || hideCommands[count].style.visibility === "hidden" || hideCommands[count].style.opacity === "0") { // Don't need to animate hiding this one, not real, or it's hidden, // so don't even need it as a sibling. hideCommands.splice(count, 1); count--; } } // Start command animations. var commandsAnimationPromise = this._baseBeginAnimateCommands(showCommands, hideCommands, siblings); // Hook end animations var that = this; if (commandsAnimationPromise) { // Needed to animate commandsAnimationPromise.done( function () { that._baseEndAnimateCommands(hideCommands); }, function () { that._baseEndAnimateCommands(hideCommands); } ); } else { // Already positioned correctly Scheduler.schedule(function Overlay_async_baseEndAnimationCommands() { that._baseEndAnimateCommands([]); }, Scheduler.Priority.normal, null, "WinJS.UI._Overlay._endAnimateCommandsWithoutAnimation"); } } // Done, clear queues this._queuedToShow = []; this._queuedToHide = []; }, _baseBeginAnimateCommands: function _Overlay_baseBeginAnimateCommands(showCommands, hideCommands, siblings) { // The parameters are 3 mutually exclusive arrays of win-command elements contained in this Overlay. // 1) showCommands[]: All of the HIDDEN win-command elements that ARE scheduled to show. // 2) hideCommands[]: All of the VISIBLE win-command elements that ARE shceduled to hide. // 3) siblings[]: i. All VISIBLE win-command elements that ARE NOT scheduled to hide. // ii. All HIDDEN win-command elements that ARE NOT scheduled to hide OR show. this._beginAnimateCommands(showCommands, hideCommands, this._getVisibleCommands(siblings)); var showAnimated = null, hideAnimated = null; // Hide commands first, with siblings if necessary, // so that the showing commands don't disrupt the hiding commands position. if (hideCommands.length > 0) { hideAnimated = Animations.createDeleteFromListAnimation(hideCommands, showCommands.length === 0 ? siblings : undefined); } if (showCommands.length > 0) { showAnimated = Animations.createAddToListAnimation(showCommands, siblings); } // Update hiding commands for (var count = 0, len = hideCommands.length; count < len; count++) { // Need to fix our position var rectangle = hideCommands[count].getBoundingClientRect(), style = _Global.getComputedStyle(hideCommands[count]); // Use the bounding box, adjusting for margins hideCommands[count].style.top = (rectangle.top - parseFloat(style.marginTop)) + "px"; hideCommands[count].style.left = (rectangle.left - parseFloat(style.marginLeft)) + "px"; hideCommands[count].style.opacity = 0; hideCommands[count].style.position = "fixed"; } // Mark as animating this._element.winAnimating = "rearranging"; // Start hiding animations // Hide needs extra cleanup when done var promise = null; if (hideAnimated) { promise = hideAnimated.execute(); } // Update showing commands, // After hiding commands so that the hiding ones fade in the right place. for (count = 0; count < showCommands.length; count++) { showCommands[count].style.visibility = ""; showCommands[count].style.display = ""; showCommands[count].style.opacity = 1; } // Start showing animations if (showAnimated) { var newPromise = showAnimated.execute(); if (promise) { promise = Promise.join([promise, newPromise]); } else { promise = newPromise; } } return promise; }, _beginAnimateCommands: function _Overlay_beginAnimateCommands() { // Nothing by default }, _getVisibleCommands: function _Overlay_getVisibleCommands(commandSubSet) { var command, commands = commandSubSet, visibleCommands = []; if (!commands) { // Crawl the inner HTML for the commands. commands = this.element.querySelectorAll(".win-command"); } for (var i = 0, len = commands.length; i < len; i++) { command = commands[i].winControl || commands[i]; if (!command.hidden) { visibleCommands.push(command); } } return visibleCommands; }, // Once animation is complete, ensure that the commands are display:none // and check if there's another animation to start. _baseEndAnimateCommands: function _Overlay_baseEndAnimateCommands(hideCommands) { if (this._disposed) { return; } // Update us var count; for (count = 0; count < hideCommands.length; count++) { // Force us back into our appbar so that we can show again correctly hideCommands[count].style.position = ""; hideCommands[count].getBoundingClientRect(); // Now make us really hidden hideCommands[count].style.visibility = "hidden"; hideCommands[count].style.display = "none"; hideCommands[count].style.opacity = 1; } // Done animating this._element.winAnimating = ""; this._endAnimateCommands(); // Might be something else to do this._checkDoNext(); }, _endAnimateCommands: function _Overlay_endAnimateCommands() { // Nothing by default }, // Resolves our commands _resolveCommands: function _Overlay_resolveCommands(commands) { // First make sure they're all DOM elements. commands = _resolveElements(commands); // Now make sure they're all in this container var result = {}; result.commands = []; result.others = []; var allCommands = this.element.querySelectorAll(".win-command"); var countAll, countIn; for (countAll = 0; countAll < allCommands.length; countAll++) { var found = false; for (countIn = 0; countIn < commands.length; countIn++) { if (commands[countIn] === allCommands[countAll]) { result.commands.push(allCommands[countAll]); commands.splice(countIn, 1); found = true; break; } } if (!found) { result.others.push(allCommands[countAll]); } } return result; }, // Find siblings, all DOM elements now. // Returns all .win-commands in this Overlay that are NOT in the passed in 'commands' array. _findSiblings: function _Overlay_findSiblings(commands) { // Now make sure they're all in this container var siblings = []; var allCommands = this.element.querySelectorAll(".win-command"); var countAll, countIn; for (countAll = 0; countAll < allCommands.length; countAll++) { var found = false; for (countIn = 0; countIn < commands.length; countIn++) { if (commands[countIn] === allCommands[countAll]) { commands.splice(countIn, 1); found = true; break; } } if (!found) { siblings.push(allCommands[countAll]); } } return siblings; }, _baseResize: function _Overlay_baseResize(event) { // Avoid the cost of a resize if the Overlay is hidden. if (this._cachedDocumentSize) { if (this.hidden) { this._cachedDocumentSize = null; } else { // Overlays will light dismiss on resize. var newDocSize = _Overlay._sizeOfDocument(); if (this._cachedDocumentSize.width !== newDocSize.width || this._cachedDocumentSize.height !== newDocSize.height) { this._cachedDocumentSize = newDocSize; if (!this._sticky) { this._hideOrDismiss(); } } } } // Call specific resize this._resize(event); }, _hideOrDismiss: function _Overlay_hideOrDismiss() { var element = this._element; if (element && _ElementUtilities.hasClass(element, _Constants.settingsFlyoutClass)) { this._dismiss(); } else { this.hide(); } }, _resize: function _Overlay_resize() { // Nothing by default }, _commandsUpdated: function _Overlay_commandsUpdated() { // Nothing by default }, _checkScrollPosition: function _Overlay_checkScrollPosition() { // Nothing by default }, _showingKeyboard: function _Overlay_showingKeyboard() { // Nothing by default }, _hidingKeyboard: function _Overlay_hidingKeyboard() { // Nothing by default }, // Verify that this HTML AppBar only has AppBar/MenuCommands. _verifyCommandsOnly: function _Overlay_verifyCommandsOnly(element, type) { var children = element.children; var commands = new Array(children.length); for (var i = 0; i < children.length; i++) { // If constructed they have win-command class, otherwise they have data-win-control if (!_ElementUtilities.hasClass(children[i], "win-command") && children[i].getAttribute("data-win-control") !== type) { // Wasn't tagged with class or AppBar/MenuCommand, not an AppBar/MenuCommand throw new _ErrorFromName("WinJS.UI._Overlay.MustContainCommands", strings.mustContainCommands); } else { // Instantiate the commands. ControlProcessor.processAll(children[i]); commands[i] = children[i].winControl; } } return commands; }, // Sets focus on what we think is the last tab stop. If nothing is focusable will // try to set focus on itself. _focusOnLastFocusableElementOrThis: function _Overlay_focusOnLastFocusableElementOrThis() { if (!this._focusOnLastFocusableElement()) { // Nothing is focusable. Set focus to this. _Overlay._trySetActive(this._element); } }, // Sets focus to what we think is the last tab stop. This element must have // a firstDiv with tabIndex equal to the lowest tabIndex in the element // and a finalDiv with tabIndex equal to the highest tabIndex in the element. // Also the firstDiv must be its first child and finalDiv be its last child. // Returns true if successful, false otherwise. _focusOnLastFocusableElement: function _Overlay_focusOnLastFocusableElement() { if (this._element.firstElementChild) { var oldFirstTabIndex = this._element.firstElementChild.tabIndex; var oldLastTabIndex = this._element.lastElementChild.tabIndex; this._element.firstElementChild.tabIndex = -1; this._element.lastElementChild.tabIndex = -1; var tabResult = _ElementUtilities._focusLastFocusableElement(this._element); if (tabResult) { _Overlay._trySelect(_Global.document.activeElement); } this._element.firstElementChild.tabIndex = oldFirstTabIndex; this._element.lastElementChild.tabIndex = oldLastTabIndex; return tabResult; } else { return false; } }, // Sets focus on what we think is the first tab stop. If nothing is focusable will // try to set focus on itself. _focusOnFirstFocusableElementOrThis: function _Overlay_focusOnFirstFocusableElementOrThis() { if (!this._focusOnFirstFocusableElement()) { // Nothing is focusable. Set focus to this. _Overlay._trySetActive(this._element); } }, // Sets focus to what we think is the first tab stop. This element must have // a firstDiv with tabIndex equal to the lowest tabIndex in the element // and a finalDiv with tabIndex equal to the highest tabIndex in the element. // Also the firstDiv must be its first child and finalDiv be its last child. // Returns true if successful, false otherwise. _focusOnFirstFocusableElement: function _Overlay__focusOnFirstFocusableElement() { if (this._element.firstElementChild) { var oldFirstTabIndex = this._element.firstElementChild.tabIndex; var oldLastTabIndex = this._element.lastElementChild.tabIndex; this._element.firstElementChild.tabIndex = -1; this._element.lastElementChild.tabIndex = -1; var tabResult = _ElementUtilities._focusFirstFocusableElement(this._element); if (tabResult) { _Overlay._trySelect(_Global.document.activeElement); } this._element.firstElementChild.tabIndex = oldFirstTabIndex; this._element.lastElementChild.tabIndex = oldLastTabIndex; return tabResult; } else { return false; } }, _addOverlayEventHandlers: function _Overlay_addOverlayEventHandlers(isFlyoutOrSettingsFlyout) { // Set up global event handlers for all overlays if (!_Overlay._flyoutEdgeLightDismissEvent) { // Dismiss on blur & resize // Focus handlers generally use WinJS.Utilities._addEventListener with focusout/focusin. This // uses the browser's blur event directly beacuse _addEventListener doesn't support focusout/focusin // on window. _Global.addEventListener("blur", _Overlay._checkBlur, false); var that = this; // Be careful so it behaves in designer as well. if (_WinRT.Windows.UI.Input.EdgeGesture) { // Catch edgy events too var commandUI = _WinRT.Windows.UI.Input.EdgeGesture.getForCurrentView(); commandUI.addEventListener("starting", _Overlay._hideAllFlyouts); commandUI.addEventListener("completed", _edgyMayHideFlyouts); } if (_WinRT.Windows.UI.ViewManagement.InputPane) { // React to Soft Keyboard events var inputPane = _WinRT.Windows.UI.ViewManagement.InputPane.getForCurrentView(); inputPane.addEventListener("showing", function (event) { that._writeProfilerMark("_showingKeyboard,StartTM"); _allOverlaysCallback(event, "_showingKeyboard"); that._writeProfilerMark("_showingKeyboard,StopTM"); }); inputPane.addEventListener("hiding", function (event) { that._writeProfilerMark("_hidingKeyboard,StartTM"); _allOverlaysCallback(event, "_hidingKeyboard"); that._writeProfilerMark("_hidingKeyboard,StopTM"); }); // Document scroll event _Global.document.addEventListener("scroll", function (event) { that._writeProfilerMark("_checkScrollPosition,StartTM"); _allOverlaysCallback(event, "_checkScrollPosition"); that._writeProfilerMark("_checkScrollPosition,StopTM"); }); } // Window resize event _Global.addEventListener("resize", function (event) { that._writeProfilerMark("_baseResize,StartTM"); _allOverlaysCallback(event, "_baseResize"); that._writeProfilerMark("_baseResize,StopTM"); }); _Overlay._flyoutEdgeLightDismissEvent = true; } // Individual handlers for Flyouts only if (isFlyoutOrSettingsFlyout) { this._handleEventsForFlyoutOrSettingsFlyout(); } }, _handleEventsForFlyoutOrSettingsFlyout: function _Overlay_handleEventsForFlyoutOrSettingsFlyout() { var that = this; // Need to hide ourselves if we lose focus _ElementUtilities._addEventListener(this._element, "focusout", function (e) { _Overlay._hideIfLostFocus(that, e); }, false); // Attempt to flag right clicks that may turn into edgy _ElementUtilities._addEventListener(this._element, "pointerdown", _Overlay._checkRightClickDown, true); _ElementUtilities._addEventListener(this._element, "pointerup", _Overlay._checkRightClickUp, true); }, _writeProfilerMark: function _Overlay_writeProfilerMark(text) { _WriteProfilerMark("WinJS.UI._Overlay:" + this._id + ":" + text); } }, { // Statics _clickEatingAppBarDiv: false, _clickEatingFlyoutDiv: false, _flyoutEdgeLightDismissEvent: false, _hideFlyouts: function (testElement, notSticky) { var elements = testElement.querySelectorAll("." + _Constants.flyoutClass); var len = elements.length; for (var i = 0; i < len; i++) { var element = elements[i]; if (element.style.visibility !== "hidden") { var flyout = element.winControl; if (flyout && (!notSticky || !flyout._sticky)) { flyout._hideOrDismiss(); } } } }, _hideSettingsFlyouts: function (testElement, notSticky) { var elements = testElement.querySelectorAll("." + _Constants.settingsFlyoutClass); var len = elements.length; for (var i = 0; i < len; i++) { var element = elements[i]; if (element.style.visibility !== "hidden") { var settingsFlyout = element.winControl; if (settingsFlyout && (!notSticky || !settingsFlyout._sticky)) { settingsFlyout._hideOrDismiss(); } } } }, _hideAllFlyouts: function () { _Overlay._hideFlyouts(_Global.document, true); _Overlay._hideSettingsFlyouts(_Global.document, true); }, _createClickEatingDivTemplate: function (divClass, hideClickEatingDivFunction) { var clickEatingDiv = _Global.document.createElement("section"); clickEatingDiv._winHideClickEater = hideClickEatingDivFunction; _ElementUtilities.addClass(clickEatingDiv, divClass); _ElementUtilities._addEventListener(clickEatingDiv, "pointerup", function (event) { _Overlay._checkSameClickEatingPointerUp(event, true); }, true); _ElementUtilities._addEventListener(clickEatingDiv, "pointerdown", function (event) { _Overlay._checkClickEatingPointerDown(event, true); }, true); clickEatingDiv.addEventListener("click", function (event) { clickEatingDiv._winHideClickEater(event); }, true); // Tell Aria that it's clickable clickEatingDiv.setAttribute("role", "menuitem"); clickEatingDiv.setAttribute("aria-label", strings.closeOverlay); // Prevent CED from removing any current selection clickEatingDiv.setAttribute("unselectable", "on"); _Global.document.body.appendChild(clickEatingDiv); return clickEatingDiv; }, // Used by AppBar, and Settings Pane _createClickEatingDivAppBar: function () { if (!_Overlay._clickEatingAppBarDiv) { _Overlay._clickEatingAppBarDiv = _Overlay._createClickEatingDivTemplate(_Constants._clickEatingAppBarClass, _Overlay._handleAppBarClickEatingClick); } }, // Used by Flyout and Menu _createClickEatingDivFlyout: function () { if (!_Overlay._clickEatingFlyoutDiv) { _Overlay._clickEatingFlyoutDiv = _Overlay._createClickEatingDivTemplate(_Constants._clickEatingFlyoutClass, _Overlay._handleFlyoutClickEatingClick); } }, // All click-eaters eat "down" clicks so that we can still eat // the "up" click that'll come later. _checkClickEatingPointerDown: function (event, stopPropagation) { var target = event.currentTarget; if (target) { try { // Remember pointer id and remember right mouse target._winPointerId = event.pointerId; // Cache right mouse if that was what happened target._winRightMouse = (event.button === 2); } catch (e) { } } if (stopPropagation && !target._winRightMouse) { event.stopPropagation(); event.preventDefault(); } }, // Make sure that if we have an up we had an earlier down of the same kind _checkSameClickEatingPointerUp: function (event, stopPropagation) { var result = false, rightMouse = false, target = event.currentTarget; // Same pointer we were watching? try { if (target && target._winPointerId === event.pointerId) { // Same pointer result = true; rightMouse = target._winRightMouse; // For click-eaters, don't count right click the same because edgy will dismiss if (rightMouse && stopPropagation) { result = false; } } } catch (e) { } if (stopPropagation && !rightMouse) { event.stopPropagation(); event.preventDefault(); target._winHideClickEater(event); } return result; }, // If they click on a click eating div, even with a right click, // touch or anything, then we want to light dismiss that layer. _handleAppBarClickEatingClick: function (event) { event.stopPropagation(); event.preventDefault(); _Overlay._hideLightDismissAppBars(null, false); _Overlay._hideClickEatingDivAppBar(); _Overlay._hideAllFlyouts(); }, // If they click on a click eating div, even with a right click, // touch or anything, then we want to light dismiss that layer. _handleFlyoutClickEatingClick: function (event) { event.stopPropagation(); event.preventDefault(); // Don't light dismiss AppBars because edgy will do that as needed, // so flyouts only. _Overlay._hideClickEatingDivFlyout(); _Overlay._hideFlyouts(_Global.document, true); }, _checkRightClickDown: function (event) { _Overlay._checkClickEatingPointerDown(event, false); }, _checkRightClickUp: function (event) { if (_Overlay._checkSameClickEatingPointerUp(event, false)) { // It was a right click we may want to eat. _Overlay._rightMouseMightEdgy = true; _BaseUtils._yieldForEvents(function () { _Overlay._rightMouseMightEdgy = false; }); } }, _showClickEatingDivAppBar: function () { Scheduler.schedule(function Overlay_async_showClickEatingDivAppBar() { if (_Overlay._clickEatingAppBarDiv) { _Overlay._clickEatingAppBarDiv.style.display = "block"; } }, Scheduler.Priority.high, null, "WinJS.UI._Overlay._showClickEatingDivAppBar"); }, _hideClickEatingDivAppBar: function () { Scheduler.schedule(function Overlay_async_hideClickEatingDivAppBar() { if (_Overlay._clickEatingAppBarDiv) { _Overlay._clickEatingAppBarDiv.style.display = "none"; } }, Scheduler.Priority.high, null, "WinJS.UI._Overlay._hideClickEatingDivAppBar"); }, _showClickEatingDivFlyout: function () { Scheduler.schedule(function Overlay_async_showClickEatingDivFlyout() { if (_Overlay._clickEatingFlyoutDiv) { _Overlay._clickEatingFlyoutDiv.style.display = "block"; } }, Scheduler.Priority.high, null, "WinJS.UI._Overlay._showClickEatingDivFlyout"); }, _hideClickEatingDivFlyout: function () { Scheduler.schedule(function Overlay_async_hideClickEatingDivFlyout() { if (_Overlay._clickEatingFlyoutDiv) { _Overlay._clickEatingFlyoutDiv.style.display = "none"; } }, Scheduler.Priority.high, null, "WinJS.UI._Overlay._hideClickEatingDivFlyout"); }, _isFlyoutVisible: function () { if (!_Overlay._clickEatingFlyoutDiv) { return false; } return (_Overlay._clickEatingFlyoutDiv.style.display === "block"); }, _hideIfLostFocus: function (overlay) { // If we're still showing we haven't really lost focus if (overlay.hidden || overlay.element.winAnimating === "showing" || overlay._sticky) { return; } // If the active thing is within our element, we haven't lost focus var active = _Global.document.activeElement; if (overlay._element && overlay._element.contains(active)) { return; } // SettingFlyouts don't dismiss if they spawned a flyout if (_ElementUtilities.hasClass(overlay._element, _Constants.settingsFlyoutClass)) { var settingsFlyout = overlay; var flyoutControl = _Overlay._getParentControlUsingClassName(active, "win-flyout"); if (flyoutControl && flyoutControl._previousFocus && settingsFlyout.element.contains(flyoutControl._previousFocus)) { _ElementUtilities._addEventListener(flyoutControl.element, 'focusout', function focusOut(event) { // When the Flyout closes, hide the SetingsFlyout if it didn't regain focus. _Overlay._hideIfLostFocus(settingsFlyout, event); _ElementUtilities._removeEventListener(flyoutControl.element, 'focusout', focusOut, false); }, false); return; } } // Do not hide focus if focus moved to a CED. Let the click handler on the CED take care of hiding us. if (active && (_ElementUtilities.hasClass(active, _Constants._clickEatingFlyoutClass) || _ElementUtilities.hasClass(active, _Constants._clickEatingAppBarClass))) { return; } overlay._hideOrDismiss(); }, // Want to hide flyouts on blur. // We get blur if we click off the window, including to an iframe within our window. // Both blurs call this function, but fortunately document.hasFocus is true if either // the document window or our iframe window has focus. _checkBlur: function () { if (!_Global.document.hasFocus()) { // The document doesn't have focus, so they clicked off the app, so light dismiss. _Overlay._hideAllFlyouts(); _Overlay._hideLightDismissAppBars(null, false); } else { if ((_Overlay._clickEatingFlyoutDiv && _Overlay._clickEatingFlyoutDiv.style.display === "block") || (_Overlay._clickEatingAppBarDiv && _Overlay._clickEatingAppBarDiv.style.display === "block")) { // We were trying to unfocus the window, but document still has focus, // so make sure the iframe that took the focus will check for blur next time. // We don't have to do this if the click eating div is hidden because then // there would be no flyout or appbar needing light dismiss. var active = _Global.document.activeElement; if (active && active.tagName === "IFRAME" && !active.msLightDismissBlur) { // - This will go away when the IFRAME goes away, and we only create one. // - This only works in IE because other browsers don't fire focus events on iframe elements. // - Can't use WinJS.Utilities._addEventListener's focusout because it doesn't fire when an // iframe loses focus due to changing windows. active.addEventListener("blur", _Overlay._checkBlur, false); active.msLightDismissBlur = true; } } } }, // Try to set us as active _trySetActive: function (element) { if (!element || !_Global.document.body || !_Global.document.body.contains(element)) { return false; } if (!_ElementUtilities._setActive(element)) { return false; } return (element === _Global.document.activeElement); }, // Try to select the text so keyboard can be used. _trySelect: function (element) { try { if (element && element.select) { element.select(); } } catch (e) { } }, // Prevent the document.activeElement from showing focus _addHideFocusClass: function (element) { if (element) { _ElementUtilities.addClass(element, _Constants.hideFocusClass); _ElementUtilities._addEventListener(element, "focusout", _Overlay._removeHideFocusClass, false); } }, // Allow the event.target (element that is losing focus) to show focus next time it gains focus _removeHideFocusClass: function (event) { // Make sure we really lost focus and was not just an App switch var target = event.target; if (target && target !== _Global.document.activeElement) { _ElementUtilities.removeClass(target, _Constants.hideFocusClass); _ElementUtilities._removeEventListener(event.target, "focusout", _Overlay._removeHideFocusClass, false); } }, _sizeOfDocument: function () { return { width: _Global.document.documentElement.offsetWidth, height: _Global.document.documentElement.offsetHeight, }; }, _getParentControlUsingClassName: function (element, className) { while (element && element !== _Global.document.body) { if (_ElementUtilities.hasClass(element, className)) { return element.winControl; } element = element.parentNode; } return null; }, // Hide all light dismiss AppBars if what has focus is not part of a AppBar or flyout. _hideIfAllAppBarsLostFocus: function _hideIfAllAppBarsLostFocus() { if (!_Overlay._isAppBarOrChild(_Global.document.activeElement)) { _Overlay._hideLightDismissAppBars(null, false); // Ensure that sticky appbars clear cached focus after light dismiss are dismissed, which moved focus. _Overlay._ElementWithFocusPreviousToAppBar = null; } }, _hideLightDismissAppBars: function (event, keyboardInvoked) { var elements = _Global.document.querySelectorAll("." + _Constants.appBarClass); var len = elements.length; var AppBars = []; for (var i = 0; i < len; i++) { var AppBar = elements[i].winControl; if (AppBar && !AppBar.sticky && !AppBar.hidden) { AppBars.push(AppBar); } } _Overlay._hideAllBars(AppBars, keyboardInvoked); }, // Show/Hide all bars _hideAllBars: function _Overlay_hideAllBars(bars, keyboardInvoked) { var allBarsAnimationPromises = bars.map(function (bar) { bar._keyboardInvoked = keyboardInvoked; bar.hide(); return bar._animationPromise; }); return Promise.join(allBarsAnimationPromises); }, _showAllBars: function _Overlay_showAllBars(bars, keyboardInvoked) { var allBarsAnimationPromises = bars.map(function (bar) { bar._keyboardInvoked = keyboardInvoked; bar._doNotFocus = false; bar._show(); return bar._animationPromise; }); return Promise.join(allBarsAnimationPromises); }, // Returns appbar element (or CED/sentinal) if the element or what had focus before the element (if a Flyout) is either: // 1) an AppBar, // 2) OR in the subtree of an AppBar, // 3) OR an AppBar click eating div. // Returns null otherwise. _isAppBarOrChild: function (element) { // If it's null, we can't do this if (!element) { return null; } // Intrinsic components of the AppBar count as the AppBar if (_ElementUtilities.hasClass(element, _Constants._clickEatingAppBarClass) || _ElementUtilities.hasClass(element, _Constants._clickEatingFlyoutClass) || _ElementUtilities.hasClass(element, _Constants.firstDivClass) || _ElementUtilities.hasClass(element, _Constants.finalDivClass) || _ElementUtilities.hasClass(element, _Constants.invokeButtonClass)) { return element; } while (element && element !== _Global.document) { if (_ElementUtilities.hasClass(element, _Constants.appBarClass)) { return element; } if (_ElementUtilities.hasClass(element, "win-flyout") && element !== element.winControl._previousFocus) { var flyoutControl = element.winControl; // If _previousFocus was in a light dismissable AppBar, then this Flyout is considered of an extension of it and that AppBar should not hide. // Hook up a 'focusout' listener to this Flyout element to make sure that light dismiss AppBars hide if focus moves anywhere other than back to an AppBar. var appBarElement = _Overlay._isAppBarOrChild(flyoutControl._previousFocus); if (appBarElement) { _ElementUtilities._addEventListener(flyoutControl.element, 'focusout', function focusOut() { // Hides any shown AppBars if the new activeElement is not in an AppBar. _Overlay._hideIfAllAppBarsLostFocus(); _ElementUtilities._removeEventListener(flyoutControl.element, 'focusout', focusOut, false); }, false); } return appBarElement; } element = element.parentNode; } return null; }, // WWA Soft Keyboard offsets _keyboardInfo: { // Determine if the keyboard is visible or not. get _visible() { try { return ( _WinRT.Windows.UI.ViewManagement.InputPane && _WinRT.Windows.UI.ViewManagement.InputPane.getForCurrentView().occludedRect.height > 0 ); } catch (e) { return false; } }, // See if we have to reserve extra space for the IHM get _extraOccluded() { var occluded; if (_WinRT.Windows.UI.ViewManagement.InputPane) { try { occluded = _WinRT.Windows.UI.ViewManagement.InputPane.getForCurrentView().occludedRect.height; } catch (e) { } } // Nothing occluded if not visible. if (occluded && !_Overlay._keyboardInfo._isResized) { // View hasn't been resized, need to return occluded height. return occluded; } // View already has space for keyboard or there's no keyboard return 0; }, // See if the view has been resized to fit a keyboard get _isResized() { // Compare ratios. Very different includes IHM space. var heightRatio = _Global.document.documentElement.clientHeight / _Global.innerHeight, widthRatio = _Global.document.documentElement.clientWidth / _Global.innerWidth; // If they're nearly identical, then the view hasn't been resized for the IHM // Only check one bound because we know the IHM will make it shorter, not skinnier. return (widthRatio / heightRatio < 0.99); }, // Get the bottom of our visible area. get _visibleDocBottom() { return _Overlay._keyboardInfo._visibleDocTop + _Overlay._keyboardInfo._visibleDocHeight; }, // Get the height of the visible document, e.g. the height of the visual viewport minus any IHM occlusion. get _visibleDocHeight() { return _Overlay._keyboardInfo._visualViewportHeight - _Overlay._keyboardInfo._extraOccluded; }, // Get total length of the IHM showPanel animation get _animationShowLength() { if (_WinRT.Windows.UI.Core.AnimationMetrics) { var a = _WinRT.Windows.UI.Core.AnimationMetrics, animationDescription = new a.AnimationDescription(a.AnimationEffect.showPanel, a.AnimationEffectTarget.primary); var animations = animationDescription.animations; var max = 0; for (var i = 0; i < animations.size; i++) { var animation = animations[i]; max = Math.max(max, animation.delay + animation.duration); } return max; } else { return 0; } }, }, _ElementWithFocusPreviousToAppBar: null, // for tests _clickEatingAppBarClass: _Constants._clickEatingAppBarClass, _clickEatingFlyoutClass: _Constants._clickEatingFlyoutClass, // Padding for IHM timer to allow for first scroll event _scrollTimeout: 150, // Events beforeShow: BEFORESHOW, beforeHide: BEFOREHIDE, afterShow: AFTERSHOW, afterHide: AFTERHIDE, commonstrings: { get cannotChangeCommandsWhenVisible() { return "Invalid argument: You must call hide() before changing {0} commands"; }, get cannotChangeHiddenProperty() { return "Unable to set hidden property while parent {0} is visible."; } } }); // Mixin for WWA's Soft Keyboard offsets when -ms-device-fixed CSS positioning is supported, or for general _Overlay positioning whenever we are in a web browser outside of WWA. // If we are in an instance of WWA, all _Overlay elements will use -ms-device-fixed positioning which fixes them to the visual viewport directly. var _keyboardInfo_Mixin = { // Get the top offset of our visible area, aka the top of the visual viewport. // This is always 0 when _Overlay elements use -ms-device-fixed positioning. _visibleDocTop: function _visibleDocTop() { return 0; }, // Get the bottom offset of the visual viewport, plus any IHM occlusion. _visibleDocBottomOffset: function _visibleDocBottomOffset() { // For -ms-device-fixed positioned elements, the bottom is just 0 when there's no IHM. // When the IHM appears, the text input that invoked it may be in a position on the page that is occluded by the IHM. // In that instance, the default browser behavior is to resize the visual viewport and scroll the input back into view. // However, if the viewport resize is prevented by an IHM event listener, the keyboard will still occlude // -ms-device-fixed elements, so we adjust the bottom offset of the appbar by the height of the occluded rect of the IHM. return (_Overlay._keyboardInfo._isResized) ? 0 : _Overlay._keyboardInfo._extraOccluded; }, // Get the visual viewport height. window.innerHeight doesn't return floating point values which are present with high DPI. _visualViewportHeight: function _visualViewportHeight() { var boundingRect = _Overlay._keyboardInfo._visualViewportSpace; return boundingRect.bottom - boundingRect.top; }, // Get the visual viewport width. window.innerWidth doesn't return floating point values which are present with high DPI. _visualViewportWidth: function _visualViewportWidth() { var boundingRect = _Overlay._keyboardInfo._visualViewportSpace; return boundingRect.right - boundingRect.left; }, _visualViewportSpace: function _visualViewportSpace() { var visualViewportSpace = _Global.document.body.querySelector("." + _Constants._visualViewportClass); if (!visualViewportSpace) { visualViewportSpace = _Global.document.createElement("DIV"); visualViewportSpace.className = _Constants._visualViewportClass; _Global.document.body.appendChild(visualViewportSpace); } return visualViewportSpace.getBoundingClientRect(); }, }; // Mixin for WWA's Soft Keyboard offsets in IE10 mode, where -ms-device-fixed positioning is not available. // In that instance, all _Overlay elements fall back to using CSS fixed positioning. // This is for backwards compatibility with Apache Cordova Apps targeting WWA since they target IE10. // This is essentially the original logic for WWA _Overlay / Soft Keyboard interactions we used when windows 8 first launched. var _keyboardInfo_Windows8WWA_Mixin = { // Get the top of our visible area in terms of its absolute distance from the top of document.documentElement. // Normalizes any offsets which have have occured between the visual viewport and the layout viewport due to resizing the viewport to fit the IHM and/or optical zoom. _visibleDocTop: function _visibleDocTop_Windows8WWA() { return _Global.window.pageYOffset - _Global.document.documentElement.scrollTop; }, // Get the bottom offset of the visual viewport from the bottom of the layout viewport, plus any IHM occlusion. _visibleDocBottomOffset: function _visibleDocBottomOffset_Windows8WWA() { return _Global.document.documentElement.clientHeight - _Overlay._keyboardInfo._visibleDocBottom; }, _visualViewportHeight: function _visualViewportHeight_Windows8WWA() { return _Global.window.innerHeight; }, _visualViewportWidth: function _visualViewportWidth_Windows8WWA() { return _Global.window.innerWidth; }, }; _Base.Class.mix(_Overlay, _Control.DOMEventMixin); // Feature detect for -ms-device-fixed positioning and fill out the // remainder of our WWA Soft KeyBoard handling logic with mixins. var visualViewportSpace = _Global.document.createElement("DIV"); visualViewportSpace.className = _Constants._visualViewportClass; _Global.document.body.appendChild(visualViewportSpace); var propertiesMixin, hasDeviceFixed = _Global.getComputedStyle(visualViewportSpace).position === "-ms-device-fixed"; if (!hasDeviceFixed && _WinRT.Windows.UI.ViewManagement.InputPane) { // If we are in WWA with IE 10 mode, use special keyboard handling knowledge for IE10 IHM. propertiesMixin = _keyboardInfo_Windows8WWA_Mixin; _Global.document.body.removeChild(visualViewportSpace); } else { // If we are in WWA on IE 11 or outside of WWA on any web browser use general positioning logic. propertiesMixin = _keyboardInfo_Mixin; } for (var propertyName in propertiesMixin) { Object.defineProperty(_Overlay._keyboardInfo, propertyName, { get: propertiesMixin[propertyName], }); } return _Overlay; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. /// appbar,Flyout,Flyouts,Statics define('WinJS/Controls/Flyout',[ 'exports', '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Resources', '../Core/_WriteProfilerMark', '../Animations', '../Utilities/_Dispose', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', './AppBar/_Constants', './Flyout/_Overlay', 'require-style!less/desktop/controls', 'require-style!less/phone/controls' ], function flyoutInit(exports, _Global, _Base, _BaseUtils, _ErrorFromName, _Resources, _WriteProfilerMark, Animations, _Dispose, _ElementUtilities, _Hoverable, _Constants, _Overlay) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { /// /// /// Displays lightweight UI that is either informational, or requires user interaction. /// Unlike a dialog, a Flyout can be light dismissed by clicking or tapping off of it. /// /// /// /// Flyout /// /// /// ]]> /// Raised just before showing a flyout. /// Raised immediately after a flyout is fully shown. /// Raised just before hiding a flyout. /// Raised immediately after a flyout is fully hidden. /// The Flyout control itself. /// /// /// Flyout: _Base.Namespace._lazy(function () { var Key = _ElementUtilities.Key; function getDimension(element, property) { return parseFloat(element, _Global.getComputedStyle(element, null)[property]); } var strings = { get ariaLabel() { return _Resources._getWinJSString("ui/flyoutAriaLabel").value; }, get noAnchor() { return "Invalid argument: Showing flyout requires a DOM element as its parameter."; }, get badPlacement() { return "Invalid argument: Flyout placement should be 'top' (default), 'bottom', 'left', 'right', or 'auto'."; }, get badAlignment() { return "Invalid argument: Flyout alignment should be 'center' (default), 'left', or 'right'."; } }; var Flyout = _Base.Class.derive(_Overlay._Overlay, function Flyout_ctor(element, options) { /// /// /// Creates a new Flyout control. /// /// /// The DOM element that hosts the control. /// /// /// The set of properties and values to apply to the new Flyout. /// /// The new Flyout control. /// /// // Simplify checking later options = options || {}; // Make sure there's an input element this._element = element || _Global.document.createElement("div"); this._id = this._element.id || _ElementUtilities._uniqueID(this._element); this._writeProfilerMark("constructor,StartTM"); this._baseFlyoutConstructor(this._element, options); var _elms = this._element.getElementsByTagName("*"); var firstDiv = this._addFirstDiv(); firstDiv.tabIndex = _ElementUtilities._getLowestTabIndexInList(_elms); var finalDiv = this._addFinalDiv(); finalDiv.tabIndex = _ElementUtilities._getHighestTabIndexInList(_elms); // Handle "esc" & "tab" key presses this._element.addEventListener("keydown", this._handleKeyDown, true); this._writeProfilerMark("constructor,StopTM"); return this; }, { _lastMaxHeight: null, _baseFlyoutConstructor: function Flyout_baseFlyoutContstructor(element, options) { // Flyout constructor // We have some options with defaults this._placement = "auto"; this._alignment = "center"; // Call the base overlay constructor helper this._baseOverlayConstructor(element, options); // Make a click eating div _Overlay._Overlay._createClickEatingDivFlyout(); // Start flyouts hidden this._element.style.visibilty = "hidden"; this._element.style.display = "none"; // Attach our css class _ElementUtilities.addClass(this._element, _Constants.flyoutClass); _ElementUtilities.addClass(this._element, _Constants.flyoutLightClass); // Make sure we have an ARIA role var role = this._element.getAttribute("role"); if (role === null || role === "" || role === undefined) { if (_ElementUtilities.hasClass(this._element, _Constants.menuClass)) { this._element.setAttribute("role", "menu"); } else { this._element.setAttribute("role", "dialog"); } } var label = this._element.getAttribute("aria-label"); if (label === null || label === "" || label === undefined) { this._element.setAttribute("aria-label", strings.ariaLabel); } // Base animation is popIn, but our flyout has different arguments this._currentAnimateIn = this._flyoutAnimateIn; this._currentAnimateOut = this._flyoutAnimateOut; // Make sure _Overlay event handlers are hooked up this._addOverlayEventHandlers(true); }, /// /// Gets or sets the Flyout control's anchor. The anchor element is the HTML element which the Flyout originates from and is positioned relative to. /// (This setting can be overridden when you call the show method.) /// /// anchor: { get: function () { return this._anchor; }, set: function (value) { this._anchor = value; } }, /// /// Gets or sets the default placement of this Flyout. (This setting can be overridden when you call the show method.) /// /// placement: { get: function () { return this._placement; }, set: function (value) { if (value !== "top" && value !== "bottom" && value !== "left" && value !== "right" && value !== "auto") { // Not a legal placement value throw new _ErrorFromName("WinJS.UI.Flyout.BadPlacement", strings.badPlacement); } this._placement = value; } }, /// /// Gets or sets the default alignment for this Flyout. (This setting can be overridden when you call the show method.) /// /// alignment: { get: function () { return this._alignment; }, set: function (value) { if (value !== "right" && value !== "left" && value !== "center") { // Not a legal alignment value throw new _ErrorFromName("WinJS.UI.Flyout.BadAlignment", strings.badAlignment); } this._alignment = value; } }, _dispose: function Flyout_dispose() { _Dispose.disposeSubTree(this.element); this._hide(); this.anchor = null; }, show: function (anchor, placement, alignment) { /// /// /// Shows the Flyout, if hidden, regardless of other states. /// /// /// The DOM element, or ID of a DOM element to anchor the Flyout, overriding the anchor property for this time only. /// /// /// The placement of the Flyout to the anchor: 'auto' (default), 'top', 'bottom', 'left', or 'right'. This parameter overrides the placement property for this show only. /// /// /// For 'top' or 'bottom' placement, the alignment of the Flyout to the anchor's edge: 'center' (default), 'left', or 'right'. /// This parameter overrides the alignment property for this show only. /// /// /// this._writeProfilerMark("show,StartTM"); // The corresponding "stop" profiler mark is handled in _Overlay._baseEndShow(). // Just call private version to make appbar flags happy this._show(anchor, placement, alignment); }, _show: function Flyout_show(anchor, placement, alignment) { this._baseFlyoutShow(anchor, placement, alignment); }, hide: function () { /// /// /// Hides the Flyout, if visible, regardless of other states. /// /// /// // Just wrap the private one, turning off keyboard invoked flag this._writeProfilerMark("hide,StartTM"); // The corresponding "stop" profiler mark is handled in _Overlay._baseEndHide(). this._keyboardInvoked = false; this._hide(); }, _hide: function Flyout_hide() { if (this._baseHide()) { // Return focus if this or the flyout CED has focus var active = _Global.document.activeElement; if (this._previousFocus && active && (this._element.contains(active) || _ElementUtilities.hasClass(active, _Overlay._Overlay._clickEatingFlyoutClass)) && this._previousFocus.focus !== undefined) { // _isAppBarOrChild may return a CED or sentinal var appBar = _Overlay._Overlay._isAppBarOrChild(this._previousFocus); if (!appBar || (appBar.winControl && !appBar.winControl.hidden && !appBar.winAnimating)) { // Don't move focus back to a appBar that is hidden // We cannot rely on element.style.visibility because it will be visible while animating var role = this._previousFocus.getAttribute("role"); var fHideRole = _Overlay._Overlay._keyboardInfo._visible && !this._keyboardWasUp; if (fHideRole) { // Convince IHM to dismiss because it only came up after the flyout was up. // Change aria role and back to get IHM to dismiss. this._previousFocus.setAttribute("role", ""); } if (this._keyboardInvoked) { this._previousFocus.focus(); } else { _Overlay._Overlay._trySetActive(this._previousFocus); } active = _Global.document.activeElement; if (fHideRole) { // Restore the role so that css is applied correctly var previousFocus = this._previousFocus; if (previousFocus) { _BaseUtils._yieldForDomModification(function () { previousFocus.setAttribute("role", role); }); } } } // If the anchor gained focus we want to hide the focus in the non-keyboarding scenario if (!this._keyboardInvoked && (this._previousFocus === active) && appBar && active) { _Overlay._Overlay._addHideFocusClass(active); } } this._previousFocus = null; // Need click-eating div to be hidden if there are no other visible flyouts if (!this._isThereVisibleFlyout()) { _Overlay._Overlay._hideClickEatingDivFlyout(); } } }, _baseFlyoutShow: function Flyout_baseFlyoutShow(anchor, placement, alignment) { // Don't do anything if disabled if (this.disabled) { return; } // Pick up defaults if (!anchor) { anchor = this._anchor; } if (!placement) { placement = this._placement; } if (!alignment) { alignment = this._alignment; } // Dereference the anchor if necessary if (typeof anchor === "string") { anchor = _Global.document.getElementById(anchor); } else if (anchor && anchor.element) { anchor = anchor.element; } // We expect an anchor if (!anchor) { // If we have _nextLeft, etc., then we were continuing an old animation, so that's OK if (!this._retryLast) { throw new _ErrorFromName("WinJS.UI.Flyout.NoAnchor", strings.noAnchor); } // Last call was incomplete, so use the previous _current values. this._retryLast = null; } else { // Remember the anchor so that if we lose focus we can go back this._currentAnchor = anchor; // Remember current values this._currentPlacement = placement; this._currentAlignment = alignment; } // Need click-eating div to be visible, no matter what if (!this._sticky) { _Overlay._Overlay._showClickEatingDivFlyout(); } // If we're animating (eg baseShow is going to fail), then don't mess up our current state. // Queue us up to wait for current animation to finish first. if (this._element.winAnimating) { this._doNext = "show"; this._retryLast = true; return; } // We call our base _baseShow to handle the actual animation if (this._baseShow()) { // (_baseShow shouldn't ever fail because we tested winAnimating above). if (!_ElementUtilities.hasClass(this.element, "win-menu")) { // Verify that the firstDiv is in the correct location. // Move it to the correct location or add it if not. var _elms = this._element.getElementsByTagName("*"); var firstDiv = this.element.querySelectorAll(".win-first"); if (this.element.children.length && !_ElementUtilities.hasClass(this.element.children[0], _Constants.firstDivClass)) { if (firstDiv && firstDiv.length > 0) { firstDiv.item(0).parentNode.removeChild(firstDiv.item(0)); } firstDiv = this._addFirstDiv(); } firstDiv.tabIndex = _ElementUtilities._getLowestTabIndexInList(_elms); // Verify that the finalDiv is in the correct location. // Move it to the correct location or add it if not. var finalDiv = this.element.querySelectorAll(".win-final"); if (!_ElementUtilities.hasClass(this.element.children[this.element.children.length - 1], _Constants.finalDivClass)) { if (finalDiv && finalDiv.length > 0) { finalDiv.item(0).parentNode.removeChild(finalDiv.item(0)); } finalDiv = this._addFinalDiv(); } finalDiv.tabIndex = _ElementUtilities._getHighestTabIndexInList(_elms); } // Hide all other flyouts this._hideAllOtherFlyouts(this); // Store what had focus before showing the Flyout. // This must happen after we hide all other flyouts so that we store the correct element. this._previousFocus = _Global.document.activeElement; } }, _endShow: function Flyout_endShow() { // Remember if the IHM was up since we may need to hide it when the flyout hides. // This check needs to happen after the IHM has a chance to hide itself after we force hide // all other visible Flyouts. this._keyboardWasUp = _Overlay._Overlay._keyboardInfo._visible; if (!_ElementUtilities.hasClass(this.element, _Constants.menuClass)) { // Put focus on the first child in the Flyout this._focusOnFirstFocusableElementOrThis(); // Prevent what is gaining focus from showing that it has focus _Overlay._Overlay._addHideFocusClass(_Global.document.activeElement); } else { // Make sure the menu has focus, but don't show a focus rect _Overlay._Overlay._trySetActive(this._element); } }, // Find our new flyout position. _findPosition: function Flyout_findPosition() { this._nextHeight = null; this._keyboardMovedUs = false; this._hasScrolls = false; this._keyboardSquishedUs = 0; // Make sure menu toggles behave if (this._checkToggle) { this._checkToggle(); } // Update margins for this alignment and remove old scrolling this._updateAdjustments(this._currentAlignment); // Set up the new position, and prep the offset for showPopup this._getTopLeft(); // Panning top offset is calculated top this._scrollTop = this._nextTop - _Overlay._Overlay._keyboardInfo._visibleDocTop; // Adjust position if (this._nextTop < 0) { // Need to attach to bottom this._element.style.bottom = "0px"; this._element.style.top = "auto"; } else { // Normal, attach to top this._element.style.top = this._nextTop + "px"; this._element.style.bottom = "auto"; } if (this._nextLeft < 0) { // Overran right, attach to right this._element.style.right = "0px"; this._element.style.left = "auto"; } else { // Normal, attach to left this._element.style.left = this._nextLeft + "px"; this._element.style.right = "auto"; } // Adjust height/scrollbar if (this._nextHeight !== null) { _ElementUtilities.addClass(this._element, _Constants.scrollsClass); this._lastMaxHeight = this._element.style.maxHeight; this._element.style.maxHeight = this._nextHeight + "px"; this._nextBottom = this._nextTop + this._nextHeight; this._hasScrolls = true; } // May need to adjust if the IHM is showing. if (_Overlay._Overlay._keyboardInfo._visible) { // Use keyboard logic this._checkKeyboardFit(); if (this._keyboardMovedUs) { this._adjustForKeyboard(); } } }, // This determines our positioning. We have 5 modes, the 1st four are explicit, the last is automatic: // * top - position explicitly on the top of the anchor, shrinking and adding scrollbar as needed. // * bottom - position explicitly below the anchor, shrinking and adding scrollbar as needed. // * left - position left of the anchor, shrinking and adding a vertical scrollbar as needed. // * right - position right of the anchor, shrinking and adding a vertical scroolbar as needed. // * auto - Automatic placement. // Auto tests the height of the anchor and the flyout. For consistency in orientation, we imagine // that the anchor is placed in the vertical center of the display. If the flyout would fit above // that centered anchor, then we will place the flyout vertically in relation to the anchor, otherwise // placement will be horizontal. // Vertical auto placement will be positioned on top of the anchor if room, otherwise below the anchor. // - this is because touch users would be more likely to obscure flyouts below the anchor. // Horizontal auto placement will be positioned to the left of the anchor if room, otherwise to the right. // - this is because right handed users would be more likely to obscure a flyout on the right of the anchor. // Auto placement will add a vertical scrollbar if necessary. _getTopLeft: function Flyout_getTopLeft() { var anchorRawRectangle = this._currentAnchor.getBoundingClientRect(), flyout = {}, anchor = {}; // Adjust for the anchor's margins. anchor.top = anchorRawRectangle.top; anchor.bottom = anchorRawRectangle.bottom; anchor.left = anchorRawRectangle.left; anchor.right = anchorRawRectangle.right; anchor.height = anchor.bottom - anchor.top; anchor.width = anchor.right - anchor.left; // Get our flyout and margins, note that getDimension calls // window.getComputedStyle, which ensures layout is updated. flyout.marginTop = getDimension(this._element, "marginTop"); flyout.marginBottom = getDimension(this._element, "marginBottom"); flyout.marginLeft = getDimension(this._element, "marginLeft"); flyout.marginRight = getDimension(this._element, "marginRight"); flyout.width = _ElementUtilities.getTotalWidth(this._element); flyout.height = _ElementUtilities.getTotalHeight(this._element); flyout.innerWidth = _ElementUtilities.getContentWidth(this._element); flyout.innerHeight = _ElementUtilities.getContentHeight(this._element); this._nextMarginPadding = (flyout.height - flyout.innerHeight); // Check fit for requested this._currentPlacement, doing fallback if necessary switch (this._currentPlacement) { case "top": if (!this._fitTop(anchor, flyout)) { // Didn't fit, needs scrollbar this._nextTop = _Overlay._Overlay._keyboardInfo._visibleDocTop; this._nextHeight = anchor.top - _Overlay._Overlay._keyboardInfo._visibleDocTop - this._nextMarginPadding; } this._centerHorizontally(anchor, flyout, this._currentAlignment); break; case "bottom": if (!this._fitBottom(anchor, flyout)) { // Didn't fit, needs scrollbar this._nextTop = -1; this._nextHeight = _Overlay._Overlay._keyboardInfo._visibleDocHeight - (anchor.bottom - _Overlay._Overlay._keyboardInfo._visibleDocTop) - this._nextMarginPadding; } this._centerHorizontally(anchor, flyout, this._currentAlignment); break; case "left": if (!this._fitLeft(anchor, flyout)) { // Didn't fit, just shove it to edge this._nextLeft = 0; } this._centerVertically(anchor, flyout); break; case "right": if (!this._fitRight(anchor, flyout)) { // Didn't fit,just shove it to edge this._nextLeft = -1; } this._centerVertically(anchor, flyout); break; case "auto": // Auto, if the anchor was in the vertical center of the display would we fit above it? if (this._sometimesFitsAbove(anchor, flyout)) { // It will fit above or below the anchor if (!this._fitTop(anchor, flyout)) { // Didn't fit above (preferred), so go below. this._fitBottom(anchor, flyout); } this._centerHorizontally(anchor, flyout, this._currentAlignment); } else { // Won't fit above or below, try a side if (!this._fitLeft(anchor, flyout) && !this._fitRight(anchor, flyout)) { // Didn't fit left or right either, is top or bottom bigger? if (this._topHasMoreRoom(anchor)) { // Top, won't fit, needs scrollbar this._nextTop = _Overlay._Overlay._keyboardInfo._visibleDocTop; this._nextHeight = anchor.top - _Overlay._Overlay._keyboardInfo._visibleDocTop - this._nextMarginPadding; } else { // Bottom, won't fit, needs scrollbar this._nextTop = -1; this._nextHeight = _Overlay._Overlay._keyboardInfo._visibleDocHeight - (anchor.bottom - _Overlay._Overlay._keyboardInfo._visibleDocTop) - this._nextMarginPadding; } this._centerHorizontally(anchor, flyout, this._currentAlignment); } else { this._centerVertically(anchor, flyout); } } break; default: // Not a legal this._currentPlacement value throw new _ErrorFromName("WinJS.UI.Flyout.BadPlacement", strings.badPlacement); } // Remember "bottom" in case we need to consider keyboard later, only tested for top-pinned bars this._nextBottom = this._nextTop + flyout.height; }, // If the anchor is centered vertically, would the flyout fit above it? _sometimesFitsAbove: function Flyout_sometimesFitsAbove(anchor, flyout) { return ((_Overlay._Overlay._keyboardInfo._visibleDocHeight - anchor.height) / 2) >= flyout.height; }, _topHasMoreRoom: function Flyout_topHasMoreRoom(anchor) { return anchor.top > _Overlay._Overlay._keyboardInfo._visibleDocHeight - anchor.bottom; }, // See if we can fit in various places, fitting in the main view, // ignoring viewport changes, like for the IHM. _fitTop: function Flyout_fitTop(anchor, flyout) { this._nextTop = anchor.top - flyout.height; this._nextAnimOffset = { top: "50px", left: "0px", keyframe: "WinJS-showFlyoutTop" }; return (this._nextTop >= _Overlay._Overlay._keyboardInfo._visibleDocTop && this._nextTop + flyout.height <= _Overlay._Overlay._keyboardInfo._visibleDocBottom); }, _fitBottom: function Flyout_fitBottom(anchor, flyout) { this._nextTop = anchor.bottom; this._nextAnimOffset = { top: "-50px", left: "0px", keyframe: "WinJS-showFlyoutBottom" }; return (this._nextTop >= _Overlay._Overlay._keyboardInfo._visibleDocTop && this._nextTop + flyout.height <= _Overlay._Overlay._keyboardInfo._visibleDocBottom); }, _fitLeft: function Flyout_fitLeft(anchor, flyout) { this._nextLeft = anchor.left - flyout.width; this._nextAnimOffset = { top: "0px", left: "50px", keyframe: "WinJS-showFlyoutLeft" }; return (this._nextLeft >= 0 && this._nextLeft + flyout.width <= _Overlay._Overlay._keyboardInfo._visualViewportWidth); }, _fitRight: function Flyout_fitRight(anchor, flyout) { this._nextLeft = anchor.right; this._nextAnimOffset = { top: "0px", left: "-50px", keyframe: "WinJS-showFlyoutRight" }; return (this._nextLeft >= 0 && this._nextLeft + flyout.width <= _Overlay._Overlay._keyboardInfo._visualViewportWidth); }, _centerVertically: function Flyout_centerVertically(anchor, flyout) { this._nextTop = anchor.top + anchor.height / 2 - flyout.height / 2; if (this._nextTop < _Overlay._Overlay._keyboardInfo._visibleDocTop) { this._nextTop = _Overlay._Overlay._keyboardInfo._visibleDocTop; } else if (this._nextTop + flyout.height >= _Overlay._Overlay._keyboardInfo._visibleDocBottom) { // Flag to put on bottom this._nextTop = -1; } }, _centerHorizontally: function Flyout_centerHorizontally(anchor, flyout, alignment) { if (alignment === "center") { this._nextLeft = anchor.left + anchor.width / 2 - flyout.width / 2; } else if (alignment === "left") { this._nextLeft = anchor.left; } else if (alignment === "right") { this._nextLeft = anchor.right - flyout.width; } else { throw new _ErrorFromName("WinJS.UI.Flyout.BadAlignment", strings.badAlignment); } if (this._nextLeft < 0) { this._nextLeft = 0; } else if (this._nextLeft + flyout.width >= _Global.document.documentElement.clientWidth) { // flag to put on right this._nextLeft = -1; } }, _updateAdjustments: function Flyout_updateAdjustments(alignment) { // Move to 0,0 in case it is off screen, so that it lays out at a reasonable size this._element.style.top = "0px"; this._element.style.bottom = "auto"; this._element.style.left = "0px"; this._element.style.right = "auto"; // Scrolling may not be necessary _ElementUtilities.removeClass(this._element, _Constants.scrollsClass); if (this._lastMaxHeight !== null) { this._element.style.maxHeight = this._lastMaxHeight; this._lastMaxHeight = null; } // Alignment if (alignment === "center") { _ElementUtilities.removeClass(this._element, "win-leftalign"); _ElementUtilities.removeClass(this._element, "win-rightalign"); } else if (alignment === "left") { _ElementUtilities.addClass(this._element, "win-leftalign"); _ElementUtilities.removeClass(this._element, "win-rightalign"); } else if (alignment === "right") { _ElementUtilities.addClass(this._element, "win-rightalign"); _ElementUtilities.removeClass(this._element, "win-leftalign"); } }, _showingKeyboard: function Flyout_showingKeyboard(event) { if (this.hidden) { return; } // The only way that we can be showing a keyboard when a flyout is up is because the input was // in the flyout itself, in which case we'll be moving ourselves. There is no practical way // for the application to override this as the focused element is in our flyout. event.ensuredFocusedElementInView = true; // See if the keyboard is going to force us to move this._checkKeyboardFit(); if (this._keyboardMovedUs) { // Pop out immediately, then move to new spot this._element.style.opacity = 0; var that = this; _Global.setTimeout(function () { that._adjustForKeyboard(); that._baseAnimateIn(); }, _Overlay._Overlay._keyboardInfo._animationShowLength); } }, _resize: function Flyout_resize() { // If hidden and not busy animating, then nothing to do if (this.hidden && !this._animating) { return; } // This should only happen if the IHM is dismissing, // the only other way is for viewstate changes, which // would dismiss any flyout. if (this._needToHandleHidingKeyboard) { // Hiding keyboard, update our position, giving the anchor a chance to update first. var that = this; _BaseUtils._setImmediate(function () { that._findPosition(); }); this._needToHandleHidingKeyboard = false; } }, _checkKeyboardFit: function Flyout_checkKeyboardFit() { // Check for moving to fit keyboard: // - Too Tall, above top, or below bottom. var height = _ElementUtilities.getTotalHeight(this._element); var viewportHeight = _Overlay._Overlay._keyboardInfo._visibleDocHeight - this._nextMarginPadding; if (height > viewportHeight) { // Too Tall, pin to top with max height this._keyboardMovedUs = true; this._scrollTop = 0; this._keyboardSquishedUs = viewportHeight; } else if (this._nextTop === -1) { // Pinned to bottom counts as moved this._keyboardMovedUs = true; } else if (this._nextTop < _Overlay._Overlay._keyboardInfo._visibleDocTop) { // Above the top of the viewport this._scrollTop = 0; this._keyboardMovedUs = true; } else if (this._nextBottom > _Overlay._Overlay._keyboardInfo._visibleDocBottom) { // Below the bottom of the viewport this._scrollTop = -1; this._keyboardMovedUs = true; } }, _adjustForKeyboard: function Flyout_adjustForKeyboard() { // Keyboard moved us, update our metrics as needed if (this._keyboardSquishedUs) { // Add scrollbar if we didn't already have scrollsClass if (!this._hasScrolls) { _ElementUtilities.addClass(this._element, _Constants.scrollsClass); this._lastMaxHeight = this._element.style.maxHeight; } // Adjust height this._element.style.maxHeight = this._keyboardSquishedUs + "px"; } // Update top/bottom this._checkScrollPosition(true); }, _hidingKeyboard: function Flyout_hidingKeyboard() { // If we aren't visible and not animating, or haven't been repositioned, then nothing to do // We don't know if the keyboard moved the anchor, so _keyboardMovedUs doesn't help here if (this.hidden && !this._animating) { return; } // Snap to the final position // We'll either just reveal the current space or resize the window if (_Overlay._Overlay._keyboardInfo._isResized) { // Flag resize that we'll need an updated position this._needToHandleHidingKeyboard = true; } else { // Not resized, update our final position, giving the anchor a chance to update first. var that = this; _BaseUtils._setImmediate(function () { that._findPosition(); }); } }, _checkScrollPosition: function Flyout_checkScrollPosition(showing) { if (this.hidden && !showing) { return; } // May need to adjust top by viewport offset if (this._scrollTop < 0) { // Need to attach to bottom this._element.style.bottom = _Overlay._Overlay._keyboardInfo._visibleDocBottomOffset + "px"; this._element.style.top = "auto"; } else { // Normal, attach to top this._element.style.top = _Overlay._Overlay._keyboardInfo._visibleDocTop + "px"; this._element.style.bottom = "auto"; } }, // AppBar flyout animations _flyoutAnimateIn: function Flyout_flyoutAnimateIn() { if (this._keyboardMovedUs) { return this._baseAnimateIn(); } else { this._element.style.opacity = 1; this._element.style.visibility = "visible"; return Animations.showPopup(this._element, this._nextAnimOffset); } }, _flyoutAnimateOut: function Flyout_flyoutAnimateOut() { if (this._keyboardMovedUs) { return this._baseAnimateOut(); } else { this._element.style.opacity = 0; return Animations.hidePopup(this._element, this._nextAnimOffset); } }, // Hide all other flyouts besides this one _hideAllOtherFlyouts: function Flyout_hideAllOtherFlyouts(thisFlyout) { var flyouts = _Global.document.querySelectorAll("." + _Constants.flyoutClass); for (var i = 0; i < flyouts.length; i++) { var flyoutControl = flyouts[i].winControl; if (flyoutControl && !flyoutControl.hidden && (flyoutControl !== thisFlyout)) { flyoutControl.hide(); } } }, // Returns true if there is a flyout in the DOM that is not hidden _isThereVisibleFlyout: function Flyout_isThereVisibleFlyout() { var flyouts = _Global.document.querySelectorAll("." + _Constants.flyoutClass); for (var i = 0; i < flyouts.length; i++) { var flyoutControl = flyouts[i].winControl; if (flyoutControl && !flyoutControl.hidden) { return true; } } return false; }, _handleKeyDown: function Flyout_handleKeyDown(event) { // Escape closes flyouts but if the user has a text box with an IME candidate // window open, we want to skip the ESC key event since it is handled by the IME. // When the IME handles a key it sets event.keyCode === Key.IME for an easy check. if (event.keyCode === Key.escape && event.keyCode !== Key.IME) { // Show a focus rect on what we move focus to event.preventDefault(); event.stopPropagation(); this.winControl._keyboardInvoked = true; this.winControl._hide(); } else if ((event.keyCode === Key.space || event.keyCode === Key.enter) && (this === _Global.document.activeElement)) { event.preventDefault(); event.stopPropagation(); this.winControl.hide(); } else if (event.shiftKey && event.keyCode === Key.tab && this === _Global.document.activeElement && !event.altKey && !event.ctrlKey && !event.metaKey) { event.preventDefault(); event.stopPropagation(); this.winControl._focusOnLastFocusableElementOrThis(); } }, // Create and add a new first div as the first child _addFirstDiv: function Flyout_addFirstDiv() { var firstDiv = _Global.document.createElement("div"); firstDiv.className = _Constants.firstDivClass; firstDiv.style.display = "inline"; firstDiv.setAttribute("role", "menuitem"); firstDiv.setAttribute("aria-hidden", "true"); // add to beginning if (this._element.children[0]) { this._element.insertBefore(firstDiv, this._element.children[0]); } else { this._element.appendChild(firstDiv); } var that = this; _ElementUtilities._addEventListener(firstDiv, "focusin", function () { that._focusOnLastFocusableElementOrThis(); }, false); return firstDiv; }, // Create and add a new final div as the last child _addFinalDiv: function Flyout_addFinalDiv() { var finalDiv = _Global.document.createElement("div"); finalDiv.className = _Constants.finalDivClass; finalDiv.style.display = "inline"; finalDiv.setAttribute("role", "menuitem"); finalDiv.setAttribute("aria-hidden", "true"); this._element.appendChild(finalDiv); var that = this; _ElementUtilities._addEventListener(finalDiv, "focusin", function () { that._focusOnFirstFocusableElementOrThis(); }, false); return finalDiv; }, _writeProfilerMark: function Flyout_writeProfilerMark(text) { _WriteProfilerMark("WinJS.UI.Flyout:" + this._id + ":" + text); } }); return Flyout; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Glyph Enumeration /// Segoe define('WinJS/Controls/AppBar/_Icon',[ 'exports', '../../Core/_Base', '../../Core/_Resources' ], function appBarIconInit(exports, _Base, _Resources) { "use strict"; var glyphs = ["previous", "next", "play", "pause", "edit", "save", "clear", "delete", "remove", "add", "cancel", "accept", "more", "redo", "undo", "home", "up", "forward", "right", "back", "left", "favorite", "camera", "settings", "video", "sync", "download", "mail", "find", "help", "upload", "emoji", "twopage", "leavechat", "mailforward", "clock", "send", "crop", "rotatecamera", "people", "closepane", "openpane", "world", "flag", "previewlink", "globe", "trim", "attachcamera", "zoomin", "bookmarks", "document", "protecteddocument", "page", "bullets", "comment", "mail2", "contactinfo", "hangup", "viewall", "mappin", "phone", "videochat", "switch", "contact", "rename", "pin", "musicinfo", "go", "keyboard", "dockleft", "dockright", "dockbottom", "remote", "refresh", "rotate", "shuffle", "list", "shop", "selectall", "orientation", "import", "importall", "browsephotos", "webcam", "pictures", "savelocal", "caption", "stop", "showresults", "volume", "repair", "message", "page2", "calendarday", "calendarweek", "calendar", "characters", "mailreplyall", "read", "link", "accounts", "showbcc", "hidebcc", "cut", "attach", "paste", "filter", "copy", "emoji2", "important", "mailreply", "slideshow", "sort", "manage", "allapps", "disconnectdrive", "mapdrive", "newwindow", "openwith", "contactpresence", "priority", "uploadskydrive", "gototoday", "font", "fontcolor", "contact2", "folder", "audio", "placeholder", "view", "setlockscreen", "settile", "cc", "stopslideshow", "permissions", "highlight", "disableupdates", "unfavorite", "unpin", "openlocal", "mute", "italic", "underline", "bold", "movetofolder", "likedislike", "dislike", "like", "alignright", "aligncenter", "alignleft", "zoom", "zoomout", "openfile", "otheruser", "admin", "street", "map", "clearselection", "fontdecrease", "fontincrease", "fontsize", "cellphone", "reshare", "tag", "repeatone", "repeatall", "outlinestar", "solidstar", "calculator", "directions", "target", "library", "phonebook", "memo", "microphone", "postupdate", "backtowindow", "fullscreen", "newfolder", "calendarreply", "unsyncfolder", "reporthacked", "syncfolder", "blockcontact", "switchapps", "addfriend", "touchpointer", "gotostart", "zerobars", "onebar", "twobars", "threebars", "fourbars", "scan", "preview"]; // Provide properties to grab resources for each of the icons /// /// The AppBarIcon enumeration provides a set of glyphs for use with the AppBarCommand icon property. /// var icons = glyphs.reduce(function (fixedIcons, item) { fixedIcons[item] = { get: function () { return _Resources._getWinJSString("ui/appBarIcons/" + item).value; } }; return fixedIcons; }, {}); _Base.Namespace._moduleDefine(exports, "WinJS.UI.AppBarIcon", icons); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // AppBarCommand /// appbar,appbars,Flyout,Flyouts,onclick,Statics define('WinJS/Controls/AppBar/_Command',[ 'exports', '../../Core/_Global', '../../Core/_WinRT', '../../Core/_Base', '../../Core/_ErrorFromName', '../../Core/_Resources', '../../Utilities/_Control', '../../Utilities/_Dispose', '../../Utilities/_ElementUtilities', '../Flyout/_Overlay', '../Tooltip', './_Constants', './_Icon' ], function appBarCommandInit(exports, _Global, _WinRT, _Base, _ErrorFromName, _Resources, _Control, _Dispose, _ElementUtilities, _Overlay, Tooltip, _Constants, _Icon) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { /// /// /// Represents a command to display in an AppBar. /// /// /// /// /// ]]> /// The AppBarCommand control itself. /// The AppBarCommand's icon box. /// The AppBarCommand's icon's image formatting. /// The AppBarCommand's icon's ring. /// The AppBarCommand's label /// /// /// AppBarCommand: _Base.Namespace._lazy(function () { function _handleClick(event) { /*jshint validthis: true */ var command = this.winControl; if (command) { if (command._type === _Constants.typeToggle) { command.selected = !command.selected; } else if (command._type === _Constants.typeFlyout && command._flyout) { var parentAppBar = _Overlay._Overlay._getParentControlUsingClassName(this, _Constants.appBarClass); var placement = "top"; if (parentAppBar && parentAppBar.placement === "top") { placement = "bottom"; } var flyout = command._flyout; // Flyout may not have processAll'd, so this may be a DOM object if (typeof flyout === "string") { flyout = _Global.document.getElementById(flyout); } if (!flyout.show) { flyout = flyout.winControl; } if (flyout && flyout.show) { flyout.show(this, placement); } } if (command.onclick) { command.onclick(event); } } } var strings = { get ariaLabel() { return _Resources._getWinJSString("ui/appBarCommandAriaLabel").value; }, get duplicateConstruction() { return "Invalid argument: Controls may only be instantiated one time for each DOM element"; }, get badClick() { return "Invalid argument: The onclick property for an {0} must be a function"; }, get badDivElement() { return "Invalid argument: For a content command, the element must be null or a div element"; }, get badHrElement() { return "Invalid argument: For a separator, the element must be null or an hr element"; }, get badButtonElement() { return "Invalid argument: For a button, toggle, or flyout command, the element must be null or a button element"; } }; return _Base.Class.define(function AppBarCommand_ctor(element, options) { /// /// /// Creates a new AppBarCommand control. /// /// /// The DOM element that will host the control. AppBarCommand will create one if null. /// /// /// The set of properties and values to apply to the new AppBarCommand. /// /// /// The new AppBarCommand control. /// /// // Check to make sure we weren't duplicated if (element && element.winControl) { throw new _ErrorFromName("WinJS.UI.AppBarCommand.DuplicateConstruction", strings.duplicateConstruction); } this._disposed = false; // Don't blow up if they didn't pass options if (!options) { options = {}; } // Need a type before we can create our element if (!options.type) { this._type = _Constants.typeButton; } options.section = options.section || _Constants.sectionGlobal; // Go ahead and create it, separator and content types look different than buttons // Don't forget to use passed in element if one was provided. this._element = element; if (options.type === _Constants.typeContent) { this._createContent(); } else if (options.type === _Constants.typeSeparator) { this._createSeparator(); } else { // This will also set the icon & label this._createButton(); } _ElementUtilities.addClass(this._element, "win-disposable"); // Remember ourselves this._element.winControl = this; // Attach our css class _ElementUtilities.addClass(this._element, _Constants.appBarCommandClass); if (options.onclick) { this.onclick = options.onclick; } // We want to handle some clicks options.onclick = _handleClick; _Control.setOptions(this, options); if (this._type === _Constants.typeToggle && !options.selected) { this.selected = false; } // Set up pointerdown handler and clean up ARIA if needed if (this._type !== _Constants.typeSeparator) { // Hide the modern focus rect on click or touch var that = this; _ElementUtilities._addEventListener(this._element, "pointerdown", function () { _Overlay._Overlay._addHideFocusClass(that._element); }, false); // Make sure we have an ARIA role var role = this._element.getAttribute("role"); if (role === null || role === "" || role === undefined) { if (this._type === _Constants.typeToggle) { role = "menuitemcheckbox"; } else if (this._type === _Constants.typeContent) { role = "group"; } else { role = "menuitem"; } this._element.setAttribute("role", role); if (this._type === _Constants.typeFlyout) { this._element.setAttribute("aria-haspopup", true); } } // Label should've been set by label, but if it was missed for some reason: var label = this._element.getAttribute("aria-label"); if (label === null || label === "" || label === undefined) { this._element.setAttribute("aria-label", strings.ariaLabel); } } }, { /// /// Gets or sets the ID of the AppBarCommand. /// id: { get: function () { return this._element.id; }, set: function (value) { // we allow setting first time only. otherwise we ignore it. if (value && !this._element.id) { this._element.id = value; } } }, /// /// Gets or sets the type of the AppBarCommand. Possible values are "button", "toggle", "flyout", "content" or "separator". /// type: { get: function () { return this._type; }, set: function (value) { // we allow setting first time only. otherwise we ignore it. if (!this._type) { if (value !== _Constants.typeContent && value !== _Constants.typeFlyout && value !== _Constants.typeToggle && value !== _Constants.typeSeparator) { this._type = _Constants.typeButton; } else { this._type = value; } } } }, /// /// Gets or sets the label of the AppBarCommand. /// label: { get: function () { return this._label; }, set: function (value) { this._label = value; if (this._labelSpan) { this._labelSpan.textContent = this.label; } // Ensure that we have a tooltip, by updating already-constructed tooltips. Separators won't have these: if (!this.tooltip && this._tooltipControl) { this._tooltip = this.label; this._tooltipControl.innerHTML = this.label; } // Update aria-label this._element.setAttribute("aria-label", this.label); // Check if we need to suppress the tooltip this._testIdenticalTooltip(); } }, /// /// Gets or sets the icon of the AppBarCommand. /// icon: { get: function () { return this._icon; }, set: function (value) { this._icon = _Icon[value] || value; if (this._imageSpan) { // If the icon's a single character, presume a glyph if (this._icon && this._icon.length === 1) { // Set the glyph this._imageSpan.textContent = this._icon; this._imageSpan.style.backgroundImage = ""; this._imageSpan.style.msHighContrastAdjust = ""; } else { // Must be an image, set that this._imageSpan.textContent = ""; this._imageSpan.style.backgroundImage = this._icon; this._imageSpan.style.msHighContrastAdjust = "none"; } } } }, /// /// Gets or sets the function to invoke when the command is clicked. /// onclick: { get: function () { return this._onclick; }, set: function (value) { if (value && typeof value !== "function") { throw new _ErrorFromName("WinJS.UI.AppBarCommand.BadClick", _Resources._formatString(strings.badClick, "AppBarCommand")); } this._onclick = value; } }, /// /// For flyout-type AppBarCommands, this property returns the WinJS.UI.Flyout that this command invokes. /// When setting this property, you may also use the String ID of the flyout to invoke, the DOM object /// for the flyout, or the WinJS.UI.Flayout object itself. /// flyout: { get: function () { // Resolve it to the flyout var flyout = this._flyout; if (typeof flyout === "string") { flyout = _Global.document.getElementById(flyout); } // If it doesn't have a .element, then we need to getControl on it if (flyout && !flyout.element) { flyout = flyout.winControl; } return flyout; }, set: function (value) { // Need to update aria-owns with the new ID. var id = value; if (id && typeof id !== "string") { // Our controls have .element properties if (id.element) { id = id.element; } // Hope it's a DOM element, get ID from DOM element if (id) { if (id.id) { id = id.id; } else { // No id, have to fake one id.id = _ElementUtilities._uniqueID(id); id = id.id; } } } if (typeof id === "string") { this._element.setAttribute("aria-owns", id); } // Remember it this._flyout = value; } }, /// /// Gets or sets the section that the AppBarCommand is in. Possible values are "selection" and "global". /// section: { get: function () { return this._section; }, set: function (value) { // we allow settings section only one time if (!this._section || _WinRT.Windows.ApplicationModel.DesignMode.designModeEnabled) { this._setSection(value); } } }, /// Gets or sets the tooltip text of the AppBarCommand. tooltip: { get: function () { return this._tooltip; }, set: function (value) { this._tooltip = value; // Update already-constructed tooltips. Separators and content commands won't have these: if (this._tooltipControl) { this._tooltipControl.innerHTML = this._tooltip; } // Check if we need to suppress the tooltip this._testIdenticalTooltip(); } }, /// Set or get the selected state of a toggle button. selected: { get: function () { // Ensure it's a boolean because we're using the DOM element to keep in-sync return this._element.getAttribute("aria-checked") === "true"; }, set: function (value) { this._element.setAttribute("aria-checked", value); } }, /// element: { get: function () { return this._element; } }, /// /// Gets or sets a value that indicates whether the AppBarCommand is disabled. A value of true disables the AppBarCommand, and a value of false enables it. /// disabled: { get: function () { // Ensure it's a boolean because we're using the DOM element to keep in-sync return !!this._element.disabled; }, set: function (value) { this._element.disabled = value; } }, /// hidden: { get: function () { // Ensure it's a boolean because we're using the DOM element to keep in-sync return this._element.style.visibility === "hidden"; }, set: function (value) { var appbarControl = _Overlay._Overlay._getParentControlUsingClassName(this._element, _Constants.appBarClass); if (appbarControl && !appbarControl.hidden) { throw new _ErrorFromName("WinJS.UI.AppBarCommand.CannotChangeHiddenProperty", _Resources._formatString(_Overlay._Overlay.commonstrings.cannotChangeHiddenProperty, "AppBar")); } if (value === this.hidden) { // No changes to make. return; } var style = this._element.style; if (value) { style.visibility = "hidden"; style.display = "none"; } else { style.visibility = ""; style.display = "inline-block"; } if (appbarControl) { appbarControl._commandsUpdated(); } } }, /// /// Gets or sets the HTMLElement within a "content" type AppBarCommand that should receive focus whenever focus moves via Home or the arrow keys, /// from the previous AppBarCommand to the this AppBarCommand. Returns the AppBarCommand object's host element by default. /// firstElementFocus: { get: function () { return this._firstElementFocus || this._lastElementFocus || this._element; }, set: function (element) { // Arguments of null and this.element should treated the same to ensure that this.element is never a tabstop when either focus property has been set. this._firstElementFocus = (element === this.element) ? null : element; this._updateTabStop(); } }, /// /// Gets or sets the HTMLElement within a "content" type AppBarCommand that should receive focus whenever focus would move, via End or arrow keys, /// from the next AppBarCommand to this AppBarCommand. Returns this AppBarCommand object's host element by default. /// lastElementFocus: { get: function () { return this._lastElementFocus || this._firstElementFocus || this._element; }, set: function (element) { // Arguments of null and this.element should treated the same to ensure that this.element is never a tabstop when either focus property has been set. this._lastElementFocus = (element === this.element) ? null : element; this._updateTabStop(); } }, dispose: function () { /// /// /// Disposes this control. /// /// if (this._disposed) { return; } this._disposed = true; if (this._tooltipControl) { this._tooltipControl.dispose(); } if (this._type === _Constants.typeContent) { _Dispose.disposeSubTree(this.element); } }, addEventListener: function (type, listener, useCapture) { /// /// /// Registers an event handler for the specified event. /// /// /// Required. The name of the event to register. It must be "beforeshow", "beforehide", "aftershow", or "afterhide". /// /// Required. The event handler function to associate with this event. /// /// Optional. Set to true to register the event handler for the capturing phase; otherwise, set to false to register the event handler for the bubbling phase. /// /// return this._element.addEventListener(type, listener, useCapture); }, removeEventListener: function (type, listener, useCapture) { /// /// /// Removes an event handler that the addEventListener method registered. /// /// Required. The name of the event to remove. /// Required. The event handler function to remove. /// /// Optional. Set to true to remove the capturing phase event handler; otherwise, set to false to remove the bubbling phase event handler. /// /// return this._element.removeEventListener(type, listener, useCapture); }, /// Adds an extra CSS class during construction. extraClass: { get: function () { return this._extraClass; }, set: function (value) { if (this._extraClass) { _ElementUtilities.removeClass(this._element, this._extraClass); } this._extraClass = value; _ElementUtilities.addClass(this._element, this._extraClass); } }, // Private _testIdenticalTooltip: function AppBarCommand_testIdenticalToolTip() { this._hideIfFullSize = (this._label === this._tooltip); }, _createContent: function AppBarCommand_createContent() { // Make sure there's an element if (!this._element) { this._element = _Global.document.createElement("div"); } else { // Verify the element was a div if (this._element.tagName !== "DIV") { throw new _ErrorFromName("WinJS.UI.AppBarCommand.BadDivElement", strings.badDivElement); } } // If a tabIndex isnt set, default to 0; if (parseInt(this._element.getAttribute("tabIndex"), 10) !== this._element.tabIndex) { this._element.tabIndex = 0; } }, _createSeparator: function AppBarCommand_createSeparator() { // Make sure there's an element if (!this._element) { this._element = _Global.document.createElement("hr"); } else { // Verify the element was an hr if (this._element.tagName !== "HR") { throw new _ErrorFromName("WinJS.UI.AppBarCommand.BadHrElement", strings.badHrElement); } } }, _createButton: function AppBarCommand_createButton() { // Make sure there's an element if (!this._element) { this._element = _Global.document.createElement("button"); } else { // Verify the element was a button if (this._element.tagName !== "BUTTON") { throw new _ErrorFromName("WinJS.UI.AppBarCommand.BadButtonElement", strings.badButtonElement); } // Make sure it has a type="button" var type = this._element.getAttribute("type"); if (type === null || type === "" || type === undefined) { this._element.setAttribute("type", "button"); } this._element.innerHTML = ""; } // AppBarCommand buttons need to look like this: //// this._element.type = "button"; this._iconSpan = _Global.document.createElement("span"); this._iconSpan.setAttribute("aria-hidden", "true"); this._iconSpan.className = "win-commandicon win-commandring"; this._iconSpan.tabIndex = -1; this._element.appendChild(this._iconSpan); this._imageSpan = _Global.document.createElement("span"); this._imageSpan.setAttribute("aria-hidden", "true"); this._imageSpan.className = "win-commandimage"; this._imageSpan.tabIndex = -1; this._iconSpan.appendChild(this._imageSpan); this._labelSpan = _Global.document.createElement("span"); this._labelSpan.setAttribute("aria-hidden", "true"); this._labelSpan.className = "win-label"; this._labelSpan.tabIndex = -1; this._element.appendChild(this._labelSpan); // 'win-global' or 'win-selection' are added later by caller. // Label and icon are added later by caller. // Attach a tooltip - Note: we're going to stomp on it's setControl so we don't have to make another DOM element to hang it off of. // This private _tooltipControl attribute is used by other pieces, changing the name could break them. this._tooltipControl = new Tooltip.Tooltip(this._element); var that = this; this._tooltipControl.addEventListener("beforeopen", function () { if (that._hideIfFullSize && !_Overlay._Overlay._getParentControlUsingClassName(that._element.parentElement, _Constants.reducedClass)) { that._tooltipControl.close(); } }, false); }, _setSection: function AppBarCommand_setSection(section) { if (!section) { section = _Constants.sectionGlobal; } if (this._section) { // Remove the old section class if (this._section === _Constants.sectionGlobal) { _ElementUtilities.removeClass(this._element, _Constants.appBarCommandGlobalClass); } else if (this.section === _Constants.sectionSelection) { _ElementUtilities.removeClass(this._element, _Constants.appBarCommandSelectionClass); } } // Add the new section class this._section = section; if (section === _Constants.sectionGlobal) { _ElementUtilities.addClass(this._element, _Constants.appBarCommandGlobalClass); } else if (section === _Constants.sectionSelection) { _ElementUtilities.addClass(this._element, _Constants.appBarCommandSelectionClass); } }, _updateTabStop: function AppBarCommand_updateTabStop() { // Whenever the firstElementFocus or lastElementFocus properties are set for content type AppBarCommands, // the containing command element is no longer a tabstop. if (this._firstElementFocus || this._lastElementFocus) { this.element.tabIndex = -1; } else { this.element.tabIndex = 0; } }, _isFocusable: function AppBarCommand_isFocusable() { return (!this.hidden && this._type !== _Constants.typeSeparator && !this.element.disabled && (this.firstElementFocus.tabIndex >= 0 || this.lastElementFocus.tabIndex >= 0)); }, }); }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/AppBar/_Layouts',[ 'exports', '../../Core/_Global', '../../Core/_Base', '../../Core/_ErrorFromName', '../../Core/_Resources', '../../Scheduler', '../../Utilities/_Control', '../../Utilities/_Dispose', '../../Utilities/_ElementUtilities', './_Command', './_Constants' ], function appBarLayoutsInit(exports, _Global, _Base, _ErrorFromName, _Resources, Scheduler, _Control, _Dispose, _ElementUtilities, _Command, _Constants) { "use strict"; // AppBar will use this when AppBar.layout property is set to "custom" _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _AppBarBaseLayout: _Base.Namespace._lazy(function () { var baseType = _Constants.appBarLayoutCustom; var strings = { get nullCommand() { return "Invalid argument: command must not be null"; } }; var _AppBarBaseLayout = _Base.Class.define(function _AppBarBaseLayout_ctor(appBarEl, options) { this._disposed = false; options = options || {}; _Control.setOptions(this, options); if (appBarEl) { this.connect(appBarEl); } }, { // Members className: { get: function _AppBarBaseLayout_get_className() { return this._className; }, }, type: { get: function _AppBarBaseLayout_get_className() { return this._type || baseType; }, }, commandsInOrder: { get: function _AppBarBaseLayout_get_commandsInOrder() { // Gets a DOM ordered Array of the AppBarCommand elements in the AppBar. var commands = this.appBarEl.querySelectorAll("." + _Constants.appBarCommandClass); // Needs to be an array, in case these are getting passed to a new layout. // The new layout will invoke the AppBar._layoutCommands, and it expects an // Array. return Array.prototype.slice.call(commands); } }, connect: function _AppBarBaseLayout_connect(appBarEl) { if (this.className) { _ElementUtilities.addClass(appBarEl, this.className); } this.appBarEl = appBarEl; }, disconnect: function _AppBarBaseLayout_disconnect() { if (this.className) { _ElementUtilities.removeClass(this.appBarEl, this.className); } this.appBarEl = null; this.dispose(); }, layout: function _AppBarBaseLayout_layout(commands) { // Append commands to the DOM. var len = commands.length; for (var i = 0; i < len; i++) { var command = this.sanitizeCommand(commands[i]); this.appBarEl.appendChild(command._element); } }, sanitizeCommand: function _AppBarBaseLayout_sanitizeCommand(command) { if (!command) { throw new _ErrorFromName("WinJS.UI.AppBar.NullCommand", strings.nullCommand); } // See if it's a command already command = command.winControl || command; if (!command._element) { // Not a command, so assume it is options for the command's constructor. command = new _Command.AppBarCommand(null, command); } // If we were attached somewhere else, detach us if (command._element.parentElement) { command._element.parentElement.removeChild(command._element); } return command; }, dispose: function _AppBarBaseLayout_dispose() { this._disposed = true; }, disposeChildren: function _AppBarBaseLayout_disposeChildren() { var appBarFirstDiv = this.appBarEl.querySelectorAll("." + _Constants.firstDivClass); appBarFirstDiv = appBarFirstDiv.length >= 1 ? appBarFirstDiv[0] : null; var appBarFinalDiv = this.appBarEl.querySelectorAll("." + _Constants.finalDivClass); appBarFinalDiv = appBarFinalDiv.length >= 1 ? appBarFinalDiv[0] : null; var children = this.appBarEl.children; var length = children.length; for (var i = 0; i < length; i++) { var element = children[i]; if (element === appBarFirstDiv || element === appBarFinalDiv) { continue; } else { _Dispose.disposeSubTree(element); } } }, handleKeyDown: function _AppBarBaseLayout_handleKeyDown() { // NOP }, commandsUpdated: function _AppBarBaseLayout_commandsUpdated() { // NOP }, beginAnimateCommands: function _AppBarBaseLayout_beginAnimateCommands() { // The parameters are 3 mutually exclusive arrays of win-command elements contained in this Overlay. // 1) showCommands[]: All of the HIDDEN win-command elements that ARE scheduled to show. // 2) hideCommands[]: All of the VISIBLE win-command elements that ARE scheduled to hide. // 3) otherVisibleCommands[]: All VISIBLE win-command elements that ARE NOT scheduled to hide. // NOP }, endAnimateCommands: function _AppBarBaseLayout_endAnimateCommands() { // NOP }, scale: function _AppBarBaseLayout_scale() { // NOP }, resize: function _AppBarBaseLayout_resize() { // NOP }, }); return _AppBarBaseLayout; }), }); // AppBar will use this when AppBar.layout property is set to "commands" _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _AppBarCommandsLayout: _Base.Namespace._lazy(function () { var layoutClassName = _Constants.commandLayoutClass; var layoutType = _Constants.appBarLayoutCommands; var _AppBarCommandsLayout = _Base.Class.derive(exports._AppBarBaseLayout, function _AppBarCommandsLayout_ctor(appBarEl) { exports._AppBarBaseLayout.call(this, appBarEl, { _className: layoutClassName, _type: layoutType }); this._commandLayoutsInit(appBarEl); }, { _getWidthOfFullSizeCommands: function _AppBarCommandsLayout_getWidthOfFullSizeCommands(commands) { // Commands layout puts primary commands and secondary commands into the primary row. // Return the total width of all visible primary and secondary commands as if they were full-size. // Perform any pending measurements on "content" type AppBarCommands. if (this._needToMeasureNewCommands) { this._measureContentCommands(); } var accumulatedWidth = 0; var separatorsCount = 0; var buttonsCount = 0; if (!commands) { // Return the cached full size width of the last known visible commands in the AppBar. return this._fullSizeWidthOfLastKnownVisibleCommands; } else { // Return the width of the specified commands. var command; for (var i = 0, len = commands.length; i < len; i++) { command = commands[i].winControl || commands[i]; if (command._type === _Constants.typeSeparator) { separatorsCount++; } else if (command._type !== _Constants.typeContent) { // button, toggle, and flyout types all have the same width. buttonsCount++; } else { accumulatedWidth += command._fullSizeWidth; } } } return accumulatedWidth += (separatorsCount * _Constants.separatorWidth) + (buttonsCount * _Constants.buttonWidth); }, _getFocusableCommandsInLogicalOrder: function _AppBarCommandsLayout_getCommandsInLogicalOrder() { // Function returns an array of all the contained AppBarCommands which are reachable by left/right arrows. var selectionCommands = this._secondaryCommands.children, globalCommands = this._primaryCommands.children, focusedIndex = -1; var getFocusableCommandsHelper = function (commandsInReach) { var focusableCommands = []; for (var i = 0, len = commandsInReach.length; i < len; i++) { var element = commandsInReach[i]; if (_ElementUtilities.hasClass(element, _Constants.appBarCommandClass) && element.winControl) { var containsFocus = element.contains(_Global.document.activeElement); // With the inclusion of content type commands, it may be possible to tab to elements in AppBarCommands that are not reachable by arrow keys. // Regardless, when an AppBarCommand contains the element with focus, we just include the whole command so that we can determine which // commands are adjacent to it when looking for the next focus destination. if (element.winControl._isFocusable() || containsFocus) { focusableCommands.push(element); if (containsFocus) { focusedIndex = focusableCommands.length - 1; } } } } return focusableCommands; }; // Determines which set of commands the user could potentially reach through Home, End, and arrow keys. // All commands in the commands layout AppBar, from left to right are in reach. Selection then Global. var commandsInReach = Array.prototype.slice.call(selectionCommands).concat(Array.prototype.slice.call(globalCommands)); var focusableCommands = getFocusableCommandsHelper(commandsInReach); focusableCommands.focusedIndex = focusedIndex; return focusableCommands; }, }); // Override some our base implementations and expand our API surface with the commandLayoutsMixin object. _Base.Class.mix(_AppBarCommandsLayout, _commandLayoutsMixin); return _AppBarCommandsLayout; }), }); // These are functions and properties that any new command layout would want to share with our existing "commands" layout. var _commandLayoutsMixin = { layout: function _commandLayoutsMixin_layout(commands) { // Insert commands and other layout specific DOM into the AppBar element. // Empty our tree. _ElementUtilities.empty(this._primaryCommands); _ElementUtilities.empty(this._secondaryCommands); // Keep track of the order we receive the commands in. this._commandsInOriginalOrder = []; // Layout commands for (var i = 0, len = commands.length; i < len; i++) { var command = this.sanitizeCommand(commands[i]); this._commandsInOriginalOrder.push(command.element); if ("global" === command.section) { this._primaryCommands.appendChild(command._element); } else { this._secondaryCommands.appendChild(command._element); } } // Append layout containers to AppBar element. // Secondary Commands should come first in Tab Order. this.appBarEl.appendChild(this._secondaryCommands); this.appBarEl.appendChild(this._primaryCommands); // Need to measure all content commands after they have been added to the AppBar to make sure we allow // user defined CSS rules based on the ancestor of the content command to take affect. this._needToMeasureNewCommands = true; // In case this is called from the constructor before the AppBar element has been appended to the DOM, // we schedule the initial scaling of commands, with the expectation that the element will be added // synchronously, in the same block of code that called the constructor. Scheduler.schedule(function () { if (this._needToMeasureNewCommands && !this._disposed) { this.scale(); } }.bind(this), Scheduler.Priority.idle, this, "WinJS._commandLayoutsMixin._scaleNewCommands"); }, commandsInOrder: { get: function () { return this._commandsInOriginalOrder.filter(function (command) { // Make sure the element is still in the AppBar. return this.appBarEl.contains(command); }, this); } }, disposeChildren: function _commandLayoutsMixin_disposeChildren() { _Dispose.disposeSubTree(this._primaryCommands); _Dispose.disposeSubTree(this._secondaryCommands); }, handleKeyDown: function _commandLayoutsMixin_handleKeyDown(event) { var Key = _ElementUtilities.Key; if (_ElementUtilities._matchesSelector(event.target, ".win-interactive, .win-interactive *")) { return; // Ignore left, right, home & end keys if focused element has win-interactive class. } var rtl = _Global.getComputedStyle(this.appBarEl).direction === "rtl"; var leftKey = rtl ? Key.rightArrow : Key.leftArrow; var rightKey = rtl ? Key.leftArrow : Key.rightArrow; if (event.keyCode === leftKey || event.keyCode === rightKey || event.keyCode === Key.home || event.keyCode === Key.end) { var globalCommandHasFocus = this._primaryCommands.contains(_Global.document.activeElement); var focusableCommands = this._getFocusableCommandsInLogicalOrder(globalCommandHasFocus); var targetCommand; if (focusableCommands.length) { switch (event.keyCode) { case leftKey: // Arrowing past the last command wraps back around to the first command. var index = Math.max(-1, focusableCommands.focusedIndex - 1) + focusableCommands.length; targetCommand = focusableCommands[index % focusableCommands.length].winControl.lastElementFocus; break; case rightKey: // Arrowing previous to the first command wraps back around to the last command. var index = focusableCommands.focusedIndex + 1 + focusableCommands.length; targetCommand = focusableCommands[index % focusableCommands.length].winControl.firstElementFocus; break; case Key.home: var index = 0; targetCommand = focusableCommands[index].winControl.firstElementFocus; break; case Key.end: var index = focusableCommands.length - 1; targetCommand = focusableCommands[index].winControl.lastElementFocus; break; } } if (targetCommand) { targetCommand.focus(); // Prevent default so that the browser doesn't also evaluate the keydown event on the newly focused element. event.preventDefault(); } } }, commandsUpdated: function _commandLayoutsMixin_commandsUpdated(newSetOfVisibleCommands) { // Whenever new commands are set or existing commands are hiding/showing in the AppBar, this // function is called to update the cached width measurement of all visible AppBarCommands. var visibleCommands = (newSetOfVisibleCommands) ? newSetOfVisibleCommands : this.commandsInOrder.filter(function (command) { return !command.winControl.hidden; }); this._fullSizeWidthOfLastKnownVisibleCommands = this._getWidthOfFullSizeCommands(visibleCommands); }, beginAnimateCommands: function _commandLayoutsMixin_beginAnimateCommands(showCommands, hideCommands, otherVisibleCommands) { // The parameters are 3 mutually exclusive arrays of win-command elements contained in this Overlay. // 1) showCommands[]: All of the HIDDEN win-command elements that ARE scheduled to show. // 2) hideCommands[]: All of the VISIBLE win-command elements that ARE scheduled to hide. // 3) otherVisibleCommands[]: All VISIBLE win-command elements that ARE NOT scheduled to hide. this._scaleAfterAnimations = false; // Determine if the overall width of visible commands in the primary row will be increasing OR decreasing. var changeInWidth = this._getWidthOfFullSizeCommands(showCommands) - this._getWidthOfFullSizeCommands(hideCommands); if (changeInWidth > 0) { // Width of contents is going to increase, update our command counts now, to what they will be after we complete the animations. var visibleCommandsAfterAnimations = otherVisibleCommands.concat(showCommands); this.commandsUpdated(visibleCommandsAfterAnimations); // Make sure we will have enough room to fit everything on a single row. this.scale(); } else if (changeInWidth < 0) { // Width of contents is going to decrease. Once animations are complete, check if // there is enough available space to make the remaining commands full size. this._scaleAfterAnimations = true; } }, endAnimateCommands: function _commandLayoutsMixin_endAnimateCommands() { if (this._scaleAfterAnimations) { this.commandsUpdated(); this.scale(); } }, scale: function _commandLayoutsMixin_scale() { // If the total width of all AppBarCommands in the primary row is greater than the // width of the AppBar, add the win-reduced class to the AppBar element and all // AppBarCommands will reduce in size. // Measure the width all visible commands in AppBar's primary row, the AppBar's offsetWidth and the AppBar horizontal padding: var fullSizeWidthOfVisibleContent = this._getWidthOfFullSizeCommands(); if (this._appBarTotalKnownWidth !== +this._appBarTotalKnownWidth) { this._appBarTotalKnownWidth = this._scaleHelper(); } if (fullSizeWidthOfVisibleContent <= this._appBarTotalKnownWidth) { // Full size commands _ElementUtilities.removeClass(this.appBarEl, _Constants.reducedClass); } else { // Reduced size commands _ElementUtilities.addClass(this.appBarEl, _Constants.reducedClass); } }, resize: function _commandLayoutsMixin_resize() { if (!this._disposed) { // Check for horizontal window resizes. this._appBarTotalKnownWidth = null; if (!this.appBarEl.winControl.hidden) { this.scale(); } } }, disconnect: function _commandLayoutsMixin_disconnect() { _ElementUtilities.removeClass(this.appBarEl, _Constants.reducedClass); exports._AppBarBaseLayout.prototype.disconnect.call(this); }, _commandLayoutsInit: function _commandLayoutsMixin_commandLayoutsInit() { // Create layout infrastructure this._primaryCommands = _Global.document.createElement("DIV"); this._secondaryCommands = _Global.document.createElement("DIV"); _ElementUtilities.addClass(this._primaryCommands, _Constants.primaryCommandsClass); _ElementUtilities.addClass(this._secondaryCommands, _Constants.secondaryCommandsClass); }, _scaleHelper: function _commandLayoutsMixin_scaleHelper() { // This exists as a single line function so that unit tests can // overwrite it since they can't resize the WWA window. // It is expected that AppBar is an immediate child of the and will have 100% width. // We measure the clientWidth of the documentElement so that we can scale the AppBar lazily // even while its element is display: 'none' var extraPadding = this.appBarEl.winControl.closedDisplayMode === "minimal" ? _Constants.appBarInvokeButtonWidth : 0; return _Global.document.documentElement.clientWidth - extraPadding; }, _measureContentCommands: function _commandLayoutsMixin_measureContentCommands() { // AppBar measures the width of content commands when they are first added // and then caches that value to avoid additional layouts in the future. // Can't measure unless We're in the document body if (_Global.document.body.contains(this.appBarEl)) { this._needToMeasureNewCommands = false; // Remove the reducedClass from AppBar to ensure fullsize measurements var hadReducedClass = _ElementUtilities.hasClass(this.appBarEl, _Constants.reducedClass); _ElementUtilities.removeClass(this.appBarEl, _Constants.reducedClass); var hadHiddenClass = _ElementUtilities.hasClass(this.appBarEl, _Constants.hiddenClass); _ElementUtilities.removeClass(this.appBarEl, _Constants.hiddenClass); // Make sure AppBar and children have width dimensions. var prevAppBarDisplay = this.appBarEl.style.display; this.appBarEl.style.display = ""; var prevCommandDisplay; var contentElements = this.appBarEl.querySelectorAll("div." + _Constants.appBarCommandClass); var element; for (var i = 0, len = contentElements.length; i < len; i++) { element = contentElements[i]; if (element.winControl && element.winControl._type === _Constants.typeContent) { // Make sure command has width dimensions before we measure. prevCommandDisplay = element.style.display; element.style.display = ""; element.winControl._fullSizeWidth = _ElementUtilities.getTotalWidth(element) || 0; element.style.display = prevCommandDisplay; } } // Restore state to AppBar. this.appBarEl.style.display = prevAppBarDisplay; if (hadReducedClass) { _ElementUtilities.addClass(this.appBarEl, _Constants.reducedClass); } if (hadHiddenClass) { _ElementUtilities.addClass(this.appBarEl, _Constants.hiddenClass); } this.commandsUpdated(); } }, }; }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // AppBar /// appbar,appBars,Flyout,Flyouts,iframe,Statics,unfocus,WinJS define('WinJS/Controls/AppBar',[ 'exports', '../Core/_Global', '../Core/_WinRT', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Resources', '../Core/_WriteProfilerMark', '../Animations', '../Promise', '../Scheduler', '../Utilities/_Control', '../Utilities/_Dispose', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', '../Utilities/_KeyboardBehavior', './AppBar/_Constants', './AppBar/_Layouts', './AppBar/_Command', './AppBar/_Icon', './Flyout/_Overlay', 'require-style!less/desktop/controls', 'require-style!less/phone/controls' ], function appBarInit(exports, _Global, _WinRT, _Base, _BaseUtils, _ErrorFromName, _Resources, _WriteProfilerMark, Animations, Promise, Scheduler, _Control, _Dispose, _ElementUtilities, _Hoverable, _KeyboardBehavior, _Constants, _Layouts, _Command, _Icon, _Overlay) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { /// /// /// Represents an application toolbar for display commands. /// /// /// /// /// /// /// ]]> /// Raised just before showing the AppBar. /// Raised immediately after the AppBar is fully shown. /// Raised just before hiding the AppBar. /// Raised immediately after the AppBar is fully hidden. /// The AppBar control itself. /// Style for a custom layout AppBar. /// /// /// AppBar: _Base.Namespace._lazy(function () { var Key = _ElementUtilities.Key; // Enum of known constant pixel values for display modes. var knownVisibleHeights = { disabled: 0, none: 0, hidden: 0, minimal: 25, }; // Maps each notion of a display modes to the corresponding visible position var displayModeVisiblePositions = { disabled: "hidden", none: "hidden", hidden: "hidden", minimal: "minimal", shown: "shown", }; // Enum of closedDisplayMode constants var closedDisplayModes = { none: "none", minimal: "minimal", }; // Constants shown/hidden states var appbarShownState = "shown", appbarHiddenState = "hidden"; // Hook into event var appBarCommandEvent = false; var edgyHappening = null; // Handler for the edgy starting/completed/cancelled events function _completedEdgy(e) { // If we had a right click on a flyout, ignore it. if (_Overlay._Overlay._rightMouseMightEdgy && e.kind === _WinRT.Windows.UI.Input.EdgeGestureKind.mouse) { return; } if (edgyHappening) { // Edgy was happening, just skip it edgyHappening = null; } else { // Edgy wasn't happening, so toggle var keyboardInvoked = e.kind === _WinRT.Windows.UI.Input.EdgeGestureKind.keyboard; AppBar._toggleAllAppBarsState(keyboardInvoked); } } function _startingEdgy() { if (!edgyHappening) { // Edgy wasn't happening, so toggle & start it edgyHappening = AppBar._toggleAllAppBarsState(false); } } function _canceledEdgy() { // Shouldn't get here unless edgy was happening. // Undo whatever we were doing. var bars = _getDynamicBarsForEdgy(); if (edgyHappening === "showing") { _Overlay._Overlay._hideAllBars(bars, false); } else if (edgyHappening === "hiding") { _Overlay._Overlay._showAllBars(bars, false); } edgyHappening = null; } function _allManipulationChanged(event) { var elements = _Global.document.querySelectorAll("." + _Constants.appBarClass); if (elements) { var len = elements.length; for (var i = 0; i < len; i++) { var element = elements[i]; var appbar = element.winControl; if (appbar && !element.disabled) { appbar._manipulationChanged(event); } } } } // Get all the non-sticky bars and return them. // Returns array of AppBar objects. // The array also has _hidden and/or _shown set if ANY are hidden or shown. function _getDynamicBarsForEdgy() { var elements = _Global.document.querySelectorAll("." + _Constants.appBarClass); var len = elements.length; var AppBars = []; AppBars._shown = false; AppBars._hidden = false; for (var i = 0; i < len; i++) { var element = elements[i]; if (element.disabled) { // Skip disabled AppBars continue; } var AppBar = element.winControl; if (AppBar) { AppBars.push(AppBar); if (_ElementUtilities.hasClass(AppBar._element, _Constants.hiddenClass) || _ElementUtilities.hasClass(AppBar._element, _Constants.hidingClass)) { AppBars._hidden = true; } else { AppBars._shown = true; } } } return AppBars; } // Sets focus to the last AppBar in the provided appBars array with given placement. // Returns true if focus was set. False otherwise. function _setFocusToPreviousAppBarHelper(startIndex, appBarPlacement, appBars) { var appBar; for (var i = startIndex; i >= 0; i--) { appBar = appBars[i].winControl; if (appBar && appBar.placement === appBarPlacement && !appBar.hidden && appBar._focusOnLastFocusableElement && appBar._focusOnLastFocusableElement()) { return true; } } return false; } // Sets focus to the last tab stop of the previous AppBar // AppBar tabbing order: // 1) Bottom AppBars // 2) Top AppBars // DOM order is respected, because an AppBar should not have a defined tabIndex function _setFocusToPreviousAppBar() { /*jshint validthis: true */ var appBars = _Global.document.querySelectorAll("." + _Constants.appBarClass); if (!appBars.length) { return; } var thisAppBarIndex = 0; for (var i = 0; i < appBars.length; i++) { if (appBars[i] === this.parentElement) { thisAppBarIndex = i; break; } } var appBarControl = this.parentElement.winControl; if (appBarControl.placement === _Constants.appBarPlacementBottom) { // Bottom appBar: Focus order: (1)previous bottom appBars (2)top appBars (3)bottom appBars if (thisAppBarIndex && _setFocusToPreviousAppBarHelper(thisAppBarIndex - 1, _Constants.appBarPlacementBottom, appBars)) { return; } if (_setFocusToPreviousAppBarHelper(appBars.length - 1, _Constants.appBarPlacementTop, appBars)) { return; } if (_setFocusToPreviousAppBarHelper(appBars.length - 1, _Constants.appBarPlacementBottom, appBars)) { return; } } else if (appBarControl.placement === _Constants.appBarPlacementTop) { // Top appBar: Focus order: (1)previous top appBars (2)bottom appBars (3)top appBars if (thisAppBarIndex && _setFocusToPreviousAppBarHelper(thisAppBarIndex - 1, _Constants.appBarPlacementTop, appBars)) { return; } if (_setFocusToPreviousAppBarHelper(appBars.length - 1, _Constants.appBarPlacementBottom, appBars)) { return; } if (_setFocusToPreviousAppBarHelper(appBars.length - 1, _Constants.appBarPlacementTop, appBars)) { return; } } } // Sets focus to the first AppBar in the provided appBars array with given placement. // Returns true if focus was set. False otherwise. function _setFocusToNextAppBarHelper(startIndex, appBarPlacement, appBars) { var appBar; for (var i = startIndex; i < appBars.length; i++) { appBar = appBars[i].winControl; if (appBar && appBar.placement === appBarPlacement && !appBar.hidden && appBar._focusOnFirstFocusableElement && appBar._focusOnFirstFocusableElement()) { return true; } } return false; } // Sets focus to the first tab stop of the next AppBar // AppBar tabbing order: // 1) Bottom AppBars // 2) Top AppBars // DOM order is respected, because an AppBar should not have a defined tabIndex function _setFocusToNextAppBar() { /*jshint validthis: true */ var appBars = _Global.document.querySelectorAll("." + _Constants.appBarClass); var thisAppBarIndex = 0; for (var i = 0; i < appBars.length; i++) { if (appBars[i] === this.parentElement) { thisAppBarIndex = i; break; } } if (this.parentElement.winControl.placement === _Constants.appBarPlacementBottom) { // Bottom appBar: Focus order: (1)next bottom appBars (2)top appBars (3)bottom appBars if (_setFocusToNextAppBarHelper(thisAppBarIndex + 1, _Constants.appBarPlacementBottom, appBars)) { return; } if (_setFocusToNextAppBarHelper(0, _Constants.appBarPlacementTop, appBars)) { return; } if (_setFocusToNextAppBarHelper(0, _Constants.appBarPlacementBottom, appBars)) { return; } } else if (this.parentElement.winControl.placement === _Constants.appBarPlacementTop) { // Top appBar: Focus order: (1)next top appBars (2)bottom appBars (3)top appBars if (_setFocusToNextAppBarHelper(thisAppBarIndex + 1, _Constants.appBarPlacementTop, appBars)) { return; } if (_setFocusToNextAppBarHelper(0, _Constants.appBarPlacementBottom, appBars)) { return; } if (_setFocusToNextAppBarHelper(0, _Constants.appBarPlacementTop, appBars)) { return; } } } // Updates the firstDiv & finalDiv of all shown AppBars function _updateAllAppBarsFirstAndFinalDiv() { var appBars = _Global.document.querySelectorAll("." + _Constants.appBarClass); var appBar; for (var i = 0; i < appBars.length; i++) { appBar = appBars[i].winControl; if (appBar && !appBar.hidden && appBar._updateFirstAndFinalDiv) { appBar._updateFirstAndFinalDiv(); } } } // Returns true if a visible non-sticky (light dismiss) AppBar is found in the document function _isThereVisibleNonStickyBar() { var appBars = _Global.document.querySelectorAll("." + _Constants.appBarClass); for (var i = 0; i < appBars.length; i++) { var appBarControl = appBars[i].winControl; if (appBarControl && !appBarControl.sticky && (!appBarControl.hidden || appBarControl._element.winAnimating === displayModeVisiblePositions.shown)) { return true; } } return false; } // If the previous focus was not a AppBar or CED, store it in the cache // (_isAppBarOrChild tests CED for us). function _checkStorePreviousFocus(focusEvent) { if (focusEvent.relatedTarget && focusEvent.relatedTarget.focus && !_Overlay._Overlay._isAppBarOrChild(focusEvent.relatedTarget)) { _storePreviousFocus(focusEvent.relatedTarget); } } // Cache the previous focus information function _storePreviousFocus(element) { if (element) { _Overlay._Overlay._ElementWithFocusPreviousToAppBar = element; } } // Try to return focus to what had focus before. // If successfully return focus to a textbox, restore the selection too. function _restorePreviousFocus() { _Overlay._Overlay._trySetActive(_Overlay._Overlay._ElementWithFocusPreviousToAppBar); } var strings = { get ariaLabel() { return _Resources._getWinJSString("ui/appBarAriaLabel").value; }, get requiresCommands() { return "Invalid argument: commands must not be empty"; }, get cannotChangePlacementWhenVisible() { return "Invalid argument: The placement property cannot be set when the AppBar is visible, call hide() first"; }, get badLayout() { return "Invalid argument: The layout property must be 'custom' or 'commands'"; }, get cannotChangeLayoutWhenVisible() { return "Invalid argument: The layout property cannot be set when the AppBar is visible, call hide() first"; } }; var AppBar = _Base.Class.derive(_Overlay._Overlay, function AppBar_ctor(element, options) { /// /// /// Creates a new AppBar control. /// /// /// The DOM element that will host the control. /// /// /// The set of properties and values to apply to the new AppBar control. /// /// /// The new AppBar control. /// /// this._initializing = true; // Simplify checking later options = options || {}; // Make sure there's an element this._element = element || _Global.document.createElement("div"); this._id = this._element.id || _ElementUtilities._uniqueID(this._element); this._writeProfilerMark("constructor,StartTM"); if (!this._element.hasAttribute("tabIndex")) { this._element.tabIndex = -1; } // Attach our css class. _ElementUtilities.addClass(this._element, _Constants.appBarClass); // Make sure we have an ARIA role var role = this._element.getAttribute("role"); if (!role) { this._element.setAttribute("role", "menubar"); } var label = this._element.getAttribute("aria-label"); if (!label) { this._element.setAttribute("aria-label", strings.ariaLabel); } // Call the _Overlay constructor helper to finish setting up our element. // Don't pass constructor options, AppBar needs to set those itself specific order. this._baseOverlayConstructor(this._element); // Start off hidden this._lastPositionVisited = displayModeVisiblePositions.none; _ElementUtilities.addClass(this._element, _Constants.hiddenClass); // validate that if they didn't set commands, but want command // layout that the HTML only contains commands. Do this first // so that we don't leave partial AppBars in the DOM. if (options.layout !== _Constants.appBarLayoutCustom && !options.commands && this._element) { // Shallow copy object so we can modify it. options = _BaseUtils._shallowCopy(options); options.commands = this._verifyCommandsOnly(this._element, "WinJS.UI.AppBarCommand"); } // Add Invoke button. this._invokeButton = _Global.document.createElement("button"); this._invokeButton.tabIndex = 0; this._invokeButton.innerHTML = ""; _ElementUtilities.addClass(this._invokeButton, _Constants.invokeButtonClass); this._element.appendChild(this._invokeButton); var that = this; _ElementUtilities._addEventListener(this._invokeButton, "pointerdown", function () { _Overlay._Overlay._addHideFocusClass(that._invokeButton); }, false); this._invokeButton.addEventListener("click", function () { AppBar._toggleAllAppBarsState(_KeyboardBehavior._keyboardSeenLast, that); }, false); // Run layout setter immediately. We need to know our layout in order to correctly // position any commands that may be getting set through the constructor. this.layout = options.layout || _Constants.appBarLayoutCommands; delete options.layout; // Need to set placement before closedDisplayMode, closedDisplayMode sets our starting position, which is dependant on placement. this.placement = options.placement || _Constants.appBarPlacementBottom; this.closedDisplayMode = options.closedDisplayMode || closedDisplayModes.minimal; _Control.setOptions(this, options); this._initializing = false; // Make a click eating div _Overlay._Overlay._createClickEatingDivAppBar(); // Handle key down (esc) and (left & right) this._element.addEventListener("keydown", this._handleKeyDown.bind(this), false); // Attach event handler if (!appBarCommandEvent) { // We'll trigger on invoking. Could also have invoked or canceled // Eventually we may want click up on invoking and drop back on invoked. // Check for namespace so it'll behave in the designer. if (_WinRT.Windows.UI.Input.EdgeGesture) { var commandUI = _WinRT.Windows.UI.Input.EdgeGesture.getForCurrentView(); commandUI.addEventListener("starting", _startingEdgy); commandUI.addEventListener("completed", _completedEdgy); commandUI.addEventListener("canceled", _canceledEdgy); } // Need to know if the IHM is done scrolling _Global.document.addEventListener("MSManipulationStateChanged", _allManipulationChanged, false); appBarCommandEvent = true; } // Make sure _Overlay event handlers are hooked up (this aids light dismiss) this._addOverlayEventHandlers(false); // Need to store what had focus before _ElementUtilities._addEventListener(this._element, "focusin", function (event) { _checkStorePreviousFocus(event); }, false); // Need to hide ourselves if we lose focus _ElementUtilities._addEventListener(this._element, "focusout", function () { _Overlay._Overlay._hideIfAllAppBarsLostFocus(); }, false); if (this.closedDisplayMode === closedDisplayModes.none && this.layout === _Constants.appBarLayoutCommands) { // Remove the commands layout AppBar from the layout tree at this point so we don't cause unnecessary layout costs whenever // the window resizes or when CSS changes are applied to the commands layout AppBar's parent element. this._element.style.display = "none"; } this._writeProfilerMark("constructor,StopTM"); return this; }, { // Public Properties /// The placement of the AppBar on the display. Values are "top" or "bottom". placement: { get: function AppBar_get_placement() { return this._placement; }, set: function AppBar_set_placement(value) { // In designer we may have to move it var wasShown = false; if (_WinRT.Windows.ApplicationModel.DesignMode.designModeEnabled) { this._hide(); wasShown = true; } if (!this.hidden) { throw new _ErrorFromName("WinJS.UI.AppBar.CannotChangePlacementWhenVisible", strings.cannotChangePlacementWhenVisible); } // Set placement, coerce invalid values to 'bottom' this._placement = (value === _Constants.appBarPlacementTop) ? _Constants.appBarPlacementTop : _Constants.appBarPlacementBottom; // Clean up win-top, win-bottom styles if (this._placement === _Constants.appBarPlacementTop) { _ElementUtilities.addClass(this._element, _Constants.topClass); _ElementUtilities.removeClass(this._element, _Constants.bottomClass); } else if (this._placement === _Constants.appBarPlacementBottom) { _ElementUtilities.removeClass(this._element, _Constants.topClass); _ElementUtilities.addClass(this._element, _Constants.bottomClass); } // Show again if we hid ourselves for the designer if (wasShown) { this._show(); } } }, /// /// Gets or sets the layout of the AppBar contents to either "commands" or "custom". /// layout: { get: function AppBar_get_layout() { return this._layout.type; }, set: function (layout) { if (layout !== _Constants.appBarLayoutCommands && layout !== _Constants.appBarLayoutCustom) { throw new _ErrorFromName("WinJS.UI.AppBar.BadLayout", strings.badLayout); } // In designer we may have to redraw it var wasShown = false; if (_WinRT.Windows.ApplicationModel.DesignMode.designModeEnabled) { this._hide(); wasShown = true; } if (!this.hidden) { throw new _ErrorFromName("WinJS.UI.AppBar.CannotChangeLayoutWhenVisible", strings.cannotChangeLayoutWhenVisible); } var commands; if (!this._initializing) { // Gather commands in preparation for hand off to new layout. // We expect prev layout to return commands in the order they were set in, // not necessarily the current DOM order the layout is using. commands = this._layout.commandsInOrder; this._layout.disconnect(); } // Set layout if (layout === _Constants.appBarLayoutCommands) { this._layout = new _Layouts._AppBarCommandsLayout(); } else { // Custom layout uses Base AppBar Layout class. this._layout = new _Layouts._AppBarBaseLayout(); } this._layout.connect(this._element); if (commands && commands.length) { // Reset AppBar since layout changed. this._layoutCommands(commands); } this._layout.connect(this._element); if (commands && commands.length) { // Reset AppBar since layout changed. this._layoutCommands(commands); } // Show again if we hid ourselves for the designer if (wasShown) { this._show(); } }, configurable: true }, /// /// Gets or sets value that indicates whether the AppBar is sticky. /// This value is true if the AppBar is sticky; otherwise, it's false. /// sticky: { get: function AppBar_get_sticky() { return this._sticky; }, set: function AppBar_set_sticky(value) { // If it doesn't change, do nothing if (this._sticky === !!value) { return; } this._sticky = !!value; // Note: caller still has to call .show() if also want it shown. // Show or hide the click eating div based on sticky value if (!this.hidden && this._element.style.visibility === "visible") { // May have changed sticky state for keyboard navigation _updateAllAppBarsFirstAndFinalDiv(); // Ensure that the click eating div is in the correct state if (this._sticky) { if (!_isThereVisibleNonStickyBar()) { _Overlay._Overlay._hideClickEatingDivAppBar(); } } else { _Overlay._Overlay._showClickEatingDivAppBar(); if (this._shouldStealFocus()) { _storePreviousFocus(_Global.document.activeElement); this._setFocusToAppBar(); } } } } }, /// /// Sets the AppBarCommands in the AppBar. This property accepts an array of AppBarCommand objects. /// commands: { set: function AppBar_set_commands(commands) { // Fail if trying to set when shown if (!this.hidden) { throw new _ErrorFromName("WinJS.UI.AppBar.CannotChangeCommandsWhenVisible", _Resources._formatString(_Overlay._Overlay.commonstrings.cannotChangeCommandsWhenVisible, "AppBar")); } // Dispose old commands before tossing them out. if (!this._initializing) { // AppBarCommands defined in markup don't want to be disposed during initialization. this._disposeChildren(); } this._layoutCommands(commands); } }, _layoutCommands: function AppBar_layoutCommands(commands) { // Function precondition: AppBar must not be shown. // Empties AppBar HTML and repopulates with passed in commands. _ElementUtilities.empty(this._element); this._element.appendChild(this._invokeButton); // Keep our Show/Hide button. // In case they had only one command to set... if (!Array.isArray(commands)) { commands = [commands]; } this._layout.layout(commands); }, /// /// Gets/Sets how AppBar will display itself while hidden. Values are "none" and "minimal". /// closedDisplayMode: { get: function AppBar_get_closedDisplayMode() { return this._closedDisplayMode; }, set: function AppBar_set_closedDisplayMode(value) { var oldValue = this._closedDisplayMode; if (oldValue !== value) { if (value === closedDisplayModes.none) { this._closedDisplayMode = closedDisplayModes.none; _ElementUtilities.removeClass(this._element, _Constants.minimalClass); } else { // Minimal is default fallback. this._closedDisplayMode = closedDisplayModes.minimal; _ElementUtilities.addClass(this._element, _Constants.minimalClass); } // The invoke button has changed the amount of available space in the AppBar. Layout might need to scale. this._layout.resize(); if (_ElementUtilities.hasClass(this._element, _Constants.hiddenClass) || _ElementUtilities.hasClass(this._element, _Constants.hidingClass)) { // If the value is being set while we are not showing, change to our new position. this._changeVisiblePosition(displayModeVisiblePositions[this._closedDisplayMode]); } } }, }, /// /// Disable an AppBar, setting or getting the HTML disabled attribute. While disabled, the AppBar is hidden completely, and will not respond to attempts to show it. /// disabled: { get: function () { // Ensure it's a boolean because we're using the DOM element to keep in-sync return !!this._element.disabled; }, set: function (disable) { var disable = !!disable; if (this.disabled !== disable) { this._element.disabled = disable; var toPosition; if (disable) { // Disabling. Move to the position mapped to the disabled state. toPosition = displayModeVisiblePositions.disabled; } else { // Enabling. Move to the position mapped to our closedDisplayMode. toPosition = displayModeVisiblePositions[this.closedDisplayMode]; } this._hide(toPosition); } }, }, /// hidden: { get: function () { // Returns true if AppBar is 'hidden'. return _ElementUtilities.hasClass(this._element, _Constants.hiddenClass) || _ElementUtilities.hasClass(this._element, _Constants.hidingClass) || this._doNext === displayModeVisiblePositions.minimal || this._doNext === displayModeVisiblePositions.none; }, }, getCommandById: function (id) { /// /// /// Retrieves the command with the specified ID from this AppBar. /// If more than one command is found, this method returns them all. /// /// Id of the command to return. /// /// The command found, an array of commands if more than one have the same ID, or null if no command is found. /// /// var commands = this.element.querySelectorAll("#" + id); var newCommands = []; for (var count = 0, len = commands.length; count < len; count++) { if (commands[count].winControl) { newCommands.push(commands[count].winControl); } } if (newCommands.length === 1) { return newCommands[0]; } else if (newCommands.length === 0) { return null; } return newCommands; }, showCommands: function (commands) { /// /// /// Show the specified commands of the AppBar. /// /// /// An array of the commands to show. The array elements may be AppBarCommand objects, or the string identifiers (IDs) of commands. /// /// if (!commands) { throw new _ErrorFromName("WinJS.UI.AppBar.RequiresCommands", strings.requiresCommands); } this._showCommands(commands); }, hideCommands: function (commands) { /// /// /// Hides the specified commands of the AppBar. /// /// Required. Command or Commands to hide, either String, DOM elements, or WinJS objects. /// if (!commands) { throw new _ErrorFromName("WinJS.UI.AppBar.RequiresCommands", strings.requiresCommands); } this._hideCommands(commands); }, showOnlyCommands: function (commands) { /// /// /// Show the specified commands, hiding all of the others in the AppBar. /// /// /// An array of the commands to show. The array elements may be AppBarCommand objects, or the string identifiers (IDs) of commands. /// /// if (!commands) { throw new _ErrorFromName("WinJS.UI.AppBar.RequiresCommands", strings.requiresCommands); } this._showOnlyCommands(commands); }, show: function () { /// /// /// Shows the AppBar, if hidden and not disabled, regardless of other state. /// /// // Just wrap the private one, turning off keyboard invoked flag this._writeProfilerMark("show,StartTM"); this._keyboardInvoked = false; this._doNotFocus = !!this.sticky; this._show(); }, _show: function AppBar_show() { var toPosition = displayModeVisiblePositions.shown; var showing = null; // If we're already shown, we are just going to animate our position, not fire events or manage focus. if (!this.disabled && (_ElementUtilities.hasClass(this._element, _Constants.hiddenClass) || _ElementUtilities.hasClass(this._element, _Constants.hidingClass))) { showing = appbarShownState; } this._changeVisiblePosition(toPosition, showing); if (showing) { // Configure shown state for lightdismiss & sticky appbars. if (!this.sticky) { // Need click-eating div to be visible ASAP. _Overlay._Overlay._showClickEatingDivAppBar(); } // Clean up tabbing behavior by making sure first and final divs are correct after showing. if (!this.sticky && _isThereVisibleNonStickyBar()) { _updateAllAppBarsFirstAndFinalDiv(); } else { this._updateFirstAndFinalDiv(); } // Check if we should steal focus if (!this._doNotFocus && this._shouldStealFocus()) { // Store what had focus if nothing currently is stored if (!_Overlay._Overlay._ElementWithFocusPreviousToAppBar) { _storePreviousFocus(_Global.document.activeElement); } this._setFocusToAppBar(); } } }, hide: function () { /// /// /// Hides the AppBar. /// /// // Just wrap the private one this._writeProfilerMark("hide,StartTM"); this._hide(); }, _hide: function AppBar_hide(toPosition) { var toPosition = toPosition || displayModeVisiblePositions[this.closedDisplayMode]; var hiding = null; // If were already hidden, we are just going to animate our position, not fire events or manage focus again. if (!_ElementUtilities.hasClass(this._element, _Constants.hiddenClass) && !_ElementUtilities.hasClass(this._element, _Constants.hidingClass)) { hiding = appbarHiddenState; } this._changeVisiblePosition(toPosition, hiding); if (hiding) { // Determine if there are any AppBars that are shown. // Set the focus to the next shown AppBar. // If there are none, set the focus to the control stored in the cache, which // is what had focus before the AppBars were given focus. var appBars = _Global.document.querySelectorAll("." + _Constants.appBarClass); var areOtherAppBars = false; var areOtherNonStickyAppBars = false; var i; for (i = 0; i < appBars.length; i++) { var appBarControl = appBars[i].winControl; if (appBarControl && !appBarControl.hidden && (appBarControl !== this)) { areOtherAppBars = true; if (!appBarControl.sticky) { areOtherNonStickyAppBars = true; break; } } } var settingsFlyouts = _Global.document.querySelectorAll("." + _Constants.settingsFlyoutClass); var areVisibleSettingsFlyouts = false; for (i = 0; i < settingsFlyouts.length; i++) { var settingsFlyoutControl = settingsFlyouts[i].winControl; if (settingsFlyoutControl && !settingsFlyoutControl.hidden) { areVisibleSettingsFlyouts = true; break; } } if (!areOtherNonStickyAppBars && !areVisibleSettingsFlyouts) { // Hide the click eating div because there are no other AppBars showing _Overlay._Overlay._hideClickEatingDivAppBar(); } var that = this; if (!areOtherAppBars) { // Set focus to what had focus before showing the AppBar if (_Overlay._Overlay._ElementWithFocusPreviousToAppBar && (!_Global.document.activeElement || _Overlay._Overlay._isAppBarOrChild(_Global.document.activeElement))) { _restorePreviousFocus(); } // Always clear the previous focus (to prevent temporary leaking of element) _Overlay._Overlay._ElementWithFocusPreviousToAppBar = null; } else if (AppBar._isWithinAppBarOrChild(_Global.document.activeElement, that.element)) { // Set focus to next visible AppBar in DOM var foundCurrentAppBar = false; for (i = 0; i <= appBars.length; i++) { if (i === appBars.length) { i = 0; } var appBar = appBars[i]; if (appBar === this.element) { foundCurrentAppBar = true; } else if (foundCurrentAppBar && !appBar.winControl.hidden) { appBar.winControl._keyboardInvoked = !!this._keyboardInvoked; appBar.winControl._setFocusToAppBar(); break; } } } // If we are hiding the last lightDismiss AppBar, // then we need to update the tabStops of the other AppBars if (!this.sticky && !_isThereVisibleNonStickyBar()) { _updateAllAppBarsFirstAndFinalDiv(); } // Reset these values this._keyboardInvoked = false; this._doNotFocus = false; } }, _dispose: function AppBar_dispose() { _Dispose.disposeSubTree(this.element); this._layout.dispose(); this.disabled = true; }, _disposeChildren: function AppBar_disposeChildren() { // Be purposeful about what we dispose. this._layout.disposeChildren(); }, _handleKeyDown: function AppBar_handleKeyDown(event) { // On Left/Right arrow keys, moves focus to previous/next AppbarCommand element. // On "Esc" key press hide flyouts and hide light dismiss AppBars. // Esc hides light-dismiss AppBars in all layouts but if the user has a text box with an IME // candidate window open, we want to skip the ESC key event since it is handled by the IME. // When the IME handles a key it sets event.keyCode === Key.IME for an easy check. if (event.keyCode === Key.escape && event.keyCode !== Key.IME) { event.preventDefault(); event.stopPropagation(); _Overlay._Overlay._hideAllFlyouts(); _Overlay._Overlay._hideLightDismissAppBars(null, true); } // If the current active element isn't an intrinsic part of the AppBar, // Layout might want to handle additional keys. if (!this._invokeButton.contains(_Global.document.activeElement)) { this._layout.handleKeyDown(event); } }, _visiblePixels: { get: function () { // Returns object containing pixel height of each visible position return { hidden: knownVisibleHeights.hidden, minimal: knownVisibleHeights.minimal, // Element can change size as content gets added or removed or if it // experinces style changes. We have to look this up at run time. shown: this._element.offsetHeight, }; } }, _visiblePosition: { // Returns string value of our nearest, stationary, visible position. get: function () { // If we're animating into a new posistion, return the position we're animating into. if (this._animating && displayModeVisiblePositions[this._element.winAnimating]) { return this._element.winAnimating; } else { return this._lastPositionVisited; } } }, _visible: { // Returns true if our visible position is not completely hidden, else false. get: function () { return (this._visiblePosition !== displayModeVisiblePositions.none); } }, _changeVisiblePosition: function (toPosition, newState) { /// /// /// Changes the visible position of the AppBar. /// /// /// Name of the visible position we want to move to. /// /// /// Name of the state we are entering. Values can be "showing", "hiding" or null. /// If the value is null, then we are not changing states, only changing visible position. /// /// if ((this._visiblePosition === toPosition && !this._keyboardObscured) || (this.disabled && toPosition !== displayModeVisiblePositions.disabled)) { // If we want to go where we already are, or we're disabled, return false. this._afterPositionChange(null); } else if (this._animating || this._needToHandleShowingKeyboard || this._needToHandleHidingKeyboard) { // Only do one thing at a time. If we are already animating, // or the IHM is animating, schedule this for later. this._doNext = toPosition; this._afterPositionChange(null); } else { // Begin position changing sequence. // Set the animating flag to block any queued position changes until we're done. this._element.winAnimating = toPosition; var performAnimation = this._initializing ? false : true; // Assume we are animating from the last position visited. var fromPosition = this._lastPositionVisited; // We'll need to measure our element to determine how far we need to animate. // Make sure we have accurate dimensions. this._element.style.display = ""; // Are we hiding completely, or about to become visible? var hidingCompletely = (toPosition === displayModeVisiblePositions.hidden); if (this._keyboardObscured) { // We're changing position while covered by the IHM. if (hidingCompletely) { // If we're covered by the IHM we already look hidden. // We can skip our animation and just hide. performAnimation = false; } else { // Some portion of the AppBar should be visible to users after its position changes. // Un-obscure ourselves and become visible to the user again. // Need to animate to our desired position as if we were coming up from behind the keyboard. fromPosition = displayModeVisiblePositions.hidden; } this._keyboardObscured = false; } // Fire "before" event if we are changing state. if (newState === appbarShownState) { this._beforeShow(); } else if (newState === appbarHiddenState) { this._beforeHide(); } // Position our element into the correct "end of animation" position, // also accounting for any viewport scrolling or soft keyboard positioning. this._ensurePosition(); this._element.style.opacity = 1; this._element.style.visibility = "visible"; this._animationPromise = (performAnimation) ? this._animatePositionChange(fromPosition, toPosition) : Promise.wrap(); this._animationPromise.then( function () { this._afterPositionChange(toPosition, newState); }.bind(this), function () { this._afterPositionChange(toPosition, newState); }.bind(this) ); } }, _afterPositionChange: function AppBar_afterPosiitonChange(newPosition, newState) { // Defines body of work to perform after changing positions. if (this._disposed) { return; } if (newPosition) { // Clear animation flag and record having visited this position. this._element.winAnimating = ""; this._lastPositionVisited = newPosition; if (this._doNext === this._lastPositionVisited) { this._doNext = ""; } if (newPosition === displayModeVisiblePositions.hidden) { // Make sure animation is finished. this._element.style.visibility = "hidden"; this._element.style.display = "none"; } // Clean up animation transforms. var transformProperty = _BaseUtils._browserStyleEquivalents["transform"].scriptName; this._element.style[transformProperty] = ""; // Fire "after" event if we changed state. if (newState === appbarShownState) { this._afterShow(); } else if (newState === appbarHiddenState) { this._afterHide(); } // If we had something queued, do that Scheduler.schedule(this._checkDoNext, Scheduler.Priority.normal, this, "WinJS.UI.AppBar._checkDoNext"); } this._afterPositionChangeCallBack(); }, _afterPositionChangeCallBack: function () { // Leave this blank for unit tests to overwrite. }, _beforeShow: function AppBar_beforeShow() { // Each overlay tracks the size of the element for triggering light-dismiss in the window resize handler. this._cachedDocumentSize = this._cachedDocumentSize || _Overlay._Overlay._sizeOfDocument(); // In case their event 'beforeshow' event listener is going to manipulate commands, // first see if there are any queued command animations we can handle while we're still hidden. if (this._queuedCommandAnimation) { this._showAndHideFast(this._queuedToShow, this._queuedToHide); this._queuedToShow = []; this._queuedToHide = []; } // Make sure everything fits before showinging this._layout.scale(); _ElementUtilities.removeClass(this._element, _Constants.hiddenClass); _ElementUtilities.addClass(this._element, _Constants.showingClass); // Send our "beforeShow" event this._sendEvent(_Overlay._Overlay.beforeShow); }, _afterShow: function AppBar_afterShow() { _ElementUtilities.removeClass(this._element, _Constants.showingClass); _ElementUtilities.addClass(this._element, _Constants.shownClass); // Send our "afterShow" event this._sendEvent(_Overlay._Overlay.afterShow); this._writeProfilerMark("show,StopTM"); }, _beforeHide: function AppBar_beforeHide() { _ElementUtilities.removeClass(this._element, _Constants.shownClass); _ElementUtilities.addClass(this._element, _Constants.hidingClass); // Send our "beforeHide" event this._sendEvent(_Overlay._Overlay.beforeHide); }, _afterHide: function AppBar_afterHide() { // In case their 'afterhide' event handler is going to manipulate commands, // first see if there are any queued command animations we can handle now we're hidden. if (this._queuedCommandAnimation) { this._showAndHideFast(this._queuedToShow, this._queuedToHide); this._queuedToShow = []; this._queuedToHide = []; } _ElementUtilities.removeClass(this._element, _Constants.hidingClass); _ElementUtilities.addClass(this._element, _Constants.hiddenClass); // Send our "afterHide" event this._sendEvent(_Overlay._Overlay.afterHide); this._writeProfilerMark("hide,StopTM"); }, _animatePositionChange: function AppBar_animatePositionChange(fromPosition, toPosition) { // Determines and executes the proper transition between visible positions // Get values in terms of pixels to perform animation. var beginningVisiblePixelHeight = this._visiblePixels[fromPosition], endingVisiblePixelHeight = this._visiblePixels[toPosition], distance = Math.abs(endingVisiblePixelHeight - beginningVisiblePixelHeight), offsetTop = (this._placement === _Constants.appBarPlacementTop) ? -distance : distance; // Animate if (endingVisiblePixelHeight > beginningVisiblePixelHeight) { var fromOffset = { top: offsetTop + "px", left: "0px" }; return Animations.showEdgeUI(this._element, fromOffset, { mechanism: "transition" }); } else { var toOffset = { top: offsetTop + "px", left: "0px" }; return Animations.hideEdgeUI(this._element, toOffset, { mechanism: "transition" }); } }, _checkDoNext: function AppBar_checkDoNext() { // Do nothing if we're still animating if (this._animating || this._needToHandleShowingKeyboard || this._needToHandleHidingKeyboard || this._disposed) { return; } if (this._doNext === displayModeVisiblePositions.disabled || this._doNext === displayModeVisiblePositions.hidden || this._doNext === displayModeVisiblePositions.minimal) { // Do hide first because animating commands would be easier this._hide(this._doNext); this._doNext = ""; } else if (this._queuedCommandAnimation) { // Do queued commands before showing if possible this._showAndHideQueue(); } else if (this._doNext === displayModeVisiblePositions.shown) { // Show last so that we don't unnecessarily animate commands this._show(); this._doNext = ""; } }, _isABottomAppBarInTheProcessOfShowing: function AppBar_isABottomAppBarInTheProcessOfShowing() { var appbars = _Global.document.querySelectorAll("." + _Constants.appBarClass + "." + _Constants.bottomClass); for (var i = 0; i < appbars.length; i++) { if (appbars[i].winAnimating === displayModeVisiblePositions.shown) { return true; } } return false; }, // Returns true if // 1) This is a bottom appbar // 2) No appbar has focus and a bottom appbar is not in the process of showing // 3) What currently has focus is neither a bottom appbar nor a top appbar // AND a bottom appbar is not in the process of showing. // Otherwise Returns false _shouldStealFocus: function AppBar_shouldStealFocus() { var activeElementAppBar = _Overlay._Overlay._isAppBarOrChild(_Global.document.activeElement); if (this._element === activeElementAppBar) { // This appbar already has focus and we don't want to move focus // from where it currently is in this appbar. return false; } if (this._placement === _Constants.appBarPlacementBottom) { // This is a bottom appbar return true; } var isBottomAppBarShowing = this._isABottomAppBarInTheProcessOfShowing(); if (!activeElementAppBar) { // Currently no appbar has focus. // Return true if a bottom appbar is not in the process of showing. return !isBottomAppBarShowing; } if (!activeElementAppBar.winControl) { // This should not happen, but if it does we want to make sure // that an AppBar ends up with focus. return true; } if ((activeElementAppBar.winControl._placement !== _Constants.appBarPlacementBottom) && (activeElementAppBar.winControl._placement !== _Constants.appBarPlacementTop) && !isBottomAppBarShowing) { // What currently has focus is neither a bottom appbar nor a top appbar // -and- // a bottom appbar is not in the process of showing. return true; } return false; }, // Set focus to the passed in AppBar _setFocusToAppBar: function AppBar_setFocusToAppBar() { if (this._focusOnFirstFocusableElement()) { // Prevent what is gaining focus from showing that it has focus, // but only in the non-keyboard scenario. if (!this._keyboardInvoked) { _Overlay._Overlay._addHideFocusClass(_Global.document.activeElement); } } else { // No first element, set it to appbar itself _Overlay._Overlay._trySetActive(this._element); } }, _commandsUpdated: function AppBar_commandsUpdated() { // If we are still initializing then we don't have a layout yet so it doesn't need updating. if (!this._initializing) { this._layout.commandsUpdated(); this._layout.scale(); } }, _beginAnimateCommands: function AppBar_beginAnimateCommands(showCommands, hideCommands, otherVisibleCommands) { // The parameters are 3 mutually exclusive arrays of win-command elements contained in this Overlay. // 1) showCommands[]: All of the HIDDEN win-command elements that ARE scheduled to show. // 2) hideCommands[]: All of the VISIBLE win-command elements that ARE scheduled to hide. // 3) otherVisibleCommands[]: All VISIBLE win-command elements that ARE NOT scheduled to hide. this._layout.beginAnimateCommands(showCommands, hideCommands, otherVisibleCommands); }, _endAnimateCommands: function AppBar_endAnimateCommands() { this._layout.endAnimateCommands(); this._endAnimateCommandsCallBack(); }, _endAnimateCommandsCallBack: function AppBar_endAnimateCommandsCallBack() { // Leave this blank for unit tests to overwrite. }, // Get the top offset for top appbars. _getTopOfVisualViewport: function AppBar_getTopOfVisualViewPort() { return _Overlay._Overlay._keyboardInfo._visibleDocTop; }, // Get the bottom offset for bottom appbars. _getAdjustedBottom: function AppBar_getAdjustedBottom() { // Need the distance the IHM moved as well. return _Overlay._Overlay._keyboardInfo._visibleDocBottomOffset; }, _showingKeyboard: function AppBar_showingKeyboard(event) { // Remember keyboard showing state. this._keyboardObscured = false; this._needToHandleHidingKeyboard = false; // If we're already moved, then ignore the whole thing if (_Overlay._Overlay._keyboardInfo._visible && this._alreadyInPlace()) { return; } this._needToHandleShowingKeyboard = true; // If focus is in the appbar, don't cause scrolling. if (!this.hidden && this._element.contains(_Global.document.activeElement)) { event.ensuredFocusedElementInView = true; } // Check if appbar moves or if we're ok leaving it obscured instead. if (this._visible && this._placement !== _Constants.appBarPlacementTop && _Overlay._Overlay._isFlyoutVisible()) { // Remember that we're obscured this._keyboardObscured = true; } else { // Don't be obscured, clear _scrollHappened flag to give us inference later on when to re-show ourselves. this._scrollHappened = false; } // Also set timeout regardless, so we can clean up our _keyboardShowing flag. var that = this; _Global.setTimeout(function (e) { that._checkKeyboardTimer(e); }, _Overlay._Overlay._keyboardInfo._animationShowLength + _Overlay._Overlay._scrollTimeout); }, _hidingKeyboard: function AppBar_hidingKeyboard() { // We'll either just reveal the current space under the IHM or restore the window height. // We won't be obscured this._keyboardObscured = false; this._needToHandleShowingKeyboard = false; this._needToHandleHidingKeyboard = true; // We'll either just reveal the current space or resize the window if (!_Overlay._Overlay._keyboardInfo._isResized) { // If we're not completely hidden, only fake hiding under keyboard, or already animating, // then snap us to our final position. if (this._visible || this._animating) { // Not resized, update our final position immediately this._checkScrollPosition(); this._element.style.display = ""; } this._needToHandleHidingKeyboard = false; } // Else resize should clear keyboardHiding. }, _resize: function AppBar_resize(event) { // If we're hidden by the keyboard, then hide bottom appbar so it doesn't pop up twice when it scrolls if (this._needToHandleShowingKeyboard) { // Top is allowed to scroll off the top, but we don't want bottom to peek up when // scrolled into view since we'll show it ourselves and don't want a stutter effect. if (this._visible) { if (this._placement !== _Constants.appBarPlacementTop && !this._keyboardObscured) { // If viewport doesn't match window, need to vanish momentarily so it doesn't scroll into view, // however we don't want to toggle the visibility="hidden" hidden flag. this._element.style.display = "none"; } } // else if we're top we stay, and if there's a flyout, stay obscured by the keyboard. } else if (this._needToHandleHidingKeyboard) { this._needToHandleHidingKeyboard = false; if (this._visible || this._animating) { // Snap to final position this._checkScrollPosition(); this._element.style.display = ""; } } // Make sure everything still fits. if (!this._initializing) { this._layout.resize(event); } }, _checkKeyboardTimer: function AppBar_checkKeyboardTimer() { if (!this._scrollHappened) { this._mayEdgeBackIn(); } }, _manipulationChanged: function AppBar_manipulationChanged(event) { // See if we're at the not manipulating state, and we had a scroll happen, // which is implicitly after the keyboard animated. if (event.currentState === 0 && this._scrollHappened) { this._mayEdgeBackIn(); } }, _mayEdgeBackIn: function AppBar_mayEdgeBackIn() { // May need to react to IHM being resized event if (this._needToHandleShowingKeyboard) { // If not top appbar or viewport isn't still at top, then need to show again this._needToHandleShowingKeyboard = false; // If obscured (IHM + flyout showing), it's ok to stay obscured. // If bottom we have to move, or if top scrolled off screen. if (!this._keyboardObscured && (this._placement !== _Constants.appBarPlacementTop || _Overlay._Overlay._keyboardInfo._visibleDocTop !== 0)) { var toPosition = this._visiblePosition; this._lastPositionVisited = displayModeVisiblePositions.hidden; this._changeVisiblePosition(toPosition, false); } else { // Ensure any animations dropped during the showing keyboard are caught up. this._checkDoNext(); } } this._scrollHappened = false; }, _ensurePosition: function AppBar_ensurePosition() { // Position the AppBar element relative to the top or bottom edge of the visible // document, based on the the visible position we think we need to be in. var offSet = this._computePositionOffset(); this._element.style.bottom = offSet.bottom; this._element.style.top = offSet.top; }, _computePositionOffset: function AppBar_computePositionOffset() { // Calculates and returns top and bottom offsets for the AppBar element, relative to the top or bottom edge of the visible // document. var positionOffSet = {}; if (this._placement === _Constants.appBarPlacementBottom) { // If the IHM is open, the bottom of the visual viewport may or may not be obscured // Use _getAdjustedBottom to account for the IHM if it is covering the bottom edge. positionOffSet.bottom = this._getAdjustedBottom() + "px"; positionOffSet.top = ""; } else if (this._placement === _Constants.appBarPlacementTop) { positionOffSet.bottom = ""; positionOffSet.top = this._getTopOfVisualViewport() + "px"; } return positionOffSet; }, _checkScrollPosition: function AppBar_checkScrollPosition() { // If IHM has appeared, then remember we may come in if (this._needToHandleShowingKeyboard) { // Tag that it's OK to edge back in. this._scrollHappened = true; return; } // We only need to update if we're not completely hidden. if (this._visible || this._animating) { this._ensurePosition(); // Ensure any animations dropped during the showing keyboard are caught up. this._checkDoNext(); } }, _alreadyInPlace: function AppBar_alreadyInPlace() { // See if we're already where we're supposed to be. var offSet = this._computePositionOffset(); return (offSet.top === this._element.style.top && offSet.bottom === this._element.style.bottom); }, // If there is a shown non-sticky AppBar then it sets the firstDiv tabIndex to // the minimum tabIndex found in the AppBars and finalDiv to the max found. // Otherwise sets their tabIndex to -1 so they are not tab stops. _updateFirstAndFinalDiv: function AppBar_updateFirstAndFinalDiv() { var appBarFirstDiv = this._element.querySelectorAll("." + _Constants.firstDivClass); appBarFirstDiv = appBarFirstDiv.length >= 1 ? appBarFirstDiv[0] : null; var appBarFinalDiv = this._element.querySelectorAll("." + _Constants.finalDivClass); appBarFinalDiv = appBarFinalDiv.length >= 1 ? appBarFinalDiv[0] : null; // Remove the firstDiv & finalDiv if they are not at the appropriate locations if (appBarFirstDiv && (this._element.children[0] !== appBarFirstDiv)) { appBarFirstDiv.parentNode.removeChild(appBarFirstDiv); appBarFirstDiv = null; } if (appBarFinalDiv && (this._element.children[this._element.children.length - 1] !== appBarFinalDiv)) { appBarFinalDiv.parentNode.removeChild(appBarFinalDiv); appBarFinalDiv = null; } // Create and add the firstDiv & finalDiv if they don't already exist if (!appBarFirstDiv) { // Add a firstDiv that will be the first child of the appBar. // On focus set focus to the previous appBar. // The div should only be focusable if there are shown non-sticky AppBars. appBarFirstDiv = _Global.document.createElement("div"); // display: inline is needed so that the div doesn't take up space and cause the page to scroll on focus appBarFirstDiv.style.display = "inline"; appBarFirstDiv.className = _Constants.firstDivClass; appBarFirstDiv.tabIndex = -1; appBarFirstDiv.setAttribute("aria-hidden", "true"); _ElementUtilities._addEventListener(appBarFirstDiv, "focusin", _setFocusToPreviousAppBar, false); // add to beginning if (this._element.children[0]) { this._element.insertBefore(appBarFirstDiv, this._element.children[0]); } else { this._element.appendChild(appBarFirstDiv); } } if (!appBarFinalDiv) { // Add a finalDiv that will be the last child of the appBar. // On focus set focus to the next appBar. // The div should only be focusable if there are shown non-sticky AppBars. appBarFinalDiv = _Global.document.createElement("div"); // display: inline is needed so that the div doesn't take up space and cause the page to scroll on focus appBarFinalDiv.style.display = "inline"; appBarFinalDiv.className = _Constants.finalDivClass; appBarFinalDiv.tabIndex = -1; appBarFinalDiv.setAttribute("aria-hidden", "true"); _ElementUtilities._addEventListener(appBarFinalDiv, "focusin", _setFocusToNextAppBar, false); this._element.appendChild(appBarFinalDiv); } // invokeButton should be the second to last element in the AppBar's tab order. Second to the finalDiv. if (this._element.children[this._element.children.length - 2] !== this._invokeButton) { this._element.insertBefore(this._invokeButton, appBarFinalDiv); } var elms = this._element.getElementsByTagName("*"); var highestTabIndex = _ElementUtilities._getHighestTabIndexInList(elms); this._invokeButton.tabIndex = highestTabIndex; // Update the tabIndex of the firstDiv & finalDiv if (_isThereVisibleNonStickyBar()) { if (appBarFirstDiv) { appBarFirstDiv.tabIndex = _ElementUtilities._getLowestTabIndexInList(elms); } if (appBarFinalDiv) { appBarFinalDiv.tabIndex = highestTabIndex; } } else { if (appBarFirstDiv) { appBarFirstDiv.tabIndex = -1; } if (appBarFinalDiv) { appBarFinalDiv.tabIndex = -1; } } }, _writeProfilerMark: function AppBar_writeProfilerMark(text) { _WriteProfilerMark("WinJS.UI.AppBar:" + this._id + ":" + text); } }, { // Statics _appBarsSynchronizationPromise: Promise.as(), // Returns true if the element or what had focus before the element (if a Flyout) is either: // 1) the appBar or subtree // 2) OR in a flyout spawned by the appBar // Returns false otherwise. _isWithinAppBarOrChild: function (element, appBar) { if (!element || !appBar) { return false; } if (appBar.contains(element)) { return true; } var flyout = _Overlay._Overlay._getParentControlUsingClassName(element, _Constants.flyoutClass); return (flyout && appBar.contains(flyout._previousFocus)); }, // Callback for AppBar invokeButton and Edgy Event Command _toggleAllAppBarsState: function (keyboardInvoked, sourceAppBar) { var bars = _getDynamicBarsForEdgy(); var hiding; if (sourceAppBar) { // If the sourceAppBar is shown, hide all AppBars, else show all AppBars. hiding = _ElementUtilities.hasClass(sourceAppBar._element, _Constants.showingClass) || _ElementUtilities.hasClass(sourceAppBar._element, _Constants.shownClass); } else { // EDGY event behavior. No sourceAppBar specified. // If every AppBar is shown, hide them. Otherwise show them all. hiding = bars._shown && !bars._hidden; } if (hiding) { AppBar._appBarsSynchronizationPromise = AppBar._appBarsSynchronizationPromise.then(function () { return _Overlay._Overlay._hideAllBars(bars, keyboardInvoked); }); return "hiding"; } else { AppBar._appBarsSynchronizationPromise = AppBar._appBarsSynchronizationPromise.then(function () { return _Overlay._Overlay._showAllBars(bars, keyboardInvoked); }); return "showing"; } }, }); return AppBar; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Menu Command /// appbar,appbars,Flyout,Flyouts,onclick,Statics define('WinJS/Controls/Menu/_Command',[ 'exports', '../../Core/_Global', '../../Core/_Base', '../../Core/_ErrorFromName', '../../Core/_Resources', '../../Utilities/_Control', '../../Utilities/_ElementUtilities', '../AppBar/_Constants', '../Flyout/_Overlay' ], function menuCommandInit(exports, _Global, _Base, _ErrorFromName, _Resources, _Control, _ElementUtilities, _Constants, _Overlay) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { /// /// /// Represents a command to be displayed in a Menu. MenuCommand objects provide button, toggle button, flyout button, /// or separator functionality for Menu controls. /// /// /// /// /// /// ]]> /// The MenuCommand control itself /// /// /// MenuCommand: _Base.Namespace._lazy(function () { function _handleMenuClick(event) { /*jshint validthis: true */ var command = this.winControl; if (command) { var hideParent = true; if (command._type === _Constants.typeToggle) { command.selected = !command.selected; } else if (command._type === _Constants.typeFlyout && command._flyout) { var flyout = command._flyout; // Flyout may not have processAll'd, so this may be a DOM object if (typeof flyout === "string") { flyout = _Global.document.getElementById(flyout); } if (!flyout.show) { flyout = flyout.winControl; } if (flyout && flyout.show) { if (command._parentFlyout) { hideParent = false; flyout.show(command._parentFlyout._currentAnchor, command._parentFlyout._currentPlacement, command._parentFlyout._currentAlignment); } else { flyout.show(this); } } } if (command.onclick) { command.onclick(event); } // Dismiss parent flyout if (hideParent && command._parentFlyout) { command._parentFlyout.hide(); } } } function _handleMouseOver() { /*jshint validthis: true */ if (this && this.focus) { this.focus(); this.addEventListener("mousemove", _handleMouseMove, false); } } function _handleMouseMove() { /*jshint validthis: true */ if (this && this.focus && this !== _Global.document.activeElement) { this.focus(); } } function _handleMouseOut() { /*jshint validthis: true */ var that = this; var parentFlyout = _getParentFlyout(that); if (parentFlyout && this === _Global.document.activeElement && _ElementUtilities.hasClass(parentFlyout, _Constants.menuClass) && parentFlyout.focus) { // Menu gives focus to the menu itself parentFlyout.focus(); } else if (parentFlyout && this === _Global.document.activeElement && parentFlyout.children && parentFlyout.children.length > 0 && parentFlyout.children[0] && _ElementUtilities.hasClass(parentFlyout.children[0], _Constants.firstDivClass) && parentFlyout.children[0].focus) { // Flyout gives focus to firstDiv parentFlyout.children[0].focus(); } this.removeEventListener("mousemove", _handleMouseMove, false); } function _getParentFlyout(element) { while (element && !_ElementUtilities.hasClass(element, _Constants.flyoutClass)) { element = element.parentElement; } return element; } var strings = { get ariaLabel() { return _Resources._getWinJSString("ui/menuCommandAriaLabel").value; }, get duplicateConstruction() { return "Invalid argument: Controls may only be instantiated one time for each DOM element"; }, get badClick() { return "Invalid argument: The onclick property for an {0} must be a function"; }, get badHrElement() { return "Invalid argument: For a separator, the element must be null or an hr element"; }, get badButtonElement() { return "Invalid argument: For a button, toggle, or flyout command, the element must be null or a button element"; } }; return _Base.Class.define(function MenuCommand_ctor(element, options) { /// /// /// Creates a new MenuCommand object. /// /// /// The DOM element that will host the control. /// /// /// The set of properties and values to apply to the new MenuCommand. /// /// /// A MenuCommand control. /// /// /// // Check to make sure we weren't duplicated if (element && element.winControl) { throw new _ErrorFromName("WinJS.UI.MenuCommand.DuplicateConstruction", strings.duplicateConstruction); } this._disposed = false; // Don't blow up if they didn't pass options if (!options) { options = {}; } // Need a type before we can create our element if (!options.type) { this._type = _Constants.typeButton; } // Go ahead and create it, separator types look different than buttons // Don't forget to use passed in element if one was provided. this._element = element; if (options.type === _Constants.typeSeparator) { this._createSeparator(); } else { // This will also set the icon & label this._createButton(); } _ElementUtilities.addClass(this._element, "win-disposable"); // Remember ourselves this._element.winControl = this; // Attach our css class _ElementUtilities.addClass(this._element, _Constants.menuCommandClass); if (!options.selected && options.type === _Constants.typeToggle) { // Make sure toggle's have selected false for CSS this.selected = false; } if (options.onclick) { this.onclick = options.onclick; } options.onclick = _handleMenuClick; _Control.setOptions(this, options); // Set our options if (this._type !== _Constants.typeSeparator) { // Make sure we have an ARIA role var role = this._element.getAttribute("role"); if (role === null || role === "" || role === undefined) { role = "menuitem"; if (this._type === _Constants.typeToggle) { role = "menuitemcheckbox"; } this._element.setAttribute("role", role); if (this._type === _Constants.typeFlyout) { this._element.setAttribute("aria-haspopup", true); } } var label = this._element.getAttribute("aria-label"); if (label === null || label === "" || label === undefined) { this._element.setAttribute("aria-label", strings.ariaLabel); } } this._element.addEventListener("mouseover", _handleMouseOver, false); this._element.addEventListener("mouseout", _handleMouseOut, false); }, { /// /// Gets the ID of the MenuCommand. /// /// id: { get: function () { return this._element.id; }, set: function (value) { // we allow setting first time only. otherwise we ignore it. if (!this._element.id) { this._element.id = value; } } }, /// /// Gets the type of the MenuCommand. Possible values are "button", "toggle", "flyout", or "separator". /// /// type: { get: function () { return this._type; }, set: function (value) { // we allow setting first time only. otherwise we ignore it. if (!this._type) { if (value !== _Constants.typeButton && value !== _Constants.typeFlyout && value !== _Constants.typeToggle && value !== _Constants.typeSeparator) { this._type = _Constants.typeButton; } else { this._type = value; } } } }, /// /// The label of the MenuCommand /// /// label: { get: function () { return this._label; }, set: function (value) { this._label = value; this._element.textContent = this.label; // Update aria-label this._element.setAttribute("aria-label", this.label); } }, /// /// Gets or sets the function to invoke when the command is clicked. /// /// onclick: { get: function () { return this._onclick; }, set: function (value) { if (value && typeof value !== "function") { throw new _ErrorFromName("WinJS.UI.MenuCommand.BadClick", _Resources._formatString(strings.badClick, "MenuCommand")); } this._onclick = value; } }, /// /// For flyout type MenuCommands, this property returns the WinJS.UI.Flyout that this command invokes. When setting this property, you can set /// it to the string ID of the Flyout, the DOM object that hosts the Flyout, or the Flyout object itself. /// /// flyout: { get: function () { // Resolve it to the flyout var flyout = this._flyout; if (typeof flyout === "string") { flyout = _Global.document.getElementById(flyout); } // If it doesn't have a .element, then we need to getControl on it if (flyout && !flyout.element) { flyout = flyout.winControl; } return flyout; }, set: function (value) { // Need to update aria-owns with the new ID. var id = value; if (id && typeof id !== "string") { // Our controls have .element properties if (id.element) { id = id.element; } // Hope it's a DOM element, get ID from DOM element if (id) { if (id.id) { id = id.id; } else { // No id, have to fake one id.id = _ElementUtilities._uniqueID(id); id = id.id; } } } if (typeof id === "string") { this._element.setAttribute("aria-owns", id); } // Remember it this._flyout = value; } }, /// /// Gets or sets the selected state of a toggle button. This property is true if the toggle button is selected; otherwise, it's false. /// /// selected: { get: function () { // Ensure it's a boolean because we're using the DOM element to keep in-sync return this._element.getAttribute("aria-checked") === "true"; }, set: function (value) { this._element.setAttribute("aria-checked", !!value); } }, /// element: { get: function () { return this._element; } }, /// /// Gets or sets a value that indicates whether the MenuCommand is disabled. This value is true if the MenuCommand is disabled; otherwise, false. /// /// disabled: { get: function () { // Ensure it's a boolean because we're using the DOM element to keep in-sync return !!this._element.disabled; }, set: function (value) { this._element.disabled = !!value; } }, /// hidden: { get: function () { // Ensure it's a boolean because we're using the DOM element to keep in-sync return this._element.style.visibility === "hidden"; }, set: function (value) { var menuControl = _Overlay._Overlay._getParentControlUsingClassName(this._element, _Constants.menuClass); if (menuControl && !menuControl.hidden) { throw new _ErrorFromName("WinJS.UI.MenuCommand.CannotChangeHiddenProperty", _Resources._formatString(_Overlay._Overlay.commonstrings.cannotChangeHiddenProperty, "Menu")); } var style = this._element.style; if (value) { style.visibility = "hidden"; style.display = "none"; } else { style.visibility = ""; style.display = "block"; } } }, /// /// Gets or sets the extra CSS class that is applied to the host DOM element. /// /// extraClass: { get: function () { return this._extraClass; }, set: function (value) { if (this._extraClass) { _ElementUtilities.removeClass(this._element, this._extraClass); } this._extraClass = value; _ElementUtilities.addClass(this._element, this._extraClass); } }, dispose: function () { /// /// /// Disposes this control. /// /// /// if (this._disposed) { return; } this._disposed = true; if (this._flyout) { this._flyout.dispose(); } }, addEventListener: function (type, listener, useCapture) { /// /// /// Registers an event handler for the specified event. /// /// The name of the event to register. /// The function that handles the event. /// /// Set to true to register the event handler for the capturing phase; otherwise, set to false to register the event handler for the bubbling phase. /// /// /// return this._element.addEventListener(type, listener, useCapture); }, removeEventListener: function (type, listener, useCapture) { /// /// /// Removes the specified event handler that the addEventListener method registered. /// /// The name of the event to remove. /// The event handler function to remove. /// /// Set to true to remove the capturing phase event handler; set to false to remove the bubbling phase event handler. /// /// /// return this._element.removeEventListener(type, listener, useCapture); }, // Private properties _createSeparator: function MenuCommand_createSeparator() { // Make sure there's an input element if (!this._element) { this._element = _Global.document.createElement("hr"); } else { // Verify the input was an hr if (this._element.tagName !== "HR") { throw new _ErrorFromName("WinJS.UI.MenuCommand.BadHrElement", strings.badHrElement); } } }, _createButton: function MenuCommand_createButton() { // Make sure there's an input element if (!this._element) { this._element = _Global.document.createElement("button"); } else { // Verify the input was a button if (this._element.tagName !== "BUTTON") { throw new _ErrorFromName("WinJS.UI.MenuCommand.BadButtonElement", strings.badButtonElement); } this._element.innerHTML = ""; } // MenuCommand buttons need to look like this: //// this._element.type = "button"; // 'textContent' label is added later by caller } }); }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Menu /// Menu,Menus,Flyout,Flyouts,Statics define('WinJS/Controls/Menu',[ '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Resources', '../Core/_WriteProfilerMark', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', './AppBar/_Constants', './Flyout', './Flyout/_Overlay', './Menu/_Command', 'require-style!less/desktop/controls', 'require-style!less/phone/controls' ], function menuInit(_Global,_Base, _BaseUtils, _ErrorFromName, _Resources, _WriteProfilerMark, _ElementUtilities, _Hoverable, _Constants, Flyout, _Overlay, _Command) { "use strict"; _Base.Namespace.define("WinJS.UI", { /// /// Represents a menu flyout for displaying commands. /// /// /// Menu /// /// /// /// /// ]]> /// Raised just before showing a menu. /// Raised immediately after a menu is fully shown. /// Raised just before hiding a menu. /// Raised immediately after a menu is fully hidden. /// The Menu control itself /// /// /// Menu: _Base.Namespace._lazy(function () { var Key = _ElementUtilities.Key; var strings = { get ariaLabel() { return _Resources._getWinJSString("ui/menuAriaLabel").value; }, get requiresCommands() { return "Invalid argument: commands must not be empty"; }, get nullCommand() { return "Invalid argument: command must not be null"; }, }; var Menu = _Base.Class.derive(Flyout.Flyout, function Menu_ctor(element, options) { /// /// /// Creates a new Menu control. /// /// /// The DOM element that will host the control. /// /// /// The set of properties and values to apply to the control. /// /// The new Menu control. /// /// // We need to be built on top of a Flyout, so stomp on the user's input options = options || {}; // Make sure there's an input element this._element = element || _Global.document.createElement("div"); this._id = this._element.id || _ElementUtilities._uniqueID(this._element); this._writeProfilerMark("constructor,StartTM"); // validate that if they didn't set commands, in which // case any HTML only contains commands. Do this first // so that we don't leave partial Menus in the DOM. if (!options.commands && this._element) { // Shallow copy object so we can modify it. options = _BaseUtils._shallowCopy(options); options.commands = this._verifyCommandsOnly(this._element, "WinJS.UI.MenuCommand"); } // Remember aria role in case base constructor changes it var role = this._element ? this._element.getAttribute("role") : null; var label = this._element ? this._element.getAttribute("aria-label") : null; // Call the base overlay constructor helper this._baseFlyoutConstructor(this._element, options); // Make sure we have an ARIA role if (role === null || role === "" || role === undefined) { this._element.setAttribute("role", "menu"); } if (label === null || label === "" || label === undefined) { this._element.setAttribute("aria-label", strings.ariaLabel); } // Handle "esc" & "up/down" key presses this._element.addEventListener("keydown", this._handleKeyDown, true); // Attach our css class _ElementUtilities.addClass(this._element, _Constants.menuClass); // Need to set our commands, making sure we're hidden first this.hide(); this._writeProfilerMark("constructor,StopTM"); }, { // Public Properties /// /// Sets the MenuCommand objects that appear in the Menu. You can set this to a single MenuCommand or an array of MenuCommand objects. /// /// commands: { set: function (value) { // Fail if trying to set when visible if (!this.hidden) { throw new _ErrorFromName("WinJS.UI.Menu.CannotChangeCommandsWhenVisible", _Resources._formatString(_Overlay._Overlay.commonstrings.cannotChangeCommandsWhenVisible, "Menu")); } // Start from scratch _ElementUtilities.empty(this._element); // In case they had only one... if (!Array.isArray(value)) { value = [value]; } // Add commands var len = value.length; for (var i = 0; i < len; i++) { this._addCommand(value[i]); } } }, getCommandById: function (id) { /// /// /// Retrieve the command with the specified ID from this Menu. If more than one command is found, all are returned. /// /// The ID of the command to find. /// /// The command found, an array of commands if more than one have the same ID, or null if no command is found. /// /// /// var commands = this.element.querySelectorAll("#" + id); var newCommands = []; for (var count = 0, len = commands.length; count < len; count++) { if (commands[count].winControl) { newCommands.push(commands[count].winControl); } } if (newCommands.length === 1) { return newCommands[0]; } else if (newCommands.length === 0) { return null; } return newCommands; }, showCommands: function (commands) { /// /// /// Shows the specified commands of the Menu. /// /// /// The commands to show. The array elements may be Menu objects, or the string identifiers (IDs) of commands. /// /// /// if (!commands) { throw new _ErrorFromName("WinJS.UI.Menu.RequiresCommands", strings.requiresCommands); } this._showCommands(commands, true); }, hideCommands: function (commands) { /// /// /// Hides the Menu. /// /// /// Required. Command or Commands to hide, either String, DOM elements, or WinJS objects. /// /// /// if (!commands) { throw new _ErrorFromName("WinJS.UI.Menu.RequiresCommands", strings.requiresCommands); } this._hideCommands(commands, true); }, showOnlyCommands: function (commands) { /// /// /// Shows the specified commands of the Menu while hiding all other commands. /// /// /// The commands to show. The array elements may be MenuCommand objects, or the string identifiers (IDs) of commands. /// /// /// if (!commands) { throw new _ErrorFromName("WinJS.UI.Menu.RequiresCommands", strings.requiresCommands); } this._showOnlyCommands(commands, true); }, show: function (anchor, placement, alignment) { /// /// /// Shows the Menu, if hidden, regardless of other states. /// /// /// The DOM element, or ID of a DOM element, to anchor the Menu. This parameter overrides the anchor property for this method call only. /// /// /// The placement of the Menu to the anchor: 'auto' (default), 'top', 'bottom', 'left', or 'right'. This parameter overrides the placement /// property for this method call only. /// /// /// For 'top' or 'bottom' placement, the alignment of the Menu to the anchor's edge: 'center' (default), 'left', or 'right'. This parameter /// overrides the alignment property for this method call only. /// /// /// // Just call private version to make appbar flags happy this._writeProfilerMark("show,StartTM"); // The corresponding "stop" profiler mark is handled in _Overlay._baseEndShow(). this._show(anchor, placement, alignment); }, _show: function Menu_show(anchor, placement, alignment) { // Before we show, we also need to check for children flyouts needing anchors this._checkForFlyoutCommands(); // Call flyout show this._baseFlyoutShow(anchor, placement, alignment); // We need to check for toggles after we send the beforeshow event, // so the developer has a chance to show or hide more commands. // Flyout's _findPosition will make that call. }, _addCommand: function Menu_addCommand(command) { if (!command) { throw new _ErrorFromName("WinJS.UI.Menu.NullCommand", strings.nullCommand); } // See if it's a command already if (!command._element) { // Not a command, so assume it's options for a command command = new _Command.MenuCommand(null, command); } // If we were attached somewhere else, detach us if (command._element.parentElement) { command._element.parentElement.removeChild(command._element); } // Reattach us this._element.appendChild(command._element); }, // Called by flyout's _findPosition so that application can update it status // we do the test and we can then fix this last-minute before showing. _checkToggle: function Menu_checkToggle() { var toggles = this._element.querySelectorAll(".win-command[aria-checked]"); var hasToggle = false; if (toggles) { for (var i = 0; i < toggles.length; i++) { if (toggles[i] && toggles[i].winControl && !toggles[i].winControl.hidden) { // Found a visible toggle control hasToggle = true; break; } } } if (hasToggle) { _ElementUtilities.addClass(this._element, _Constants.menuToggleClass); } else { _ElementUtilities.removeClass(this._element, _Constants.menuToggleClass); } }, _checkForFlyoutCommands: function Menu_checkForFlyoutCommands() { var commands = this._element.querySelectorAll(".win-command"); for (var count = 0; count < commands.length; count++) { if (commands[count].winControl) { // Remember our anchor in case it's a flyout commands[count].winControl._parentFlyout = this; } } }, _handleKeyDown: function Menu_handleKeyDown(event) { var that = this; if (event.keyCode === Key.escape) { // Show a focus rect on what we move focus to this.winControl._keyboardInvoked = true; this.winControl._hide(); } else if ((event.keyCode === Key.space || event.keyCode === Key.enter) && (this === _Global.document.activeElement)) { event.preventDefault(); this.winControl.hide(); } else if (event.keyCode === Key.upArrow) { Menu._focusOnPreviousElement(that); // Prevent the page from scrolling event.preventDefault(); } else if (event.keyCode === Key.downArrow) { Menu._focusOnNextElement(that); // Prevent the page from scrolling event.preventDefault(); } else if (event.keyCode === Key.tab) { event.preventDefault(); } }, _writeProfilerMark: function Menu_writeProfilerMark(text) { _WriteProfilerMark("WinJS.UI.Menu:" + this._id + ":" + text); } }); // Statics // Set focus to next focusable element in the menu (loop if necessary). // Note: The loop works by first setting focus to the menu itself. If the menu is // what had focus before, then we break. Otherwise we try the first child next. // Focus remains on the menu if nothing is focusable. Menu._focusOnNextElement = function (menu) { var _currentElement = _Global.document.activeElement; do { if (_currentElement === menu) { _currentElement = _currentElement.firstElementChild; } else { _currentElement = _currentElement.nextElementSibling; } if (_currentElement) { _currentElement.focus(); } else { _currentElement = menu; } } while (_currentElement !== _Global.document.activeElement); }; // Set focus to previous focusable element in the menu (loop if necessary). // Note: The loop works by first setting focus to the menu itself. If the menu is // what had focus before, then we break. Otherwise we try the last child next. // Focus remains on the menu if nothing is focusable. Menu._focusOnPreviousElement = function (menu) { var _currentElement = _Global.document.activeElement; do { if (_currentElement === menu) { _currentElement = _currentElement.lastElementChild; } else { _currentElement = _currentElement.previousElementSibling; } if (_currentElement) { _currentElement.focus(); } else { _currentElement = menu; } } while (_currentElement !== _Global.document.activeElement); }; return Menu; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/SearchBox/_SearchSuggestionManagerShim',[ 'exports', '../../_Signal', '../../Core/_Base', '../../Core/_BaseUtils', '../../Core/_Events', '../../BindingList', ], function SearchSuggestionManagerShimInit(exports, _Signal, _Base, _BaseUtils, _Events, BindingList) { "use strict"; var CollectionChange = { reset: 0, itemInserted: 1, itemRemoved: 2, itemChanged: 3 }; var SearchSuggestionKind = { Query: 0, Result: 1, Separator: 2 }; var SuggestionVectorShim = _Base.Class.derive(Array, function SuggestionVectorShim_ctor() { }, { reset: function () { this.length = 0; this.dispatchEvent("vectorchanged", { collectionChange: CollectionChange.reset, index: 0 }); }, insert: function (index, data) { this.splice(index, 0, data); this.dispatchEvent("vectorchanged", { collectionChange: CollectionChange.itemInserted, index: index }); }, remove: function (index) { this.splice(index, 1); this.dispatchEvent("vectorchanged", { collectionChange: CollectionChange.itemRemoved, index: index }); }, }); _Base.Class.mix(SuggestionVectorShim, _Events.eventMixin); var SearchSuggestionCollectionShim = _Base.Class.define(function SearchSuggestionCollectionShim_ctor() { this._data = []; }, { size: { get: function () { return this._data.length; } }, appendQuerySuggestion: function (text) { this._data.push({ kind: SearchSuggestionKind.Query, text: text }); }, appendQuerySuggestions: function (suggestions) { suggestions.forEach(this.appendQuerySuggestion.bind(this)); }, appendResultSuggestion: function (text, detailText, tag, imageUrl, imageAlternateText) { // 'image' must be null (not undefined) for SearchBox to fallback to use imageUrl instead this._data.push({ kind: SearchSuggestionKind.Result, text: text, detailText: detailText, tag: tag, imageUrl: imageUrl, imageAlternateText: imageAlternateText, image: null }); }, appendSearchSeparator: function (label) { this._data.push({ kind: SearchSuggestionKind.Separator, text: label }); } }); var SuggestionsRequestedEventArgShim = _Base.Class.define(function SuggestionsRequestedEventArgShim_ctor(queryText, language, linguisticDetails) { this._queryText = queryText; this._language = language; this._linguisticDetails = linguisticDetails; this._searchSuggestionCollection = new SearchSuggestionCollectionShim(); }, { language: { get: function () { return this._language; } }, linguisticDetails: { get: function () { return this._linguisticDetails; } }, queryText: { get: function () { return this._queryText; } }, searchSuggestionCollection: { get: function () { return this._searchSuggestionCollection; } }, getDeferral: function () { return this._deferralSignal || (this._deferralSignal = new _Signal()); }, _deferralSignal: null, }); var SearchSuggestionManagerShim = _Base.Class.define(function SearchSuggestionManagerShim_ctor() { this._updateVector = this._updateVector.bind(this); this._suggestionVector = new SuggestionVectorShim(); this._query = ""; this._history = { "": [] }; this._dataSource = []; this.searchHistoryContext = ""; this.searchHistoryEnabled = true; }, { addToHistory: function (queryText /*, language */) { if (!queryText || !queryText.trim()) { return; } var history = this._history[this.searchHistoryContext]; var dupeIndex = -1; for (var i = 0, l = history.length; i < l; i++) { var item = history[i]; if (item.text.toLowerCase() === queryText.toLowerCase()) { dupeIndex = i; break; } } if (dupeIndex >= 0) { history.splice(dupeIndex, 1); } history.splice(0, 0, { text: queryText, kind: SearchSuggestionKind.Query }); this._updateVector(); }, clearHistory: function () { this._history[this.searchHistoryContext] = []; this._updateVector(); }, setLocalContentSuggestionSettings: function (settings) { }, setQuery: function (queryText) { var that = this; function update(arr) { that._dataSource = arr; that._updateVector(); } this._query = queryText; var arg = new SuggestionsRequestedEventArgShim(queryText); this.dispatchEvent("suggestionsrequested", { request: arg }); if (arg._deferralSignal) { arg._deferralSignal.promise.then(update.bind(this, arg.searchSuggestionCollection._data)); } else { update(arg.searchSuggestionCollection._data); } }, searchHistoryContext: { get: function () { return "" + this._searchHistoryContext; }, set: function (value) { value = "" + value; if (!this._history[value]) { this._history[value] = []; } this._searchHistoryContext = value; } }, searchHistoryEnabled: { get: function () { return this._searchHistoryEnabled; }, set: function (value) { this._searchHistoryEnabled = value; } }, suggestions: { get: function () { return this._suggestionVector; } }, _updateVector: function () { // Can never clear the entire suggestions list or it will cause a visual flash because // the SearchBox control removes the suggestions list UI when the SSM fires vectorChanged // with size === 0, then re-renders it when the first suggestion is added. // Workaround is to insert a dummy entry, remove all old entries, add the new set of // eligible suggestions, then remove the dummy entry. this.suggestions.insert(this.suggestions.length, { text: "", kind: SearchSuggestionKind.Query }); while (this.suggestions.length > 1) { this.suggestions.remove(0); } var index = 0; var added = {}; if (this.searchHistoryEnabled) { var q = this._query.toLowerCase(); this._history[this.searchHistoryContext].forEach(function (item) { var text = item.text.toLowerCase(); if (text.indexOf(q) === 0) { this.suggestions.insert(index, item); added[text] = true; index++; } }, this); } this._dataSource.forEach(function (item) { if (item.kind === SearchSuggestionKind.Query) { if (!added[item.text.toLowerCase()]) { this.suggestions.insert(index, item); index++; } } else { this.suggestions.insert(index, item); index++; } }, this); this.suggestions.remove(this.suggestions.length - 1); }, }); _Base.Class.mix(SearchSuggestionManagerShim, _Events.eventMixin); _Base.Namespace._moduleDefine(exports, null, { _CollectionChange: CollectionChange, _SearchSuggestionKind: SearchSuggestionKind, _SearchSuggestionManagerShim: SearchSuggestionManagerShim, }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/SearchBox',[ '../Core/_Global', '../Core/_WinRT', '../Core/_Base', '../Core/_ErrorFromName', '../Core/_Events', '../Core/_Resources', '../Animations', '../BindingList', '../Controls/Repeater', '../Utilities/_Control', '../Utilities/_ElementListUtilities', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', './SearchBox/_SearchSuggestionManagerShim', 'require-style!less/desktop/controls', 'require-style!less/phone/controls', ], function searchboxInit(_Global, _WinRT, _Base, _ErrorFromName, _Events, _Resources, Animations, BindingList, Repeater, _Control, _ElementListUtilities, _ElementUtilities, _Hoverable, _SearchSuggestionManagerShim) { "use strict"; _Base.Namespace.define("WinJS.UI", { /// /// /// Enables the user to perform search queries and select suggestions. /// /// /// /// /// /// ]]> /// Raised when user or app changes the query text. /// Raised when user clicks on search glyph or presses Enter. /// Raised when user clicks one of the displayed suggestions. /// Raised when the system requests search suggestions from this app. /// /// Raised when the app automatically redirects focus to the search box. This event can only be raised when the focusOnKeyboardInput property is set to true. /// /// Styles the entire Search box control. /// Styles the query input box. /// Styles the search button. /// Styles the result suggestions flyout. /// Styles the result type suggestion. /// Styles the query type suggestion. /// /// Styles the separator type suggestion. /// /// /// Styles the currently selected suggestion. /// /// /// /// SearchBox: _Base.Namespace._lazy(function () { var createEvent = _Events._createEventProperty; var Key = _ElementUtilities.Key; // Enums var ClassName = { searchBox: "win-searchbox", searchBoxInput: "win-searchbox-input", searchBoxButton: "win-searchbox-button", searchBoxFlyout: "win-searchbox-flyout", searchBoxSuggestionResult: "win-searchbox-suggestion-result", searchBoxSuggestionQuery: "win-searchbox-suggestion-query", searchBoxSuggestionSeparator: "win-searchbox-suggestion-separator", searchBoxSuggestionSelected: "win-searchbox-suggestion-selected", searchBoxFlyoutHighlightText: "win-searchbox-flyout-highlighttext", searchBoxButtonInputFocus: "win-searchbox-button-input-focus", searchBoxInputFocus: "win-searchbox-input-focus", searchBoxSuggestionResultText: "win-searchbox-suggestion-result-text", searchBoxSuggestionResultDetailedText: "win-searchbox-suggestion-result-detailed-text", searchboxDisabled: "win-searchbox-disabled", searchboxHitHighlightSpan: "win-searchbox-hithighlight-span", }; var EventName = { querychanged: "querychanged", querysubmitted: "querysubmitted", resultsuggestionchosen: "resultsuggestionchosen", suggestionsrequested: "suggestionsrequested", receivingfocusonkeyboardinput: "receivingfocusonkeyboardinput" }; var strings = { get duplicateConstruction() { return "Invalid argument: Controls may only be instantiated one time for each DOM element"; }, get invalidSearchBoxSuggestionKind() { return "Error: Invalid search suggestion kind."; }, get ariaLabel() { return _Resources._getWinJSString("ui/searchBoxAriaLabel").value; }, get ariaLabelInputNoPlaceHolder() { return _Resources._getWinJSString("ui/searchBoxAriaLabelInputNoPlaceHolder").value; }, get ariaLabelInputPlaceHolder() { return _Resources._getWinJSString("ui/searchBoxAriaLabelInputPlaceHolder").value; }, get ariaLabelButton() { return _Resources._getWinJSString("ui/searchBoxAriaLabelButton").value; }, get ariaLabelQuery() { return _Resources._getWinJSString("ui/searchBoxAriaLabelQuery").value; }, get ariaLabelSeparator() { return _Resources._getWinJSString("ui/searchBoxAriaLabelSeparator").value; }, get ariaLabelResult() { return _Resources._getWinJSString("ui/searchBoxAriaLabelResult").value; } }; var SearchBox = _Base.Class.define(function SearchBox_ctor(element, options) { /// /// /// Creates a new SearchBox. /// /// /// The DOM element that hosts the SearchBox. /// /// /// An object that contains one or more property/value pairs to apply to the new control. /// Each property of the options object corresponds to one of the control's properties or events. /// Event names must begin with "on". For example, to provide a handler for the querychanged event, /// add a property named "onquerychanged" to the options object and set its value to the event handler. /// This parameter is optional. /// /// /// The new SearchBox. /// /// /// element = element || _Global.document.createElement("div"); if (element.winControl) { throw new _ErrorFromName("WinJS.UI.SearchBox.DuplicateConstruction", strings.duplicateConstruction); } element.winControl = this; // Elements this._domElement = null; this._inputElement = null; this._buttonElement = null; this._flyout = null; this._flyoutDivElement = null; this._repeaterDivElement = null; this._repeater = null; // Variables this._disposed = false; this._focusOnKeyboardInput = false; this._chooseSuggestionOnEnter = false; this._lastKeyPressLanguage = ""; // These are used to eliminate redundant query submitted events this._prevQueryText = ""; this._prevLinguisticDetails = this._createSearchQueryLinguisticDetails([], 0, 0, "", ""); this._prevCompositionStart = 0; this._prevCompositionLength = 0; this._isProcessingDownKey = false; this._isProcessingUpKey = false; this._isProcessingTabKey = false; this._isProcessingEnterKey = false; this._isFlyoutPointerDown = false; this._reflowImeOnPointerRelease = false; // Focus and selection related variables this._currentFocusedIndex = -1; this._currentSelectedIndex = -1; this._suggestionRendererBind = this._suggestionRenderer.bind(this); this._requestingFocusOnKeyboardInputHandlerBind = this._requestingFocusOnKeyboardInputHandler.bind(this); this._suggestionsRequestedHandlerBind = this._suggestionsRequestedHandler.bind(this); this._suggestionsChangedHandlerBind = this._suggestionsChangedHandler.bind(this); this._keydownCaptureHandlerBind = this._keydownCaptureHandler.bind(this); this._frameLoadCaptureHandlerBind = this._frameLoadCaptureHandler.bind(this); // Find out if we are in local compartment and if search APIs are available. this._searchSuggestionManager = null; this._searchSuggestions = null; // Get the search suggestion provider if it is available if (_WinRT.Windows.ApplicationModel.Search.Core.SearchSuggestionManager) { this._searchSuggestionManager = new _WinRT.Windows.ApplicationModel.Search.Core.SearchSuggestionManager(); } else { this._searchSuggestionManager = new _SearchSuggestionManagerShim._SearchSuggestionManagerShim(); } this._searchSuggestions = this._searchSuggestionManager.suggestions; this._hitFinder = null; this._setElement(element); _Control.setOptions(this, options); this._setAccessibilityProperties(); _ElementUtilities.addClass(element, "win-disposable"); }, { /// element: { get: function () { return this._domElement; } }, /// /// Gets or sets the placeholder text for the SearchBox. This text is displayed if there is no /// other text in the input box. /// /// placeholderText: { get: function () { return this._inputElement.placeholder; }, set: function (value) { this._inputElement.placeholder = value; this._updateInputElementAriaLabel(); } }, /// /// Gets or sets the query text for the SearchBox. /// /// queryText: { get: function () { return this._inputElement.value; }, set: function (value) { this._inputElement.value = value; } }, /// /// Gets or sets a value that specifies whether search history is disabled for the SearchBox. The default value is false. /// /// searchHistoryDisabled: { get: function () { if (this._searchSuggestionManager) { return !this._searchSuggestionManager.searchHistoryEnabled; } else { return true; } }, set: function (value) { if (this._searchSuggestionManager) { this._searchSuggestionManager.searchHistoryEnabled = !value; } } }, /// /// Gets or sets the search history context for the SearchBox. The search history context string is used as a secondary key for storing search history. /// (The primary key is the AppId.) An app can use the search history context string to store different search histories based on the context of the application. /// If you don't set this property, the system assumes that all searches in your app occur in the same context. /// If you update this property while the search pane is open with suggestions showing, the changes won't take effect until the user enters the next character. /// /// searchHistoryContext: { get: function () { if (this._searchSuggestionManager) { return this._searchSuggestionManager.searchHistoryContext; } else { return ""; } }, set: function (value) { if (this._searchSuggestionManager) { this._searchSuggestionManager.searchHistoryContext = value; } } }, /// /// Enable automatically focusing the search box when the user types into the app window (off by default) While this is enabled, /// input on the current thread will be intercepted and redirected to the search box. Only textual input will trigger the search box to focus. /// The caller will continue to receive non-text keys (such as arrows, tab, etc /// This will also not affect WIN/CTRL/ALT key combinations (except for Ctrl-V for paste). /// If the client needs more to happen than just set focus in the box (make control visible, etc.), they will need to handle the event. /// If enabled, the app must be sure to disable this if the user puts focus in some other edit field. /// /// focusOnKeyboardInput: { get: function () { return this._focusOnKeyboardInput; }, set: function (value) { if (this._focusOnKeyboardInput && !value) { if (!(this._searchSuggestionManager instanceof _SearchSuggestionManagerShim._SearchSuggestionManagerShim)) { this._searchSuggestionManager.removeEventListener("requestingfocusonkeyboardinput", this._requestingFocusOnKeyboardInputHandlerBind); } else { this._updateKeydownCaptureListeners(_Global.top, false /*add*/); } } else if (!this._focusOnKeyboardInput && !!value) { if (!(this._searchSuggestionManager instanceof _SearchSuggestionManagerShim._SearchSuggestionManagerShim)) { this._searchSuggestionManager.addEventListener("requestingfocusonkeyboardinput", this._requestingFocusOnKeyboardInputHandlerBind); } else { this._updateKeydownCaptureListeners(_Global.top, true /*add*/); } } this._focusOnKeyboardInput = !!value; } }, /// /// Gets or sets whether the first suggestion is chosen when the user presses Enter. /// When set to true, as the user types in the search box, a focus rectangle is drawn on the first search suggestion /// (if present and no IME composition in progress). Pressing enter will behave the same as if clicked on the focused suggestion, /// and the down arrow key press will put real focus to the second suggestion and the up arrow key will remove focus. /// /// chooseSuggestionOnEnter: { get: function () { return this._chooseSuggestionOnEnter; }, set: function (value) { this._chooseSuggestionOnEnter = !!value; this._updateSearchButtonClass(); } }, /// /// Gets or sets a value that specifies whether the SearchBox is disabled. /// /// disabled: { get: function () { return this._inputElement.disabled; }, set: function (value) { if (this._inputElement.disabled === !!value) { return; } if (!value) { // Enable control this._inputElement.disabled = false; this._buttonElement.disabled = false; this._domElement.disabled = false; _ElementUtilities.removeClass(this.element, ClassName.searchboxDisabled); if (_Global.document.activeElement === this.element) { _ElementUtilities._setActive(this._inputElement); } } else { // Disable control if (this._isFlyoutShown) { this._hideFlyout(); } _ElementUtilities.addClass(this.element, ClassName.searchboxDisabled); this._inputElement.disabled = true; this._buttonElement.disabled = true; this._domElement.disabled = true; } } }, // Methods setLocalContentSuggestionSettings: function SearchBox_setLocalContentSuggestionSettings(settings) { /// /// /// Specifies whether suggestions based on local files are automatically displayed in the search pane, and defines the criteria that /// the system uses to locate and filter these suggestions. /// /// /// The new settings for local content suggestions. /// /// /// if (this._searchSuggestionManager) { this._searchSuggestionManager.setLocalContentSuggestionSettings(settings); } }, dispose: function SearchBox() { /// /// /// Disposes this control. /// /// /// if (this._disposed) { return; } // Cancel pending promises. if (this._flyoutOpenPromise) { this._flyoutOpenPromise.cancel(); } // Detach winrt events. if (this._focusOnKeyboardInput) { if (!(this._searchSuggestionManager instanceof _SearchSuggestionManagerShim._SearchSuggestionManagerShim)) { this._searchSuggestionManager.removeEventListener("requestingfocusonkeyboardinput", this._requestingFocusOnKeyboardInputHandlerBind); } else { this._updateKeydownCaptureListeners(_Global.top, false /*add*/); } } this._searchSuggestions.removeEventListener("vectorchanged", this._suggestionsChangedHandlerBind); this._searchSuggestionManager.removeEventListener("suggestionsrequested", this._suggestionsRequestedHandlerBind); this._searchSuggestionManager = null; this._searchSuggestions = null; this._hitFinder = null; this._disposed = true; }, /// /// Raised when user or app changes the query text. /// /// onquerychanged: createEvent(EventName.querychanged), /// /// Raised when user clicks on search glyph or presses enter button. /// /// onquerysubmitted: createEvent(EventName.querysubmitted), /// /// Raised when user clicks on one of the suggestions displayed. /// /// onresultsuggestionchosen: createEvent(EventName.resultsuggestionchosen), /// /// Raised when Windows requests search suggestions from the app. /// /// onsuggestionsrequested: createEvent(EventName.suggestionsrequested), // Private methods _isFlyoutShown: function SearchBox_isFlyoutShown() { return (this._flyoutDivElement.style.display !== "none"); }, _isFlyoutBelow: function SearchBox_isFlyoutBelow() { if (this._flyoutDivElement.getBoundingClientRect().top > this._inputElement.getBoundingClientRect().top) { return true; } return false; }, _getFlyoutTop: function SearchBox_getFlyoutTop() { if (this._isFlyoutBelow()) { return this._inputElement.getBoundingClientRect().bottom; } var popupHeight = this._flyoutDivElement.getBoundingClientRect().bottom - this._flyoutDivElement.getBoundingClientRect().top; return this._inputElement.getBoundingClientRect().top - popupHeight; }, _getFlyoutBottom: function SearchBox_getFlyoutBottom() { if (this._isFlyoutBelow()) { var popupHeight = this._flyoutDivElement.getBoundingClientRect().bottom - this._flyoutDivElement.getBoundingClientRect().top; return this._inputElement.getBoundingClientRect().bottom + popupHeight; } return this._inputElement.getBoundingClientRect().top; }, _updateFlyoutTopAndTouchAction: function SearchBox_updateFlyoutTopAndTouchAction() { var popupHeight = this._flyoutDivElement.getBoundingClientRect().bottom - this._flyoutDivElement.getBoundingClientRect().top; if (!this._isFlyoutBelow()) { this._flyoutDivElement.style.top = "-" + popupHeight + "px"; } // ms-scroll-chaining:none will still chain scroll parent element if child div does // not have a scroll bar. Prevent this by setting and updating touch action if (this._flyoutDivElement.scrollHeight > popupHeight) { this._flyoutDivElement.style.touchAction = "pan-y"; } else { this._flyoutDivElement.style.touchAction = "none"; } }, _showFlyout: function SearchBox_showFlyout() { if (this._isFlyoutShown()) { return; } if (this._suggestionsData.length === 0) { return; } this._flyoutDivElement.style.display = "block"; // Display above vs below var minPopupHeight = this._flyoutDivElement.clientHeight; if (minPopupHeight < SearchBox._Constants.MIN_POPUP_HEIGHT) { minPopupHeight = SearchBox._Constants.MIN_POPUP_HEIGHT; } var flyoutRect = this._flyoutDivElement.getBoundingClientRect(); var searchBoxRect = this.element.getBoundingClientRect(); var popupHeight = flyoutRect.bottom - flyoutRect.top; var popupWidth = flyoutRect.right - flyoutRect.left; var searchBoxWidth = searchBoxRect.right - searchBoxRect.left; var documentClientHeight = _Global.document.documentElement.clientHeight; var documentClientWidth = _Global.document.documentElement.clientWidth; var searchBoxClientHeight = this.element.clientHeight; var searchBoxClientLeft = this.element.clientLeft; var flyoutBelowSearchBox = true; if ((searchBoxRect.bottom + minPopupHeight) <= documentClientHeight) { // There is enough space below. Show below this._flyoutDivElement.style.top = searchBoxClientHeight + "px"; } else if ((searchBoxRect.top - minPopupHeight) >= 0) { // There is enough space above. Show above this._flyoutDivElement.style.top = "-" + popupHeight + "px"; flyoutBelowSearchBox = false; } else { // Not enough space above or below. Show below. this._flyoutDivElement.style.top = searchBoxClientHeight + "px"; } // Align left vs right edge var alignRight; if (_Global.getComputedStyle(this._flyoutDivElement).direction === "rtl") { // RTL: Align to the right edge if there is enough space to the left of the search box's // right edge, or if there is not enough space to fit the flyout aligned to either edge. alignRight = ((searchBoxRect.right - popupWidth) >= 0) || ((searchBoxRect.left + popupWidth) > documentClientWidth); } else { // LTR: Align to the right edge if there isn't enough space to the right of the search box's // left edge, but there is enough space to the left of the search box's right edge. alignRight = ((searchBoxRect.left + popupWidth) > documentClientWidth) && ((searchBoxRect.right - popupWidth) >= 0); } if (alignRight) { this._flyoutDivElement.style.left = (searchBoxWidth - popupWidth - searchBoxClientLeft) + "px"; } else { this._flyoutDivElement.style.left = "-" + searchBoxClientLeft + "px"; } // ms-scroll-chaining:none will still chain scroll parent element if child div does // not have a scroll bar. Prevent this by setting and updating touch action if (this._flyoutDivElement.scrollHeight > popupHeight) { this._flyoutDivElement.style.touchAction = "pan-y"; } else { this._flyoutDivElement.style.touchAction = "none"; } this._addFlyoutIMEPaddingIfRequired(); if (this._flyoutOpenPromise) { this._flyoutOpenPromise.cancel(); this._flyoutOpenPromise = null; } var animationKeyframe = flyoutBelowSearchBox ? "WinJS-flyoutBelowSearchBox-showPopup" : "WinJS-flyoutAboveSearchBox-showPopup"; this._flyoutOpenPromise = Animations.showPopup(this._flyoutDivElement, { top: "0px", left: "0px", keyframe: animationKeyframe }); }, _hideFlyout: function SearchBox_hideFlyout() { if (this._isFlyoutShown()) { this._flyoutDivElement.style.display = "none"; this._updateSearchButtonClass(); } }, _addNewSpan: function SearchBox_addNewSpan(element, textContent, insertBefore) { // Adds new span element with specified inner text as child to element, placed before insertBefore var spanElement = _Global.document.createElement("span"); spanElement.textContent = textContent; spanElement.setAttribute("aria-hidden", "true"); _ElementUtilities.addClass(spanElement, ClassName.searchboxHitHighlightSpan); element.insertBefore(spanElement, insertBefore); return spanElement; }, _addHitHighlightedText: function SearchBox_addHitHighlightedText(element, item, text) { if (text) { // Remove any existing hit highlighted text spans _ElementListUtilities.query("." + ClassName.searchboxHitHighlightSpan, element).forEach(function (childElement) { childElement.parentNode.removeChild(childElement); }); // Insert spans at the front of element var firstChild = element.firstChild; var hitsProvided = item.hits; if ((!hitsProvided) && (this._hitFinder !== null) && (item.kind !== _SearchSuggestionManagerShim._SearchSuggestionKind.Separator)) { hitsProvided = this._hitFinder.find(text); } var hits = SearchBox._sortAndMergeHits(hitsProvided); var lastPosition = 0; for (var i = 0; i < hits.length; i++) { var hit = hits[i]; // Add previous normal text this._addNewSpan(element, text.substring(lastPosition, hit.startPosition), firstChild); lastPosition = hit.startPosition + hit.length; // Add hit highlighted text var spanHitHighlightedText = this._addNewSpan(element, text.substring(hit.startPosition, lastPosition), firstChild); _ElementUtilities.addClass(spanHitHighlightedText, ClassName.searchBoxFlyoutHighlightText); } // Add final normal text if (lastPosition < text.length) { this._addNewSpan(element, text.substring(lastPosition), firstChild); } } }, _findSuggestionElementIndex: function SearchBox_findSuggestionElementIndex(curElement) { if (curElement) { for (var i = 0; i < this._suggestionsData.length; i++) { if (this._repeater.elementFromIndex(i) === curElement) { return i; } } } return -1; }, _isSuggestionSelectable: function SearchBox_isSuggestionSelectable(suggestion) { return ((suggestion.kind === _SearchSuggestionManagerShim._SearchSuggestionKind.Query) || (suggestion.kind === _SearchSuggestionManagerShim._SearchSuggestionKind.Result)); }, _findNextSuggestionElementIndex: function SearchBox_findNextSuggestionElementIndex(curIndex) { // Returns -1 if there are no focusable elements after curIndex // Returns first element if curIndex < 0 var startIndex = curIndex + 1; if (startIndex < 0) { startIndex = 0; } for (var i = startIndex; i < this._suggestionsData.length; i++) { if ((this._repeater.elementFromIndex(i)) && (this._isSuggestionSelectable(this._suggestionsData.getAt(i)))) { return i; } } return -1; }, _findPreviousSuggestionElementIndex: function SearchBox_findPreviousSuggestionElementIndex(curIndex) { // Returns -1 if there are no focusable elements before curIndex // Returns last element if curIndex >= suggestionsdata.length var startIndex = curIndex - 1; if (startIndex >= this._suggestionsData.length) { startIndex = this._suggestionsData.length - 1; } for (var i = startIndex; i >= 0; i--) { if ((this._repeater.elementFromIndex(i)) && (this._isSuggestionSelectable(this._suggestionsData.getAt(i)))) { return i; } } return -1; }, _trySetFocusOnSuggestionIndex: function SearchBox_trySetFocusOnSuggestionIndex(index) { try { this._repeater.elementFromIndex(index).focus(); } catch (e) { } }, _updateFakeFocus: function SearchBox_updateFakeFocus() { var firstElementIndex; if (this._isFlyoutShown() && (this._chooseSuggestionOnEnter)) { firstElementIndex = this._findNextSuggestionElementIndex(-1); } else { // This will clear the fake focus. firstElementIndex = -1; } this._selectSuggestionAtIndex(firstElementIndex); }, _updateSearchButtonClass: function SearchBox_updateSearchButtonClass() { if ((this._currentSelectedIndex !== -1) || (_Global.document.activeElement !== this._inputElement)) { // Focus is not in input. remove class _ElementUtilities.removeClass(this._buttonElement, ClassName.searchBoxButtonInputFocus); } else if (_Global.document.activeElement === this._inputElement) { _ElementUtilities.addClass(this._buttonElement, ClassName.searchBoxButtonInputFocus); } }, _selectSuggestionAtIndex: function SearchBox_selectSuggestionAtIndex(indexToSelect) { // Sets focus on the specified element and removes focus from others. // Clears selection if index is outside of suggestiondata index range. var curElement = null; for (var i = 0; i < this._suggestionsData.length; i++) { curElement = this._repeater.elementFromIndex(i); if (i !== indexToSelect) { _ElementUtilities.removeClass(curElement, ClassName.searchBoxSuggestionSelected); curElement.setAttribute("aria-selected", "false"); } else { _ElementUtilities.addClass(curElement, ClassName.searchBoxSuggestionSelected); this._scrollToView(curElement); curElement.setAttribute("aria-selected", "true"); } } this._updateSearchButtonClass(); this._currentSelectedIndex = indexToSelect; if (curElement) { this._inputElement.setAttribute("aria-activedescendant", this._repeaterDivElement.id + indexToSelect); } else if (this._inputElement.hasAttribute("aria-activedescendant")) { this._inputElement.removeAttribute("aria-activedescendant"); } }, _scrollToView: function SearchBox_scrollToView(targetElement) { var popupHeight = this._flyoutDivElement.getBoundingClientRect().bottom - this._flyoutDivElement.getBoundingClientRect().top; if ((targetElement.offsetTop + targetElement.offsetHeight) > (this._flyoutDivElement.scrollTop + popupHeight)) { // Element to scroll is below popup visible area var scrollDifference = (targetElement.offsetTop + targetElement.offsetHeight) - (this._flyoutDivElement.scrollTop + popupHeight); _ElementUtilities._zoomTo(this._flyoutDivElement, { contentX: 0, contentY: (this._flyoutDivElement.scrollTop + scrollDifference), viewportX: 0, viewportY: 0 }); } else if (targetElement.offsetTop < this._flyoutDivElement.scrollTop) { // Element to scroll is above popup visible area _ElementUtilities._zoomTo(this._flyoutDivElement, { contentX: 0, contentY: targetElement.offsetTop, viewportX: 0, viewportY: 0 }); } }, _querySuggestionRenderer: function SearchBox_querySuggestionRenderer(item) { var root = _Global.document.createElement("div"); this._addHitHighlightedText(root, item, item.text); root.title = item.text; _ElementUtilities.addClass(root, ClassName.searchBoxSuggestionQuery); var that = this; _ElementUtilities._addEventListener(root, "pointerup", function (ev) { that._inputElement.focus(); that._processSuggestionChosen(item, ev); }); root.setAttribute("role", "option"); var ariaLabel = _Resources._formatString(strings.ariaLabelQuery, item.text); root.setAttribute("aria-label", ariaLabel); return root; }, _separatorSuggestionRenderer: function SearchBox_separatorSuggestionRenderer(item) { var root = _Global.document.createElement("div"); if (item.text.length > 0) { var textElement = _Global.document.createElement("div"); textElement.textContent = item.text; textElement.title = item.text; textElement.setAttribute("aria-hidden", "true"); root.appendChild(textElement); } root.insertAdjacentHTML("beforeend", "
"); _ElementUtilities.addClass(root, ClassName.searchBoxSuggestionSeparator); root.setAttribute("role", "separator"); var ariaLabel = _Resources._formatString(strings.ariaLabelSeparator, item.text); root.setAttribute("aria-label", ariaLabel); return root; }, _resultSuggestionRenderer: function SearchBox_resultSuggestionRenderer(item) { var root = _Global.document.createElement("div"); var image = new _Global.Image(); image.style.opacity = 0; var loadImage = function (url) { function onload() { image.removeEventListener("load", onload, false); Animations.fadeIn(image); } image.addEventListener("load", onload, false); image.src = url; }; if (item.image !== null) { item.image.openReadAsync().then(function (streamWithContentType) { if (streamWithContentType !== null) { loadImage(_Global.URL.createObjectURL(streamWithContentType, { oneTimeOnly: true })); } }); } else if (item.imageUrl !== null) { loadImage(item.imageUrl); } image.setAttribute("aria-hidden", "true"); root.appendChild(image); var divElement = _Global.document.createElement("div"); _ElementUtilities.addClass(divElement, ClassName.searchBoxSuggestionResultText); this._addHitHighlightedText(divElement, item, item.text); divElement.title = item.text; divElement.setAttribute("aria-hidden", "true"); root.appendChild(divElement); var brElement = _Global.document.createElement("br"); divElement.appendChild(brElement); var divDetailElement = _Global.document.createElement("span"); _ElementUtilities.addClass(divDetailElement, ClassName.searchBoxSuggestionResultDetailedText); this._addHitHighlightedText(divDetailElement, item, item.detailText); divDetailElement.title = item.detailText; divDetailElement.setAttribute("aria-hidden", "true"); divElement.appendChild(divDetailElement); _ElementUtilities.addClass(root, ClassName.searchBoxSuggestionResult); var that = this; _ElementUtilities._addEventListener(root, "pointerup", function (ev) { that._inputElement.focus(); that._processSuggestionChosen(item, ev); }); root.setAttribute("role", "option"); var ariaLabel = _Resources._formatString(strings.ariaLabelResult, item.text, item.detailText); root.setAttribute("aria-label", ariaLabel); return root; }, _suggestionRenderer: function SearchBox_suggestionRenderer(item) { var root = null; if (!item) { return root; } if (item.kind === _SearchSuggestionManagerShim._SearchSuggestionKind.Query) { root = this._querySuggestionRenderer(item); } else if (item.kind === _SearchSuggestionManagerShim._SearchSuggestionKind.Separator) { root = this._separatorSuggestionRenderer(item); } else if (item.kind === _SearchSuggestionManagerShim._SearchSuggestionKind.Result) { root = this._resultSuggestionRenderer(item); } else { throw new _ErrorFromName("WinJS.UI.SearchBox.invalidSearchBoxSuggestionKind", strings.invalidSearchBoxSuggestionKind); } return root; }, _setElement: function SearchBox_setElement(element) { this._domElement = element; _ElementUtilities.addClass(this._domElement, ClassName.searchBox); this._inputElement = _Global.document.createElement("input"); this._inputElement.type = "search"; _ElementUtilities.addClass(this._inputElement, ClassName.searchBoxInput); this._buttonElement = _Global.document.createElement("div"); this._buttonElement.tabIndex = -1; _ElementUtilities.addClass(this._buttonElement, ClassName.searchBoxButton); this._flyoutDivElement = _Global.document.createElement('div'); _ElementUtilities.addClass(this._flyoutDivElement, ClassName.searchBoxFlyout); this._repeaterDivElement = _Global.document.createElement('div'); this._suggestionsData = new BindingList.List(); this._repeater = new Repeater.Repeater(this._repeaterDivElement, { data: this._suggestionsData, template: this._suggestionRendererBind }); this._domElement.appendChild(this._inputElement); this._domElement.appendChild(this._buttonElement); this._domElement.appendChild(this._flyoutDivElement); this._flyoutDivElement.appendChild(this._repeaterDivElement); this._hideFlyout(); this._wireupUserEvents(); this._wireupWinRTEvents(); this._wireupRepeaterEvents(); }, _setAccessibilityProperties: function Searchbox_setAccessibilityProperties() { // Set up accessibility properties var label = this._domElement.getAttribute("aria-label"); if (!label) { this._domElement.setAttribute("aria-label", strings.ariaLabel); } this._domElement.setAttribute("role", "group"); this._updateInputElementAriaLabel(); this._inputElement.setAttribute("role", "textbox"); this._buttonElement.setAttribute("role", "button"); this._buttonElement.setAttribute("aria-label", strings.ariaLabelButton); this._repeaterDivElement.setAttribute("role", "listbox"); _ElementUtilities._ensureId(this._repeaterDivElement); this._inputElement.setAttribute("aria-controls", this._repeaterDivElement.id); this._repeaterDivElement.setAttribute("aria-live", "polite"); }, _updateInputElementAriaLabel: function Searchbox_updateInputElementAriaLabel() { var ariaLabel = strings.ariaLabelInputNoPlaceHolder; if (this._inputElement.placeholder && this._inputElement.placeholder) { ariaLabel = _Resources._formatString(strings.ariaLabelInputPlaceHolder, this._inputElement.placeholder); } this._inputElement.setAttribute("aria-label", ariaLabel); }, _submitQuery: function Searchbox_submitQuery(queryText, fillLinguisticDetails, event) { if (this._disposed) { return; } // get the most up to date value of the input langauge from WinRT if available if (_WinRT.Windows.Globalization.Language) { this._lastKeyPressLanguage = _WinRT.Windows.Globalization.Language.currentInputMethodLanguageTag; } this._fireEvent(SearchBox._EventName.querysubmitted, { language: this._lastKeyPressLanguage, linguisticDetails: this._getLinguisticDetails(true /*useCache*/, fillLinguisticDetails), // allow caching, but generate empty linguistic details if suggestion is used queryText: queryText, keyModifiers: SearchBox._getKeyModifiers(event) }); if (this._searchSuggestionManager) { this._searchSuggestionManager.addToHistory( this._inputElement.value, this._lastKeyPressLanguage ); } }, _processSuggestionChosen: function Searchbox_processSuggestionChosen(item, event) { this.queryText = item.text; if (item.kind === _SearchSuggestionManagerShim._SearchSuggestionKind.Query) { this._submitQuery(item.text, false /*fillLinguisticDetails*/, event); // force empty linguistic details since explicitly chosen suggestion from list } else if (item.kind === _SearchSuggestionManagerShim._SearchSuggestionKind.Result) { this._fireEvent(SearchBox._EventName.resultsuggestionchosen, { tag: item.tag, keyModifiers: SearchBox._getKeyModifiers(event), storageFile: null }); } this._hideFlyout(); }, _buttonClickHandler: function SearchBox_buttonClickHandler(event) { this._inputElement.focus(); this._submitQuery(this._inputElement.value, true /*fillLinguisticDetails*/, event); this._hideFlyout(); }, _inputOrImeChangeHandler: function SearchBox_inputImeChangeHandler() { var isButtonDown = _ElementUtilities._matchesSelector(this._buttonElement, ":active"); // swallow the IME change event that gets fired when composition is ended due to keyboarding down to the suggestion list & mouse down on the button if (!this._isProcessingImeFocusLossKey() && !isButtonDown && !this._isFlyoutPointerDown) { var linguisticDetails = this._getLinguisticDetails(false /*useCache*/, true /*createFilled*/); // never cache on explicit user changes var hasLinguisticDetailsChanged = this._hasLinguisticDetailsChanged(linguisticDetails); // updates this._prevLinguisticDetails // Keep the previous composition cache up to date, execpt when composition ended with no text change and alternatives are kept. // In that case, we need to use the cached values to correctly generate the query prefix/suffix for substituting alternatives, but still report to the client that the composition has ended (via start & length of composition of 0) if ((this._inputElement.value !== this._prevQueryText) || (this._prevCompositionLength === 0) || (linguisticDetails.queryTextCompositionLength > 0)) { this._prevCompositionStart = linguisticDetails.queryTextCompositionStart; this._prevCompositionLength = linguisticDetails.queryTextCompositionLength; } if ((this._prevQueryText === this._inputElement.value) && !hasLinguisticDetailsChanged) { // Sometimes the input change is fired even if there is no change in input. // Swallow event in those cases. return; } this._prevQueryText = this._inputElement.value; // get the most up to date value of the input langauge from WinRT if available if (_WinRT.Windows.Globalization.Language) { this._lastKeyPressLanguage = _WinRT.Windows.Globalization.Language.currentInputMethodLanguageTag; } if (_WinRT.Windows.Data.Text.SemanticTextQuery) { if (this._inputElement.value !== "") { this._hitFinder = new _WinRT.Windows.Data.Text.SemanticTextQuery(this._inputElement.value, this._lastKeyPressLanguage); } else { this._hitFinder = null; } } this._fireEvent(SearchBox._EventName.querychanged, { language: this._lastKeyPressLanguage, queryText: this._inputElement.value, linguisticDetails: linguisticDetails }); if (this._searchSuggestionManager) { this._searchSuggestionManager.setQuery( this._inputElement.value, this._lastKeyPressLanguage, linguisticDetails ); } } }, _createSearchQueryLinguisticDetails: function SearchBox_createSearchQueryLinguisticDetails(compositionAlternatives, compositionStartOffset, compositionLength, queryTextPrefix, queryTextSuffix) { var linguisticDetails = null; // The linguistic alternatives we receive are only for the composition string being composed. We need to provide the linguistic alternatives // in the form of the full query text with alternatives embedded. var fullCompositionAlternatives = []; for (var i = 0; i < compositionAlternatives.length; i++) { fullCompositionAlternatives[i] = queryTextPrefix + compositionAlternatives[i] + queryTextSuffix; } if (_WinRT.Windows.ApplicationModel.Search.SearchQueryLinguisticDetails) { linguisticDetails = new _WinRT.Windows.ApplicationModel.Search.SearchQueryLinguisticDetails(fullCompositionAlternatives, compositionStartOffset, compositionLength); } else { // If we're in web compartment, create a script version of the WinRT SearchQueryLinguisticDetails object linguisticDetails = { queryTextAlternatives: fullCompositionAlternatives, queryTextCompositionStart: compositionStartOffset, queryTextCompositionLength: compositionLength }; } return linguisticDetails; }, _getLinguisticDetails: function SearchBox_getLinguisticDetails(useCache, createFilled) { // createFilled=false always creates an empty linguistic details object, otherwise generate it or use the cache var linguisticDetails = null; if ((this._inputElement.value === this._prevQueryText) && useCache && this._prevLinguisticDetails && createFilled) { linguisticDetails = this._prevLinguisticDetails; } else { var compositionAlternatives = []; var compositionStartOffset = 0; var compositionLength = 0; var queryTextPrefix = ""; var queryTextSuffix = ""; if (createFilled && this._inputElement.msGetInputContext && this._inputElement.msGetInputContext().getCompositionAlternatives) { var context = this._inputElement.msGetInputContext(); compositionAlternatives = context.getCompositionAlternatives(); compositionStartOffset = context.compositionStartOffset; compositionLength = context.compositionEndOffset - context.compositionStartOffset; if ((this._inputElement.value !== this._prevQueryText) || (this._prevCompositionLength === 0) || (compositionLength > 0)) { queryTextPrefix = this._inputElement.value.substring(0, compositionStartOffset); queryTextSuffix = this._inputElement.value.substring(compositionStartOffset + compositionLength); } else { // composition ended, but alternatives have been kept, need to reuse the previous query prefix/suffix, but still report to the client that the composition has ended (start & length of composition of 0) queryTextPrefix = this._inputElement.value.substring(0, this._prevCompositionStart); queryTextSuffix = this._inputElement.value.substring(this._prevCompositionStart + this._prevCompositionLength); } } linguisticDetails = this._createSearchQueryLinguisticDetails(compositionAlternatives, compositionStartOffset, compositionLength, queryTextPrefix, queryTextSuffix); } return linguisticDetails; }, _handleTabKeyDown: function SearchBox_handleTabKeyDown(event) { var closeFlyout = true; if (event.shiftKey) { // If focus is not in input if (this._currentFocusedIndex !== -1) { // Remove selection. this._currentFocusedIndex = -1; this._selectSuggestionAtIndex(this._currentFocusedIndex); this._updateSearchButtonClass(); event.preventDefault(); event.stopPropagation(); closeFlyout = false; } } else if (this._currentFocusedIndex === -1) { if (this._isFlyoutBelow()) { // Move to first element this._currentFocusedIndex = this._findNextSuggestionElementIndex(this._currentFocusedIndex); } else { // Move to last element this._currentFocusedIndex = this._findPreviousSuggestionElementIndex(this._suggestionsData.length); } if (this._currentFocusedIndex !== -1) { this._selectSuggestionAtIndex(this._currentFocusedIndex); this._updateQueryTextWithSuggestionText(this._currentFocusedIndex); this._updateSearchButtonClass(); event.preventDefault(); event.stopPropagation(); closeFlyout = false; } } if (closeFlyout) { this._hideFlyout(); } }, _keyDownHandler: function SearchBox_keyDownHandler(event) { this._lastKeyPressLanguage = event.locale; if (event.keyCode === Key.tab) { this._isProcessingTabKey = true; } else if (event.keyCode === Key.upArrow) { this._isProcessingUpKey = true; } else if (event.keyCode === Key.downArrow) { this._isProcessingDownKey = true; } else if ((event.keyCode === Key.enter) && (event.locale === "ko")) { this._isProcessingEnterKey = true; } // Ignore keys handled by ime. if (event.keyCode !== Key.IME) { if (event.keyCode === Key.tab) { this._handleTabKeyDown(event); } else if (event.keyCode === Key.escape) { // If focus is not in input if (this._currentFocusedIndex !== -1) { this.queryText = this._prevQueryText; this._currentFocusedIndex = -1; this._selectSuggestionAtIndex(this._currentFocusedIndex); this._updateSearchButtonClass(); event.preventDefault(); event.stopPropagation(); } else if (this.queryText !== "") { this.queryText = ""; this._inputOrImeChangeHandler(null); this._updateSearchButtonClass(); event.preventDefault(); event.stopPropagation(); } } else if (event.keyCode === Key.upArrow) { var prevIndex; if (this._currentSelectedIndex !== -1) { prevIndex = this._findPreviousSuggestionElementIndex(this._currentSelectedIndex); // Restore user entered query when user navigates back to input. if (prevIndex === -1) { this.queryText = this._prevQueryText; } } else { prevIndex = this._findPreviousSuggestionElementIndex(this._suggestionsData.length); } this._currentFocusedIndex = prevIndex; this._selectSuggestionAtIndex(prevIndex); this._updateQueryTextWithSuggestionText(this._currentFocusedIndex); this._updateSearchButtonClass(); event.preventDefault(); event.stopPropagation(); } else if (event.keyCode === Key.downArrow) { var nextIndex = this._findNextSuggestionElementIndex(this._currentSelectedIndex); // Restore user entered query when user navigates back to input. if ((this._currentSelectedIndex !== -1) && (nextIndex === -1)) { this.queryText = this._prevQueryText; } this._currentFocusedIndex = nextIndex; this._selectSuggestionAtIndex(nextIndex); this._updateQueryTextWithSuggestionText(this._currentFocusedIndex); this._updateSearchButtonClass(); event.preventDefault(); event.stopPropagation(); } else if (event.keyCode === Key.enter) { if (this._currentSelectedIndex === -1) { this._submitQuery(this._inputElement.value, true /*fillLinguisticDetails*/, event); } else { this._processSuggestionChosen(this._suggestionsData.getAt(this._currentSelectedIndex), event); } this._hideFlyout(); } else if (SearchBox._isTypeToSearchKey(event)) { // Type to search on suggestions scenario. if (this._currentFocusedIndex !== -1) { this._currentFocusedIndex = -1; this._selectSuggestionAtIndex(-1); this._updateFakeFocus(); } } } }, _keyPressHandler: function SearchBox_keyPressHandler(event) { this._lastKeyPressLanguage = event.locale; }, _keyUpHandler: function SearchBox_keyUpHandler(event) { if (event.keyCode === Key.tab) { this._isProcessingTabKey = false; } else if (event.keyCode === Key.upArrow) { this._isProcessingUpKey = false; } else if (event.keyCode === Key.downArrow) { this._isProcessingDownKey = false; } else if (event.keyCode === Key.enter) { this._isProcessingEnterKey = false; } }, _inputFocusHandler: function SearchBox_inputFocusHandler(event) { // Refresh hit highlighting if text has changed since focus was present // This can happen if the user committed a suggestion previously. if (this._inputElement.value !== this._prevQueryText) { if (_WinRT.Windows.Data.Text.SemanticTextQuery) { if (this._inputElement.value !== "") { this._hitFinder = new _WinRT.Windows.Data.Text.SemanticTextQuery(this._inputElement.value, this._inputElement.lang); } else { this._hitFinder = null; } } } // If focus is returning to the input box from outside the search control, show the flyout and refresh the suggestions if ((event.target === this._inputElement) && !this._internalFocusMove) { this._showFlyout(); // If focus is not in input if (this._currentFocusedIndex !== -1) { this._selectSuggestionAtIndex(this._currentFocusedIndex); } else { this._updateFakeFocus(); } if (this._searchSuggestionManager) { this._searchSuggestionManager.setQuery( this._inputElement.value, this._lastKeyPressLanguage, this._getLinguisticDetails(true /*useCache*/, true /*createFilled*/) ); } } this._internalFocusMove = false; _ElementUtilities.addClass(this.element, ClassName.searchBoxInputFocus); this._updateSearchButtonClass(); }, _flyoutBlurHandler: function SearchBox_flyoutBlurHandler() { if (this._isElementInSearchControl(_Global.document.activeElement)) { this._internalFocusMove = true; } else { this._hideFlyout(); _ElementUtilities.removeClass(this.element, ClassName.searchBoxInputFocus); } }, _inputBlurHandler: function SearchBox_inputBlurHandler(event) { if (!this._isElementInSearchControl(_Global.document.activeElement)) { this._hideFlyout(); _ElementUtilities.removeClass(this.element, ClassName.searchBoxInputFocus); } this._updateSearchButtonClass(); this._isProcessingDownKey = false; this._isProcessingUpKey = false; this._isProcessingTabKey = false; this._isProcessingEnterKey = false; }, _isIMEOccludingFlyout: function SearchBox_isIMEOccludingFlyout(imeRect) { var flyoutTop = this._getFlyoutTop(); var flyoutBottom = this._getFlyoutBottom(); if (((imeRect.top >= flyoutTop) && (imeRect.top <= flyoutBottom)) || ((imeRect.bottom >= flyoutTop) && (imeRect.bottom <= flyoutBottom))) { return true; } return false; }, _addFlyoutIMEPaddingIfRequired: function SearchBox_addFlyoutIMEPaddingIfRequired() { if (this._isFlyoutShown() && this._isFlyoutBelow() && this._inputElement.msGetInputContext && this._inputElement.msGetInputContext()) { var context = this._inputElement.msGetInputContext(); var rect = context.getCandidateWindowClientRect(); if (this._isIMEOccludingFlyout(rect)) { var animation = Animations.createRepositionAnimation(this._flyoutDivElement.children); this._flyoutDivElement.style.paddingTop = (rect.bottom - rect.top) + "px"; animation.execute(); } } }, _msCandidateWindowShowHandler: function SearchBox_msCandidateWindowShowHandler() { this._addFlyoutIMEPaddingIfRequired(); this._reflowImeOnPointerRelease = false; }, _msCandidateWindowHideHandler: function SearchBox_msCandidateWindowHideHandler() { if (!this._isFlyoutPointerDown) { var animation = Animations.createRepositionAnimation(this._flyoutDivElement.children); this._flyoutDivElement.style.paddingTop = ""; animation.execute(); } else { this._reflowImeOnPointerRelease = true; } }, _wireupUserEvents: function SearchBox_wireupUserEvents() { var inputOrImeChangeHandler = this._inputOrImeChangeHandler.bind(this); this._buttonElement.addEventListener("click", this._buttonClickHandler.bind(this)); this._inputElement.addEventListener("input", inputOrImeChangeHandler); this._inputElement.addEventListener("keydown", this._keyDownHandler.bind(this)); this._inputElement.addEventListener("keypress", this._keyPressHandler.bind(this)); this._inputElement.addEventListener("keyup", this._keyUpHandler.bind(this)); this._inputElement.addEventListener("focus", this._inputFocusHandler.bind(this)); this._inputElement.addEventListener("blur", this._inputBlurHandler.bind(this)); _ElementUtilities._addEventListener(this._inputElement, "pointerdown", this._inputPointerDownHandler.bind(this)); this._flyoutDivElement.addEventListener("blur", this._flyoutBlurHandler.bind(this)); _ElementUtilities._addEventListener(this._flyoutDivElement, "pointerdown", this._flyoutPointerDownHandler.bind(this)); _ElementUtilities._addEventListener(this._flyoutDivElement, "pointerup", this._flyoutPointerReleasedHandler.bind(this)); _ElementUtilities._addEventListener(this._flyoutDivElement, "pointercancel", this._flyoutPointerReleasedHandler.bind(this)); _ElementUtilities._addEventListener(this._flyoutDivElement, "pointerout", this._flyoutPointerReleasedHandler.bind(this)); this._inputElement.addEventListener("compositionstart", inputOrImeChangeHandler); this._inputElement.addEventListener("compositionupdate", inputOrImeChangeHandler); this._inputElement.addEventListener("compositionend", inputOrImeChangeHandler); if (this._inputElement.msGetInputContext && this._inputElement.msGetInputContext()) { var context = this._inputElement.msGetInputContext(); context.addEventListener("MSCandidateWindowShow", this._msCandidateWindowShowHandler.bind(this)); context.addEventListener("MSCandidateWindowHide", this._msCandidateWindowHideHandler.bind(this)); } }, _repeaterChangedHandler: function SearchBox_repeaterChangedHandler() { this._updateFlyoutTopAndTouchAction(); if (this._isFlyoutShown()) { this._repeaterDivElement.style.display = "none"; this._repeaterDivElement.style.display = "block"; } }, _wireupRepeaterEvents: function SearchBox_wireupRepeaterEvents() { var repeaterChangeHandler = this._repeaterChangedHandler.bind(this); this._repeater.addEventListener("itemchanged", repeaterChangeHandler); this._repeater.addEventListener("iteminserted", repeaterChangeHandler); this._repeater.addEventListener("itemremoved", repeaterChangeHandler); this._repeater.addEventListener("itemsreloaded", repeaterChangeHandler); }, _inputPointerDownHandler: function SearchBox_inputPointerDownHandler() { if ((_Global.document.activeElement === this._inputElement) && (this._currentSelectedIndex !== -1)) { this._currentFocusedIndex = -1; this._selectSuggestionAtIndex(this._currentFocusedIndex); } }, _flyoutPointerDownHandler: function SearchBox_flyoutPointerDownHandler(ev) { this._isFlyoutPointerDown = true; var srcElement = ev.target; while (srcElement && (srcElement.parentNode !== this._repeaterDivElement)) { srcElement = srcElement.parentNode; } var index = this._findSuggestionElementIndex(srcElement); if ((index >= 0) && (index < this._suggestionsData.length) && (this._currentFocusedIndex !== index)) { if (this._isSuggestionSelectable(this._suggestionsData.getAt(index))) { this._currentFocusedIndex = index; this._selectSuggestionAtIndex(index); this._updateQueryTextWithSuggestionText(this._currentFocusedIndex); } } // Prevent default so focus does not leave input element. ev.preventDefault(); }, _flyoutPointerReleasedHandler: function SearchBox_flyoutPointerReleasedHandler() { this._isFlyoutPointerDown = false; if (this._reflowImeOnPointerRelease) { this._reflowImeOnPointerRelease = false; var animation = Animations.createRepositionAnimation(this._flyoutDivElement.children); this._flyoutDivElement.style.paddingTop = ""; animation.execute(); } }, _isElementInSearchControl: function SearchBox_isElementInSearchControl(targetElement) { return this.element.contains(targetElement) || (this.element === targetElement); }, _hideFlyoutIfLeavingSearchControl: function SearchBox_hideFlyoutIfLeavingSearchControl(targetElement) { if (!this._isFlyoutShown()) { return; } if (!this._isElementInSearchControl(targetElement)) { this._hideFlyout(); } }, _wireupWinRTEvents: function SearchBox_wireupWinRTEvents() { this._searchSuggestions.addEventListener("vectorchanged", this._suggestionsChangedHandlerBind); this._searchSuggestionManager.addEventListener("suggestionsrequested", this._suggestionsRequestedHandlerBind); }, _suggestionsChangedHandler: function SearchBox_suggestionsChangedHandler(event) { var collectionChange = event.collectionChange || event.detail.collectionChange; var changeIndex = (+event.index === event.index) ? event.index : event.detail.index; var ChangeEnum = _SearchSuggestionManagerShim._CollectionChange; if (collectionChange === ChangeEnum.reset) { if (this._isFlyoutShown()) { this._hideFlyout(); } this._suggestionsData.splice(0, this._suggestionsData.length); } else if (collectionChange === ChangeEnum.itemInserted) { var suggestion = this._searchSuggestions[changeIndex]; this._suggestionsData.splice(changeIndex, 0, suggestion); this._showFlyout(); } else if (collectionChange === ChangeEnum.itemRemoved) { if ((this._suggestionsData.length === 1)) { _ElementUtilities._setActive(this._inputElement); this._hideFlyout(); } this._suggestionsData.splice(changeIndex, 1); } else if (collectionChange === ChangeEnum.itemChanged) { var suggestion = this._searchSuggestions[changeIndex]; if (suggestion !== this._suggestionsData.getAt(changeIndex)) { this._suggestionsData.setAt(changeIndex, suggestion); } else { // If the suggestions manager gives us an identical item, it means that only the hit highlighted text has changed. var existingElement = this._repeater.elementFromIndex(changeIndex); if (_ElementUtilities.hasClass(existingElement, ClassName.searchBoxSuggestionQuery)) { this._addHitHighlightedText(existingElement, suggestion, suggestion.text); } else { var resultSuggestionDiv = existingElement.querySelector("." + ClassName.searchBoxSuggestionResultText); if (resultSuggestionDiv) { this._addHitHighlightedText(resultSuggestionDiv, suggestion, suggestion.text); var resultSuggestionDetailDiv = existingElement.querySelector("." + ClassName.searchBoxSuggestionResultDetailedText); if (resultSuggestionDetailDiv) { this._addHitHighlightedText(resultSuggestionDetailDiv, suggestion, suggestion.detailText); } } } } } if (_Global.document.activeElement === this._inputElement) { this._updateFakeFocus(); } }, _suggestionsRequestedHandler: function SearchBox_suggestionsRequestedHandler(event) { // get the most up to date value of the input langauge from WinRT if available if (_WinRT.Windows.Globalization.Language) { this._lastKeyPressLanguage = _WinRT.Windows.Globalization.Language.currentInputMethodLanguageTag; } var request = event.request || event.detail.request; var deferral; this._fireEvent(SearchBox._EventName.suggestionsrequested, { setPromise: function (promise) { deferral = request.getDeferral(); promise.then(function () { deferral.complete(); }); }, searchSuggestionCollection: request.searchSuggestionCollection, language: this._lastKeyPressLanguage, linguisticDetails: this._getLinguisticDetails(true /*useCache*/, true /*createFilled*/), queryText: this._inputElement.value }); }, _fireEvent: function SearchBox_fireEvent(type, detail) { // Returns true if ev.preventDefault() was not called var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent(type, true, true, detail); return this.element.dispatchEvent(event); }, _requestingFocusOnKeyboardInputHandler: function SearchBox_requestingFocusOnKeyboardInputHandler() { this._fireEvent(SearchBox._EventName.receivingfocusonkeyboardinput, null); if (_Global.document.activeElement !== this._inputElement) { try { this._inputElement.focus(); } catch (e) { } } }, _keydownCaptureHandler: function SearchBox_keydownCaptureHandler(event) { if (this._focusOnKeyboardInput && this._shouldKeyTriggerTypeToSearch(event)) { this._requestingFocusOnKeyboardInputHandler(event); } }, _frameLoadCaptureHandler: function SearchBox_frameLoadCaptureHandler(event) { if (this._focusOnKeyboardInput) { this._updateKeydownCaptureListeners(event.target.contentWindow, true /*add*/); } }, _updateKeydownCaptureListeners: function SearchBox_updateTypeToSearchListeners(win, add) { // Register for child frame keydown events in order to support FocusOnKeyboardInput // when focus is in a child frame. Also register for child frame load events so // it still works after frame navigations. // Note: This won't catch iframes added programmatically later, but that can be worked // around by toggling FocusOnKeyboardInput off/on after the new iframe is added. try { if (add) { win.document.addEventListener('keydown', this._keydownCaptureHandlerBind, true); } else { win.document.removeEventListener('keydown', this._keydownCaptureHandlerBind, true); } } catch (e) { // if the IFrame crosses domains, we'll get a permission denied error } if (win.frames) { for (var i = 0, l = win.frames.length; i < l; i++) { var childWin = win.frames[i]; this._updateKeydownCaptureListeners(childWin, add); try { if (add) { if (childWin.frameElement) { childWin.frameElement.addEventListener('load', this._frameLoadCaptureHandlerBind, true); } } else { if (childWin.frameElement) { childWin.frameElement.removeEventListener('load', this._frameLoadCaptureHandlerBind, true); } } } catch (e) { // if the IFrame crosses domains, we'll get a permission denied error } } } }, _shouldKeyTriggerTypeToSearch: function SearchBox_shouldKeyTriggerTypeToSearch(event) { var shouldTrigger = false; // First, check if a metaKey is pressed (only applies to MacOS). If so, do nothing here. if (!event.metaKey) { // We also don't handle CTRL/ALT combinations, unless ALTGR is also set. Since there is no shortcut for checking AltGR, // we need to use getModifierState, however, Safari currently doesn't support this. if ((!event.ctrlKey && !event.altKey) || (event.getModifierState && event.getModifierState("AltGraph"))) { // Show on most keys for visible characters like letters, numbers, etc. switch (event.keyCode) { case 0x30: //0x30 0 key case 0x31: //0x31 1 key case 0x32: //0x32 2 key case 0x33: //0x33 3 key case 0x34: //0x34 4 key case 0x35: //0x35 5 key case 0x36: //0x36 6 key case 0x37: //0x37 7 key case 0x38: //0x38 8 key case 0x39: //0x39 9 key case 0x41: //0x41 A key case 0x42: //0x42 B key case 0x43: //0x43 C key case 0x44: //0x44 D key case 0x45: //0x45 E key case 0x46: //0x46 F key case 0x47: //0x47 G key case 0x48: //0x48 H key case 0x49: //0x49 I key case 0x4A: //0x4A J key case 0x4B: //0x4B K key case 0x4C: //0x4C L key case 0x4D: //0x4D M key case 0x4E: //0x4E N key case 0x4F: //0x4F O key case 0x50: //0x50 P key case 0x51: //0x51 Q key case 0x52: //0x52 R key case 0x53: //0x53 S key case 0x54: //0x54 T key case 0x55: //0x55 U key case 0x56: //0x56 V key case 0x57: //0x57 W key case 0x58: //0x58 X key case 0x59: //0x59 Y key case 0x5A: //0x5A Z key case 0x60: // VK_NUMPAD0, //0x60 Numeric keypad 0 key case 0x61: // VK_NUMPAD1, //0x61 Numeric keypad 1 key case 0x62: // VK_NUMPAD2, //0x62 Numeric keypad 2 key case 0x63: // VK_NUMPAD3, //0x63 Numeric keypad 3 key case 0x64: // VK_NUMPAD4, //0x64 Numeric keypad 4 key case 0x65: // VK_NUMPAD5, //0x65 Numeric keypad 5 key case 0x66: // VK_NUMPAD6, //0x66 Numeric keypad 6 key case 0x67: // VK_NUMPAD7, //0x67 Numeric keypad 7 key case 0x68: // VK_NUMPAD8, //0x68 Numeric keypad 8 key case 0x69: // VK_NUMPAD9, //0x69 Numeric keypad 9 key case 0x6A: // VK_MULTIPLY, //0x6A Multiply key case 0x6B: // VK_ADD, //0x6B Add key case 0x6C: // VK_SEPARATOR, //0x6C Separator key case 0x6D: // VK_SUBTRACT, //0x6D Subtract key case 0x6E: // VK_DECIMAL, //0x6E Decimal key case 0x6F: // VK_DIVIDE, //0x6F Divide key case 0xBA: // VK_OEM_1, //0xBA Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ';:' key case 0xBB: // VK_OEM_PLUS, //0xBB For any country/region, the '+' key case 0xBC: // VK_OEM_COMMA, //0xBC For any country/region, the ',' key case 0xBD: // VK_OEM_MINUS, //0xBD For any country/region, the '-' key case 0xBE: // VK_OEM_PERIOD, //0xBE For any country/region, the '.' key case 0xBF: // VK_OEM_2, //0xBF Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '/?' key case 0xC0: // VK_OEM_3, //0xC0 Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '`~' key case 0xDB: // VK_OEM_4, //0xDB Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '[{' key case 0xDC: // VK_OEM_5, //0xDC Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '\|' key case 0xDD: // VK_OEM_6, //0xDD Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ']}' key case 0xDE: // VK_OEM_7, //0xDE Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the 'single-quote/double-quote' key case 0xDF: // VK_OEM_8, //0xDF Used for miscellaneous characters; it can vary by keyboard. case 0xE2: // VK_OEM_102, //0xE2 Either the angle bracket key or the backslash key on the RT 102-key keyboard case 0xE5: // VK_PROCESSKEY, //0xE5 IME PROCESS key case 0xE7: // VK_PACKET, //0xE7 Used to pass Unicode characters as if they were keystrokes. The VK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. For more information, see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP shouldTrigger = true; break; } } } return shouldTrigger; }, _hasLinguisticDetailsChanged: function SearchBox_hasLinguisticDetailsChanged(newLinguisticDetails) { var hasLinguisticDetailsChanged = false; if ((this._prevLinguisticDetails.queryTextCompositionStart !== newLinguisticDetails.queryTextCompositionStart) || (this._prevLinguisticDetails.queryTextCompositionLength !== newLinguisticDetails.queryTextCompositionLength) || (this._prevLinguisticDetails.queryTextAlternatives.length !== newLinguisticDetails.queryTextAlternatives.length)) { hasLinguisticDetailsChanged = true; } this._prevLinguisticDetails = newLinguisticDetails; return hasLinguisticDetailsChanged; }, _isProcessingImeFocusLossKey: function SearchBox_isProcessingImeFocusLossKey() { return this._isProcessingDownKey || this._isProcessingUpKey || this._isProcessingTabKey || this._isProcessingEnterKey; }, _updateQueryTextWithSuggestionText: function SearchBox_updateQueryTextWithSuggestionText(suggestionIndex) { if ((suggestionIndex >= 0) && (suggestionIndex < this._suggestionsData.length)) { this.queryText = this._suggestionsData.getAt(suggestionIndex).text; } } }, { _EventName: { querychanged: EventName.querychanged, querysubmitted: EventName.querysubmitted, resultsuggestionchosen: EventName.resultsuggestionchosen, suggestionsrequested: EventName.suggestionsrequested, receivingfocusonkeyboardinput: EventName.receivingfocusonkeyboardinput }, _Constants: { MIN_POPUP_HEIGHT: 152, }, createResultSuggestionImage: function SearchBox_createResultSuggestionImage(url) { /// /// /// Creates the image argument for SearchSuggestionCollection.appendResultSuggestion. /// /// /// The url of the image. /// /// /// if (_WinRT.Windows.Foundation.Uri && _WinRT.Windows.Storage.Streams.RandomAccessStreamReference) { return _WinRT.Windows.Storage.Streams.RandomAccessStreamReference.createFromUri(new _WinRT.Windows.Foundation.Uri(url)); } return url; }, _getKeyModifiers: function SearchBox_getKeyModifiers(ev) { // Returns the same value as http://msdn.microsoft.com/en-us/library/windows/apps/xaml/windows.system.virtualkeymodifiers var VirtualKeys = { ctrlKey: 1, altKey: 2, shiftKey: 4 }; var keyModifiers = 0; if (ev.ctrlKey) { keyModifiers |= VirtualKeys.ctrlKey; } if (ev.altKey) { keyModifiers |= VirtualKeys.altKey; } if (ev.shiftKey) { keyModifiers |= VirtualKeys.shiftKey; } return keyModifiers; }, _sortAndMergeHits: function searchBox_sortAndMergeHits(hitsProvided) { var reducedHits = []; if (hitsProvided) { // Copy hitsprovided array as winrt objects are immutable. var hits = new Array(hitsProvided.length); for (var i = 0; i < hitsProvided.length; i++) { hits.push({ startPosition: hitsProvided[i].startPosition, length: hitsProvided[i].length }); } hits.sort(SearchBox._hitStartPositionAscendingSorter); hits.reduce(SearchBox._hitIntersectionReducer, reducedHits); } return reducedHits; }, _hitStartPositionAscendingSorter: function searchBox_hitStartPositionAscendingSorter(firstHit, secondHit) { var returnValue = 0; if (firstHit.startPosition < secondHit.startPosition) { returnValue = -1; } else if (firstHit.startPosition > secondHit.startPosition) { returnValue = 1; } return returnValue; }, _hitIntersectionReducer: function searchBox_hitIntersectionReducer(reducedHits, nextHit, currentIndex) { if (currentIndex === 0) { reducedHits.push(nextHit); } else { var curHit = reducedHits[reducedHits.length - 1]; var curHitEndPosition = curHit.startPosition + curHit.length; if (nextHit.startPosition <= curHitEndPosition) { // The next hit intersects or is next to current hit. Merge it. var nextHitEndPosition = nextHit.startPosition + nextHit.length; if (nextHitEndPosition > curHitEndPosition) { curHit.length = nextHitEndPosition - curHit.startPosition; } } else { // No intersection, simply add to reduced list. reducedHits.push(nextHit); } } return reducedHits; }, _isTypeToSearchKey: function searchBox_isTypeToSearchKey(event) { if (event.shiftKey || event.ctrlKey || event.altKey) { return false; } return true; } }); _Base.Class.mix(SearchBox, _Control.DOMEventMixin); return SearchBox; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. /// appbar,Flyout,Flyouts,registeredforsettings,SettingsFlyout,Statics,Syriac define('WinJS/Controls/SettingsFlyout',[ '../Core/_Global', '../Core/_WinRT', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Resources', '../Core/_WriteProfilerMark', '../Animations', '../Pages', '../Promise', '../Utilities/_Dispose', '../Utilities/_ElementUtilities', '../Utilities/_ElementListUtilities', '../Utilities/_Hoverable', './AppBar/_Constants', './Flyout/_Overlay', 'require-style!less/desktop/controls', 'require-style!less/phone/controls' ], function settingsFlyoutInit(_Global,_WinRT, _Base, _BaseUtils, _ErrorFromName, _Resources, _WriteProfilerMark, Animations, Pages, Promise, _Dispose, _ElementUtilities, _ElementListUtilities, _Hoverable, _Constants, _Overlay) { "use strict"; _Base.Namespace.define("WinJS.UI", { /// /// Provides users with fast, in-context access to settings that affect the current app. /// /// /// Settings Flyout /// /// /// ///
/// ///
Custom Settings
///
///
/// {Your Content Here} ///
/// ]]>
/// Raised just before showing a SettingsFlyout. /// Raised immediately after a SettingsFlyout is fully shown. /// Raised just before hiding a SettingsFlyout. /// Raised immediately after a SettingsFlyout is fully hidden. /// The SettingsFlyout control itself. /// /// /// SettingsFlyout: _Base.Namespace._lazy(function () { var Key = _ElementUtilities.Key; var settingsPageIsFocusedOnce; // Constants for width var settingsNarrow = "narrow", settingsWide = "wide"; // Determine if the settings pane (system language) is RTL or not. function _shouldAnimateFromLeft() { if (_WinRT.Windows.UI.ApplicationSettings.SettingsEdgeLocation) { var appSettings = _WinRT.Windows.UI.ApplicationSettings; return (appSettings.SettingsPane.edge === appSettings.SettingsEdgeLocation.left); } else { return false; } } // Get the settings control by matching the settingsCommandId // if no match we'll try to match element id function _getChildSettingsControl(parentElement, id) { var settingElements = parentElement.querySelectorAll("." + _Constants.settingsFlyoutClass); var retValue, control; for (var i = 0; i < settingElements.length; i++) { control = settingElements[i].winControl; if (control) { if (control.settingsCommandId === id) { retValue = control; break; } if (settingElements[i].id === id) { retValue = retValue || control; } } } return retValue; } var SettingsFlyout = _Base.Class.derive(_Overlay._Overlay, function SettingsFlyout_ctor(element, options) { /// /// Creates a new SettingsFlyout control. /// /// The DOM element that will host the control. /// /// /// The set of properties and values to apply to the new SettingsFlyout. /// /// The new SettingsFlyout control. /// /// // Make sure there's an input element this._element = element || _Global.document.createElement("div"); this._id = this._element.id || _ElementUtilities._uniqueID(this._element); this._writeProfilerMark("constructor,StartTM"); // Call the base overlay constructor helper this._baseOverlayConstructor(this._element, options); this._addFirstDiv(); this._addFinalDiv(); // Handle "esc" & "tab" key presses this._element.addEventListener("keydown", this._handleKeyDown, true); // Make a click eating div _Overlay._Overlay._createClickEatingDivAppBar(); // Start settings hidden this._element.style.visibilty = "hidden"; this._element.style.display = "none"; // Attach our css class _ElementUtilities.addClass(this._element, _Constants.settingsFlyoutClass); // apply the light theme styling to the win-content elements inside the SettingsFlyout _ElementListUtilities.query("div.win-content", this._element). forEach(function (e) { if (!_ElementUtilities._matchesSelector(e, '.win-ui-dark, .win-ui-dark *')){ _ElementUtilities.addClass(e, _Constants.flyoutLightClass); } }); // Make sure we have an ARIA role var role = this._element.getAttribute("role"); if (role === null || role === "" || role === undefined) { this._element.setAttribute("role", "dialog"); } var label = this._element.getAttribute("aria-label"); if (label === null || label === "" || label === undefined) { this._element.setAttribute("aria-label", strings.ariaLabel); } // Make sure _Overlay event handlers are hooked up this._addOverlayEventHandlers(true); // Make sure animations are hooked up this._currentAnimateIn = this._animateSlideIn; this._currentAnimateOut = this._animateSlideOut; this._writeProfilerMark("constructor,StopTM"); }, { // Public Properties /// /// Width of the SettingsFlyout, "narrow", or "wide". /// /// SettingsFlyout.width may be altered or unavailable in future versions. Instead, style the CSS width property on elements with the .win-settingsflyout class. /// /// /// width: { get: function () { return this._width; }, set: function (value) { _ElementUtilities._deprecated(strings.widthDeprecationMessage); if (value === this._width) { return; } // Get rid of old class if (this._width === settingsNarrow) { _ElementUtilities.removeClass(this._element, _Constants.narrowClass); } else if (this._width === settingsWide) { _ElementUtilities.removeClass(this._element, _Constants.wideClass); } this._width = value; // Attach our new css class if (this._width === settingsNarrow) { _ElementUtilities.addClass(this._element, _Constants.narrowClass); } else if (this._width === settingsWide) { _ElementUtilities.addClass(this._element, _Constants.wideClass); } } }, /// /// Define the settings command Id for the SettingsFlyout control. /// /// settingsCommandId: { get: function () { return this._settingsCommandId; }, set: function (value) { this._settingsCommandId = value; } }, show: function () { /// /// /// Shows the SettingsFlyout, if hidden. /// /// /// // Just call private version to make appbar flags happy // Don't do anything if disabled if (this.disabled) { return; } this._writeProfilerMark("show,StartTM"); // The corresponding "stop" profiler mark is handled in _Overlay._baseEndShow(). this._show(); }, _dispose: function SettingsFlyout_dispose() { _Dispose.disposeSubTree(this.element); this._dismiss(); }, _show: function SettingsFlyout_show() { // We call our base "_baseShow" because SettingsFlyout overrides show this._baseShow(); // Need click-eating div to be visible, // (even if now hiding, we'll show and need click eater) _Overlay._Overlay._showClickEatingDivAppBar(); }, _endShow: function SettingsFlyout_endShow() { // Clean up after showing this._initAfterAnimation(); }, _initAfterAnimation: function SettingsFlyout_initAfterAnimation() { settingsPageIsFocusedOnce = 0; // Verify that the firstDiv and finalDiv are in the correct location. // Move them to the correct location or add them if they are not. if (!_ElementUtilities.hasClass(this.element.children[0], _Constants.firstDivClass)) { var firstDiv = this.element.querySelectorAll(".win-first"); if (firstDiv && firstDiv.length > 0) { firstDiv.item(0).parentNode.removeChild(firstDiv.item(0)); } this._addFirstDiv(); } // Set focus to the firstDiv if (this.element.children[0]) { _ElementUtilities._addEventListener(this.element.children[0], "focusout", function () { settingsPageIsFocusedOnce = 1; }, false); this.element.children[0].focus(); } if (!_ElementUtilities.hasClass(this.element.children[this.element.children.length - 1], _Constants.finalDivClass)) { var finalDiv = this.element.querySelectorAll(".win-final"); if (finalDiv && finalDiv.length > 0) { finalDiv.item(0).parentNode.removeChild(finalDiv.item(0)); } this._addFinalDiv(); } this._setBackButtonsAriaLabel(); }, _setBackButtonsAriaLabel: function SettingsFlyout_setBackButtonsAriaLabel() { var backbuttons = this.element.querySelectorAll(".win-backbutton"); var label; for (var i = 0; i < backbuttons.length; i++) { label = backbuttons[i].getAttribute("aria-label"); if (label === null || label === "" || label === undefined) { backbuttons[i].setAttribute("aria-label", strings.backbuttonAriaLabel); } } }, hide: function () { /// /// /// Hides the SettingsFlyout, if visible, regardless of other state. /// /// /// // Just call private version to make appbar flags happy this._writeProfilerMark("hide,StartTM"); // The corresponding "stop" profiler mark is handled in _Overlay._baseEndHide(). this._hide(); }, _hide: function SettingsFlyout_hide() { if (this._baseHide()) { // Need click-eating div to be hidden _Overlay._Overlay._hideClickEatingDivAppBar(); } }, // SettingsFlyout animations _animateSlideIn: function SettingsFlyout_animateSlideIn() { var animateFromLeft = _shouldAnimateFromLeft(); var offset = animateFromLeft ? "-100px" : "100px"; _ElementListUtilities.query("div.win-content", this._element). forEach(function (e) { Animations.enterPage(e, { left: offset }); }); var where, width = this._element.offsetWidth; // Slide in from right side or left side? if (animateFromLeft) { // RTL where = { top: "0px", left: "-" + width + "px" }; this._element.style.right = "auto"; this._element.style.left = "0px"; } else { // From right side where = { top: "0px", left: width + "px" }; this._element.style.right = "0px"; this._element.style.left = "auto"; } this._element.style.opacity = 1; this._element.style.visibility = "visible"; return Animations.showPanel(this._element, where); }, _animateSlideOut: function SettingsFlyout_animateSlideOut() { var where, width = this._element.offsetWidth; if (_shouldAnimateFromLeft()) { // RTL where = { top: "0px", left: width + "px" }; this._element.style.right = "auto"; this._element.style.left = "-" + width + "px"; } else { // From right side where = { top: "0px", left: "-" + width + "px" }; this._element.style.right = "-" + width + "px"; this._element.style.left = "auto"; } return Animations.showPanel(this._element, where); }, _fragmentDiv: { get: function SettingsFlyout_fragmentDiv_get() { return this._fragDiv; }, set: function SettingsFlyout_fragmentDiv_set(value) { this._fragDiv = value; } }, _unloadPage: function SettingsFlyout_unloadPage(event) { var settingsControl = event.currentTarget.winControl; settingsControl.removeEventListener(_Overlay._Overlay.afterHide, this._unloadPage, false); Promise.as().then(function () { if (settingsControl._fragmentDiv) { _Global.document.body.removeChild(settingsControl._fragmentDiv); settingsControl._fragmentDiv = null; } }); }, _dismiss: function SettingsFlyout_dismiss() { this.addEventListener(_Overlay._Overlay.afterHide, this._unloadPage, false); this._hide(); }, _handleKeyDown: function SettingsFlyout_handleKeyDown(event) { if (event.keyCode === Key.escape) { event.preventDefault(); event.stopPropagation(); this.winControl._dismiss(); } else if ((event.keyCode === Key.space || event.keyCode === Key.enter) && (this.children[0] === _Global.document.activeElement)) { event.preventDefault(); event.stopPropagation(); this.winControl._dismiss(); } else if (event.shiftKey && event.keyCode === Key.tab && this.children[0] === _Global.document.activeElement) { event.preventDefault(); event.stopPropagation(); var _elms = this.getElementsByTagName("*"); for (var i = _elms.length - 2; i >= 0; i--) { _elms[i].focus(); if (_elms[i] === _Global.document.activeElement) { break; } } } }, _focusOnLastFocusableElementFromParent: function SettingsFlyout_focusOnLastFocusableElementFromParent() { var active = _Global.document.activeElement; if (!settingsPageIsFocusedOnce || !active || !_ElementUtilities.hasClass(active, _Constants.firstDivClass)) { return; } var _elms = this.parentElement.getElementsByTagName("*"); // There should be at least 1 element in addition to the firstDiv & finalDiv if (_elms.length <= 2) { return; } // Get the tabIndex set to the finalDiv (which is the highest) var _highestTabIndex = _elms[_elms.length - 1].tabIndex; // If there are positive tabIndices, set focus to the element with the highest tabIndex. // Otherwise set focus to the last focusable element in DOM order. var i; if (_highestTabIndex) { for (i = _elms.length - 2; i > 0; i--) { if (_elms[i].tabIndex === _highestTabIndex) { _elms[i].focus(); break; } } } else { for (i = _elms.length - 2; i > 0; i--) { // Skip
with undefined tabIndex (To work around Win8 bug #622245) if ((_elms[i].tagName !== "DIV") || (_elms[i].getAttribute("tabIndex") !== null)) { _elms[i].focus(); if (_elms[i] === _Global.document.activeElement) { break; } } } } }, _focusOnFirstFocusableElementFromParent: function SettingsFlyout_focusOnFirstFocusableElementFromParent() { var active = _Global.document.activeElement; if (!active || !_ElementUtilities.hasClass(active, _Constants.finalDivClass)) { return; } var _elms = this.parentElement.getElementsByTagName("*"); // There should be at least 1 element in addition to the firstDiv & finalDiv if (_elms.length <= 2) { return; } // Get the tabIndex set to the firstDiv (which is the lowest) var _lowestTabIndex = _elms[0].tabIndex; // If there are positive tabIndices, set focus to the element with the lowest tabIndex. // Otherwise set focus to the first focusable element in DOM order. var i; if (_lowestTabIndex) { for (i = 1; i < _elms.length - 1; i++) { if (_elms[i].tabIndex === _lowestTabIndex) { _elms[i].focus(); break; } } } else { for (i = 1; i < _elms.length - 1; i++) { // Skip
with undefined tabIndex (To work around Win8 bug #622245) if ((_elms[i].tagName !== "DIV") || (_elms[i].getAttribute("tabIndex") !== null)) { _elms[i].focus(); if (_elms[i] === _Global.document.activeElement) { break; } } } } }, // Create and add a new first div to the beginning of the list _addFirstDiv: function SettingsFlyout_addFirstDiv() { var _elms = this._element.getElementsByTagName("*"); var _minTab = 0; for (var i = 0; i < _elms.length; i++) { if ((0 < _elms[i].tabIndex) && (_minTab === 0 || _elms[i].tabIndex < _minTab)) { _minTab = _elms[i].tabIndex; } } var firstDiv = _Global.document.createElement("div"); firstDiv.className = _Constants.firstDivClass; firstDiv.style.display = "inline"; firstDiv.setAttribute("role", "menuitem"); firstDiv.setAttribute("aria-hidden", "true"); firstDiv.tabIndex = _minTab; _ElementUtilities._addEventListener(firstDiv, "focusin", this._focusOnLastFocusableElementFromParent, false); // add to beginning if (this._element.children[0]) { this._element.insertBefore(firstDiv, this._element.children[0]); } else { this._element.appendChild(firstDiv); } }, // Create and add a new final div to the end of the list _addFinalDiv: function SettingsFlyout_addFinalDiv() { var _elms = this._element.getElementsByTagName("*"); var _maxTab = 0; for (var i = 0; i < _elms.length; i++) { if (_elms[i].tabIndex > _maxTab) { _maxTab = _elms[i].tabIndex; } } var finalDiv = _Global.document.createElement("div"); finalDiv.className = _Constants.finalDivClass; finalDiv.style.display = "inline"; finalDiv.setAttribute("role", "menuitem"); finalDiv.setAttribute("aria-hidden", "true"); finalDiv.tabIndex = _maxTab; _ElementUtilities._addEventListener(finalDiv, "focusin", this._focusOnFirstFocusableElementFromParent, false); this._element.appendChild(finalDiv); }, _writeProfilerMark: function SettingsFlyout_writeProfilerMark(text) { _WriteProfilerMark("WinJS.UI.SettingsFlyout:" + this._id + ":" + text); } }); // Statics SettingsFlyout.show = function () { /// /// /// Shows the SettingsPane UI, if hidden, regardless of other states. /// /// /// /// Show the main settings pane if (_WinRT.Windows.UI.ApplicationSettings.SettingsPane) { _WinRT.Windows.UI.ApplicationSettings.SettingsPane.show(); } // And hide the WWA one var elements = _Global.document.querySelectorAll('div[data-win-control="WinJS.UI.SettingsFlyout"]'); var len = elements.length; for (var i = 0; i < len; i++) { var settingsFlyout = elements[i].winControl; if (settingsFlyout) { settingsFlyout._dismiss(); } } }; var _settingsEvent = { event: undefined }; SettingsFlyout.populateSettings = function (e) { /// /// /// Loads a portion of the SettingsFlyout. Your app calls this when the user invokes a settings command and the WinJS.Application.onsettings event occurs. /// /// /// An object that contains information about the event, received from the WinJS.Application.onsettings event. The detail property of this object contains /// the applicationcommands sub-property that you set to an array of settings commands. /// /// /// _settingsEvent.event = e.detail; if (_settingsEvent.event.applicationcommands) { var n = _WinRT.Windows.UI.ApplicationSettings; Object.keys(_settingsEvent.event.applicationcommands).forEach(function (name) { var setting = _settingsEvent.event.applicationcommands[name]; if (!setting.title) { setting.title = name; } var command = new n.SettingsCommand(name, setting.title, SettingsFlyout._onSettingsCommand); _settingsEvent.event.e.request.applicationCommands.append(command); }); } }; SettingsFlyout._onSettingsCommand = function (command) { var id = command.id; if (_settingsEvent.event.applicationcommands && _settingsEvent.event.applicationcommands[id]) { SettingsFlyout.showSettings(id, _settingsEvent.event.applicationcommands[id].href); } }; SettingsFlyout.showSettings = function (id, path) { /// /// /// Show the SettingsFlyout using the settings element identifier (ID) and the path of the page that contains the settings element. /// /// /// The ID of the settings element. /// /// /// The path of the page that contains the settings element. /// /// /// var control = _getChildSettingsControl(_Global.document, id); if (control) { control.show(); } else if (path) { var divElement = _Global.document.createElement("div"); divElement = _Global.document.body.appendChild(divElement); Pages.render(path, divElement).then(function () { control = _getChildSettingsControl(divElement, id); if (control) { control._fragmentDiv = divElement; control.show(); } else { _Global.document.body.removeChild(divElement); } }); } else { throw new _ErrorFromName("WinJS.UI.SettingsFlyout.BadReference", strings.badReference); } }; var strings = { get ariaLabel() { return _Resources._getWinJSString("ui/settingsFlyoutAriaLabel").value; }, get badReference() { return "Invalid argument: Invalid href to settings flyout fragment"; }, get backbuttonAriaLabel() { return _Resources._getWinJSString("ui/backbuttonarialabel").value; }, get widthDeprecationMessage() { return "SettingsFlyout.width may be altered or unavailable in future versions. Instead, style the CSS width property on elements with the .win-settingsflyout class."; }, }; return SettingsFlyout; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/NavBar/_Command',[ 'exports', '../../Core/_Global', '../../Core/_Base', '../../Core/_ErrorFromName', '../../Core/_Resources', '../../ControlProcessor', '../../Navigation', '../../Utilities/_Control', '../../Utilities/_ElementUtilities', '../AppBar/_Icon' ], function NavBarCommandInit(exports, _Global, _Base, _ErrorFromName, _Resources, ControlProcessor, Navigation, _Control, _ElementUtilities, _Icon) { "use strict"; _Base.Namespace._moduleDefine(exports, "WinJS.UI", { _WinPressed: _Base.Namespace._lazy(function () { var WinPressed = _Base.Class.define(function _WinPressed_ctor(element) { // WinPressed is the combination of :hover:active // :hover is delayed by trident for touch by 300ms so if you want :hover:active to work quickly you need to // use this behavior. // :active does not bubble to its parent like :hover does so this is also useful for that scenario. this._element = element; _ElementUtilities._addEventListener(this._element, "pointerdown", this._MSPointerDownButtonHandler.bind(this)); }, { _MSPointerDownButtonHandler: function _WinPressed_MSPointerDownButtonHandler(ev) { if (!this._pointerUpBound) { this._pointerUpBound = this._MSPointerUpHandler.bind(this); this._pointerCancelBound = this._MSPointerCancelHandler.bind(this); this._pointerOverBound = this._MSPointerOverHandler.bind(this); this._pointerOutBound = this._MSPointerOutHandler.bind(this); } if (ev.isPrimary) { if (this._pointerId) { this._resetPointer(); } if (!_ElementUtilities._matchesSelector(ev.target, ".win-interactive, .win-interactive *")) { this._pointerId = ev.pointerId; _ElementUtilities._addEventListener(_Global, "pointerup", this._pointerUpBound, true); _ElementUtilities._addEventListener(_Global, "pointercancel", this._pointerCancelBound), true; _ElementUtilities._addEventListener(this._element, "pointerover", this._pointerOverBound, true); _ElementUtilities._addEventListener(this._element, "pointerout", this._pointerOutBound, true); _ElementUtilities.addClass(this._element, WinPressed.winPressed); } } }, _MSPointerOverHandler: function _WinPressed_MSPointerOverHandler(ev) { if (this._pointerId === ev.pointerId) { _ElementUtilities.addClass(this._element, WinPressed.winPressed); } }, _MSPointerOutHandler: function _WinPressed_MSPointerOutHandler(ev) { if (this._pointerId === ev.pointerId) { _ElementUtilities.removeClass(this._element, WinPressed.winPressed); } }, _MSPointerCancelHandler: function _WinPressed_MSPointerCancelHandler(ev) { if (this._pointerId === ev.pointerId) { this._resetPointer(); } }, _MSPointerUpHandler: function _WinPressed_MSPointerUpHandler(ev) { if (this._pointerId === ev.pointerId) { this._resetPointer(); } }, _resetPointer: function _WinPressed_resetPointer() { this._pointerId = null; _ElementUtilities._removeEventListener(_Global, "pointerup", this._pointerUpBound, true); _ElementUtilities._removeEventListener(_Global, "pointercancel", this._pointerCancelBound, true); _ElementUtilities._removeEventListener(this._element, "pointerover", this._pointerOverBound, true); _ElementUtilities._removeEventListener(this._element, "pointerout", this._pointerOutBound, true); _ElementUtilities.removeClass(this._element, WinPressed.winPressed); }, dispose: function _WinPressed_dispose() { if (this._disposed) { return; } this._disposed = true; this._resetPointer(); } }, { winPressed: "win-pressed" }); return WinPressed; }), /// /// /// Represents a navigation command in an NavBarContainer. /// /// /// /// /// ///
]]> /// Styles the entire NavBarCommand control. /// Styles the main button in a NavBarCommand. /// Styles the split button in a NavBarCommand /// Styles the icon in the main button of a NavBarCommand. /// Styles the label in the main button of a NavBarCommand. /// /// /// NavBarCommand: _Base.Namespace._lazy(function () { var Key = _ElementUtilities.Key; var strings = { get duplicateConstruction() { return "Invalid argument: Controls may only be instantiated one time for each DOM element"; } }; var NavBarCommand = _Base.Class.define(function NavBarCommand_ctor(element, options) { /// /// /// Creates a new NavBarCommand. /// /// /// The DOM element that will host the new NavBarCommand control. /// /// /// An object that contains one or more property/value pairs to apply to the new control. /// Each property of the options object corresponds to one of the control's properties or events. /// Event names must begin with "on". /// /// /// The new NavBarCommand. /// /// /// element = element || _Global.document.createElement("DIV"); options = options || {}; if (element.winControl) { throw new _ErrorFromName("WinJS.UI.NavBarCommand.DuplicateConstruction", strings.duplicateConstruction); } // Attaching JS control to DOM element element.winControl = this; this._element = element; _ElementUtilities.addClass(this.element, NavBarCommand._ClassName.navbarcommand); _ElementUtilities.addClass(this.element, "win-disposable"); this._tooltip = null; this._splitOpened = false; this._buildDom(); element.addEventListener('keydown', this._keydownHandler.bind(this)); _Control.setOptions(this, options); }, { /// element: { get: function () { return this._element; } }, /// /// Gets or sets the label of the NavBarCommand. /// /// label: { get: function () { return this._label; }, set: function (value) { this._label = value; this._labelEl.textContent = value; } }, /// /// Gets or sets the tooltip of the NavBarCommand. /// /// tooltip: { get: function () { return this._tooltip; }, set: function (value) { this._tooltip = value; if (this._tooltip || this._tooltip === "") { this._element.setAttribute('title', this._tooltip); } else { this._element.removeAttribute('title'); } } }, /// /// Gets or sets the icon of the NavBarCommand. This value is either one of the values of the AppBarIcon enumeration or the path of a custom PNG file. /// /// icon: { get: function () { return this._icon; }, set: function (value) { this._icon = (_Icon[value] || value); // If the icon's a single character, presume a glyph if (this._icon && this._icon.length === 1) { // Set the glyph this._imageSpan.textContent = this._icon; this._imageSpan.style.backgroundImage = ""; this._imageSpan.style.msHighContrastAdjust = ""; this._imageSpan.style.display = ""; } else if (this._icon && this._icon.length > 1) { // Must be an image, set that this._imageSpan.textContent = ""; this._imageSpan.style.backgroundImage = this._icon; this._imageSpan.style.msHighContrastAdjust = "none"; this._imageSpan.style.display = ""; } else { this._imageSpan.textContent = ""; this._imageSpan.style.backgroundImage = ""; this._imageSpan.style.msHighContrastAdjust = ""; this._imageSpan.style.display = "none"; } } }, /// /// Gets or sets the command's target location. /// /// location: { get: function () { return this._location; }, set: function (value) { this._location = value; } }, /// /// Gets or sets the state value used for navigation. The command passes this object to the WinJS.Navigation.navigate function. /// /// state: { get: function () { return this._state; }, set: function (value) { this._state = value; } }, /// /// Gets or sets a value that specifies whether the NavBarCommand has a split button. /// /// splitButton: { get: function () { return this._split; }, set: function (value) { this._split = value; if (this._split) { this._splitButtonEl.style.display = ""; } else { this._splitButtonEl.style.display = "none"; } } }, /// splitOpened: { get: function () { return this._splitOpened; }, set: function (value) { if (this._splitOpened !== !!value) { this._toggleSplit(); } } }, _toggleSplit: function NavBarCommand_toggleSplit() { this._splitOpened = !this._splitOpened; if (this._splitOpened) { _ElementUtilities.addClass(this._splitButtonEl, NavBarCommand._ClassName.navbarcommandsplitbuttonopened); this._splitButtonEl.setAttribute("aria-expanded", "true"); } else { _ElementUtilities.removeClass(this._splitButtonEl, NavBarCommand._ClassName.navbarcommandsplitbuttonopened); this._splitButtonEl.setAttribute("aria-expanded", "false"); } this._fireEvent(NavBarCommand._EventName._splitToggle); }, _rtl: { get: function () { return _Global.getComputedStyle(this.element).direction === "rtl"; } }, _keydownHandler: function NavBarCommand_keydownHandler(ev) { if (_ElementUtilities._matchesSelector(ev.target, ".win-interactive, .win-interactive *")) { return; } var leftStr = this._rtl ? Key.rightArrow : Key.leftArrow; var rightStr = this._rtl ? Key.leftArrow : Key.rightArrow; if (!ev.altKey && (ev.keyCode === leftStr || ev.keyCode === Key.home || ev.keyCode === Key.end) && ev.target === this._splitButtonEl) { _ElementUtilities._setActive(this._buttonEl); if (ev.keyCode === leftStr) { ev.stopPropagation(); } ev.preventDefault(); } else if (!ev.altKey && ev.keyCode === rightStr && this.splitButton && (ev.target === this._buttonEl || this._buttonEl.contains(ev.target))) { _ElementUtilities._setActive(this._splitButtonEl); if (ev.keyCode === rightStr) { ev.stopPropagation(); } ev.preventDefault(); } else if ((ev.keyCode === Key.space || ev.keyCode === Key.enter) && (ev.target === this._buttonEl || this._buttonEl.contains(ev.target))) { if (this.location) { Navigation.navigate(this.location, this.state); } this._fireEvent(NavBarCommand._EventName._invoked); } else if ((ev.keyCode === Key.space || ev.keyCode === Key.enter) && ev.target === this._splitButtonEl) { this._toggleSplit(); } }, _getFocusInto: function NavBarCommand_getFocusInto(keyCode) { var leftStr = this._rtl ? Key.rightArrow : Key.leftArrow; if ((keyCode === leftStr) && this.splitButton) { return this._splitButtonEl; } else { return this._buttonEl; } }, _buildDom: function NavBarCommand_buildDom() { var markup = '' + ''; this.element.insertAdjacentHTML("afterBegin", markup); this._buttonEl = this.element.firstElementChild; this._buttonPressedBehavior = new exports._WinPressed(this._buttonEl); this._contentEl = this._buttonEl.firstElementChild; this._imageSpan = this._contentEl.firstElementChild; this._imageSpan.style.display = "none"; this._labelEl = this._imageSpan.nextElementSibling; this._splitButtonEl = this._buttonEl.nextElementSibling; this._splitButtonPressedBehavior = new exports._WinPressed(this._splitButtonEl); this._splitButtonEl.style.display = "none"; _ElementUtilities._ensureId(this._buttonEl); this._splitButtonEl.setAttribute("aria-labelledby", this._buttonEl.id); this._buttonEl.addEventListener("click", this._handleButtonClick.bind(this)); var mutationObserver = new _ElementUtilities._MutationObserver(this._splitButtonAriaExpandedPropertyChangeHandler.bind(this)); mutationObserver.observe(this._splitButtonEl, { attributes: true, attributeFilter: ["aria-expanded"] }); this._splitButtonEl.addEventListener("click", this._handleSplitButtonClick.bind(this)); // reparent any other elements. var tempEl = this._splitButtonEl.nextSibling; while (tempEl) { this._buttonEl.insertBefore(tempEl, this._contentEl); if (tempEl.nodeName !== "#text") { ControlProcessor.processAll(tempEl); } tempEl = this._splitButtonEl.nextSibling; } }, _handleButtonClick: function NavBarCommand_handleButtonClick(ev) { var srcElement = ev.target; if (!_ElementUtilities._matchesSelector(srcElement, ".win-interactive, .win-interactive *")) { if (this.location) { Navigation.navigate(this.location, this.state); } this._fireEvent(NavBarCommand._EventName._invoked); } }, _splitButtonAriaExpandedPropertyChangeHandler: function NavBarCommand_splitButtonAriaExpandedPropertyChangeHandler() { if ((this._splitButtonEl.getAttribute("aria-expanded") === "true") !== this._splitOpened) { this._toggleSplit(); } }, _handleSplitButtonClick: function NavBarCommand_handleSplitButtonClick() { this._toggleSplit(); }, _fireEvent: function NavBarCommand_fireEvent(type, detail) { var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent(type, true, false, detail); this.element.dispatchEvent(event); }, dispose: function NavBarCommand_dispose() { /// /// /// Disposes this control. /// /// /// if (this._disposed) { return; } this._disposed = true; this._buttonPressedBehavior.dispose(); this._splitButtonPressedBehavior.dispose(); } }, { _ClassName: { navbarcommand: "win-navbarcommand", navbarcommandbutton: "win-navbarcommand-button", navbarcommandbuttoncontent: "win-navbarcommand-button-content", navbarcommandsplitbutton: "win-navbarcommand-splitbutton", navbarcommandsplitbuttonopened: "win-navbarcommand-splitbutton-opened", navbarcommandicon: "win-navbarcommand-icon", navbarcommandlabel: "win-navbarcommand-label" }, _EventName: { _invoked: "_invoked", _splitToggle: "_splittoggle" } }); _Base.Class.mix(NavBarCommand, _Control.DOMEventMixin); return NavBarCommand; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/NavBar/_Container',[ 'exports', '../../Core/_Global', '../../Core/_Base', '../../Core/_BaseUtils', '../../Core/_ErrorFromName', '../../Core/_Events', '../../Core/_Log', '../../Core/_Resources', '../../Core/_WriteProfilerMark', '../../Animations', '../../Animations/_TransitionAnimation', '../../BindingList', '../../ControlProcessor', '../../Navigation', '../../Promise', '../../Scheduler', '../../Utilities/_Control', '../../Utilities/_ElementUtilities', '../../Utilities/_KeyboardBehavior', '../../Utilities/_UI', '../AppBar/_Constants', '../Repeater', './_Command' ], function NavBarContainerInit(exports, _Global, _Base, _BaseUtils, _ErrorFromName, _Events, _Log, _Resources, _WriteProfilerMark, Animations, _TransitionAnimation, BindingList, ControlProcessor, Navigation, Promise, Scheduler, _Control, _ElementUtilities, _KeyboardBehavior, _UI, _Constants, Repeater, _Command) { "use strict"; function nobodyHasFocus() { return _Global.document.activeElement === null || _Global.document.activeElement === _Global.document.body; } _Base.Namespace._moduleDefine(exports, "WinJS.UI", { /// /// /// Contains a group of NavBarCommand objects in a NavBar. /// /// /// /// /// /// ///
///
]]> /// Raised when a NavBarCommand is invoked. /// Raised when the split button on a NavBarCommand is toggled. /// Styles the entire NavBarContainer control. /// /// Styles the page indication for the NavBarContainer. /// /// Styles the page indication for each page. /// /// Styles the indication of the current page. /// /// Styles the area that contains items for the NavBarContainer. /// Styles left and right navigation arrows. /// Styles the left navigation arrow. /// Styles the right navigation arrow. /// /// /// NavBarContainer: _Base.Namespace._lazy(function () { var Key = _ElementUtilities.Key; var buttonFadeDelay = 3000; var PT_TOUCH = _ElementUtilities._MSPointerEvent.MSPOINTER_TYPE_TOUCH || "touch"; var MS_MANIPULATION_STATE_STOPPED = 0; var createEvent = _Events._createEventProperty; var eventNames = { invoked: "invoked", splittoggle: "splittoggle" }; var strings = { get duplicateConstruction() { return "Invalid argument: Controls may only be instantiated one time for each DOM element"; }, get navBarContainerViewportAriaLabel() { return _Resources._getWinJSString("ui/navBarContainerViewportAriaLabel").value; } }; var NavBarContainer = _Base.Class.define(function NavBarContainer_ctor(element, options) { /// /// /// Creates a new NavBarContainer. /// /// /// The DOM element that will host the NavBarContainer control. /// /// /// An object that contains one or more property/value pairs to apply to the new control. /// Each property of the options object corresponds to one of the control's properties or events. /// Event names must begin with "on". /// /// /// The new NavBarContainer. /// /// /// element = element || _Global.document.createElement("DIV"); this._id = element.id || _ElementUtilities._uniqueID(element); this._writeProfilerMark("constructor,StartTM"); options = options || {}; if (element.winControl) { throw new _ErrorFromName("WinJS.UI.NavBarContainer.DuplicateConstruction", strings.duplicateConstruction); } // Attaching JS control to DOM element element.winControl = this; this._element = element; _ElementUtilities.addClass(this.element, NavBarContainer._ClassName.navbarcontainer); _ElementUtilities.addClass(this.element, "win-disposable"); if (!element.getAttribute("tabIndex")) { element.tabIndex = -1; } this._focusCurrentItemPassivelyBound = this._focusCurrentItemPassively.bind(this); this._closeSplitAndResetBound = this._closeSplitAndReset.bind(this); this._currentManipulationState = MS_MANIPULATION_STATE_STOPPED; this._panningDisabled = !_ElementUtilities._supportsSnapPoints; this._fixedSize = false; this._maxRows = 1; this._sizes = {}; this._setupTree(); this._duringConstructor = true; this._dataChangingBound = this._dataChanging.bind(this); this._dataChangedBound = this._dataChanged.bind(this); Navigation.addEventListener('navigated', this._closeSplitAndResetBound); // Don't use set options for the properties so we can control the ordering to avoid rendering multiple times. this.layout = options.layout || _UI.Orientation.horizontal; if (options.maxRows) { this.maxRows = options.maxRows; } if (options.template) { this.template = options.template; } if (options.data) { this.data = options.data; } if (options.fixedSize) { this.fixedSize = options.fixedSize; } // Events only _Control._setOptions(this, options, true); this._duringConstructor = false; if (options.currentIndex) { this.currentIndex = options.currentIndex; } this._updatePageUI(); Scheduler.schedule(function NavBarContainer_async_initialize() { this._updateAppBarReference(); }, Scheduler.Priority.normal, this, "WinJS.UI.NavBarContainer_async_initialize"); this._writeProfilerMark("constructor,StopTM"); }, { /// element: { get: function () { return this._element; } }, /// /// Gets or sets a Template or custom rendering function that defines the HTML of each item within the NavBarContainer. /// /// template: { get: function () { return this._template; }, set: function (value) { this._template = value; if (this._repeater) { var hadFocus = this.element.contains(_Global.document.activeElement); if (!this._duringConstructor) { this._closeSplitIfOpen(); } // the repeater's template is wired up to this._render() so just resetting it will rebuild the tree. this._repeater.template = this._repeater.template; if (!this._duringConstructor) { this._measured = false; this._sizes.itemMeasured = false; this._reset(); if (hadFocus) { this._keyboardBehavior._focus(0); } } } } }, _render: function NavBarContainer_render(item) { var navbarCommandEl = _Global.document.createElement('div'); var template = this._template; if (template) { if (template.render) { template.render(item, navbarCommandEl); } else if (template.winControl && template.winControl.render) { template.winControl.render(item, navbarCommandEl); } else { navbarCommandEl.appendChild(template(item)); } } // Create the NavBarCommand after calling render so that the reparenting in navbarCommand works. var navbarCommand = new _Command.NavBarCommand(navbarCommandEl, item); return navbarCommand._element; }, /// /// Gets or sets the WinJS.Binding.List that provides the NavBarContainer with items to display. /// /// data: { get: function () { return this._repeater && this._repeater.data; }, set: function (value) { if (!value) { value = new BindingList.List(); } if (!this._duringConstructor) { this._closeSplitIfOpen(); } this._removeDataChangingEvents(); this._removeDataChangedEvents(); var hadFocus = this.element.contains(_Global.document.activeElement); if (!this._repeater) { this._surfaceEl.innerHTML = ""; this._repeater = new Repeater.Repeater(this._surfaceEl, { template: this._render.bind(this) }); } this._addDataChangingEvents(value); this._repeater.data = value; this._addDataChangedEvents(value); if (!this._duringConstructor) { this._measured = false; this._sizes.itemMeasured = false; this._reset(); if (hadFocus) { this._keyboardBehavior._focus(0); } } } }, /// /// Gets or sets the number of rows allowed to be used before items are placed on additional pages. /// /// maxRows: { get: function () { return this._maxRows; }, set: function (value) { value = (+value === value) ? value : 1; this._maxRows = Math.max(1, value); if (!this._duringConstructor) { this._closeSplitIfOpen(); this._measured = false; this._reset(); } } }, /// /// Gets or sets a value that specifies whether the NavBarContainer has a horizontal or vertical layout. The default is "horizontal". /// /// layout: { get: function () { return this._layout; }, set: function (value) { if (value === _UI.Orientation.vertical) { this._layout = _UI.Orientation.vertical; _ElementUtilities.removeClass(this.element, NavBarContainer._ClassName.horizontal); _ElementUtilities.addClass(this.element, NavBarContainer._ClassName.vertical); } else { this._layout = _UI.Orientation.horizontal; _ElementUtilities.removeClass(this.element, NavBarContainer._ClassName.vertical); _ElementUtilities.addClass(this.element, NavBarContainer._ClassName.horizontal); } this._viewportEl.style.msScrollSnapType = ""; this._zooming = false; if (!this._duringConstructor) { this._measured = false; this._sizes.itemMeasured = false; this._ensureVisible(this._keyboardBehavior.currentIndex, true); this._updatePageUI(); this._closeSplitIfOpen(); } } }, /// currentIndex: { get: function () { return this._keyboardBehavior.currentIndex; }, set: function (value) { if (value === +value) { var hadFocus = this.element.contains(_Global.document.activeElement); this._keyboardBehavior.currentIndex = value; this._ensureVisible(this._keyboardBehavior.currentIndex, true); if (hadFocus) { this._keyboardBehavior._focus(); } } } }, /// /// Gets or sets a value that specifies whether child NavBarCommand objects should be a fixed width when there are multiple pages. A value of true indicates /// that the NavBarCommand objects use a fixed width; a value of false indicates that they use a dynamic width. /// /// fixedSize: { get: function () { return this._fixedSize; }, set: function (value) { this._fixedSize = !!value; if (!this._duringConstructor) { this._closeSplitIfOpen(); if (!this._measured) { this._measure(); } else if (this._surfaceEl.children.length > 0) { this._updateGridStyles(); } } } }, /// /// Raised when a NavBarCommand has been invoked. /// /// oninvoked: createEvent(eventNames.invoked), /// /// Raised when the split button on a NavBarCommand is toggled. /// /// onsplittoggle: createEvent(eventNames.splittoggle), forceLayout: function NavBarContainer_forceLayout() { /// /// /// Forces the NavBarContainer to update scroll positions and if the NavBar has changed size, it will also re-measure. /// Use this function when making the NavBarContainer visible again after you set its style.display property to "none". /// /// /// this._resizeHandler(); if (this._measured) { this._scrollPosition = _ElementUtilities.getScrollPosition(this._viewportEl)[(this.layout === _UI.Orientation.horizontal ? "scrollLeft" : "scrollTop")]; } this._duringForceLayout = true; this._ensureVisible(this._keyboardBehavior.currentIndex, true); this._updatePageUI(); this._duringForceLayout = false; }, _updateAppBarReference: function NavBarContainer_updateAppBarReference() { if (!this._appBarEl || !this._appBarEl.contains(this.element)) { if (this._appBarEl) { this._appBarEl.removeEventListener('beforeshow', this._closeSplitAndResetBound); this._appBarEl.removeEventListener('beforeshow', this._resizeImplBound); this._appBarEl.removeEventListener('aftershow', this._focusCurrentItemPassivelyBound); } var appBarEl = this.element.parentNode; while (appBarEl && !_ElementUtilities.hasClass(appBarEl, _Constants.appBarClass)) { appBarEl = appBarEl.parentNode; } this._appBarEl = appBarEl; if (this._appBarEl) { this._appBarEl.addEventListener('beforeshow', this._closeSplitAndResetBound); this._appBarEl.addEventListener('aftershow', this._focusCurrentItemPassivelyBound); } } }, _closeSplitAndReset: function NavBarContainer_closeSplitAndReset() { this._closeSplitIfOpen(); this._reset(); }, _dataChanging: function NavBarContainer_dataChanging(ev) { // Store the element that was active so that we can detect // if the focus went away because of the data change. this._elementHadFocus = _Global.document.activeElement; if (this._currentSplitNavItem && this._currentSplitNavItem.splitOpened) { if (ev.type === "itemremoved") { if (this._surfaceEl.children[ev.detail.index].winControl === this._currentSplitNavItem) { this._closeSplitIfOpen(); } } else if (ev.type === "itemchanged") { if (this._surfaceEl.children[ev.detail.index].winControl === this._currentSplitNavItem) { this._closeSplitIfOpen(); } } else if (ev.type === "itemmoved") { if (this._surfaceEl.children[ev.detail.oldIndex].winControl === this._currentSplitNavItem) { this._closeSplitIfOpen(); } } else if (ev.type === "reload") { this._closeSplitIfOpen(); } } }, _dataChanged: function NavBarContainer_dataChanged(ev) { this._measured = false; if (ev.type === "itemremoved") { if (ev.detail.index < this._keyboardBehavior.currentIndex) { this._keyboardBehavior.currentIndex--; } else if (ev.detail.index === this._keyboardBehavior.currentIndex) { // This clamps if the item being removed was the last item in the list this._keyboardBehavior.currentIndex = this._keyboardBehavior.currentIndex; if (nobodyHasFocus() && this._elementHadFocus) { this._keyboardBehavior._focus(); } } } else if (ev.type === "itemchanged") { if (ev.detail.index === this._keyboardBehavior.currentIndex) { if (nobodyHasFocus() && this._elementHadFocus) { this._keyboardBehavior._focus(); } } } else if (ev.type === "iteminserted") { if (ev.detail.index <= this._keyboardBehavior.currentIndex) { this._keyboardBehavior.currentIndex++; } } else if (ev.type === "itemmoved") { if (ev.detail.oldIndex === this._keyboardBehavior.currentIndex) { this._keyboardBehavior.currentIndex = ev.detail.newIndex; if (nobodyHasFocus() && this._elementHadFocus) { this._keyboardBehavior._focus(); } } } else if (ev.type === "reload") { this._keyboardBehavior.currentIndex = 0; if (nobodyHasFocus() && this._elementHadFocus) { this._keyboardBehavior._focus(); } } this._ensureVisible(this._keyboardBehavior.currentIndex, true); this._updatePageUI(); }, _focusCurrentItemPassively: function NavBarContainer_focusCurrentItemPassively() { if (this.element.contains(_Global.document.activeElement)) { this._keyboardBehavior._focus(); } }, _reset: function NavBarContainer_reset() { this._keyboardBehavior.currentIndex = 0; if (this.element.contains(_Global.document.activeElement)) { this._keyboardBehavior._focus(0); } this._viewportEl.style.msScrollSnapType = ""; this._zooming = false; this._ensureVisible(0, true); this._updatePageUI(); }, _removeDataChangedEvents: function NavBarContainer_removeDataChangedEvents() { if (this._repeater) { this._repeater.data.removeEventListener("itemchanged", this._dataChangedBound); this._repeater.data.removeEventListener("iteminserted", this._dataChangedBound); this._repeater.data.removeEventListener("itemmoved", this._dataChangedBound); this._repeater.data.removeEventListener("itemremoved", this._dataChangedBound); this._repeater.data.removeEventListener("reload", this._dataChangedBound); } }, _addDataChangedEvents: function NavBarContainer_addDataChangedEvents() { if (this._repeater) { this._repeater.data.addEventListener("itemchanged", this._dataChangedBound); this._repeater.data.addEventListener("iteminserted", this._dataChangedBound); this._repeater.data.addEventListener("itemmoved", this._dataChangedBound); this._repeater.data.addEventListener("itemremoved", this._dataChangedBound); this._repeater.data.addEventListener("reload", this._dataChangedBound); } }, _removeDataChangingEvents: function NavBarContainer_removeDataChangingEvents() { if (this._repeater) { this._repeater.data.removeEventListener("itemchanged", this._dataChangingBound); this._repeater.data.removeEventListener("iteminserted", this._dataChangingBound); this._repeater.data.removeEventListener("itemmoved", this._dataChangingBound); this._repeater.data.removeEventListener("itemremoved", this._dataChangingBound); this._repeater.data.removeEventListener("reload", this._dataChangingBound); } }, _addDataChangingEvents: function NavBarContainer_addDataChangingEvents(bindingList) { bindingList.addEventListener("itemchanged", this._dataChangingBound); bindingList.addEventListener("iteminserted", this._dataChangingBound); bindingList.addEventListener("itemmoved", this._dataChangingBound); bindingList.addEventListener("itemremoved", this._dataChangingBound); bindingList.addEventListener("reload", this._dataChangingBound); }, _mouseleave: function NavBarContainer_mouseleave() { if (this._mouseInViewport) { this._mouseInViewport = false; this._updateArrows(); } }, _MSPointerDown: function NavBarContainer_MSPointerDown(ev) { if (ev.pointerType === PT_TOUCH) { if (this._mouseInViewport) { this._mouseInViewport = false; this._updateArrows(); } } }, _MSPointerMove: function NavBarContainer_MSPointerMove(ev) { if (ev.pointerType !== PT_TOUCH) { if (!this._mouseInViewport) { this._mouseInViewport = true; this._updateArrows(); } } }, _setupTree: function NavBarContainer_setupTree() { this._animateNextPreviousButtons = Promise.wrap(); this._element.addEventListener('mouseleave', this._mouseleave.bind(this)); _ElementUtilities._addEventListener(this._element, 'pointerdown', this._MSPointerDown.bind(this)); _ElementUtilities._addEventListener(this._element, 'pointermove', this._MSPointerMove.bind(this)); _ElementUtilities._addEventListener(this._element, "focusin", this._focusHandler.bind(this), false); this._pageindicatorsEl = _Global.document.createElement('div'); _ElementUtilities.addClass(this._pageindicatorsEl, NavBarContainer._ClassName.pageindicators); this._element.appendChild(this._pageindicatorsEl); this._ariaStartMarker = _Global.document.createElement("div"); this._element.appendChild(this._ariaStartMarker); this._viewportEl = _Global.document.createElement('div'); _ElementUtilities.addClass(this._viewportEl, NavBarContainer._ClassName.viewport); this._element.appendChild(this._viewportEl); this._viewportEl.setAttribute("role", "group"); this._viewportEl.setAttribute("aria-label", strings.navBarContainerViewportAriaLabel); this._boundResizeHandler = this._resizeHandler.bind(this); _ElementUtilities._resizeNotifier.subscribe(this._element, this._boundResizeHandler); this._viewportEl.addEventListener("mselementresize", this._resizeHandler.bind(this)); this._viewportEl.addEventListener("scroll", this._scrollHandler.bind(this)); this._viewportEl.addEventListener("MSManipulationStateChanged", this._MSManipulationStateChangedHandler.bind(this)); this._ariaEndMarker = _Global.document.createElement("div"); this._element.appendChild(this._ariaEndMarker); this._surfaceEl = _Global.document.createElement('div'); _ElementUtilities.addClass(this._surfaceEl, NavBarContainer._ClassName.surface); this._viewportEl.appendChild(this._surfaceEl); this._surfaceEl.addEventListener("_invoked", this._navbarCommandInvokedHandler.bind(this)); this._surfaceEl.addEventListener("_splittoggle", this._navbarCommandSplitToggleHandler.bind(this)); _ElementUtilities._addEventListener(this._surfaceEl, "focusin", this._itemsFocusHandler.bind(this), false); this._surfaceEl.addEventListener("keydown", this._keyDownHandler.bind(this)); // Reparent NavBarCommands which were in declarative markup var tempEl = this.element.firstElementChild; while (tempEl !== this._pageindicatorsEl) { this._surfaceEl.appendChild(tempEl); ControlProcessor.process(tempEl); tempEl = this.element.firstElementChild; } this._leftArrowEl = _Global.document.createElement('div'); _ElementUtilities.addClass(this._leftArrowEl, NavBarContainer._ClassName.navleftarrow); _ElementUtilities.addClass(this._leftArrowEl, NavBarContainer._ClassName.navarrow); this._element.appendChild(this._leftArrowEl); this._leftArrowEl.addEventListener('click', this._goLeft.bind(this)); this._leftArrowEl.style.opacity = 0; this._leftArrowEl.style.visibility = 'hidden'; this._leftArrowFadeOut = Promise.wrap(); this._rightArrowEl = _Global.document.createElement('div'); _ElementUtilities.addClass(this._rightArrowEl, NavBarContainer._ClassName.navrightarrow); _ElementUtilities.addClass(this._rightArrowEl, NavBarContainer._ClassName.navarrow); this._element.appendChild(this._rightArrowEl); this._rightArrowEl.addEventListener('click', this._goRight.bind(this)); this._rightArrowEl.style.opacity = 0; this._rightArrowEl.style.visibility = 'hidden'; this._rightArrowFadeOut = Promise.wrap(); this._keyboardBehavior = new _KeyboardBehavior._KeyboardBehavior(this._surfaceEl, { scroller: this._viewportEl }); this._winKeyboard = new _KeyboardBehavior._WinKeyboard(this._surfaceEl); }, _goRight: function NavBarContainer_goRight() { if (this._sizes.rtl) { this._goPrev(); } else { this._goNext(); } }, _goLeft: function NavBarContainer_goLeft() { if (this._sizes.rtl) { this._goNext(); } else { this._goPrev(); } }, _goNext: function NavBarContainer_goNext() { this._measure(); var itemsPerPage = this._sizes.rowsPerPage * this._sizes.columnsPerPage; var targetPage = Math.min(Math.floor(this._keyboardBehavior.currentIndex / itemsPerPage) + 1, this._sizes.pages - 1); this._keyboardBehavior.currentIndex = Math.min(itemsPerPage * targetPage, this._surfaceEl.children.length); this._keyboardBehavior._focus(); }, _goPrev: function NavBarContainer_goPrev() { this._measure(); var itemsPerPage = this._sizes.rowsPerPage * this._sizes.columnsPerPage; var targetPage = Math.max(0, Math.floor(this._keyboardBehavior.currentIndex / itemsPerPage) - 1); this._keyboardBehavior.currentIndex = Math.max(itemsPerPage * targetPage, 0); this._keyboardBehavior._focus(); }, _currentPage: { get: function () { if (this.layout === _UI.Orientation.horizontal) { this._measure(); if (this._sizes.viewportOffsetWidth > 0) { return Math.min(this._sizes.pages - 1, Math.round(this._scrollPosition / this._sizes.viewportOffsetWidth)); } } return 0; } }, _resizeHandler: function NavBarContainer_resizeHandler() { if (this._disposed) { return; } if (!this._measured) { return; } var viewportResized = this.layout === _UI.Orientation.horizontal ? this._sizes.viewportOffsetWidth !== parseFloat(_Global.getComputedStyle(this._viewportEl).width) : this._sizes.viewportOffsetHeight !== parseFloat(_Global.getComputedStyle(this._viewportEl).height); if (!viewportResized) { return; } this._measured = false; if (!this._pendingResize) { this._pendingResize = true; this._resizeImplBound = this._resizeImplBound || this._resizeImpl.bind(this); this._updateAppBarReference(); if (this._appBarEl && this._appBarEl.winControl && this._appBarEl.winControl.hidden) { // Do resize lazily. Scheduler.schedule(this._resizeImplBound, Scheduler.Priority.idle, null, "WinJS.UI.NavBarContainer._resizeImpl"); this._appBarEl.addEventListener('beforeshow', this._resizeImplBound); } else { // Do resize now this._resizeImpl(); } } }, _resizeImpl: function NavBarContainer_resizeImpl() { if (!this._disposed && this._pendingResize) { this._pendingResize = false; if (this._appBarEl) { this._appBarEl.removeEventListener('beforeshow', this._resizeImplBound); } this._keyboardBehavior.currentIndex = 0; if (this.element.contains(_Global.document.activeElement)) { this._keyboardBehavior._focus(this._keyboardBehavior.currentIndex); } this._closeSplitIfOpen(); this._ensureVisible(this._keyboardBehavior.currentIndex, true); this._updatePageUI(); } }, _keyDownHandler: function NavBarContainer_keyDownHandler(ev) { var keyCode = ev.keyCode; if (!ev.altKey && (keyCode === Key.pageUp || keyCode === Key.pageDown)) { var srcElement = ev.target; if (_ElementUtilities._matchesSelector(srcElement, ".win-interactive, .win-interactive *")) { return; } var index = this._keyboardBehavior.currentIndex; this._measure(); var sizes = this._sizes; var page = Math.floor(index / (sizes.columnsPerPage * sizes.rowsPerPage)); var scrollPositionTarget = null; if (keyCode === Key.pageUp) { if (this.layout === _UI.Orientation.horizontal) { var indexOfFirstItemOnPage = page * sizes.columnsPerPage * sizes.rowsPerPage; if (index === indexOfFirstItemOnPage && this._surfaceEl.children[index].winControl._buttonEl === _Global.document.activeElement) { // First item on page so go back 1 page. index = index - sizes.columnsPerPage * sizes.rowsPerPage; } else { // Not first item on page so go to the first item on page. index = indexOfFirstItemOnPage; } } else { var currentItem = this._surfaceEl.children[index]; var top = currentItem.offsetTop; var bottom = top + currentItem.offsetHeight; var scrollPosition = this._zooming ? this._zoomPosition : this._scrollPosition; if (top >= scrollPosition && bottom < scrollPosition + sizes.viewportOffsetHeight) { // current item is fully on screen. while (index > 0 && this._surfaceEl.children[index - 1].offsetTop > scrollPosition) { index--; } } if (this._keyboardBehavior.currentIndex === index) { var scrollPositionForOnePageAboveItem = bottom - sizes.viewportOffsetHeight; index = Math.max(0, index - 1); while (index > 0 && this._surfaceEl.children[index - 1].offsetTop > scrollPositionForOnePageAboveItem) { index--; } if (index > 0) { scrollPositionTarget = this._surfaceEl.children[index].offsetTop - this._sizes.itemMarginTop; } else { scrollPositionTarget = 0; } } } index = Math.max(index, 0); this._keyboardBehavior.currentIndex = index; var element = this._surfaceEl.children[index].winControl._buttonEl; if (scrollPositionTarget !== null) { this._scrollTo(scrollPositionTarget); } _ElementUtilities._setActive(element, this._viewportEl); } else { if (this.layout === _UI.Orientation.horizontal) { var indexOfLastItemOnPage = (page + 1) * sizes.columnsPerPage * sizes.rowsPerPage - 1; if (index === indexOfLastItemOnPage) { // Last item on page so go forward 1 page. index = index + sizes.columnsPerPage * sizes.rowsPerPage; } else { // Not Last item on page so go to last item on page. index = indexOfLastItemOnPage; } } else { var currentItem = this._surfaceEl.children[this._keyboardBehavior.currentIndex]; var top = currentItem.offsetTop; var bottom = top + currentItem.offsetHeight; var scrollPosition = this._zooming ? this._zoomPosition : this._scrollPosition; if (top >= scrollPosition && bottom < scrollPosition + sizes.viewportOffsetHeight) { // current item is fully on screen. while (index < this._surfaceEl.children.length - 1 && this._surfaceEl.children[index + 1].offsetTop + this._surfaceEl.children[index + 1].offsetHeight < scrollPosition + sizes.viewportOffsetHeight) { index++; } } if (index === this._keyboardBehavior.currentIndex) { var scrollPositionForOnePageBelowItem = top + sizes.viewportOffsetHeight; index = Math.min(this._surfaceEl.children.length - 1, index + 1); while (index < this._surfaceEl.children.length - 1 && this._surfaceEl.children[index + 1].offsetTop + this._surfaceEl.children[index + 1].offsetHeight < scrollPositionForOnePageBelowItem) { index++; } if (index < this._surfaceEl.children.length - 1) { scrollPositionTarget = this._surfaceEl.children[index + 1].offsetTop - this._sizes.viewportOffsetHeight; } else { scrollPositionTarget = this._scrollLength - this._sizes.viewportOffsetHeight; } } } index = Math.min(index, this._surfaceEl.children.length - 1); this._keyboardBehavior.currentIndex = index; var element = this._surfaceEl.children[index].winControl._buttonEl; if (scrollPositionTarget !== null) { this._scrollTo(scrollPositionTarget); } try { _ElementUtilities._setActive(element, this._viewportEl); } catch (e) { } } } }, _focusHandler: function NavBarContainer_focusHandler(ev) { var srcElement = ev.target; if (!this._surfaceEl.contains(srcElement)) { // Forward focus from NavBarContainer, viewport or surface to the currentIndex. this._skipEnsureVisible = true; this._keyboardBehavior._focus(this._keyboardBehavior.currentIndex); } }, _itemsFocusHandler: function NavBarContainer_itemsFocusHandler(ev) { // Find the item which is being focused and scroll it to view. var srcElement = ev.target; if (srcElement === this._surfaceEl) { return; } while (srcElement.parentNode !== this._surfaceEl) { srcElement = srcElement.parentNode; } var index = -1; while (srcElement) { index++; srcElement = srcElement.previousSibling; } if (this._skipEnsureVisible) { this._skipEnsureVisible = false; } else { this._ensureVisible(index); } }, _ensureVisible: function NavBarContainer_ensureVisible(index, withoutAnimation) { this._measure(); if (this.layout === _UI.Orientation.horizontal) { var page = Math.floor(index / (this._sizes.rowsPerPage * this._sizes.columnsPerPage)); this._scrollTo(page * this._sizes.viewportOffsetWidth, withoutAnimation); } else { var element = this._surfaceEl.children[index]; var maxScrollPosition; if (index > 0) { maxScrollPosition = element.offsetTop - this._sizes.itemMarginTop; } else { maxScrollPosition = 0; } var minScrollPosition; if (index < this._surfaceEl.children.length - 1) { minScrollPosition = this._surfaceEl.children[index + 1].offsetTop - this._sizes.viewportOffsetHeight; } else { minScrollPosition = this._scrollLength - this._sizes.viewportOffsetHeight; } var newScrollPosition = this._zooming ? this._zoomPosition : this._scrollPosition; newScrollPosition = Math.max(newScrollPosition, minScrollPosition); newScrollPosition = Math.min(newScrollPosition, maxScrollPosition); this._scrollTo(newScrollPosition, withoutAnimation); } }, _scrollTo: function NavBarContainer_scrollTo(targetScrollPosition, withoutAnimation) { this._measure(); if (this.layout === _UI.Orientation.horizontal) { targetScrollPosition = Math.max(0, Math.min(this._scrollLength - this._sizes.viewportOffsetWidth, targetScrollPosition)); } else { targetScrollPosition = Math.max(0, Math.min(this._scrollLength - this._sizes.viewportOffsetHeight, targetScrollPosition)); } if (withoutAnimation) { if (Math.abs(this._scrollPosition - targetScrollPosition) > 1) { this._zooming = false; this._scrollPosition = targetScrollPosition; this._updatePageUI(); if (!this._duringForceLayout) { this._closeSplitIfOpen(); } var newScrollPos = {}; newScrollPos[(this.layout === _UI.Orientation.horizontal ? "scrollLeft" : "scrollTop")] = targetScrollPosition; _ElementUtilities.setScrollPosition(this._viewportEl, newScrollPos); } } else { if ((!this._zooming && Math.abs(this._scrollPosition - targetScrollPosition) > 1) || (this._zooming && Math.abs(this._zoomPosition - targetScrollPosition) > 1)) { this._zoomPosition = targetScrollPosition; this._zooming = true; if (this.layout === _UI.Orientation.horizontal) { this._viewportEl.style.msScrollSnapType = "none"; _ElementUtilities._zoomTo(this._viewportEl, { contentX: targetScrollPosition, contentY: 0, viewportX: 0, viewportY: 0 }); } else { _ElementUtilities._zoomTo(this._viewportEl, { contentX: 0, contentY: targetScrollPosition, viewportX: 0, viewportY: 0 }); } this._closeSplitIfOpen(); } } }, _MSManipulationStateChangedHandler: function NavBarContainer_MSManipulationStateChangedHandler(e) { this._currentManipulationState = e.currentState; if (e.currentState === e.MS_MANIPULATION_STATE_ACTIVE) { this._viewportEl.style.msScrollSnapType = ""; this._zooming = false; } _Global.clearTimeout(this._manipulationStateTimeoutId); // The extra stop event is firing when an zoomTo is called during another zoomTo and // also the first zoomTo after a resize. if (e.currentState === e.MS_MANIPULATION_STATE_STOPPED) { this._manipulationStateTimeoutId = _Global.setTimeout(function () { this._viewportEl.style.msScrollSnapType = ""; this._zooming = false; this._updateCurrentIndexIfPageChanged(); }.bind(this), 100); } }, _scrollHandler: function NavBarContainer_scrollHandler() { if (this._disposed) { return; } this._measured = false; if (!this._checkingScroll) { var that = this; this._checkingScroll = _BaseUtils._requestAnimationFrame(function () { if (that._disposed) { return; } that._checkingScroll = null; var newScrollPosition = _ElementUtilities.getScrollPosition(that._viewportEl)[(that.layout === _UI.Orientation.horizontal ? "scrollLeft" : "scrollTop")]; if (newScrollPosition !== that._scrollPosition) { that._scrollPosition = newScrollPosition; that._closeSplitIfOpen(); } that._updatePageUI(); if (!that._zooming && that._currentManipulationState === MS_MANIPULATION_STATE_STOPPED) { that._updateCurrentIndexIfPageChanged(); } }); } }, _updateCurrentIndexIfPageChanged: function NavBarContainer_updateCurrentIndexIfPageChanged() { // If you change pages via pagination arrows, mouse wheel, or panning we need to update the current // item to be the first item on the new page. if (this.layout === _UI.Orientation.horizontal) { this._measure(); var currentPage = this._currentPage; var firstIndexOnPage = currentPage * this._sizes.rowsPerPage * this._sizes.columnsPerPage; var lastIndexOnPage = (currentPage + 1) * this._sizes.rowsPerPage * this._sizes.columnsPerPage - 1; if (this._keyboardBehavior.currentIndex < firstIndexOnPage || this._keyboardBehavior.currentIndex > lastIndexOnPage) { // Page change occurred. this._keyboardBehavior.currentIndex = firstIndexOnPage; if (this.element.contains(_Global.document.activeElement)) { this._keyboardBehavior._focus(this._keyboardBehavior.currentIndex); } } } }, _measure: function NavBarContainer_measure() { if (!this._measured) { this._resizeImpl(); this._writeProfilerMark("measure,StartTM"); var sizes = this._sizes; sizes.rtl = _Global.getComputedStyle(this._element).direction === "rtl"; var itemCount = this._surfaceEl.children.length; if (itemCount > 0) { if (!this._sizes.itemMeasured) { this._writeProfilerMark("measureItem,StartTM"); var elementToMeasure = this._surfaceEl.firstElementChild; // Clear inline margins set by NavBarContainer before measuring. elementToMeasure.style.margin = ""; elementToMeasure.style.width = ""; var elementComputedStyle = _Global.getComputedStyle(elementToMeasure); sizes.itemOffsetWidth = parseFloat(_Global.getComputedStyle(elementToMeasure).width); if (elementToMeasure.offsetWidth === 0) { sizes.itemOffsetWidth = 0; } sizes.itemMarginLeft = parseFloat(elementComputedStyle.marginLeft); sizes.itemMarginRight = parseFloat(elementComputedStyle.marginRight); sizes.itemWidth = sizes.itemOffsetWidth + sizes.itemMarginLeft + sizes.itemMarginRight; sizes.itemOffsetHeight = parseFloat(_Global.getComputedStyle(elementToMeasure).height); if (elementToMeasure.offsetHeight === 0) { sizes.itemOffsetHeight = 0; } sizes.itemMarginTop = parseFloat(elementComputedStyle.marginTop); sizes.itemMarginBottom = parseFloat(elementComputedStyle.marginBottom); sizes.itemHeight = sizes.itemOffsetHeight + sizes.itemMarginTop + sizes.itemMarginBottom; if (sizes.itemOffsetWidth > 0 && sizes.itemOffsetHeight > 0) { sizes.itemMeasured = true; } this._writeProfilerMark("measureItem,StopTM"); } sizes.viewportOffsetWidth = parseFloat(_Global.getComputedStyle(this._viewportEl).width); if (this._viewportEl.offsetWidth === 0) { sizes.viewportOffsetWidth = 0; } sizes.viewportOffsetHeight = parseFloat(_Global.getComputedStyle(this._viewportEl).height); if (this._viewportEl.offsetHeight === 0) { sizes.viewportOffsetHeight = 0; } if (sizes.viewportOffsetWidth === 0 || sizes.itemOffsetHeight === 0) { this._measured = false; } else { this._measured = true; } if (this.layout === _UI.Orientation.horizontal) { this._scrollPosition = _ElementUtilities.getScrollPosition(this._viewportEl).scrollLeft; sizes.leadingEdge = this._leftArrowEl.offsetWidth + parseInt(_Global.getComputedStyle(this._leftArrowEl).marginLeft) + parseInt(_Global.getComputedStyle(this._leftArrowEl).marginRight); var usableSpace = sizes.viewportOffsetWidth - sizes.leadingEdge * 2; sizes.maxColumns = sizes.itemWidth ? Math.max(1, Math.floor(usableSpace / sizes.itemWidth)) : 1; sizes.rowsPerPage = Math.min(this.maxRows, Math.ceil(itemCount / sizes.maxColumns)); sizes.columnsPerPage = Math.min(sizes.maxColumns, itemCount); sizes.pages = Math.ceil(itemCount / (sizes.columnsPerPage * sizes.rowsPerPage)); sizes.trailingEdge = sizes.leadingEdge; sizes.extraSpace = usableSpace - (sizes.columnsPerPage * sizes.itemWidth); this._scrollLength = sizes.viewportOffsetWidth * sizes.pages; this._keyboardBehavior.fixedSize = sizes.rowsPerPage; this._keyboardBehavior.fixedDirection = _KeyboardBehavior._KeyboardBehavior.FixedDirection.height; this._surfaceEl.style.height = (sizes.itemHeight * sizes.rowsPerPage) + "px"; this._surfaceEl.style.width = this._scrollLength + "px"; } else { this._scrollPosition = this._viewportEl.scrollTop; sizes.leadingEdge = 0; sizes.rowsPerPage = itemCount; sizes.columnsPerPage = 1; sizes.pages = 1; sizes.trailingEdge = 0; // Reminder there is margin collapsing so just use scrollHeight instead of itemHeight * itemCount this._scrollLength = this._viewportEl.scrollHeight; this._keyboardBehavior.fixedSize = sizes.columnsPerPage; this._keyboardBehavior.fixedDirection = _KeyboardBehavior._KeyboardBehavior.FixedDirection.width; this._surfaceEl.style.height = ""; this._surfaceEl.style.width = ""; } this._updateGridStyles(); } else { sizes.pages = 1; this._hasPreviousContent = false; this._hasNextContent = false; this._surfaceEl.style.height = ""; this._surfaceEl.style.width = ""; } this._writeProfilerMark("measure,StopTM"); } }, _updateGridStyles: function NavBarContainer_updateGridStyles() { var sizes = this._sizes; var itemCount = this._surfaceEl.children.length; for (var index = 0; index < itemCount; index++) { var itemEl = this._surfaceEl.children[index]; var marginRight; var marginLeft; var width = ""; if (this.layout === _UI.Orientation.horizontal) { var column = Math.floor(index / sizes.rowsPerPage); var isFirstColumnOnPage = column % sizes.columnsPerPage === 0; var isLastColumnOnPage = column % sizes.columnsPerPage === sizes.columnsPerPage - 1; var extraTrailingMargin = sizes.trailingEdge; if (this.fixedSize) { extraTrailingMargin += sizes.extraSpace; } else { var spaceToDistribute = sizes.extraSpace - (sizes.maxColumns - sizes.columnsPerPage) * sizes.itemWidth; width = (sizes.itemOffsetWidth + (spaceToDistribute / sizes.maxColumns)) + "px"; } var extraMarginRight; var extraMarginLeft; if (sizes.rtl) { extraMarginRight = (isFirstColumnOnPage ? sizes.leadingEdge : 0); extraMarginLeft = (isLastColumnOnPage ? extraTrailingMargin : 0); } else { extraMarginRight = (isLastColumnOnPage ? extraTrailingMargin : 0); extraMarginLeft = (isFirstColumnOnPage ? sizes.leadingEdge : 0); } marginRight = extraMarginRight + sizes.itemMarginRight + "px"; marginLeft = extraMarginLeft + sizes.itemMarginLeft + "px"; } else { marginRight = ""; marginLeft = ""; } if (itemEl.style.marginRight !== marginRight) { itemEl.style.marginRight = marginRight; } if (itemEl.style.marginLeft !== marginLeft) { itemEl.style.marginLeft = marginLeft; } if (itemEl.style.width !== width) { itemEl.style.width = width; } } }, _updatePageUI: function NavBarContainer_updatePageUI() { this._measure(); var currentPage = this._currentPage; this._hasPreviousContent = (currentPage !== 0); this._hasNextContent = (currentPage < this._sizes.pages - 1); this._updateArrows(); // Always output the pagination indicators so they reserves up space. if (this._indicatorCount !== this._sizes.pages) { this._indicatorCount = this._sizes.pages; this._pageindicatorsEl.innerHTML = new Array(this._sizes.pages + 1).join(''); } for (var i = 0; i < this._pageindicatorsEl.children.length; i++) { if (i === currentPage) { _ElementUtilities.addClass(this._pageindicatorsEl.children[i], NavBarContainer._ClassName.currentindicator); } else { _ElementUtilities.removeClass(this._pageindicatorsEl.children[i], NavBarContainer._ClassName.currentindicator); } } if (this._sizes.pages > 1) { this._viewportEl.style.overflowX = this._panningDisabled ? "hidden" : ""; this._pageindicatorsEl.style.visibility = ""; } else { this._viewportEl.style.overflowX = "hidden"; this._pageindicatorsEl.style.visibility = "hidden"; } if (this._sizes.pages <= 1 || this._layout !== _UI.Orientation.horizontal) { this._ariaStartMarker.removeAttribute("aria-flowto"); this._ariaEndMarker.removeAttribute("x-ms-aria-flowfrom"); } else { var firstIndexOnCurrentPage = currentPage * this._sizes.rowsPerPage * this._sizes.columnsPerPage; var firstItem = this._surfaceEl.children[firstIndexOnCurrentPage].winControl._buttonEl; _ElementUtilities._ensureId(firstItem); this._ariaStartMarker.setAttribute("aria-flowto", firstItem.id); var lastIndexOnCurrentPage = Math.min(this._surfaceEl.children.length - 1, (currentPage + 1) * this._sizes.rowsPerPage * this._sizes.columnsPerPage - 1); var lastItem = this._surfaceEl.children[lastIndexOnCurrentPage].winControl._buttonEl; _ElementUtilities._ensureId(lastItem); this._ariaEndMarker.setAttribute("x-ms-aria-flowfrom", lastItem.id); } }, _closeSplitIfOpen: function NavBarContainer_closeSplitIfOpen() { if (this._currentSplitNavItem) { if (this._currentSplitNavItem.splitOpened) { this._currentSplitNavItem._toggleSplit(); } this._currentSplitNavItem = null; } }, _updateArrows: function NavBarContainer_updateArrows() { var hasLeftContent = this._sizes.rtl ? this._hasNextContent : this._hasPreviousContent; var hasRightContent = this._sizes.rtl ? this._hasPreviousContent : this._hasNextContent; var that = this; // Previous and next are the arrows, not states. On mouse hover the arrows fade in immediately. If you // mouse out the arrows fade out after a delay. When you reach the last/first page, the corresponding // arrow fades out immediately as well. if ((this._mouseInViewport || this._panningDisabled) && hasLeftContent) { this._leftArrowWaitingToFadeOut && this._leftArrowWaitingToFadeOut.cancel(); this._leftArrowWaitingToFadeOut = null; this._leftArrowFadeOut && this._leftArrowFadeOut.cancel(); this._leftArrowFadeOut = null; this._leftArrowEl.style.visibility = ''; this._leftArrowFadeIn = this._leftArrowFadeIn || Animations.fadeIn(this._leftArrowEl); } else { if (hasLeftContent) { // If we need a delayed fade out and we are already running a delayed fade out just use that one, don't extend it. // Otherwise create a delayed fade out. this._leftArrowWaitingToFadeOut = this._leftArrowWaitingToFadeOut || Promise.timeout(_TransitionAnimation._animationTimeAdjustment(buttonFadeDelay)); } else { // If we need a immediate fade out and already have a delayed fade out cancel that one and create an immediate one. this._leftArrowWaitingToFadeOut && this._leftArrowWaitingToFadeOut.cancel(); this._leftArrowWaitingToFadeOut = Promise.wrap(); } this._leftArrowWaitingToFadeOut.then(function () { // After the delay cancel any fade in if running. If we already were fading out continue it otherwise start the fade out. this._leftArrowFadeIn && this._leftArrowFadeIn.cancel(); this._leftArrowFadeIn = null; this._leftArrowFadeOut = this._leftArrowFadeOut || Animations.fadeOut(this._leftArrowEl).then(function () { that._leftArrowEl.style.visibility = 'hidden'; }); }.bind(this)); } // Same pattern for Next arrow. if ((this._mouseInViewport || this._panningDisabled) && hasRightContent) { this._rightArrowWaitingToFadeOut && this._rightArrowWaitingToFadeOut.cancel(); this._rightArrowWaitingToFadeOut = null; this._rightArrowFadeOut && this._rightArrowFadeOut.cancel(); this._rightArrowFadeOut = null; this._rightArrowEl.style.visibility = ''; this._rightArrowFadeIn = this._rightArrowFadeIn || Animations.fadeIn(this._rightArrowEl); } else { if (hasRightContent) { this._rightArrowWaitingToFadeOut = this._rightArrowWaitingToFadeOut || Promise.timeout(_TransitionAnimation._animationTimeAdjustment(buttonFadeDelay)); } else { this._rightArrowWaitingToFadeOut && this._rightArrowWaitingToFadeOut.cancel(); this._rightArrowWaitingToFadeOut = Promise.wrap(); } this._rightArrowWaitingToFadeOut.then(function () { this._rightArrowFadeIn && this._rightArrowFadeIn.cancel(); this._rightArrowFadeIn = null; this._rightArrowFadeOut = this._rightArrowFadeOut || Animations.fadeOut(this._rightArrowEl).then(function () { that._rightArrowEl.style.visibility = 'hidden'; }); }.bind(this)); } }, _navbarCommandInvokedHandler: function NavBarContainer_navbarCommandInvokedHandler(ev) { var srcElement = ev.target; var index = -1; while (srcElement) { index++; srcElement = srcElement.previousSibling; } this._fireEvent(NavBarContainer._EventName.invoked, { index: index, navbarCommand: ev.target.winControl, data: this._repeater ? this._repeater.data.getAt(index) : null }); }, _navbarCommandSplitToggleHandler: function NavBarContainer_navbarCommandSplitToggleHandler(ev) { var srcElement = ev.target; var index = -1; while (srcElement) { index++; srcElement = srcElement.previousSibling; } var navbarCommand = ev.target.winControl; this._closeSplitIfOpen(); if (navbarCommand.splitOpened) { this._currentSplitNavItem = navbarCommand; } this._fireEvent(NavBarContainer._EventName.splitToggle, { opened: navbarCommand.splitOpened, index: index, navbarCommand: navbarCommand, data: this._repeater ? this._repeater.data.getAt(index) : null }); }, _fireEvent: function NavBarContainer_fireEvent(type, detail) { var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent(type, true, false, detail); this.element.dispatchEvent(event); }, _writeProfilerMark: function NavBarContainer_writeProfilerMark(text) { var message = "WinJS.UI.NavBarContainer:" + this._id + ":" + text; _WriteProfilerMark(message); _Log.log && _Log.log(message, null, "navbarcontainerprofiler"); }, dispose: function NavBarContainer_dispose() { /// /// /// Disposes this control. /// /// /// if (this._disposed) { return; } this._disposed = true; if (this._appBarEl) { this._appBarEl.removeEventListener('beforeshow', this._closeSplitAndResetBound); this._appBarEl.removeEventListener('beforeshow', this._resizeImplBound); } Navigation.removeEventListener('navigated', this._closeSplitAndResetBound); this._leftArrowWaitingToFadeOut && this._leftArrowWaitingToFadeOut.cancel(); this._leftArrowFadeOut && this._leftArrowFadeOut.cancel(); this._leftArrowFadeIn && this._leftArrowFadeIn.cancel(); this._rightArrowWaitingToFadeOut && this._rightArrowWaitingToFadeOut.cancel(); this._rightArrowFadeOut && this._rightArrowFadeOut.cancel(); this._rightArrowFadeIn && this._rightArrowFadeIn.cancel(); _ElementUtilities._resizeNotifier.unsubscribe(this._element, this._boundResizeHandler); this._removeDataChangingEvents(); this._removeDataChangedEvents(); } }, { // Names of classes used by the NavBarContainer. _ClassName: { navbarcontainer: "win-navbarcontainer", pageindicators: "win-navbarcontainer-pageindicator-box", indicator: "win-navbarcontainer-pageindicator", currentindicator: "win-navbarcontainer-pageindicator-current", vertical: "win-navbarcontainer-vertical", horizontal: "win-navbarcontainer-horizontal", viewport: "win-navbarcontainer-viewport", surface: "win-navbarcontainer-surface", navarrow: "win-navbarcontainer-navarrow", navleftarrow: "win-navbarcontainer-navleft", navrightarrow: "win-navbarcontainer-navright" }, _EventName: { invoked: eventNames.invoked, splitToggle: eventNames.splittoggle } }); _Base.Class.mix(NavBarContainer, _Control.DOMEventMixin); return NavBarContainer; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS/Controls/NavBar',[ '../Core/_Global', '../Core/_WinRT', '../Core/_Base', '../Core/_BaseUtils', '../Core/_Events', '../Core/_WriteProfilerMark', '../Promise', '../Scheduler', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', './AppBar', './NavBar/_Command', './NavBar/_Container', 'require-style!less/desktop/controls', 'require-style!less/phone/controls' ], function NavBarInit(_Global,_WinRT, _Base, _BaseUtils, _Events, _WriteProfilerMark, Promise, Scheduler, _ElementUtilities, _Hoverable, AppBar, _Command, _Container) { "use strict"; var customLayout = "custom"; _Base.Namespace.define("WinJS.UI", { /// /// /// Displays navigation commands in a toolbar that the user can show or hide. /// /// /// /// /// /// ///
///
///
/// ]]>
/// Raised just before showing the NavBar. /// Raised immediately after an NavBar is fully shown. /// Raised just before hiding the NavBar. /// Raised immediately after the NavBar is fully hidden. /// Fired when children of NavBar control have been processed from a WinJS.UI.processAll call. /// Styles the entire NavBar. /// /// /// NavBar: _Base.Namespace._lazy(function () { var childrenProcessedEventName = "childrenprocessed"; var createEvent = _Events._createEventProperty; var NavBar = _Base.Class.derive(AppBar.AppBar, function NavBar_ctor(element, options) { /// /// /// Creates a new NavBar. /// /// /// The DOM element that will host the new NavBar control. /// /// /// An object that contains one or more property/value pairs to apply to the new control. Each property of the options object corresponds to one of the control's /// properties or events. /// /// /// The new NavBar control. /// /// /// options = options || {}; // Shallow copy object so we can modify it. options = _BaseUtils._shallowCopy(options); // Default to Placement = Top and Layout = Custom options.placement = options.placement || "top"; options.layout = customLayout; AppBar.AppBar.call(this, element, options); this._element.addEventListener("beforeshow", this._handleBeforeShow.bind(this)); _ElementUtilities.addClass(this.element, NavBar._ClassName.navbar); if (_WinRT.Windows.ApplicationModel.DesignMode.designModeEnabled) { this._processChildren(); } else { Scheduler.schedule(this._processChildren.bind(this), Scheduler.Priority.idle, null, "WinJS.UI.NavBar.processChildren"); } }, { // Block others from setting the layout property. /// /// The layout of the NavBar contents. /// /// layout: { get: function () { return customLayout; }, set: function () { Object.getOwnPropertyDescriptor(AppBar.AppBar.prototype, "layout").set.call(this, customLayout); }, }, /// /// Raised when children of NavBar control have been processed by a WinJS.UI.processAll call. /// /// onchildrenprocessed: createEvent(childrenProcessedEventName), _processChildren: function NavBar_processChildren() { // The NavBar control schedules processAll on its children at idle priority to avoid hurting startup // performance. If the NavBar is shown before the scheduler gets to the idle job, the NavBar will // immediately call processAll on its children. If your app needs the children to be processed before // the scheduled job executes, you may call processChildren to force the processAll call. if (!this._processed) { this._processed = true; this._writeProfilerMark("processChildren,StartTM"); var that = this; var processed = Promise.as(); if (this._processors) { this._processors.forEach(function (processAll) { for (var i = 0, len = that.element.children.length; i < len; i++) { (function (child) { processed = processed.then(function () { processAll(child); }); }(that.element.children[i])); } }); } return processed.then( function () { that._writeProfilerMark("processChildren,StopTM"); that._fireEvent(NavBar._EventName.childrenProcessed); }, function () { that._writeProfilerMark("processChildren,StopTM"); that._fireEvent(NavBar._EventName.childrenProcessed); } ); } return Promise.wrap(); }, _show: function NavBar_show() { // Override _show to call processChildren first. // if (this.disabled) { return; } var that = this; this._processChildren().then(function () { AppBar.AppBar.prototype._show.call(that); }); }, _handleBeforeShow: function NavBar_handleBeforeShow() { // Navbar needs to ensure its elements to have their correct height and width after AppBar changes display="none" // to display="" and AppBar needs the elements to have their final height before it measures its own element height // to do the slide in animation over the correct amount of pixels. if (this._disposed) { return; } var navbarcontainerEls = this.element.querySelectorAll('.win-navbarcontainer'); for (var i = 0; i < navbarcontainerEls.length; i++) { navbarcontainerEls[i].winControl.forceLayout(); } }, _fireEvent: function NavBar_fireEvent(type, detail) { var event = _Global.document.createEvent("CustomEvent"); event.initCustomEvent(type, true, false, detail || {}); this.element.dispatchEvent(event); }, _writeProfilerMark: function NavBar_writeProfilerMark(text) { _WriteProfilerMark("WinJS.UI.NavBar:" + this._id + ":" + text); } }, { _ClassName: { navbar: "win-navbar" }, _EventName: { childrenProcessed: childrenProcessedEventName }, isDeclarativeControlContainer: _BaseUtils.markSupportedForProcessing(function (navbar, callback) { if (navbar._processed) { for (var i = 0, len = navbar.element.children.length; i < len; i++) { callback(navbar.element.children[i]); } } else { navbar._processors = navbar._processors || []; navbar._processors.push(callback); } }) }); return NavBar; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // ViewBox control define('WinJS/Controls/ViewBox',[ '../Core/_Global', '../Core/_Base', '../Core/_BaseUtils', '../Core/_ErrorFromName', '../Core/_Resources', '../Scheduler', '../Utilities/_Control', '../Utilities/_Dispose', '../Utilities/_ElementUtilities', '../Utilities/_Hoverable', 'require-style!less/desktop/controls', 'require-style!less/phone/controls' ], function viewboxInit(_Global, _Base, _BaseUtils, _ErrorFromName, _Resources, Scheduler, _Control, _Dispose, _ElementUtilities, _Hoverable) { "use strict"; _Base.Namespace.define("WinJS.UI", { /// /// /// Scales a single child element to fill the available space without /// resizing it. This control reacts to changes in the size of the container as well as /// changes in size of the child element. For example, a media query may result in /// a change in aspect ratio. /// /// /// View Box /// /// ///
ViewBox
]]>
/// /// /// ViewBox: _Base.Namespace._lazy(function () { var strings = { get invalidViewBoxChildren() { return "ViewBox expects to only have one child element"; }, }; function onresize(control) { if (control && !control._resizing) { control._resizing = control._resizing || 0; control._resizing++; try { control._updateLayout(); } finally { control._resizing--; } } } function onresizeBox(ev) { if (ev.target) { onresize(ev.target.winControl); } } function onresizeSizer(ev) { if (ev.target) { onresize(ev.target.parentElement.winControl); } } var ViewBox = _Base.Class.define(function ViewBox_ctor(element) { /// /// Initializes a new instance of the ViewBox control /// /// The DOM element that functions as the scaling box. This element fills 100% of the width and height allotted to it. /// /// /// The set of options to be applied initially to the ViewBox control. /// /// A constructed ViewBox control. /// this._disposed = false; this._element = element || _Global.document.createElement("div"); var box = this.element; box.winControl = this; _ElementUtilities.addClass(box, "win-disposable"); _ElementUtilities.addClass(box, "win-viewbox"); this.forceLayout(); }, { _sizer: null, _element: null, /// element: { get: function () { return this._element; } }, _rtl: { get: function () { return _Global.getComputedStyle(this.element).direction === "rtl"; } }, _initialize: function () { var box = this.element; if (box.firstElementChild !== this._sizer) { if (_BaseUtils.validation) { if (box.childElementCount !== 1) { throw new _ErrorFromName("WinJS.UI.ViewBox.InvalidChildren", strings.invalidViewBoxChildren); } } if (this._sizer) { this._sizer.onresize = null; } var sizer = box.firstElementChild; this._sizer = sizer; if (sizer) { _ElementUtilities._resizeNotifier.subscribe(box, onresizeBox); box.addEventListener("mselementresize", onresizeBox); _ElementUtilities._resizeNotifier.subscribe(sizer, onresizeSizer); sizer.addEventListener("mselementresize", onresizeSizer); } if (box.clientWidth === 0 && box.clientHeight === 0) { var that = this; // Wait for the viewbox to get added to the DOM. It should be added // in the synchronous block in which _initialize was called. Scheduler.schedule(function ViewBox_async_initialize() { that._updateLayout(); }, Scheduler.Priority.normal, null, "WinJS.UI.ViewBox._updateLayout"); } } }, _updateLayout: function () { var sizer = this._sizer; if (sizer) { var box = this.element; var w = sizer.clientWidth; var h = sizer.clientHeight; var bw = box.clientWidth; var bh = box.clientHeight; var wRatio = bw / w; var hRatio = bh / h; var mRatio = Math.min(wRatio, hRatio); var transX = Math.abs(bw - (w * mRatio)) / 2; var transY = Math.abs(bh - (h * mRatio)) / 2; var rtl = this._rtl; this._sizer.style[_BaseUtils._browserStyleEquivalents["transform"].scriptName] = "translate(" + (rtl ? "-" : "") + transX + "px," + transY + "px) scale(" + mRatio + ")"; this._sizer.style[_BaseUtils._browserStyleEquivalents["transform-origin"].scriptName] = rtl ? "top right" : "top left"; } }, dispose: function () { /// /// /// Disposes this ViewBox. /// /// if (this._disposed) { return; } if (this.element) { _ElementUtilities._resizeNotifier.unsubscribe(this.element, onresizeBox); } if (this._sizer) { _ElementUtilities._resizeNotifier.unsubscribe(this._sizer, onresizeSizer); } this._disposed = true; _Dispose.disposeSubTree(this._element); }, forceLayout: function () { this._initialize(); this._updateLayout(); } }); _Base.Class.mix(ViewBox, _Control.DOMEventMixin); return ViewBox; }) }); }); // Copyright (c) Microsoft Open Technologies, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. define('WinJS',[ 'WinJS/Core/_WinJS', 'WinJS/Core', 'WinJS/Promise', 'WinJS/_Signal', 'WinJS/Scheduler', 'WinJS/Utilities', 'WinJS/Fragments', 'WinJS/Application', 'WinJS/Navigation', 'WinJS/Animations', 'WinJS/Binding', 'WinJS/BindingTemplate', 'WinJS/BindingList', 'WinJS/Res', 'WinJS/Pages', 'WinJS/ControlProcessor', 'WinJS/Controls/HtmlControl', 'WinJS/VirtualizedDataSource', 'WinJS/Controls/IntrinsicControls', 'WinJS/Controls/ListView', 'WinJS/Controls/FlipView', 'WinJS/Controls/ItemContainer', 'WinJS/Controls/Repeater', 'WinJS/Controls/DatePicker', 'WinJS/Controls/TimePicker', 'WinJS/Controls/BackButton', 'WinJS/Controls/Rating', 'WinJS/Controls/ToggleSwitch', 'WinJS/Controls/SemanticZoom', 'WinJS/Controls/Pivot', 'WinJS/Controls/Hub', 'WinJS/Controls/Flyout', 'WinJS/Controls/AppBar', 'WinJS/Controls/Menu', 'WinJS/Controls/SearchBox', 'WinJS/Controls/SettingsFlyout', 'WinJS/Controls/NavBar', 'WinJS/Controls/Tooltip', 'WinJS/Controls/ViewBox' ], function (_WinJS) { "use strict"; return _WinJS; }); require(['WinJS/Core/_WinJS', 'WinJS'], function (_WinJS) { global.WinJS = _WinJS; return _WinJS; }); })); }(this));