diff --git a/AppInstallerReset.sln b/AppInstallerReset.sln index d5d1e22..cc8bcd7 100644 --- a/AppInstallerReset.sln +++ b/AppInstallerReset.sln @@ -5,12 +5,6 @@ VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "pkgread", "pkgread\pkgread.vcxproj", "{A7753282-AA16-43D9-8ACA-7065239DD702}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dlltest", "dlltest\dlltest.vcxproj", "{F5CCB3AB-AC43-432A-862D-4F264760B09B}" - ProjectSection(ProjectDependencies) = postProject - {8EAC0230-4990-4E41-8E0F-D641D1561396} = {8EAC0230-4990-4E41-8E0F-D641D1561396} - {33D91B58-1981-4A3C-B4D1-86EE406CDE12} = {33D91B58-1981-4A3C-B4D1-86EE406CDE12} - EndProjectSection -EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "priformatcli", "..\priformatcli\priformatcli\priformatcli.vcxproj", "{33D91B58-1981-4A3C-B4D1-86EE406CDE12}" ProjectSection(ProjectDependencies) = postProject {EF4012D4-EF08-499C-B803-177739350B2D} = {EF4012D4-EF08-499C-B803-177739350B2D} @@ -30,7 +24,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "appinstaller", "appinstalle {33D91B58-1981-4A3C-B4D1-86EE406CDE12} = {33D91B58-1981-4A3C-B4D1-86EE406CDE12} {A7753282-AA16-43D9-8ACA-7065239DD702} = {A7753282-AA16-43D9-8ACA-7065239DD702} {798ED492-EECE-457D-8FD8-129DA93CE126} = {798ED492-EECE-457D-8FD8-129DA93CE126} - {F5CCB3AB-AC43-432A-862D-4F264760B09B} = {F5CCB3AB-AC43-432A-862D-4F264760B09B} {E04CCAB9-35DB-495C-A279-5B483C707CD0} = {E04CCAB9-35DB-495C-A279-5B483C707CD0} EndProjectSection EndProject @@ -54,16 +47,6 @@ Global {A7753282-AA16-43D9-8ACA-7065239DD702}.Release|x64.Build.0 = Release|x64 {A7753282-AA16-43D9-8ACA-7065239DD702}.Release|x86.ActiveCfg = Release|Win32 {A7753282-AA16-43D9-8ACA-7065239DD702}.Release|x86.Build.0 = Release|Win32 - {F5CCB3AB-AC43-432A-862D-4F264760B09B}.Debug|Any CPU.ActiveCfg = Debug|Win32 - {F5CCB3AB-AC43-432A-862D-4F264760B09B}.Debug|x64.ActiveCfg = Debug|x64 - {F5CCB3AB-AC43-432A-862D-4F264760B09B}.Debug|x64.Build.0 = Debug|x64 - {F5CCB3AB-AC43-432A-862D-4F264760B09B}.Debug|x86.ActiveCfg = Debug|Win32 - {F5CCB3AB-AC43-432A-862D-4F264760B09B}.Debug|x86.Build.0 = Debug|Win32 - {F5CCB3AB-AC43-432A-862D-4F264760B09B}.Release|Any CPU.ActiveCfg = Release|Win32 - {F5CCB3AB-AC43-432A-862D-4F264760B09B}.Release|x64.ActiveCfg = Release|x64 - {F5CCB3AB-AC43-432A-862D-4F264760B09B}.Release|x64.Build.0 = Release|x64 - {F5CCB3AB-AC43-432A-862D-4F264760B09B}.Release|x86.ActiveCfg = Release|Win32 - {F5CCB3AB-AC43-432A-862D-4F264760B09B}.Release|x86.Build.0 = Release|Win32 {33D91B58-1981-4A3C-B4D1-86EE406CDE12}.Debug|Any CPU.ActiveCfg = Debug|Win32 {33D91B58-1981-4A3C-B4D1-86EE406CDE12}.Debug|x64.ActiveCfg = Debug|x64 {33D91B58-1981-4A3C-B4D1-86EE406CDE12}.Debug|x64.Build.0 = Debug|x64 diff --git a/appinstaller/appinstaller.rc b/appinstaller/appinstaller.rc index ca47be9..cb2caa6 100644 Binary files a/appinstaller/appinstaller.rc and b/appinstaller/appinstaller.rc differ diff --git a/appinstaller/appinstaller.vcxproj b/appinstaller/appinstaller.vcxproj index caf425b..b06a7bb 100644 --- a/appinstaller/appinstaller.vcxproj +++ b/appinstaller/appinstaller.vcxproj @@ -164,6 +164,7 @@ + diff --git a/appinstaller/appinstaller.vcxproj.filters b/appinstaller/appinstaller.vcxproj.filters index 9ac7b42..eebd9bc 100644 --- a/appinstaller/appinstaller.vcxproj.filters +++ b/appinstaller/appinstaller.vcxproj.filters @@ -90,6 +90,9 @@ 头文件 + + 头文件 + diff --git a/appinstaller/appxinfo.h b/appinstaller/appxinfo.h index 13a94d5..e21a464 100644 --- a/appinstaller/appxinfo.h +++ b/appinstaller/appxinfo.h @@ -347,4 +347,4 @@ struct pkginfo std::wstring_convert > conv; return conv.from_bytes (utf8); } -}; \ No newline at end of file +}; diff --git a/appinstaller/bridge.h b/appinstaller/bridge.h new file mode 100644 index 0000000..e6b4fd1 --- /dev/null +++ b/appinstaller/bridge.h @@ -0,0 +1,87 @@ +#pragma once +#include +#include "mpstr.h" +#include "nstring.h" +using namespace System; +using namespace System::Runtime::InteropServices; + +std::wstring HResultToMessage (HRESULT hr) +{ + _com_error err (hr); + auto msgptr = err.ErrorMessage (); + return msgptr ? msgptr : L""; +} +[ComVisible (true)] +public ref class _I_HResult +{ + private: + HRESULT hr = S_OK; + String ^errorcode = ""; + String ^detailmsg = ""; + public: + _I_HResult (HRESULT hres) + { + hr = hres; + detailmsg = CStringToMPString (HResultToMessage (hres)); + } + _I_HResult (HRESULT hres, String ^error, String ^message) + { + hr = hres; + errorcode = error; + detailmsg = message; + } + property HRESULT HResult { HRESULT get () { return hr; }} + property String ^ErrorCode { String ^get () { return errorcode; }} + property String ^Message { String ^get () { return detailmsg; }} + property bool Succeeded { bool get () { return SUCCEEDED (hr); }} + property bool Failed { bool get () { return FAILED (hr); }} +}; +System::String ^FormatString (System::String ^fmt, ... array ^args) { return System::String::Format (fmt, args); } + +String ^ EscapeToInnerXml (String ^str) +{ + using namespace System::Xml; + auto doc = gcnew System::Xml::XmlDocument (); + doc->LoadXml (""); + auto root = doc->FirstChild; + root->InnerText = str; + return root->InnerXml; +} +std::wstring EscapeToInnerXml (const std::wstring &str) { return MPStringToStdW (EscapeToInnerXml (CStringToMPString (str))); } +[ComVisible (true)] +public ref class _I_String +{ + public: + ref class _I_NString + { + public: + bool NEquals (String ^l, String ^r) { return IsNormalizeStringEquals (MPStringToPtrW (l), MPStringToPtrW (r)); } + bool Empty (String ^l) { return IsNormalizeStringEmpty (MPStringToStdW (l)); } + int Compare (String ^l, String ^r) { return NormalizeStringCompare (MPStringToPtrW (l), MPStringToPtrW (r)); } + int Length (String ^l) { return GetNormalizeStringLength (MPStringToStdW (l)); } + }; + private: + _I_NString ^nstr = gcnew _I_NString (); + public: + property _I_NString ^NString { _I_NString ^get () { return nstr; }} + String ^Trim (String ^src) + { + std::wstring csrc = MPStringToStdW (src); + return CStringToMPString (::StringTrim (csrc)); + } + String ^ToLower (String ^src) { return CStringToMPString (StringToLower (MPStringToStdW (src))); } + String ^ToUpper (String ^src) { return CStringToMPString (StringToUpper (MPStringToStdW (src))); } + String ^Format (String ^fmt, ... array ^args) { return FormatString (fmt, args); } + String ^FormatInnerHTML (String ^fmt, ... array ^args) + { + std::wstring ihtml = EscapeToInnerXml (MPStringToStdW (fmt)); + auto pih = CStringToMPString (ihtml); + auto newargs = gcnew array (args->Length); + for (size_t i = 0; i < args->Length; i ++) + { + auto %p = newargs [i]; + p = Format ("{0}", EscapeToInnerXml (Format ("{0}", args [i]))); + } + return Format (pih, newargs); + } +}; \ No newline at end of file diff --git a/appinstaller/initfile.h b/appinstaller/initfile.h index f97ce89..ce22595 100644 --- a/appinstaller/initfile.h +++ b/appinstaller/initfile.h @@ -44,7 +44,7 @@ BOOL WritePrivateProfileStringA (const std::string &filePath, const std::string } BOOL WritePrivateProfileStringW (const std::wstring &filePath, const std::wstring §ion, const std::wstring &key, const std::wstring &value) { - return WritePrivateProfileStringW ( section.c_str (), key.c_str (), value.c_str (), filePath.c_str ()); + return WritePrivateProfileStringW (section.c_str (), key.c_str (), value.c_str (), filePath.c_str ()); } size_t GetPrivateProfileSectionA (const std::string &filePath, const std::string §ion, std::vector &output) { @@ -106,7 +106,7 @@ bool WritePrivateProfileSectionA (const std::string &filePath, const std::string { std::string buf; for (const auto &line : lines) buf.append (line).push_back ('\0'); - buf.push_back ('\0'); + buf.push_back ('\0'); return WritePrivateProfileSectionA (section.c_str (), buf.c_str (), filePath.c_str ()) != 0; } bool WritePrivateProfileSectionW (const std::wstring &filePath, const std::wstring §ion, const std::vector &lines) @@ -167,7 +167,7 @@ size_t GetPrivateProfileKeysA (const std::string &filePath, const std::string &s } bool DeletePrivateProfileKeyA (const std::string &filePath, const std::string §ion, const std::string &key) { - return WritePrivateProfileStringA (section.c_str (), key.c_str (), NULL, filePath.c_str ()) != FALSE; + return WritePrivateProfileStringA (section.c_str (), key.c_str (), NULL, filePath.c_str ()) != FALSE; } bool DeletePrivateProfileKeyW (const std::wstring &filePath, const std::wstring §ion, const std::wstring &key) { @@ -260,19 +260,19 @@ class initkey #define OPERATOR_TYPE_READ(_type_, _method_) \ operator _type_ () { return _method_ (); } OPERATOR_TYPE_READ (int, read_int) - OPERATOR_TYPE_READ (unsigned int, read_uint) - OPERATOR_TYPE_READ (long, read_long) - OPERATOR_TYPE_READ (unsigned long, read_ulong) - OPERATOR_TYPE_READ (long long, read_llong) - OPERATOR_TYPE_READ (unsigned long long, read_ullong) - OPERATOR_TYPE_READ (short, read_short) - OPERATOR_TYPE_READ (unsigned short, read_ushort) - OPERATOR_TYPE_READ (float, read_float) - OPERATOR_TYPE_READ (double, read_double) - OPERATOR_TYPE_READ (bool, read_bool) -#ifdef OPERATOR_TYPE_READ -#undef OPERATOR_TYPE_READ -#endif + OPERATOR_TYPE_READ (unsigned int, read_uint) + OPERATOR_TYPE_READ (long, read_long) + OPERATOR_TYPE_READ (unsigned long, read_ulong) + OPERATOR_TYPE_READ (long long, read_llong) + OPERATOR_TYPE_READ (unsigned long long, read_ullong) + OPERATOR_TYPE_READ (short, read_short) + OPERATOR_TYPE_READ (unsigned short, read_ushort) + OPERATOR_TYPE_READ (float, read_float) + OPERATOR_TYPE_READ (double, read_double) + OPERATOR_TYPE_READ (bool, read_bool) + #ifdef OPERATOR_TYPE_READ + #undef OPERATOR_TYPE_READ + #endif }; class initsection { @@ -285,8 +285,8 @@ class initsection if (IsNormalizeStringEmpty (res)) return defaultvalue; return (T)process (res.c_str ()); } - template bool write_t (const std::basic_string &key, T value) - { + template bool write_t (const std::basic_string &key, T value) + { std::basic_string temp; return write_string (key, TypeToString (value, temp)); } @@ -424,22 +424,22 @@ class initfile #define METHOD_INIT_READ(_type_, _typename_, _dfltvalue_, _process_) \ _type_ read_##_typename_ (INIT_READ_WARGS (_type_, _dfltvalue_)) const { return read_t (section, key, dflt, _process_); } METHOD_INIT_READ (int, int, 0, _wtoi) - METHOD_INIT_READ (unsigned int, uint, 0, _wtou) - METHOD_INIT_READ (long, long, 0, _wtol) - METHOD_INIT_READ (unsigned long, ulong, 0, _wtoul) - METHOD_INIT_READ (long long, llong, 0, _wtoll) - METHOD_INIT_READ (unsigned long, ullong, 0, _wtou64) - METHOD_INIT_READ (short, short, 0, _wtoi16) - METHOD_INIT_READ (unsigned short, ushort, 0, _wtoui16) - METHOD_INIT_READ (int16_t, i16, 0, _wtoi16) - METHOD_INIT_READ (uint16_t, u16, 0, _wtoui16) - METHOD_INIT_READ (int32_t, i32, 0, _wtoi32) - METHOD_INIT_READ (uint32_t, u32, 0, _wtoui32) - METHOD_INIT_READ (int64_t, i64, 0, _wtoi64) - METHOD_INIT_READ (uint64_t, u64, 0, _wtou64) - METHOD_INIT_READ (float, float , 0, _wtof) - METHOD_INIT_READ (double, double, 0, _wtod) - int8_t read_i8 (INIT_READ_WARGS (int8_t, 0)) const { return read_t (section, key, dflt, _wtoi8); } + METHOD_INIT_READ (unsigned int, uint, 0, _wtou) + METHOD_INIT_READ (long, long, 0, _wtol) + METHOD_INIT_READ (unsigned long, ulong, 0, _wtoul) + METHOD_INIT_READ (long long, llong, 0, _wtoll) + METHOD_INIT_READ (unsigned long, ullong, 0, _wtou64) + METHOD_INIT_READ (short, short, 0, _wtoi16) + METHOD_INIT_READ (unsigned short, ushort, 0, _wtoui16) + METHOD_INIT_READ (int16_t, i16, 0, _wtoi16) + METHOD_INIT_READ (uint16_t, u16, 0, _wtoui16) + METHOD_INIT_READ (int32_t, i32, 0, _wtoi32) + METHOD_INIT_READ (uint32_t, u32, 0, _wtoui32) + METHOD_INIT_READ (int64_t, i64, 0, _wtoi64) + METHOD_INIT_READ (uint64_t, u64, 0, _wtou64) + METHOD_INIT_READ (float, float, 0, _wtof) + METHOD_INIT_READ (double, double, 0, _wtod) + int8_t read_i8 (INIT_READ_WARGS (int8_t, 0)) const { return read_t (section, key, dflt, _wtoi8); } uint8_t read_u8 (INIT_READ_WARGS (uint8_t, 0)) const { return read_t (section, key, dflt, _wtoui8); } bool read_bool (INIT_READ_WARGS (bool, false)) const { @@ -462,16 +462,16 @@ _type_ read_##_typename_ (INIT_READ_WARGS (_type_, _dfltvalue_)) const { return #define METHOD_INIT_WRITE(_type_) \ bool write (INIT_WRITE_WARGS (_type_)) { return write_t (section, key, value); } METHOD_INIT_WRITE (short) - METHOD_INIT_WRITE (unsigned short) - METHOD_INIT_WRITE (int) - METHOD_INIT_WRITE (unsigned int) - METHOD_INIT_WRITE (long) - METHOD_INIT_WRITE (unsigned long) - METHOD_INIT_WRITE (long long) - METHOD_INIT_WRITE (unsigned long long) - METHOD_INIT_WRITE (float) - METHOD_INIT_WRITE (double) - bool write (INIT_WRITE_WARGS (bool)) { return write (section, key, value ? L"true" : L"false"); } + METHOD_INIT_WRITE (unsigned short) + METHOD_INIT_WRITE (int) + METHOD_INIT_WRITE (unsigned int) + METHOD_INIT_WRITE (long) + METHOD_INIT_WRITE (unsigned long) + METHOD_INIT_WRITE (long long) + METHOD_INIT_WRITE (unsigned long long) + METHOD_INIT_WRITE (float) + METHOD_INIT_WRITE (double) + bool write (INIT_WRITE_WARGS (bool)) { return write (section, key, value ? L"true" : L"false"); } bool write (INIT_WRITE_WARGS (int8_t)) { return write_t (section, key, (int16_t)value); } bool write (INIT_WRITE_WARGS (uint8_t)) { return write_t (section, key, (uint16_t)value); } bool write (pcwstring section, pcwstring key, void *buf, size_t bufsize) { return WritePrivateProfileStructW (filepath, section, key, buf, bufsize); } @@ -485,4 +485,114 @@ bool write (INIT_WRITE_WARGS (_type_)) { return write_t (section, key, value); } #ifdef INIT_WRITE_WARGS #undef INIT_WRITE_WARGS #endif -}; \ No newline at end of file +}; + +#ifdef __cplusplus_cli +namespace Win32 +{ + using namespace System; + using namespace System::Runtime::InteropServices; + [ComVisible (true)] + public ref class Key + { + private: + String ^filepath = ""; + String ^section = ""; + String ^key = ""; + public: + property String ^FilePath { String ^get () { return filepath; }} + property String ^Section { String ^get () { return section; }} + property String ^KeyName { String ^get () { return key; }} + Key (String ^file, String ^sect, String ^k): filepath (file), section (sect), key (k) {} + Object ^Get (Object ^dflt) + { + auto res = GetPrivateProfileStringW ( + MPStringToStdW (filepath), + MPStringToStdW (section), + MPStringToStdW (key), + dflt ? MPStringToStdW (dflt->ToString ()).c_str () : L"" + ); + return CStringToMPString (res); + } + Object ^Get () + { + auto res = GetPrivateProfileStringW ( + MPStringToStdW (filepath), + MPStringToStdW (section), + MPStringToStdW (key) + ); + return CStringToMPString (res); + } + bool Set (Object ^value) + { + return WritePrivateProfileStringW ( + MPStringToStdW (filepath), + MPStringToStdW (section), + MPStringToStdW (key), + MPStringToStdW (value ? value->ToString () : L"") + ); + } + property Object ^Value { Object ^get () { return Get (); } void set (Object ^value) { Set (value); } } + Key %operator = (Object ^value) { Value = value; return *this; } + operator String ^ () { return Value->ToString (); } + explicit operator bool () + { + auto boolstr = Value->ToString ()->Trim ()->ToLower (); + if (boolstr == "true" || boolstr == "zhen" || boolstr == "yes" || boolstr == "") return true; + else if (boolstr == "false" || boolstr == "jia" || boolstr == "no" || boolstr == "") return false; + else return false; + } + #define OPERATOR_TRANSITION_DEFINE(type, transfunc, defaultret) \ +operator type () { try { transfunc (Value->ToString ()); } catch (...) { return defaultret; }} + OPERATOR_TRANSITION_DEFINE (int8_t, Convert::ToSByte, 0) + OPERATOR_TRANSITION_DEFINE (uint8_t, Convert::ToByte, 0) + OPERATOR_TRANSITION_DEFINE (int16_t, Convert::ToInt16, 0) + OPERATOR_TRANSITION_DEFINE (uint16_t, Convert::ToUInt16, 0) + OPERATOR_TRANSITION_DEFINE (int32_t, Convert::ToInt32, 0) + OPERATOR_TRANSITION_DEFINE (uint32_t, Convert::ToUInt32, 0) + OPERATOR_TRANSITION_DEFINE (int64_t, Convert::ToInt64, 0) + OPERATOR_TRANSITION_DEFINE (uint64_t, Convert::ToUInt64, 0) + OPERATOR_TRANSITION_DEFINE (float, Convert::ToSingle, 0) + OPERATOR_TRANSITION_DEFINE (double, Convert::ToDouble, 0) + OPERATOR_TRANSITION_DEFINE (System::Decimal, Convert::ToDecimal, 0) + OPERATOR_TRANSITION_DEFINE (System::DateTime, Convert::ToDateTime, Convert::ToDateTime (0)) + #ifdef OPERATOR_TRANSITION_DEFINE + #undef OPERATOR_TRANSITION_DEFINE + #endif + }; + [ComVisible (true)] + public ref class Section + { + private: + String ^filepath = ""; + String ^section = ""; + public: + property String ^FilePath { String ^get () { return filepath; } } + property String ^SectionName { String ^get () { return section; } } + Section (String ^file, String ^sect): filepath (file), section (sect) {} + Key ^GetKey (String ^key) { return gcnew Key (filepath, section, key); } + Object ^Get (String ^key, Object ^dflt) { return GetKey (key)->Get (dflt); } + Object ^Get (String ^key) { return GetKey (key)->Get (); } + bool Set (String ^key, Object ^value) { return GetKey (key)->Set (value); } + Key ^operator [] (String ^key) { return GetKey (key); } + }; + [ComVisible (true)] + public ref class InitConfig + { + private: + String ^filepath = ""; + public: + property String ^FilePath { String ^get () { return filepath; } void set (String ^path) { filepath = path; } } + InitConfig (String ^path): filepath (path) {} + InitConfig () {} + Section ^GetSection (String ^section) { return gcnew Section (filepath, section); } + Key ^GetKey (String ^section, String ^key) { return gcnew Key (filepath, section, key); } + Object ^Get (String ^section, String ^key, String ^dflt) { return GetKey (section, key)->Get (dflt); } + Object ^Get (String ^section, String ^key) { return GetKey (section, key)->Get (); } + Section ^Get (String ^section) { return GetSection (section); } + bool Set (String ^section, String ^key, String ^value) { return GetKey (section, key)->Set (value); } + Section ^operator [] (String ^section) { return GetSection (section); } + }; +} + +#endif \ No newline at end of file diff --git a/appinstaller/main.cpp b/appinstaller/main.cpp index ff2de35..6902de1 100644 --- a/appinstaller/main.cpp +++ b/appinstaller/main.cpp @@ -21,6 +21,7 @@ #include "pkgmgr.h" #include "notice.h" #include "certmgr.h" +#include "bridge.h" using namespace System; using namespace System::Runtime::InteropServices; @@ -91,6 +92,118 @@ HRESULT GetWebBrowser2Interface (System::Windows::Forms::WebBrowser ^fwb, IWebBr Marshal::Release (pUnk); return hr; } +[ComVisible (true)] +public ref class _I_InitConfig +{ + public: + Win32::InitConfig ^Create (String ^filepath) { return gcnew Win32::InitConfig (filepath); } + Win32::InitConfig ^GetConfig () { return Create (CStringToMPString (g_initfile.filepath)); } +}; +[ComVisible (true)] +public ref class _I_Package +{ + public: + ref class _I_Package_Manager + { + public: + _I_Package_Manager () {} + }; + private: + _I_Package_Manager ^mgr = gcnew _I_Package_Manager (); + public: + String ^GetPackagesToJson () + { + rapidjson::Document doc; + doc.SetArray (); + auto &alloc = doc.GetAllocator (); + for (auto &it : g_pkginfo) + { + rapidjson::Value member (rapidjson::kStringType); + member.SetString (ws2utf8 (it.filepath).c_str (), alloc); + doc.PushBack (member, alloc); + } + rapidjson::StringBuffer buffer; + rapidjson::Writer writer (buffer); + doc.Accept (writer); + std::string utf8 = buffer.GetString (); + std::wstring_convert > conv; + return CStringToMPString (conv.from_bytes (utf8)); + } + String ^GetPackageInfoToJson (String ^filepath) + { + std::wstring fpath = MPStringToStdW (filepath); + for (auto &it : g_pkginfo) + { + if (PathEquals (it.filepath, fpath)) + { + return CStringToMPString (it.parseJson ()); + } + } + return "{}"; + } + property _I_Package_Manager ^Manager { _I_Package_Manager ^get () { return mgr; }} + String ^GetCapabilityDisplayName (String ^capabilityName) + { + return CStringToMPString (GetPackageCapabilityDisplayName (MPStringToStdW (capabilityName))); + } + _I_HResult ^GetPackageInstallResult (String ^filepath) + { + std::wstring path = MPStringToStdW (filepath); + if (g_pkgresult.find (path) == g_pkgresult.end ()) return nullptr; + auto &pres = g_pkgresult.at (path); + return gcnew _I_HResult ( + pres.result, + CStringToMPString (pres.error), + CStringToMPString (pres.reason) + ); + } + _I_HResult ^Activate (String ^appid) + { + DWORD dwProgressId = 0; + HRESULT hr = ActivateAppxApplication (MPStringToStdW (appid ? appid : "").c_str (), &dwProgressId); + return gcnew _I_HResult (hr); + } +}; +[ComVisible (true)] +public ref class _I_Bridge_Base +{ + protected: + _I_String ^str = gcnew _I_String (); + _I_Package ^pkg = gcnew _I_Package (); + _I_InitConfig ^initconfig = gcnew _I_InitConfig (); + public: + property _I_String ^String { _I_String ^get () { return str; }} + property _I_Package ^Package { _I_Package ^get () { return pkg; }} + property _I_InitConfig ^Config { _I_InitConfig ^get () { return initconfig; }} +}; +[ComVisible (true)] +public interface class IScriptBridge +{ + public: + virtual Object ^CallEvent (String ^funcName, Object ^e) = 0; +}; +[ComVisible (true)] +public ref class _I_Window +{ + private: + IScriptBridge ^wndinst = nullptr; + public: + _I_Window (IScriptBridge ^wnd): wndinst (wnd) {} + Object ^CallEvent (String ^name, ... array ^args) { return wndinst->CallEvent (name, args [0]); } +}; +[ComVisible (true)] +public ref class _I_Bridge_Base2: public _I_Bridge_Base +{ + protected: + _I_Window ^window; + public: + _I_Bridge_Base2 (IScriptBridge ^wnd) + { + window = gcnew _I_Window (wnd); + } + property _I_Window ^Window { _I_Window ^get () { return window; }} +}; + public ref class SplashForm: public System::Windows::Forms::Form { @@ -185,17 +298,21 @@ public ref class SplashForm: public System::Windows::Forms::Form } catch (...) {} if (splashimg) picbox->Image = splashimg; - if (backcolor != Drawing::Color::Transparent) + try { - background = backcolor; - picbox->BackColor = backcolor; - this->BackColor = backcolor; - } - else - { - picbox->BackColor = background; - this->BackColor = background; + if (backcolor != Drawing::Color::Transparent) + { + background = backcolor; + picbox->BackColor = backcolor; + this->BackColor = backcolor; + } + else + { + picbox->BackColor = background; + this->BackColor = background; + } } + catch (...) {} if (this->Owner != nullptr) { this->Owner->Resize += gcnew System::EventHandler (this, &SplashForm::OnResizeOwner); @@ -271,23 +388,7 @@ public ref class SplashForm: public System::Windows::Forms::Form } } }; -System::String ^FormatString (System::String ^fmt, ... array ^args) { return System::String::Format (fmt, args); } -std::wstring HResultToMessage (HRESULT hr) -{ - _com_error err (hr); - auto msgptr = err.ErrorMessage (); - return msgptr ? msgptr : L""; -} -String ^ EscapeToInnerXml (String ^str) -{ - using namespace System::Xml; - auto doc = gcnew System::Xml::XmlDocument (); - doc->LoadXml (""); - auto root = doc->FirstChild; - root->InnerText = str; - return root->InnerXml; -} -std::wstring EscapeToInnerXml (const std::wstring &str) { return MPStringToStdW (EscapeToInnerXml (CStringToMPString (str))); } + enum class InstallType { normal, @@ -452,42 +553,19 @@ void ActivateApp (Object ^appid) auto res = ActivateAppxApplication (MPStringToStdW (appid->ToString ())); } [ComVisible (true)] -public ref class AppListWnd: public System::Windows::Forms::Form +public ref class AppListWnd: public System::Windows::Forms::Form, public IScriptBridge { public: using WebBrowser = System::Windows::Forms::WebBrowser; using Timer = System::Windows::Forms::Timer; [ComVisible (true)] - ref class IBridge + ref class IBridge: public _I_Bridge_Base2 { private: AppListWnd ^wndinst = nullptr; public: - IBridge (AppListWnd ^wnd): wndinst (wnd) {} - ref class _I_HResult - { - private: - HRESULT hr = S_OK; - String ^errorcode = ""; - String ^detailmsg = ""; - public: - _I_HResult (HRESULT hres) - { - hr = hres; - detailmsg = CStringToMPString (HResultToMessage (hres)); - } - _I_HResult (HRESULT hres, String ^error, String ^message) - { - hr = hres; - errorcode = error; - detailmsg = message; - } - property HRESULT HResult { HRESULT get () { return hr; }} - property String ^ErrorCode { String ^get () { return errorcode; }} - property String ^Message { String ^get () { return detailmsg; }} - property bool Succeeded { bool get () { return SUCCEEDED (hr); }} - property bool Failed { bool get () { return FAILED (hr); }} - }; + using String = System::String; + IBridge (AppListWnd ^wnd): wndinst (wnd), _I_Bridge_Base2 (wnd) {} ref class _I_System { private: @@ -541,23 +619,6 @@ public ref class AppListWnd: public System::Windows::Forms::Form } } }; - ref class _I_Resources - { - public: - String ^GetById (unsigned int uiResId) { return GetRCStringCli (uiResId); } - unsigned ToId (String ^lpResName) - { - auto it = g_nameToId.find (MPStringToStdA (lpResName)); - return (it != g_nameToId.end ()) ? it->second : 0; - } - String ^ToName (unsigned int ulResId) - { - for (auto &it : g_nameToId) { if (it.second == ulResId) return CStringToMPString (it.first); } - return ""; - } - String ^GetByName (String ^lpResId) { return GetById (ToId (lpResId)); } - String ^operator [] (unsigned int uiResId) { return GetRCStringCli (uiResId); } - }; private: _I_UI ^ui = gcnew _I_UI (wndinst); _I_Resources ^ires = gcnew _I_Resources (); @@ -613,112 +674,12 @@ public ref class AppListWnd: public System::Windows::Forms::Form return "{}"; } }; - ref class _I_String - { - public: - ref class _I_NString - { - public: - bool NEquals (String ^l, String ^r) { return IsNormalizeStringEquals (MPStringToPtrW (l), MPStringToPtrW (r)); } - bool Empty (String ^l) { return IsNormalizeStringEmpty (MPStringToStdW (l)); } - int Compare (String ^l, String ^r) { return NormalizeStringCompare (MPStringToPtrW (l), MPStringToPtrW (r)); } - int Length (String ^l) { return GetNormalizeStringLength (MPStringToStdW (l)); } - }; - private: - _I_NString ^nstr = gcnew _I_NString (); - public: - property _I_NString ^NString { _I_NString ^get () { return nstr; }} - String ^Trim (String ^src) - { - std::wstring csrc = MPStringToStdW (src); - return CStringToMPString (::StringTrim (csrc)); - } - String ^ToLower (String ^src) { return CStringToMPString (StringToLower (MPStringToStdW (src))); } - String ^ToUpper (String ^src) { return CStringToMPString (StringToUpper (MPStringToStdW (src))); } - String ^Format (String ^fmt, ... array ^args) { return FormatString (fmt, args); } - String ^FormatInnerHTML (String ^fmt, ... array ^args) - { - std::wstring ihtml = EscapeToInnerXml (MPStringToStdW (fmt)); - auto pih = CStringToMPString (ihtml); - auto newargs = gcnew array (args->Length); - for (size_t i = 0; i < args->Length; i ++) - { - auto %p = newargs [i]; - p = Format ("{0}", EscapeToInnerXml (Format ("{0}", args [i]))); - } - return Format (pih, newargs); - } - }; - ref class _I_Package - { - public: - String ^GetPackagesToJson () - { - rapidjson::Document doc; - doc.SetArray (); - auto &alloc = doc.GetAllocator (); - for (auto &it : g_pkginfo) - { - rapidjson::Value member (rapidjson::kStringType); - member.SetString (ws2utf8 (it.filepath).c_str (), alloc); - doc.PushBack (member, alloc); - } - rapidjson::StringBuffer buffer; - rapidjson::Writer writer (buffer); - doc.Accept (writer); - std::string utf8 = buffer.GetString (); - std::wstring_convert > conv; - return CStringToMPString (conv.from_bytes (utf8)); - } - String ^GetPackageInfoToJson (String ^filepath) - { - std::wstring fpath = MPStringToStdW (filepath); - for (auto &it : g_pkginfo) - { - if (PathEquals (it.filepath, fpath)) - { - return CStringToMPString (it.parseJson ()); - } - } - return "{}"; - } - String ^GetCapabilityDisplayName (String ^capabilityName) - { - return CStringToMPString (GetPackageCapabilityDisplayName (MPStringToStdW (capabilityName))); - } - _I_HResult ^GetPackageInstallResult (String ^filepath) - { - std::wstring path = MPStringToStdW (filepath); - if (g_pkgresult.find (path) == g_pkgresult.end ()) return nullptr; - auto &pres = g_pkgresult.at (path); - return gcnew _I_HResult ( - pres.result, - CStringToMPString (pres.error), - CStringToMPString (pres.reason) - ); - } - void Activate (String ^appid) { ActivateApp (appid); } - }; - ref class _I_Window - { - private: - AppListWnd ^wndinst = nullptr; - public: - _I_Window (AppListWnd ^wnd): wndinst (wnd) {} - Object ^CallEvent (String ^name, ... array ^args) { return wndinst->CallEvent (name, args [0]); } - }; private: _I_System ^system = gcnew _I_System (wndinst); _I_IEFrame ^ieframe = gcnew _I_IEFrame (wndinst); - _I_String ^str = gcnew _I_String (); - _I_Package ^pkg = gcnew _I_Package (); - _I_Window ^wnd = gcnew _I_Window (wndinst); public: property _I_System ^System { _I_System ^get () { return system; }} property _I_IEFrame ^IEFrame { _I_IEFrame ^get () { return ieframe; }} - property _I_String ^String { _I_String ^get () { return str; }} - property _I_Package ^Package { _I_Package ^get () { return pkg; }} - property _I_Window ^Window { _I_Window ^get () { return wnd; }} }; private: WebBrowser ^webui = nullptr; @@ -818,7 +779,11 @@ public ref class AppListWnd: public System::Windows::Forms::Form } void OnPress_AppItem () { - OnPress_Cancel (); + // OnPress_Cancel (); + if (!this->IsHandleCreated) return; + if (InvokeRequired) this->Invoke (gcnew Action (this, &AppListWnd::Close)); + else this->Close (); + return; } void OnDeactivate (Object ^sender, EventArgs ^e) { @@ -892,7 +857,7 @@ public ref class AppListWnd: public System::Windows::Forms::Form return nullptr; } Object ^ExecScript (... array ^alpScript) { return InvokeCallScriptFunction ("eval", alpScript); } - Object ^CallEvent (String ^funcName, Object ^e) + Object ^CallEvent (String ^funcName, Object ^e) override { std::wstring fname = MPStringToStdW (funcName); if (IsNormalizeStringEquals (fname.c_str (), L"OnPress_CancelButton")) OnPress_Cancel (); @@ -940,7 +905,7 @@ public ref class AppListWnd: public System::Windows::Forms::Form } }; [ComVisible (true)] -public ref class MainHtmlWnd: public System::Windows::Forms::Form +public ref class MainHtmlWnd: public System::Windows::Forms::Form, public IScriptBridge { public: using WebBrowser = System::Windows::Forms::WebBrowser; @@ -950,38 +915,16 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form String ^pagetag = "splash"; InstallType insmode = InstallType::normal; size_t nowinstall = 0; + ITaskbarList3 *taskbar = nullptr; public: [ComVisible (true)] - ref class IBridge + ref class IBridge: public _I_Bridge_Base2 { private: MainHtmlWnd ^wndinst = nullptr; public: - IBridge (MainHtmlWnd ^wnd): wndinst (wnd) {} - ref class _I_HResult - { - private: - HRESULT hr = S_OK; - String ^errorcode = ""; - String ^detailmsg = ""; - public: - _I_HResult (HRESULT hres) - { - hr = hres; - detailmsg = CStringToMPString (HResultToMessage (hres)); - } - _I_HResult (HRESULT hres, String ^error, String ^message) - { - hr = hres; - errorcode = error; - detailmsg = message; - } - property HRESULT HResult { HRESULT get () { return hr; }} - property String ^ErrorCode { String ^get () { return errorcode; }} - property String ^Message { String ^get () { return detailmsg; }} - property bool Succeeded { bool get () { return SUCCEEDED (hr); }} - property bool Failed { bool get () { return FAILED (hr); }} - }; + using String = System::String; + IBridge (MainHtmlWnd ^wnd): wndinst (wnd), _I_Bridge_Base2 (wnd) {} ref class _I_System { private: @@ -998,8 +941,6 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form int m_width = 0; int m_height = 0; public: - property int width { int get () { return m_width; } } - property int height { int get () { return m_height; }} property int Width { int get () { return m_width; } } property int Height { int get () { return m_height; }} int getWidth () { return m_width; } @@ -1019,7 +960,26 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form return uri->AbsoluteUri; } } - property String ^SplashBackgroundColor { String ^get () { return CStringToMPString (g_vemani.splash_screen_backgroundcolor (L"App")); } } + property String ^SplashBackgroundColor + { + String ^get () + { + std::wnstring ret = L""; + auto personal = g_initfile [L"Personalization"]; + auto thememode = personal [L"AppInstaller:ThemeMode"]; + auto custommode = personal [L"AppInstaller:CustomThemeMode"]; + bool nowdark = + IsNormalizeStringEquals (thememode.read_wstring ().c_str (), L"dark") || + IsNormalizeStringEquals (thememode.read_wstring ().c_str (), L"auto") && IsAppInDarkMode () || + IsNormalizeStringEquals (thememode.read_wstring ().c_str (), L"custom") && IsNormalizeStringEquals (custommode.read_wstring ().c_str (), L"dark") || + IsNormalizeStringEquals (thememode.read_wstring ().c_str (), L"custom") && IsNormalizeStringEquals (custommode.read_wstring ().c_str (), L"auto") && IsAppInDarkMode (); + if (nowdark) ret = g_vemani.splash_screen_backgroundcolor_darkmode (L"App"); + else ret = g_vemani.splash_screen_backgroundcolor (L"App"); + if (ret.empty ()) ret = g_vemani.splash_screen_backgroundcolor (L"App"); + if (ret.empty ()) ret = g_vemani.background_color (L"App"); + return CStringToMPString (ret); + } + } void ShowSplash () { if (wndinst->SplashScreen->IsHandleCreated) wndinst->SplashScreen->Show (); else wndinst->SplashScreen->ReInit (); } void FadeAwaySplash () { wndinst->SplashScreen->FadeAway (); } void FadeOutSplash () { wndinst->SplashScreen->FadeOut (); } @@ -1047,86 +1007,6 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form } } }; - ref class _I_Resources - { - public: - String ^GetById (unsigned int uiResId) { return GetRCStringCli (uiResId); } - unsigned ToId (String ^lpResName) - { - auto it = g_nameToId.find (MPStringToStdA (lpResName)); - return (it != g_nameToId.end ()) ? it->second : 0; - } - String ^ToName (unsigned int ulResId) - { - for (auto &it : g_nameToId) { if (it.second == ulResId) return CStringToMPString (it.first); } - return ""; - } - String ^GetByName (String ^lpResId) { return GetById (ToId (lpResId)); } - String ^operator [] (unsigned int uiResId) { return GetRCStringCli (uiResId); } - }; - ref class _I_Version - { - private: - UINT16 major = 0, minor = 0, build = 0, revision = 0; - public: - property UINT16 Major { UINT16 get () { return major; } void set (UINT16 value) { major = value; } } - property UINT16 Minor { UINT16 get () { return minor; } void set (UINT16 value) { minor = value; } } - property UINT16 Build { UINT16 get () { return build; } void set (UINT16 value) { build = value; } } - property UINT16 Revision { UINT16 get () { return revision; } void set (UINT16 value) { revision = value; } } - property array ^Data - { - array ^get () - { - return gcnew array { - major, minor, build, revision - }; - } - void set (array ^arr) - { - major = minor = build = revision = 0; - for (size_t i = 0; i < arr->Length; i ++) - { - switch (i) - { - case 0: major = arr [i]; break; - case 1: minor = arr [i]; break; - case 2: build = arr [i]; break; - case 3: revision = arr [i]; break; - default: break; - } - } - } - } - property String ^DataStr - { - String ^get () { return Stringify (); } - void set (String ^str) { Parse (str); } - } - _I_Version (UINT16 p_ma, UINT16 p_mi, UINT16 p_b, UINT16 p_r): - major (p_ma), minor (p_mi), build (p_b), revision (p_r) {} - _I_Version (UINT16 p_ma, UINT16 p_mi, UINT16 p_b): - major (p_ma), minor (p_mi), build (p_b), revision (0) {} - _I_Version (UINT16 p_ma, UINT16 p_mi): - major (p_ma), minor (p_mi), build (0), revision (0) {} - _I_Version (UINT16 p_ma): - major (p_ma), minor (0), build (0), revision (0) {} - _I_Version () {} - _I_Version %Parse (String ^ver) - { - auto strarr = ver->Split ('.'); - auto arr = gcnew array (4); - for (size_t i = 0; i < strarr->Length; i ++) - { - try { arr [i] = Convert::ToUInt16 (strarr [i]); } - catch (...) {} - } - Data = arr; - return *this; - } - String ^Stringify () { return major + "." + minor + "." + build + "." + revision; } - String ^ToString () override { return Stringify (); } - bool Valid () { return Major != 0 && Minor != 0 && Build != 0 && Revision != 0; } - }; ref class _I_Locale { public: @@ -1227,100 +1107,6 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form return "{}"; } }; - ref class _I_String - { - public: - ref class _I_NString - { - public: - bool NEquals (String ^l, String ^r) { return IsNormalizeStringEquals (MPStringToPtrW (l), MPStringToPtrW (r)); } - bool Empty (String ^l) { return IsNormalizeStringEmpty (MPStringToStdW (l)); } - int Compare (String ^l, String ^r) { return NormalizeStringCompare (MPStringToPtrW (l), MPStringToPtrW (r)); } - int Length (String ^l) { return GetNormalizeStringLength (MPStringToStdW (l)); } - }; - private: - _I_NString ^nstr = gcnew _I_NString (); - public: - property _I_NString ^NString { _I_NString ^get () { return nstr; }} - String ^Trim (String ^src) - { - std::wstring csrc = MPStringToStdW (src); - return CStringToMPString (::StringTrim (csrc)); - } - String ^ToLower (String ^src) { return CStringToMPString (StringToLower (MPStringToStdW (src))); } - String ^ToUpper (String ^src) { return CStringToMPString (StringToUpper (MPStringToStdW (src))); } - String ^Format (String ^fmt, ... array ^args) { return FormatString (fmt, args); } - String ^FormatInnerHTML (String ^fmt, ... array ^args) - { - std::wstring ihtml = EscapeToInnerXml (MPStringToStdW (fmt)); - auto pih = CStringToMPString (ihtml); - auto newargs = gcnew array (args->Length); - for (size_t i = 0; i < args->Length; i ++) - { - auto %p = newargs [i]; - p = Format ("{0}", EscapeToInnerXml (Format ("{0}", args [i]))); - } - return Format (pih, newargs); - } - }; - ref class _I_Package - { - public: - ref class _I_Package_Manager - { - public: - _I_Package_Manager () {} - }; - private: - _I_Package_Manager ^mgr = gcnew _I_Package_Manager (); - public: - String ^GetPackagesToJson () - { - rapidjson::Document doc; - doc.SetArray (); - auto &alloc = doc.GetAllocator (); - for (auto &it : g_pkginfo) - { - rapidjson::Value member (rapidjson::kStringType); - member.SetString (ws2utf8 (it.filepath).c_str (), alloc); - doc.PushBack (member, alloc); - } - rapidjson::StringBuffer buffer; - rapidjson::Writer writer (buffer); - doc.Accept (writer); - std::string utf8 = buffer.GetString (); - std::wstring_convert > conv; - return CStringToMPString (conv.from_bytes (utf8)); - } - String ^GetPackageInfoToJson (String ^filepath) - { - std::wstring fpath = MPStringToStdW (filepath); - for (auto &it : g_pkginfo) - { - if (PathEquals (it.filepath, fpath)) - { - return CStringToMPString (it.parseJson ()); - } - } - return "{}"; - } - property _I_Package_Manager ^Manager { _I_Package_Manager ^get () { return mgr; }} - String ^GetCapabilityDisplayName (String ^capabilityName) - { - return CStringToMPString (GetPackageCapabilityDisplayName (MPStringToStdW (capabilityName))); - } - _I_HResult ^GetPackageInstallResult (String ^filepath) - { - std::wstring path = MPStringToStdW (filepath); - if (g_pkgresult.find (path) == g_pkgresult.end ()) return nullptr; - auto &pres = g_pkgresult.at (path); - return gcnew _I_HResult ( - pres.result, - CStringToMPString (pres.error), - CStringToMPString (pres.reason) - ); - } - }; ref class _I_Window { private: @@ -1332,14 +1118,10 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form private: _I_System ^system = gcnew _I_System (wndinst); _I_IEFrame ^ieframe = gcnew _I_IEFrame (wndinst); - _I_String ^str = gcnew _I_String (); - _I_Package ^pkg = gcnew _I_Package (); _I_Window ^wnd = gcnew _I_Window (wndinst); public: property _I_System ^System { _I_System ^get () { return system; }} property _I_IEFrame ^IEFrame { _I_IEFrame ^get () { return ieframe; }} - property _I_String ^String { _I_String ^get () { return str; }} - property _I_Package ^Package { _I_Package ^get () { return pkg; }} property _I_Window ^Window { _I_Window ^get () { return wnd; }} }; protected: @@ -1427,28 +1209,24 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form splash->ChangePosAndSize (); splash->Show (); splash->Update (); - webui->Navigate (CStringToMPString (CombinePath (GetProgramRootDirectoryW (), L"html\\install.html"))); + splash->SetSplashImage (GetSuitSplashImage ()); + System::Windows::Forms::Application::DoEvents (); + auto htmlpath = CombinePath (GetProgramRootDirectoryW (), L"html\\install.html"); + webui->Navigate (CStringToMPString (htmlpath)); + if (!IsFileExists (htmlpath)) + { + std::wstring msg = L"Error: cannot find file \"" + htmlpath + L"\"."; + MessageBoxW (InvokeGetHWND (), msg.c_str (), GetRCStringSW (IDS_WINTITLE).c_str (), MB_ICONERROR); + this->Close (); + return; + } } void OnPreviewKeyDown_WebBrowser (System::Object ^sender, System::Windows::Forms::PreviewKeyDownEventArgs ^e) { if (e->KeyCode == System::Windows::Forms::Keys::F5 || (e->KeyCode == System::Windows::Forms::Keys::R && e->Control)) e->IsInputKey = true; } - void OnResize (Object ^sender, EventArgs ^e) - { - ResizeEvent (); - } - void OnResizeEnd (Object ^sender, EventArgs ^e) - { - ResizeEvent (); - } - std::wstring GetSuitSplashImage () - { - std::wstring path = g_scaleres [this->Width >= 800 * DPI && this->Height >= 600 * DPI ? L"splashlarge" : L"splash"]; - if (IsNormalizeStringEmpty (path)) path = g_vemani.splash_screen_image (L"App"); - return path; - } - void ResizeEvent () + void ResponseSplashChange () { splash->SetSplashImage (GetSuitSplashImage ()); ExecScript ( @@ -1464,6 +1242,45 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form "}) ();" ); } + void OnResize (Object ^sender, EventArgs ^e) { ResizeEvent (); } + void OnResizeEnd (Object ^sender, EventArgs ^e) {} + std::wstring GetSuitSplashImage () + { + int limitw = 800 * DPI; + int limith = 600 * DPI; + std::wstring tag = L"splash"; + int noww = this->Width; + int nowh = this->Height; + if (noww >= limitw && nowh >= limith) + tag = L"splashlarge"; + std::wstring path = g_scaleres [tag]; + if (IsNormalizeStringEmpty (path)) + path = g_scaleres [L"splash"]; + if (IsNormalizeStringEmpty (path)) + path = g_vemani.splash_screen_image (L"App"); + return path; + } + void ResizeEvent () + { + auto &ini = g_initfile; + auto setsect = ini ["Settings"]; + auto lasts = setsect [L"AppInstaller:LastWndState"]; + auto savepos = setsect [L"AppInstaller:SavePosAndSizeBeforeCancel"]; + auto lastw = setsect [L"AppInstaller:LastWidth"]; + auto lasth = setsect [L"AppInstaller:LastHeight"]; + switch (this->WindowState) + { + case System::Windows::Forms::FormWindowState::Normal: + case System::Windows::Forms::FormWindowState::Maximized: + lasts = (int)this->WindowState; + } + if (this->WindowState == System::Windows::Forms::FormWindowState::Normal && savepos) + { + lastw = (int)(this->ClientSize.Width / DPI); + lasth = (int)(this->ClientSize.Height / DPI); + } + ResponseSplashChange (); + } System::Threading::Thread ^ThreadPackageLoadTask () { auto thread = gcnew Threading::Thread (gcnew Threading::ThreadStart (this, &MainHtmlWnd::PackageLoadTask)); @@ -1621,12 +1438,14 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form { InvokeCallScriptFunction ("setInstallingProgress", dwProgress); InvokeCallScriptFunction ("setInstallingStatus", String::Format (GetRCStringCli (IDS_INSTALLING_SINSTALLING_PROGRESS), dwProgress)); + taskbar->SetProgressValue (InvokeGetHWND (), dwProgress, 100); } void InstallProgressCallbackMultiple (DWORD dwProgress) { double progress = (dwProgress * 0.01 + nowinstall) / (double)g_pkginfo.size () * 100; InvokeCallScriptFunction ("setInstallingProgress", progress); InvokeCallScriptFunction ("setInstallingStatus", String::Format (GetRCStringCli (IDS_INSTALLING_MSINSTALLING_PROGRESS), dwProgress, nowinstall + 1, g_pkginfo.size ())); + taskbar->SetProgressValue (InvokeGetHWND (), progress * g_pkginfo.size (), 100 * g_pkginfo.size ()); } System::Threading::Thread ^ThreadPackageInstallTask () { @@ -1655,6 +1474,7 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form } else pir.result = AddAppxPackageFromPath (pi.filepath, blankdeplist, DEPOLYOPTION_NONE, gcnew InstallProgressCallbackDelegate (this, &MainHtmlWnd::InstallProgressCallback), pir.error, pir.reason); g_pkgresult [pi.filepath] = pir; + taskbar->SetProgressState (InvokeGetHWND (), TBPF_NOPROGRESS); if (pir.succeeded ()) { InvokeCallScriptFunction ("noticeLoadInstallSuccessPage", false); @@ -1722,8 +1542,10 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form pir.error, pir.reason ); + if (pir.failed ()) taskbar->SetProgressState (InvokeGetHWND (), TBPF_ERROR); g_pkgresult [it.filepath] = pir; } + taskbar->SetProgressState (InvokeGetHWND (), TBPF_NOPROGRESS); bool allsuccess = true; for (auto &it : g_pkgresult) { @@ -1910,6 +1732,7 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form MainHtmlWnd () { InitSize (); + System::Windows::Forms::Application::DoEvents (); splash = gcnew SplashForm ( CStringToMPString (GetSuitSplashImage ()), StringToColor (CStringToMPString (g_vemani.splash_screen_backgroundcolor (L"App"))), @@ -1917,17 +1740,44 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form ); System::Windows::Forms::Application::DoEvents (); Init (); + ITaskbarList3 *ptr = nullptr; + HRESULT hr = CoCreateInstance (CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, IID_ITaskbarList3, (void **)&ptr); + if (SUCCEEDED (hr)) + { + taskbar = ptr; + taskbar->HrInit (); + } + else + { + taskbar = nullptr; + if (ptr) ptr->Release (); + } } Object ^CallScriptFunction (String ^lpFuncName, ... array ^alpParams) { try { return this->webui->Document->InvokeScript (lpFuncName, alpParams); } - catch (Exception ^e) {} + catch (Exception ^e) + { + try + { + this->webui->Document->InvokeScript ("messageBoxAsync", gcnew array { + e->Message, + e->Source, + 0, + CStringToMPString (g_vemani.background_color (L"App")) + }); + } + catch (Exception ^ex) + { + MessageBoxW (InvokeGetHWND (), MPStringToStdW (e->Message).c_str (), MPStringToStdW (e->Source).c_str (), 0); + } + } return nullptr; } Object ^CallScriptFunction (String ^lpScriptName) { try { return this->webui->Document->InvokeScript (lpScriptName); } - catch (Exception ^e) {} + catch (Exception ^ex) { System::Windows::Forms::MessageBox::Show ("Error calling JavaScript function: " + ex->Message); } return nullptr; } Object ^InvokeCallScriptFunction (String ^lpFuncName, ... array ^alpParams) @@ -1951,7 +1801,7 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form return nullptr; } Object ^ExecScript (... array ^alpScript) { return InvokeCallScriptFunction ("eval", alpScript); } - Object ^CallEvent (String ^funcName, Object ^e) + Object ^CallEvent (String ^funcName, Object ^e) override { std::wstring fname = MPStringToStdW (funcName); if (IsNormalizeStringEquals (fname.c_str (), L"OnPress_Button1")) OnPress_Button1 (); @@ -1985,6 +1835,11 @@ public ref class MainHtmlWnd: public System::Windows::Forms::Form web2->ExecWB (OLECMDID_OPTICAL_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, &v, nullptr); } } + ~MainHtmlWnd () + { + if (taskbar) taskbar->Release (); + taskbar = nullptr; + } }; using MainWnd = MainHtmlWnd; std::vector LoadFileListW (const std::wstring &filePath) @@ -2051,10 +1906,7 @@ DWORD CmdMapsToFlags (std::map cmdpairs, std::vector g_nameToId = { MAKENAMEIDMAP (IDS_FAILED_STITLE), MAKENAMEIDMAP (IDS_APPLIST_WINTITLE), MAKENAMEIDMAP (IDS_APPLIST_TITLE), - MAKENAMEIDMAP (IDS_APPLIST_CANCEL) + MAKENAMEIDMAP (IDS_APPLIST_CANCEL), + MAKENAMEIDMAP (IDS_MSGBOX_OK), + MAKENAMEIDMAP (IDS_MSGBOX_CANCEL), + MAKENAMEIDMAP (IDS_MSGBOX_ABORT), + MAKENAMEIDMAP (IDS_MSGBOX_RETRY), + MAKENAMEIDMAP (IDS_MSGBOX_IGNORE), + MAKENAMEIDMAP (IDS_MSGBOX_YES), + MAKENAMEIDMAP (IDS_MSGBOX_NO), + MAKENAMEIDMAP (IDS_MSGBOX_CLOSE), + MAKENAMEIDMAP (IDS_MSGBOX_HELP), + MAKENAMEIDMAP (IDS_MSGBOX_TRYAGAIN), + MAKENAMEIDMAP (IDS_MSGBOX_CONTINUE) }; #ifdef MAKENAMEIDMAP #undef MAKENAMEIDMAP +#endif + +#ifdef __cplusplus_cli +using namespace System; +using namespace System::Runtime::InteropServices; +[ComVisible (true)] +public ref class _I_Resources +{ + public: + String ^GetById (unsigned int uiResId) { return GetRCStringCli (uiResId); } + unsigned ToId (String ^lpResName) + { + auto it = g_nameToId.find (MPStringToStdA (lpResName)); + return (it != g_nameToId.end ()) ? it->second : 0; + } + String ^ToName (unsigned int ulResId) + { + for (auto &it : g_nameToId) { if (it.second == ulResId) return CStringToMPString (it.first); } + return ""; + } + String ^GetByName (String ^lpResId) { return GetById (ToId (lpResId)); } + String ^operator [] (unsigned int uiResId) { return GetRCStringCli (uiResId); } +}; #endif \ No newline at end of file diff --git a/appinstaller/resource.h b/appinstaller/resource.h index 61b0ac4..9c0038f 100644 Binary files a/appinstaller/resource.h and b/appinstaller/resource.h differ diff --git a/appinstaller/vemani.h b/appinstaller/vemani.h index 3544a06..dfa822e 100644 --- a/appinstaller/vemani.h +++ b/appinstaller/vemani.h @@ -147,6 +147,15 @@ class vemanifest return !bg.empty () ? bg : visual.attribute ("BackgroundColor").as_string (); } std::wstring splash_screen_backgroundcolor (const std::wstring &id = L"App") const { return pugi::as_wide (this->splash_screen_backgroundcolor (pugi::as_utf8 (id))); } + std::string splash_screen_backgroundcolor_darkmode (const std::string &id = "App") const + { + pugi::xml_node visual = visual_element_node (id); + if (!visual) return ""; + pugi::xml_node splash = visual.child ("SplashScreen"); + std::string bg = splash ? splash.attribute ("DarkModeBackgroundColor").as_string () : ""; + return !bg.empty () ? bg : visual.attribute ("DarkModeBackgroundColor").as_string (); + } + std::wstring splash_screen_backgroundcolor_darkmode (const std::wstring &id = L"App") const { return pugi::as_wide (this->splash_screen_backgroundcolor_darkmode (pugi::as_utf8 (id))); } bool is_appid_exists (const std::string &id) const { pugi::xml_node root = doc.document_element (); diff --git a/appinstaller/version.h b/appinstaller/version.h index 2253e1c..fbf63ce 100644 --- a/appinstaller/version.h +++ b/appinstaller/version.h @@ -121,4 +121,73 @@ typedef struct version } return result; } -} Version; \ No newline at end of file +} Version; + +#ifdef __cplusplus_cli +using namespace System; +using namespace System::Runtime::InteropServices; +[ComVisible (true)] +public ref class _I_Version +{ + private: + UINT16 major = 0, minor = 0, build = 0, revision = 0; + public: + property UINT16 Major { UINT16 get () { return major; } void set (UINT16 value) { major = value; } } + property UINT16 Minor { UINT16 get () { return minor; } void set (UINT16 value) { minor = value; } } + property UINT16 Build { UINT16 get () { return build; } void set (UINT16 value) { build = value; } } + property UINT16 Revision { UINT16 get () { return revision; } void set (UINT16 value) { revision = value; } } + property array ^Data + { + array ^get () + { + return gcnew array { + major, minor, build, revision + }; + } + void set (array ^arr) + { + major = minor = build = revision = 0; + for (size_t i = 0; i < arr->Length; i ++) + { + switch (i) + { + case 0: major = arr [i]; break; + case 1: minor = arr [i]; break; + case 2: build = arr [i]; break; + case 3: revision = arr [i]; break; + default: break; + } + } + } + } + property String ^DataStr + { + String ^get () { return Stringify (); } + void set (String ^str) { Parse (str); } + } + _I_Version (UINT16 p_ma, UINT16 p_mi, UINT16 p_b, UINT16 p_r): + major (p_ma), minor (p_mi), build (p_b), revision (p_r) {} + _I_Version (UINT16 p_ma, UINT16 p_mi, UINT16 p_b): + major (p_ma), minor (p_mi), build (p_b), revision (0) {} + _I_Version (UINT16 p_ma, UINT16 p_mi): + major (p_ma), minor (p_mi), build (0), revision (0) {} + _I_Version (UINT16 p_ma): + major (p_ma), minor (0), build (0), revision (0) {} + _I_Version () {} + _I_Version %Parse (String ^ver) + { + auto strarr = ver->Split ('.'); + auto arr = gcnew array (4); + for (size_t i = 0; i < strarr->Length; i ++) + { + try { arr [i] = Convert::ToUInt16 (strarr [i]); } + catch (...) {} + } + Data = arr; + return *this; + } + String ^Stringify () { return major + "." + minor + "." + build + "." + revision; } + String ^ToString () override { return Stringify (); } + bool Valid () { return Major != 0 && Minor != 0 && Build != 0 && Revision != 0; } +}; +#endif \ No newline at end of file diff --git a/notice/notice.cpp b/notice/notice.cpp index e8b0f82..c92daa4 100644 --- a/notice/notice.cpp +++ b/notice/notice.cpp @@ -242,7 +242,7 @@ Windows::Data::Xml::Dom::XmlDocument ^SimpleToastNoticeXml (const std::wstring & auto node = dynamic_cast (imageNodes->Item (0)); if (node) { - node->SetAttribute (L"src", ref new Platform::String (imguri && imguri->ToString ()->Data () ? imguri->ToString ()->Data () : img.c_str ())); + node->SetAttribute (L"src", ref new Platform::String (imguri && imguri->DisplayUri ? imguri->DisplayUri->Data () : img.c_str ())); node->SetAttribute (L"alt", ref new Platform::String (L"image")); } } @@ -313,7 +313,7 @@ Windows::Data::Xml::Dom::XmlDocument ^SimpleToastNoticeXml2 (const std::wstring auto node = dynamic_cast (imageNodes->Item (0)); if (node) { - node->SetAttribute (L"src", ref new Platform::String (imguri && imguri->ToString ()->Data () ? imguri->ToString ()->Data () : img.c_str ())); + node->SetAttribute (L"src", ref new Platform::String (imguri && imguri->DisplayUri ? imguri->DisplayUri->Data () : img.c_str ())); node->SetAttribute (L"alt", ref new Platform::String (L"image")); } } diff --git a/pkgmgr/pkgmgr.cpp b/pkgmgr/pkgmgr.cpp index 1f22943..1a96b5d 100644 --- a/pkgmgr/pkgmgr.cpp +++ b/pkgmgr/pkgmgr.cpp @@ -65,7 +65,12 @@ static std::wstring StringToWString (const std::string &str, UINT codePage = CP_ using onprogress = AsyncOperationProgressHandler ; using onprogresscomp = AsyncOperationWithProgressCompletedHandler ; using progressopt = IAsyncOperationWithProgress ^; -template HRESULT RunPackageManagerOperation (TAsyncOpCreator asyncCreator, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) +typedef struct PROGRESSCALLBACK_DATA__ +{ + PKGMRR_PROGRESSCALLBACK pfCallback; + void *pCustom; +} PROGRESSCALLBACK_DATA, PPROGRESSCALLBACK_DATA; +template HRESULT RunPackageManagerOperation (TAsyncOpCreator asyncCreator, PROGRESSCALLBACK_DATA pCallbackData, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) { g_swExceptionCode = L""; g_swExceptionDetail = L""; @@ -80,8 +85,8 @@ template HRESULT RunPackageManagerOperation (TAsyncOp hCompEvt = CreateEventExW (nullptr, nullptr, CREATE_EVENT_MANUAL_RESET, EVENT_ALL_ACCESS); if (!hCompEvt) return E_FAIL; auto depopt = asyncCreator (); - depopt->Progress = ref new onprogress ([pfCallback, pCustom] (progressopt operation, DeploymentProgress progress) { - if (pfCallback) pfCallback ((DWORD)progress.percentage, pCustom); + depopt->Progress = ref new onprogress ([pCallbackData] (progressopt operation, DeploymentProgress progress) { + if (pCallbackData.pfCallback) pCallbackData.pfCallback ((DWORD)progress.percentage, pCallbackData.pCustom); }); depopt->Completed = ref new onprogresscomp ([&hCompEvt] (progressopt, Windows::Foundation::AsyncStatus) { SetEvent (hCompEvt); @@ -136,11 +141,13 @@ template HRESULT RunPackageManagerOperation (TAsyncOp [MTAThread] HRESULT AddAppxPackageFromURI (LPCWSTR lpPkgFileUri, PCREGISTER_PACKAGE_DEFENDENCIES alpDepUrlList, DWORD dwDeployOption, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) { + PROGRESSCALLBACK_DATA pdata = {pfCallback, pCustom}; return RunPackageManagerOperation ([=] () { auto pkgmgr = ref new PackageManager (); - auto depuris = ref new Platform::Collections::Vector (); + Platform::Collections::Vector ^depuris = nullptr; if (alpDepUrlList) { + depuris = ref new Platform::Collections::Vector (); for (size_t i = 0; i < alpDepUrlList->dwSize; i ++) { auto &pstr = alpDepUrlList->alpDepUris [i]; @@ -166,13 +173,13 @@ HRESULT AddAppxPackageFromURI (LPCWSTR lpPkgFileUri, PCREGISTER_PACKAGE_DEFENDEN } catch (Exception ^e) { continue; } } + if (depuris->Size == 0) depuris = nullptr; } - if (depuris->Size > 0) depuris = nullptr; + else depuris = nullptr; auto pkguri = ref new Uri (ref new String (lpPkgFileUri)); - auto pkguristr = pkguri->ToString (); - auto ope = pkgmgr->AddPackageAsync (pkguri, depuris, (DeploymentOptions)dwDeployOption); - return ope; - }, pfCallback, pCustom, pErrorCode, pDetailMsg); + auto depopt = pkgmgr->AddPackageAsync (pkguri, depuris, (DeploymentOptions)dwDeployOption); + return depopt; + }, pdata, pErrorCode, pDetailMsg); } [MTAThread] HRESULT AddAppxPackageFromPath (LPCWSTR lpPkgPath, PCREGISTER_PACKAGE_DEFENDENCIES alpDepUrlList, DWORD dwDeployOption, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) @@ -444,22 +451,25 @@ HRESULT GetAppxPackages (PKGMGR_FINDENUMCALLBACK pfCallback, void *pCustom, LPWS [MTAThread] HRESULT RemoveAppxPackage (LPCWSTR lpPkgFullName, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) { + PROGRESSCALLBACK_DATA pdata = {pfCallback, pCustom}; return RunPackageManagerOperation ([=] () { auto pkgmgr = ref new PackageManager (); return pkgmgr->RemovePackageAsync (ref new String (lpPkgFullName)); - }, pfCallback, pCustom, pErrorCode, pDetailMsg); + }, pdata, pErrorCode, pDetailMsg); } [MTAThread] HRESULT CleanupAppxPackage (LPCWSTR lpPkgName, LPCWSTR lpUserSID, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) { + PROGRESSCALLBACK_DATA pdata = {pfCallback, pCustom}; return RunPackageManagerOperation ([=] () { auto pkgmgr = ref new PackageManager (); return pkgmgr->CleanupPackageForUserAsync (ref new String (lpPkgName), ref new String (lpUserSID)); - }, pfCallback, pCustom, pErrorCode, pDetailMsg); + }, pdata, pErrorCode, pDetailMsg); } [MTAThread] HRESULT RegisterAppxPackageByUri (LPCWSTR lpManifestUri, PCREGISTER_PACKAGE_DEFENDENCIES alpDependencyUriList, DWORD dwDeployOption, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) { + PROGRESSCALLBACK_DATA pdata = {pfCallback, pCustom}; return RunPackageManagerOperation ([=] () { auto pkgmgr = ref new PackageManager (); auto depuris = ref new Platform::Collections::Vector (); @@ -491,9 +501,9 @@ HRESULT RegisterAppxPackageByUri (LPCWSTR lpManifestUri, PCREGISTER_PACKAGE_DEFE catch (Exception ^e) { continue; } } } - if (depuris->Size > 0) depuris = nullptr; + if (depuris->Size == 0) depuris = nullptr; return pkgmgr->RegisterPackageAsync (ref new Uri (ref new String (lpManifestUri)), depuris, (DeploymentOptions)dwDeployOption); - }, pfCallback, pCustom, pErrorCode, pDetailMsg); + }, pdata, pErrorCode, pDetailMsg); } [MTAThread] HRESULT RegisterAppxPackageByPath (LPCWSTR lpManifestPath, PCREGISTER_PACKAGE_DEFENDENCIES alpDependencyUriList, DWORD dwDeployOption, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) @@ -505,6 +515,7 @@ HRESULT RegisterAppxPackageByPath (LPCWSTR lpManifestPath, PCREGISTER_PACKAGE_DE [MTAThread] HRESULT RegisterAppxPackageByFullName (LPCWSTR lpPackageFullName, PCREGISTER_PACKAGE_DEFENDENCIES alpDepFullNameList, DWORD dwDeployOption, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) { + PROGRESSCALLBACK_DATA pdata = {pfCallback, pCustom}; return RunPackageManagerOperation ([=] () { auto pkgmgr = ref new PackageManager (); auto depuris = ref new Platform::Collections::Vector (); @@ -517,9 +528,9 @@ HRESULT RegisterAppxPackageByFullName (LPCWSTR lpPackageFullName, PCREGISTER_PAC catch (Exception ^e) { continue; } } } - if (depuris->Size > 0) depuris = nullptr; + if (depuris->Size == 0) depuris = nullptr; return pkgmgr->RegisterPackageByFullNameAsync (ref new String (lpPackageFullName), depuris, (DeploymentOptions)dwDeployOption); - }, pfCallback, pCustom, pErrorCode, pDetailMsg); + }, pdata, pErrorCode, pDetailMsg); } template HRESULT ExecPackageManagerFunctionNoReturn (TFunction func, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) { @@ -561,6 +572,7 @@ HRESULT SetAppxPackageStatus (LPCWSTR lpPackageFullName, DWORD dwStatus, LPWSTR [MTAThread] HRESULT StageAppxPackageFromURI (LPCWSTR lpFileUri, PCREGISTER_PACKAGE_DEFENDENCIES alpDepUriList, DWORD dwDeployOption, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) { + PROGRESSCALLBACK_DATA pdata = {pfCallback, pCustom}; return RunPackageManagerOperation ([=] () { auto pkgmgr = ref new PackageManager (); auto depuris = ref new Platform::Collections::Vector (); @@ -592,9 +604,9 @@ HRESULT StageAppxPackageFromURI (LPCWSTR lpFileUri, PCREGISTER_PACKAGE_DEFENDENC catch (Exception ^e) { continue; } } } - if (depuris->Size > 0) depuris = nullptr; + if (depuris->Size == 0) depuris = nullptr; return pkgmgr->StagePackageAsync (ref new Uri (ref new String (lpFileUri)), depuris, (DeploymentOptions)dwDeployOption); - }, pfCallback, pCustom, pErrorCode, pDetailMsg); + }, pdata, pErrorCode, pDetailMsg); } [MTAThread] HRESULT StageAppxPackageFromPath (LPCWSTR lpPkgPath, PCREGISTER_PACKAGE_DEFENDENCIES alpDepUriList, DWORD dwDeployOption, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) @@ -606,14 +618,16 @@ HRESULT StageAppxPackageFromPath (LPCWSTR lpPkgPath, PCREGISTER_PACKAGE_DEFENDEN [MTAThread] HRESULT StageAppxUserData (LPCWSTR lpPackageFullName, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) { + PROGRESSCALLBACK_DATA pdata = {pfCallback, pCustom}; return RunPackageManagerOperation ([=] () { auto pkgmgr = ref new PackageManager (); return pkgmgr->StageUserDataAsync (ref new String (lpPackageFullName)); - }, pfCallback, pCustom, pErrorCode, pDetailMsg); + }, pdata, pErrorCode, pDetailMsg); } [MTAThread] HRESULT UpdateAppxPackageFromURI (LPCWSTR lpPkgFileUri, PCREGISTER_PACKAGE_DEFENDENCIES alpDepUrlList, DWORD dwDeployOption, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) { + PROGRESSCALLBACK_DATA pdata = {pfCallback, pCustom}; return RunPackageManagerOperation ([=] () { auto pkgmgr = ref new PackageManager (); auto depuris = ref new Platform::Collections::Vector (); @@ -645,9 +659,9 @@ HRESULT UpdateAppxPackageFromURI (LPCWSTR lpPkgFileUri, PCREGISTER_PACKAGE_DEFEN catch (Exception ^e) { continue; } } } - if (depuris->Size > 0) depuris = nullptr; + if (depuris->Size == 0) depuris = nullptr; return pkgmgr->UpdatePackageAsync (ref new Uri (ref new String (lpPkgFileUri)), depuris, (DeploymentOptions)dwDeployOption); - }, pfCallback, pCustom, pErrorCode, pDetailMsg); + }, pdata, pErrorCode, pDetailMsg); } [MTAThread] HRESULT UpdateAppxPackageFromPath (LPCWSTR lpPkgPath, PCREGISTER_PACKAGE_DEFENDENCIES alpDepUrlList, DWORD dwDeployOption, PKGMRR_PROGRESSCALLBACK pfCallback, void *pCustom, LPWSTR *pErrorCode, LPWSTR *pDetailMsg) diff --git a/shared/VisualElementsManifest.xml b/shared/VisualElementsManifest.xml index bad6ca1..b7b46f2 100644 --- a/shared/VisualElementsManifest.xml +++ b/shared/VisualElementsManifest.xml @@ -7,6 +7,6 @@ Lnk32x32Logo="Icons\Main.ico" BackgroundColor='#0078D7'> - + \ No newline at end of file diff --git a/shared/config.ini b/shared/config.ini index fa1aab7..f8ba521 100644 Binary files a/shared/config.ini and b/shared/config.ini differ diff --git a/shared/html/css/pages.css b/shared/html/css/pages.css index f5ed633..c8ba708 100644 --- a/shared/html/css/pages.css +++ b/shared/html/css/pages.css @@ -11,6 +11,44 @@ body * { -ms-overflow-style: -ms-autohiding-scrollbar; } +*, +button, +input, +select, +textarea, +a, +label, +p, +span, +h1, +h2, +h3, +h4, +h5, +h6, +ul, +ol, +li, +dl, +dt, +dd, +table, +th, +td, +tr, +img, +iframe, +object, +embed, +audio, +video, +canvas, +form, +fieldset, +legend { + font-family: "Microsoft YaHei", "Segoe UI", "Ebrima", "Nirmala", "Gadugi", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo", "Leelawadee", "Microsoft JhengHei", "Malgun Gothic", "Estrangelo Edessa", "Microsoft Himalaya", "Microsoft New Tai Lue", "Microsoft PhagsPa", "Microsoft Tai Le", "Microsoft Yi Baiti", "Mongolian Baiti", "MV Boli", "Myanmar Text", "Javanese Text", "Cambria Math"; +} + .pagecontainer { padding: 0px; position: absolute; @@ -54,7 +92,8 @@ body * { .page.splash { padding: 0px; - background-color: #333; + /* background-color: #333; */ + background-color: #0078d7; } .page.splash>img { @@ -331,14 +370,28 @@ progress.win-ring:indeterminate::-ms-fill { box-sizing: border-box; } -.content #moreinfo-flyout-content p>span:nth-child(1) { +#moreinfo-flyout-content p>span:nth-child(1) { -ms-user-select: none; } -.content #moreinfo-flyout-content>span:nth-child(2) { +#moreinfo-flyout-content>span:nth-child(2) { color: darkgray; } +#moreinfo-flyout p { + margin: 0; + font-weight: normal; +} + +#moreinfo-flyout ul { + margin: 0; + padding-left: 16px; +} + +#moreinfo-flyout ul li { + margin: 0; +} + .progress * { width: 100%; } @@ -380,6 +433,15 @@ progress.win-ring:indeterminate::-ms-fill { display: none; } +.page>.controls { + display: flex; +} + +.page.splash>.controls, +.page.loading>.controls { + display: none; +} + .page.splash>.splash { display: block; } @@ -413,23 +475,28 @@ progress.win-ring:indeterminate::-ms-fill { display: block; } -.page.select>.controls.select, -.page.installsuccess>.controls.installsuccess, -.page.installfailed>.controls.installfailed { +.page.select .controls, +.page.preinstall .controls, +.page.installing .controls, +.page.installsuccess .controls, +.page.installfailed .controls { display: flex; + display: -ms-flexbox; } -.page.preinstall>.controls.preinstall, -.page.installing>.controls.installing { +.page.preinstall .controls, +.page.installing .controls { display: flex; + display: -ms-flexbox; flex-direction: row; + -ms-flex-direction: row; } -.page.select>.controls.select>.checkbox { +.page.select>.controls>.checkbox { display: none; } -.page.select>.controls.select>.command button:nth-of-type(2) { +.page.select>.controls>.command button:nth-of-type(2) { /*display: none;*/ } @@ -488,7 +555,7 @@ progress.win-ring:indeterminate::-ms-fill { display: none; } -.page.installing>.controls.installing .command { +.page.installing>.controls .command { display: none; } @@ -496,8 +563,8 @@ progress.win-ring:indeterminate::-ms-fill { display: block; } -.page.installsuccess>.controls.installsuccess .checkbox, -.page.installfailed>.controls.installfailed .checkbox { +.page.installsuccess>.controls .checkbox, +.page.installfailed>.controls .checkbox { display: none; } @@ -541,7 +608,7 @@ progress.win-ring:indeterminate::-ms-fill { display: block; } -.page.installsuccess>.controls.installsuccess>.command button:nth-of-type(2), -.page.installfailed>.controls.installsuccess>.command button:nth-of-type(2) { +.page.installsuccess>.controls>.command button:nth-of-type(2), +.page.installfailed>.controls>.command button:nth-of-type(2) { display: none; } \ No newline at end of file diff --git a/shared/html/install.html b/shared/html/install.html index 95efea0..6bdc1d3 100644 --- a/shared/html/install.html +++ b/shared/html/install.html @@ -8,9 +8,9 @@ - - - + + + @@ -22,8 +22,12 @@ + + + + @@ -176,30 +180,7 @@ -
-
-
-

-

:

-

:

-

:

-

:

-

:

-

:

-
-
-

-

:

-

:

-
-
-

-

:

-

:

-
    -
    -
    -
    + -
    @@ -321,7 +290,7 @@

    -
    +
    @@ -333,6 +302,42 @@
    +
    +
    +
    +

    +

    :

    +

    :

    +

    :

    +

    :

    +

    :

    +

    :

    +
    +
    +

    +

    :

    +

    :

    +
    +
    +

    +

    :

    +

    :

    +
      +
      +
      +
      + \ No newline at end of file diff --git a/shared/html/js/pages.js b/shared/html/js/pages.js index f5434f6..4ac8c08 100644 --- a/shared/html/js/pages.js +++ b/shared/html/js/pages.js @@ -57,6 +57,16 @@ break; } } + var strutils = Bridge.NString; + if (swPageLabel == "splash" || swPageLabel == "loading") { + var controls = page.querySelector(".controls"); + if (controls) controls.style.display = "none"; + } else { + var controls = page.querySelector(".controls"); + if (controls) controls.style.display = "flex"; + if (controls.style.display == 'none') controls.style.display = "-ms-flex"; + if (controls.style.display == 'none') controls.style.display = "-ms-flexbox"; + } (function() { if (Bridge.NString.equals(swPageLabel, "loading")) { var content = page.querySelector(".content.loading"); @@ -90,7 +100,7 @@ } } var content = page.querySelector(".content." + Bridge.String.trim(swPageLabel)); - var controls = page.querySelector(".controls." + Bridge.String.trim(swPageLabel)); + var controls = page.querySelector(".controls"); var progress = page.querySelector(".progress"); var reason = page.querySelector(".reason"); var titlepart = []; diff --git a/shared/html/js/theme.js b/shared/html/js/theme.js new file mode 100644 index 0000000..6ac9eac --- /dev/null +++ b/shared/html/js/theme.js @@ -0,0 +1,5 @@ +(function(global) { + "use strict"; + + function changeDarkMode(mode) {} +})(this); \ No newline at end of file diff --git a/shared/html/libs/msgbox/msgbox.css b/shared/html/libs/msgbox/msgbox.css new file mode 100644 index 0000000..a4465be --- /dev/null +++ b/shared/html/libs/msgbox/msgbox.css @@ -0,0 +1,75 @@ +.notice-back { + background-color: rgba(0, 0, 0, 0.5); + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + height: auto; + width: auto; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-content: center; + justify-content: center; + transition: all 0.15s linear; + opacity: 0; +} + +.notice-body { + background: #0078d7; + padding: 25px 172px; + left: 0; + right: 0; + top: auto; + bottom: auto; + max-height: 80%; + height: auto; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0 !important; + box-sizing: border-box; +} + +.notice-body>h1, +.notice-body>h2, +.notice-body>h3, +.notice-body>h4, +.notice-body>h5, +.notice-body>h6, +.notice-body>p, +.notice-body>a, +.notice-body>span { + font-family: "Microsoft YaHei", "Segoe UI", "Ebrima", "Nirmala", "Gadugi", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo", "Leelawadee", "Microsoft JhengHei", "Malgun Gothic", "Estrangelo Edessa", "Microsoft Himalaya", "Microsoft New Tai Lue", "Microsoft PhagsPa", "Microsoft Tai Le", "Microsoft Yi Baiti", "Mongolian Baiti", "MV Boli", "Myanmar Text", "Javanese Text", "Cambria Math"; + white-space: pre-wrap; + word-break: break-all; +} + +.notice-title { + color: white; + font-weight: normal; + white-space: pre-wrap; +} + +.notice-text { + color: white; + font-weight: normal; + white-space: pre-wrap; + -ms-overflow-style: -ms-autohiding-scrollbar; +} + +.notice-controls { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-content: center; + justify-content: flex-end; + align-items: center; + margin-top: 10px; + /*gap: 20px;*/ +} + +.notice-btn { + margin-left: 20px; +} \ No newline at end of file diff --git a/shared/html/libs/msgbox/msgbox.js b/shared/html/libs/msgbox/msgbox.js new file mode 100644 index 0000000..5131c6b --- /dev/null +++ b/shared/html/libs/msgbox/msgbox.js @@ -0,0 +1,349 @@ +function IsBlackLabel(text) { + if (text === null || text === undefined) return true; + var trimmed = String(text).replace(/^\s+|\s+$/g, ''); + return trimmed === ''; +} + +var MBFLAGS = { + MB_OK: 0x00000000, + MB_OKCANCEL: 0x00000001, + MB_ABORTRETRYIGNORE: 0x00000002, + MB_YESNOCANCEL: 0x00000003, + MB_YESNO: 0x00000004, + MB_RETRYCANCEL: 0x00000005, + MB_CANCELTRYCONTINUE: 0x00000006, + MB_HELP: 0x00004000, + MB_DEFBUTTON1: 0x00000000, + MB_DEFBUTTON2: 0x00000100, + MB_DEFBUTTON3: 0x00000200, + MB_DEFBUTTON4: 0x00000300, + MB_ICONERROR: 0x00000010, + MB_ICONWARNING: 0x00000030, + MB_ICONINFORMATION: 0x00000040 +}; + +var MBRET = { + IDOK: 1, + IDCANCEL: 2, + IDABORT: 3, + IDRETRY: 4, + IDIGNORE: 5, + IDYES: 6, + IDNO: 7, + IDCLOSE: 8, + IDHELP: 9, + IDTRYAGAIN: 10, + IDCONTINUE: 11 +}; + +Object.freeze(MBFLAGS); +Object.freeze(MBRET); + +function GetLocaleStringFromResId(resId) { return Bridge.Resources.byid(resId); } +var msgboxResult = {}; + +function MessageBox(_lpText, _lpCaption, _uType, _objColor) { + var id = MessageBoxForJS(_lpText, _lpCaption, _uType, _objColor, function(valueReturn) { + getRes = valueReturn; + msgboxResult[id] = valueReturn; + }); + return id; +} + +function GetMessageBoxResult(msgboxId) { + var result = msgboxResult[msgboxId]; + if (result === undefined || result === null) return null; + msgboxResult[msgboxId] = null; + return result; +} + +function ClearAllMessageBoxResults() { + msgboxResult = null; + msgboxResult = {}; +} + +// 注意:callback 函数无返回值,传入参数:整数型 按下的按钮值。 +/* + 使用示例: + MessageBoxForJS("请选择一个按钮", "", MBFLAGS.MB_OKCANCEL, "#0078d7", function(value) { + console.log("MessageBoxForJS callback value: " + value); + }) +*/ +function MessageBoxForJS(_lpText, _lpCaption, _uType, _objColor, _callback) { + var msgbox = document.createElement("div"); + msgbox.classList.add("notice-back"); + msgbox.classList.add("win-ui-dark"); + var uniqueId = "msgbox_" + new Date().getTime(); + msgbox.id = uniqueId; + var msgbody = document.createElement("div"); + msgbody.classList.add("notice-body"); + if (!IsBlackLabel(_objColor)) { + msgbody.style.backgroundColor = _objColor; + } + msgbox.appendChild(msgbody); + var msgcontainter = document.createElement("div"); + msgcontainter.style.height = "100%"; + msgcontainter.style.width = "100%"; + msgcontainter.style.maxHeight = "100%"; + msgcontainter.style.minHeight = "0px"; + msgcontainter.style.boxSizing = "border-box"; + msgbody.appendChild(msgcontainter); + var msgcaption = document.createElement("div"); + msgcontainter.appendChild(msgcaption); + msgcontainter.style.display = "flex"; + msgcontainter.style.flexDirection = "column"; + var msgcontent = document.createElement("div"); + msgcontent.style.flex = "1 1 auto"; + msgcontent.style.marginRight = "3px"; + msgcontent.style.overflowX = "hidden"; + msgcontent.style.overflowY = "auto"; + msgcontent.style.minHeight = "0px"; + msgcontainter.appendChild(msgcontent); + if (_lpCaption instanceof HTMLElement) { + msgcaption.appendChild(_lpCaption); + msgcaption.classList.add("notice-title"); + } else { + if (!IsBlackLabel(_lpCaption)) { + var msgtitle = document.createElement("h2"); + msgtitle.textContent = _lpCaption; + msgtitle.classList.add("notice-title"); + msgcaption.appendChild(msgtitle); + } else { + var msgtitle = document.createElement("h2"); + msgtitle.textContent = ""; + msgtitle.classList.add("notice-title"); + msgcaption.appendChild(msgtitle); + } + } + if (_lpText instanceof HTMLElement) { + _lpText.classList.add("notice-text"); + msgcontent.appendChild(_lpText); + } else { + if (!IsBlackLabel(_lpText)) { + var msgtext = document.createElement("p"); + msgtext.textContent = _lpText; + msgtext.classList.add("notice-text"); + if (IsBlackLabel(_lpCaption)) { + msgtext.style.marginTop = "0"; + } + msgcontent.appendChild(msgtext); + } else { + var msgtext = document.createElement("p"); + msgtext.innerText = ""; + msgtext.classList.add("notice-text"); + if (IsBlackLabel(_lpCaption)) { + msgtext.style.marginTop = "0"; + } + msgcontent.appendChild(msgtext); + } + } + var msgctrls = document.createElement("div"); + msgctrls.classList.add("notice-controls"); + msgcontainter.appendChild(msgctrls); + var cnt = 0; + var cbFuncPress = function(valueReturn) { + getRes = valueReturn; + msgbox.style.opacity = 0; + setTimeout(function() { + document.body.removeChild(msgbox); + }, 500); + if (_callback) { + _callback(valueReturn); + } + }; + var pfCreateButton = function(displayNameResId, valueReturn) { + var btn = document.createElement("button"); + btn.innerHTML = GetLocaleStringFromResId(displayNameResId); + btn.classList.add("notice-btn"); + btn.addEventListener("click", function() { + cbFuncPress(valueReturn); + }); + msgctrls.appendChild(btn); + }; + if ((_uType & MBFLAGS.MB_HELP) === MBFLAGS.MB_HELP) { + pfCreateButton(808, MBRET.IDHELP); + } + for (cnt = 0; cnt <= MBFLAGS.MB_RETRYCANCEL; cnt++) { + if ((_uType & 0xF) === cnt) { + switch (cnt) { + case MBFLAGS.MB_OK: + { + pfCreateButton(800, MBRET.IDOK); + } + break; + case MBFLAGS.MB_OKCANCEL: + { + pfCreateButton(800, MBRET.IDOK); + pfCreateButton(801, MBRET.IDCANCEL); + } + break; + case MBFLAGS.MB_ABORTRETRYIGNORE: + { + pfCreateButton(802, MBRET.IDABORT); + pfCreateButton(803, MBRET.IDRETRY); + pfCreateButton(804, MBRET.IDIGNORE); + } + break; + case MBFLAGS.MB_YESNOCANCEL: + { + pfCreateButton(805, MBRET.IDYES); + pfCreateButton(806, MBRET.IDNO); + pfCreateButton(801, MBRET.IDCANCEL); + } + break; + case MBFLAGS.MB_YESNO: + { + pfCreateButton(805, MBRET.IDYES); + pfCreateButton(806, MBRET.IDNO); + } + break; + case MBFLAGS.MB_RETRYCANCEL: + { + pfCreateButton(803, MBRET.IDRETRY); + pfCreateButton(801, MBRET.IDCANCEL); + } + break; + case MBFLAGS.MB_CANCELTRYCONTINUE: + { + pfCreateButton(801, MBRET.IDCANCEL); + pfCreateButton(803, MBRET.IDRETRY); + pfCreateButton(810, MBRET.IDCONTINUE); + } + break; + } + } + } + var btns = msgctrls.querySelectorAll("button"); + var defaultBtnCnt = 0; + if ((_uType & MBFLAGS.MB_DEFBUTTON1) === MBFLAGS.MB_DEFBUTTON1) defaultBtnCnt = 0; + if ((_uType & MBFLAGS.MB_DEFBUTTON2) === MBFLAGS.MB_DEFBUTTON2) defaultBtnCnt = 1; + if ((_uType & MBFLAGS.MB_DEFBUTTON3) === MBFLAGS.MB_DEFBUTTON3) defaultBtnCnt = 2; + if ((_uType & MBFLAGS.MB_DEFBUTTON4) === MBFLAGS.MB_DEFBUTTON4) defaultBtnCnt = 3; + for (cnt = 0; cnt < btns.length; cnt++) { + if (cnt === defaultBtnCnt) { + btns[cnt].focus(); + break; + } + } + document.body.appendChild(msgbox); + setTimeout(function() { + msgbox.style.opacity = 1; + }, 1); + return msgbox.id; +} + +function MsgBoxObj() { + this.elementId = ""; + this.callback = null; + this.type = MBFLAGS.MB_OK; + this._boundCallback = this._internalCallback.bind(this); + this._text = ""; + this._title = ""; + this._color = "#0078d7"; + this.show = function() { + this.elementId = MessageBoxForJS( + this._text, + this._title, + this.type, + this._color, + this._boundCallback + ); + setTimeout(function() { + var element = document.getElementById(this.elementId); + if (element) { + var bodyElement = element.querySelector(".notice-body"); + if (bodyElement) { + bodyElement.style.transition = "all 0.5s linear"; + } + } + }.bind(this), 100); + } + Object.defineProperty(this, "text", { + get: function() { + return this._text; + }, + set: function(value) { + this._text = value; + if (this.elementId) { + var element = document.getElementById(this.elementId); + if (element) { + var textElement = element.querySelector(".notice-text"); + if (textElement) { + if (value instanceof HTMLElement) { + value.classList.add("notice-text"); + textElement.parentNode.replaceChild(value, textElement); + } else { + textElement.innerHTML = value; + } + } + } + } + } + }); + Object.defineProperty(this, "title", { + get: function() { + return this._title; + }, + set: function(value) { + this._title = value; + if (this.elementId) { + var element = document.getElementById(this.elementId); + if (element) { + var titleElement = element.querySelector(".notice-title"); + if (titleElement) { + titleElement.innerHTML = value; + } + } + } + } + }); + Object.defineProperty(this, "color", { + get: function() { + return this._color; + }, + set: function(value) { + this._color = value; + if (this.elementId) { + var element = document.getElementById(this.elementId); + if (element) { + var bodyElement = element.querySelector(".notice-body"); + if (bodyElement) { + bodyElement.style.backgroundColor = value; + } + } + } + } + }); + this.getElement = function() { + return document.getElementById(this.elementId); + }; +} + +MsgBoxObj.prototype._internalCallback = function(returnValue) { + if (typeof this.callback === "function") { + this.callback(returnValue); + } +}; +/** + * 异步显示消息框,返回一个 Promise 对象。 + * @param {string | HTMLElement} swText 内容 + * @param {string} swTitle 标题 + * @param {MBFLAGS} uType 标志,使用 MBFLAGS 常量 + * @param {string} swColor 背景颜色文本。 + * @returns + */ +function messageBoxAsync(swText, swTitle, uType, swColor, pfCallback) { + if (typeof Promise === "undefined") { + console.error("Promise is not supported in this environment."); + MessageBoxForJS(swText, swTitle, uType, swColor, pfCallback); + } + return new Promise(function(resolve, reject) { + try { + MessageBoxForJS(swText, swTitle, uType, swColor, function(valueReturn) { + if (resolve) resolve(valueReturn); + }); + } catch (ex) { + if (reject) reject(ex); + } + }); +} \ No newline at end of file diff --git a/shared/html/libs/winjs/1.0/css/ui-dark.css b/shared/html/libs/winjs/1.0/css/ui-dark.css new file mode 100644 index 0000000..c97dcad --- /dev/null +++ b/shared/html/libs/winjs/1.0/css/ui-dark.css @@ -0,0 +1,3922 @@ +/*! + © Microsoft. All rights reserved. + + This library is supported for use in Windows Store apps only. + + Build: 1.0.9200.20602.win8_ldr.130108-1504 + + Version: Microsoft.WinJS.1.0 +*/ + +/* +Modifications for top-level elements html, body and iframe. +*/ +html, body { + height: 100%; + width: 100%; + margin: 0; + -ms-user-select: none; + cursor: default; + -ms-scroll-translation: vertical-to-horizontal; +} +html { + overflow: hidden; +} +body { + -ms-content-zooming: none; +} +iframe { + border: 0; +} +html:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + direction: rtl; +} + +/* +Explicitly define a Segoe UI font-family so that we can assign Segoe UI +Semilight to an appropriate font-weight. +*/ +@font-face { + font-family: "Segoe UI"; + font-weight: 200; + src: local("Segoe UI Light"); +} +@font-face { + font-family: "Segoe UI"; + font-weight: 300; + src: local("Segoe UI Semilight"); +} +@font-face { + font-family: "Segoe UI"; + font-weight: 400; + src: local("Segoe UI"); +} +@font-face { + font-family: "Segoe UI"; + font-weight: 600; + src: local("Segoe UI Semibold"); +} +@font-face { + font-family: "Segoe UI"; + font-weight: 700; + src: local("Segoe UI Bold"); +} +@font-face { + font-family: "Segoe UI"; + font-style: italic; + font-weight: 400; + src: local("Segoe UI Italic"); +} +@font-face { + font-family: "Segoe UI"; + font-style: italic; + font-weight: 700; + src: local("Segoe UI Bold Italic"); +} + +/* +Weight and size definitions for typographic classes and elements. +*/ +h1, .win-type-xx-large { + font-size: 42pt; + font-weight: 200; + letter-spacing: 0; + line-height: 1.1429; /* 64px when font-size is 42pt */ +} +h2, .win-type-x-large { + font-size: 20pt; + font-weight: 200; + line-height: 1.2; /* 32px when font-size is 20pt */ +} +h3, .win-type-large { + font-size: 11pt; + font-weight: 600; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +h4, .win-type-medium, code, pre, samp { + font-size: 11pt; + font-weight: 400; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +body, h5, .win-type-small, legend { + font-size: 11pt; + font-weight: 300; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +.win-type-x-small { + font-size: 11pt; + font-weight: 300; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +h6, caption, figcaption, small, .win-type-xx-small { + font-size: 9pt; + font-weight: 400; + line-height: 1.6667; /* 20px when font-size is 9pt */ +} +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0; +} +.win-type-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +h1.win-type-ellipsis, .win-type-xx-large.win-type-ellipsis { + line-height: 1.4286; /* 80px when font-size is 42pt */ +} +h2.win-type-ellipsis, .win-type-x-large.win-type-ellipsis { + line-height: 1.5; /* 40px when font-size is 20pt */ +} +dt, th { + font-size: 11pt; + font-weight: 700; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +abbr, acronym, address, blockquote, cite, dl, dd, li, ol, p, q, td, tr { + font-weight: 300; +} +b, strong { + font-weight: 700; +} +em { + font-style: italic; +} + +@media (-ms-view-state: snapped) { + h1, .win-type-xx-large { + font-size: 20pt; + font-weight: 200; + line-height: 1.2; /* 32px when font-size is 20pt */ + } + h2, .win-type-x-large { + font-size: 11pt; + font-weight: 600; + line-height: 1.3636; /* 20px when font-size is 11pt */ + } + h3, .win-type-large { + font-size: 11pt; + font-weight: 400; + line-height: 1.3636; /* 20px when font-size is 11pt */ + } + h1.win-type-ellipsis, .win-type-xx-large.win-type-ellipsis { + line-height: 1.5; /* 40px when font-size is 20pt */ + } + h2.win-type-ellipsis, .win-type-x-large.win-type-ellipsis { + line-height: 1.3636; /* 20px when font-size is 11pt */ + } +} + +/* +Additional letter-spacing for the document and its controls. +*/ +body, +button, input, +textarea, .win-textarea, +select, option { + letter-spacing: 0.02em; +} + +/* +Fixed font-family rules. +*/ +code, pre, samp { + font-family: "Consolas"; +} + +/* +Text input controls. +*/ + +input[type=text], input[type=password], input[type=email], input[type=number], +input[type=tel], input[type=url], input[type=search], textarea, .win-textarea { + -ms-user-select: element; + min-height: 28px; + min-width: 64px; + margin: 4px 0; + border-width: 2px; + border-style: solid; + font-size: 11pt; + font-weight: 400; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} + +input[type=text], input[type=password], input[type=email], input[type=number], +input[type=tel], input[type=url], input[type=search] { + width: 276px; + padding: 0; +} +input::-ms-value { + margin: 4px 8px; +} + +textarea, .win-textarea { + overflow-y: scroll; + word-wrap: break-word; + padding: 4px 8px; + /* Leave space for autohiding scrollbar on elements that enable text selection. */ + padding-right: 17px; +} +textarea:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm), .win-textarea:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + /* Flip sides for autohiding scrollbar padding on RTL layouts. */ + padding-right: 8px; + padding-left: 17px; +} +textarea { + min-width: 260px; + min-height: 39px; +} +.win-textarea { + width: 260px; + height: 39px; +} + +/* +Clear and reveal button styles. +*/ + +input::-ms-clear, input::-ms-reveal { + margin-left: 2px; +} +input:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-clear, +input:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-reveal { + margin-left: 0; + margin-right: 2px; +} + +/* +Button control. +*/ +button, input[type=button], input[type=reset], input[type=submit] { + min-height: 32px; /* content (20px) + padding + border */ + min-width: 90px; + padding: 4px 8px; + border-width: 2px; + border-style: solid; + background-clip: padding-box; + font-size: 11pt; + font-weight: 600; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} + +/* +File upload control. +*/ +input[type=file] { + border: none; + min-width: 100px; + min-height: 20px; + width: 340px; + height: 32px; + padding: 0; + margin: 7px 8px 21px 8px; + background-clip: padding-box; +} +input[type=file]::-ms-value { + margin: 0; + border-width: 2px; + border-style: solid; + border-right-style: none; + border-radius: 0; + background-clip: padding-box; + font-size: 11pt; + font-weight: 400; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +input[type=file]:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-value { + border-left-style: none; + border-right-style: solid; +} +input[type=file]::-ms-browse { + margin: 0; + padding: 0 18px; + border-width: 2px; + border-style: solid; + background-clip: padding-box; + font-size: 11pt; + font-weight: 600; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} + +/* +No underline or special cursor on links. +*/ +a { + text-decoration: none; + cursor: default; +} + +/* +No border on images when in a link. +*/ +img { + border-style: none; +} + +/* +Select control. +*/ +select { + min-height: 32px; + min-width: 80px; + border-width: 2px; + border-style: solid; + margin: 4px 0; + font-size: 11pt; + font-weight: 400; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +select::-ms-value { + padding: 4px 8px; + margin: 0; +} +select::-ms-expand { + border: 0; + padding-left: 6px; + padding-right: 6px; + background-color: transparent; +} + +/* +Radio button and checkbox shared styles. +*/ +input::-ms-check { + border-width: 2px; + border-style: solid; + display: inline-block; +} + +/* +Radio button. +*/ +input[type=radio] { + width: 23px; + height: 23px; + margin-left: -1px; + margin-right: 4px; + margin-bottom: -2px; +} +input[type=radio]:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-left: 4px; + margin-right: -1px; +} + +/* +Checkbox. +*/ +input[type=checkbox] { + width: 21px; + height: 21px; + margin-right: 5px; +} +input[type=checkbox]:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-right: 0; + margin-left: 5px; +} + +/* +Range control. +*/ +input[type=range] { + width: 280px; + height: auto; + padding: 17px 0 32px 0; +} +input[type=range]::-ms-track { + padding: 0; + width: auto; + height: 11px; + border-style: none; + color: transparent; /* ticks hidden by default */ +} +input[type=range]::-ms-thumb { + width: 11px; + height: 11px; + border-style: none; +} +input[type=range]::-ms-ticks-before, input[type=range]::-ms-ticks-after { + width: 100%; + height: 5px; + display: none; +} +input[type=range]:disabled::-ms-fill-lower { + margin-right: 5px; +} +input[type=range]:disabled::-ms-fill-upper { + margin-left: 6px; +} +input[type=range]:disabled:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-fill-lower { + margin-right: 0; + margin-left: 5px; +} +input[type=range]:disabled:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-fill-upper { + margin-left: 0; + margin-right: 6px; +} + +/* +Vertical range control. +*/ +input[type=range].win-vertical { + width: auto; + height: 191px; + padding: 0 17px; + writing-mode: bt-lr; +} +input[type=range].win-vertical:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + writing-mode: bt-rl; +} +input[type=range].win-vertical::-ms-track { + width: 11px; + height: auto; +} +input[type=range].win-vertical:disabled::-ms-fill-lower { + margin-right: 0; + margin-top: 6px; +} +input[type=range].win-vertical:disabled::-ms-fill-upper { + margin-left: 0; + margin-bottom: 5px; +} +input[type=range].win-vertical:disabled:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-fill-lower { + margin-left: 0; +} +input[type=range].win-vertical:disabled:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-fill-upper { + margin-right: 0; +} + +input[type=range].win-vertical::-ms-ticks-before, +input[type=range].win-vertical::-ms-ticks-after { + width: 5px; + height: 100%; + display: none; +} + +/* +Progress control. +*/ +progress { + width: 180px; + height: 6px; + border-style: none; +} +progress.win-medium { + width: 280px; +} +progress.win-large { + width: 100%; +} +progress::-ms-fill { + border-style: none; +} +progress:indeterminate { + height: 4px; + padding: 1px 0; /* Indeterminate dots should be 4px tall, but overall control stays 6px. */ +} +@keyframes win-progress-fade-out { + from { + opacity: 1.0; + } + to { + opacity: 0.5; + } +} +progress.win-paused:not(:indeterminate) { + animation-name: win-progress-fade-out; + animation-duration: 3s; + animation-timing-function: cubic-bezier(0.03, 0.76, 0.31, 1.0); + opacity: 0.5; +} +progress.win-error::-ms-fill { + opacity: 0; +} +progress.win-ring:indeterminate::-ms-fill { + animation-name: -ms-ring; +} +progress.win-ring { + width: 20px; + height: 20px; +} +progress.win-medium.win-ring { + width: 40px; + height: 40px; +} +progress.win-large.win-ring { + width: 60px; + height: 60px; +} + +form { + margin: 0; + padding: 0; +} +legend { + margin: 0 0 10px 0; + padding: 0; + color: inherit; +} + +/* +AppBar/Flyout z-index values: + 1000 - AppBar/settings click-eating div + 1002 - AppBar + 1004 - Settings Flyout + 1006 - Flyout click-eating div + 1008 - Flyout +*/ + +@font-face { + font-family: "Segoe UI Command"; + src: local("Segoe UI Symbol"); + font-weight: normal; + font-style: normal; +} + +.win-appbarclickeater { + z-index: 1000; + opacity: 0; + background-color: Purple; + display: none; + width: 100%; + height: 100%; + left: 0; + top: 0; + position: fixed; + -ms-touch-action: none; +} +.win-flyoutmenuclickeater { + z-index: 1006; + opacity: 0; + background-color: Lime; + display: none; + width: 100%; + height: 100%; + left: 0; + top: 0; + position: fixed; + -ms-touch-action: none; +} + +/* +Back button. +*/ +.win-backbutton { + display: inline-block; + min-width: 0; + min-height: 0; + background-clip: border-box; + box-sizing: border-box; + border-radius: 50%; + border-width: 2px; + border-style: solid; + padding: 0; + text-align: center; + + /* Normal sizing. */ + width: 41px; + height: 41px; + font-size: 14pt; + line-height: 37px; /* line-height must match the content box height. */ + vertical-align: baseline; +} +.win-backbutton:hover, .win-backbutton:hover:active { + border-width: 2px; + border-style: solid; + border-radius: 50%; +} +.win-backbutton::before { + font-family: "Segoe UI Symbol"; + font-weight: normal; + content: "\E0D5"; + vertical-align: 50%; +} +.win-backbutton:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::before { + content: "\E0AE"; +} +.win-backbutton:disabled, .win-backbutton:disabled:active { + visibility: hidden; +} + +/* +Back button snapped view sizing. +*/ +@media (-ms-view-state: snapped) { + .win-backbutton { + width: 30px; + height: 30px; + font-size: 10pt; + line-height: 26px; /* line-height must match the content box height */ + vertical-align: -0.37em; + } +} + +/* +Command buttons. +*/ +button.win-command { + background: none; + background-clip: border-box; + height: auto; /* height is 88px (label) or 68px (no label) */ + padding: 12px 0; /* bottom dependent on label/img, 2px margin */ + margin: 0; + border: 2px solid; /* reserve focus rect */ + min-width: 40px; + text-align: center; + font-size: 9pt; + line-height: 16px; + font-weight: normal; + + /* Commands are lrtb */ + writing-mode: lr-tb; +} +button.win-command:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + writing-mode: rl-tb; +} +button.win-command:focus { + outline: none; +} + +/* +Command button icons. +*/ +.win-commandicon { + display: inline-block; + margin: 0 28px; /* padding for command buttons, 2px/side for focus */ + min-width: 0; + min-height: 0; + padding: 0; + + /* Normal sizing */ + width: 40px; + height: 40px; + box-sizing: border-box; + cursor: default; +} +.win-commandimage { + /* Default font for glyphs. */ + font-family: "Segoe UI Command"; + letter-spacing: 0; + + /* Applications provide their own content, like . */ + vertical-align: middle; + font-size: 14pt; + margin: -2px; + line-height: 40px; /* line-height must match the content box height */ + background-position: 0 0; + background-origin: border-box; + display: inline-block; + /* Still 40px */ + width: 40px; + height: 40px; + background-size: 160px 80px; +} + +/* +Offsets for sprite versions. +*/ +button:hover .win-commandimage, button:active .win-commandimage { + background-position: -40px 0; +} +button:hover:active .win-commandimage { + background-position: -80px 0; +} + +button:-ms-keyboard-active .win-commandimage { + background-position: -80px 0; +} +button:disabled .win-commandimage, +button:disabled:active .win-commandimage { + background-position: -120px 0; +} + +/* +Offsets for sprite versions in selected state. +*/ +button[aria-checked=true] .win-commandimage { + background-position: 0 -40px; +} +button[aria-checked=true]:hover .win-commandimage, +button[aria-checked=true]:active .win-commandimage { + background-position: -40px -40px; +} +button[aria-checked=true]:hover:active .win-commandimage { + background-position: -80px -40px; +} +button[aria-checked=true]:-ms-keyboard-active .win-commandimage { + background-position: -80px -40px; +} +button[aria-checked=true]:disabled .win-commandimage, +button[aria-checked=true]:disabled:active .win-commandimage { + background-position: -120px -40px; +} + +/* +Command button "ring". +*/ +.win-commandring, +button:hover .win-commandring, +button:active .win-commandring, +button.win-command:disabled .win-commandring, +button.win-command:disabled:active .win-commandring { + border-width: 2px; + border-style: solid; + border-radius: 50%; + background-clip: border-box; +} +button:hover:active .win-commandring, +button[aria-checked=true] .win-commandring, +button[aria-checked=true]:active .win-commandring, +button[aria-checked=true]:disabled .win-commandring, +button[aria-checked=true]:disabled:active .win-commandring { + background-clip: border-box; +} + +/* +Command button labels. +*/ +button.win-command .win-label { + position: relative; + line-height: 16px; + display: block; + max-width: 88px; /* 100px button, but allow for 2px margins and 4px padding on each side */ + margin-top: 5px; + margin-bottom: -1px; + padding-left: 4px; /* 12px between buttons, 6px per side, minus 2px margins */ + padding-right: 4px; + overflow: hidden; + word-wrap: break-word; + word-break: keep-all; +} + +/* +AppBarCommand separator. +*/ +hr.win-command { + display: inline-block; + padding: 0; + margin: 14px 29px 34px 30px; + width: 1px; + height: 40px; + border: 0; + vertical-align: top; +} +hr.win-command:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-left: 29px; + margin-right: 30px; +} + +/* +AppBar Edgy Container +*/ +.win-appbar { + z-index: 1002; + border-width: 0; + width: 100%; + height: auto; + left: 0; + position: fixed; + -ms-touch-select: none; +} + +/* +AppBar control. +*/ +.win-commandlayout { + text-align: right; + padding-left: 10px; + padding-right: calc(((100% + 5px) mod 20px) + 5px); + width: calc(100% - 10px - (((100% + 5px) mod 20px) + 5px)); + + /* Hide whitespace between buttons. */ + font-size: 0; + + min-height: 88px; +} +.win-commandlayout .win-selection { + float: left; +} +.win-commandlayout:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + text-align: left; +} +.win-commandlayout .win-selection:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + float: right; +} + +/* Narrow buttons and no labels when < 1024px. */ +@media (max-width: 1023px) { + button.win-command .win-label { + display: none; + } + button.win-command .win-commandicon { + margin: 0 8px; /* 2px margin for focus */ + } + .win-commandlayout { + min-height: 68px; + } + hr.win-command { + margin-bottom: 14px; + margin-top: 14px; + } +} + +@media (min-width: 321px) and (max-width: 1023px) { + hr.win-command { + margin-left: 10px; + margin-right: 9px; + } + hr.win-command:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-left: 9px; + margin-right: 10px; + } +} + +/* Snapped AppBar reduced margins. */ +@media (max-width: 320px) { + .win-commandlayout { + padding-right: 10px; + width: calc(100% - 20px); + } + + .win-commandlayout.win-bottom { + /* Overflow buttons up instead of down */ + writing-mode: lr-bt; + } + + .win-commandlayout.win-bottom:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + writing-mode: rl-bt; + } +} + +/* +High contrast AppBar needs a border +*/ +@media (-ms-high-contrast) { + /* + AppBar Borders + */ + .win-appbar { + border: solid 2px; + } + .win-appbar.win-top { + border-top: none; + border-left: none; + border-right: none; + } + .win-appbar.win-bottom { + border-bottom: none; + border-left: none; + border-right: none; + } + .win-appbar.win-top button.win-command { + padding-bottom: 10px; + } + .win-appbar.win-bottom button.win-command { + padding-top: 10px; + } + .win-appbar.win-top hr.win-command { + margin-bottom: 32px; + } + .win-appbar.win-bottom hr.win-command { + margin-top: 12px; + } + .win-commandlayout { + min-height: 66px; + } +} +/* Portrait and snapped slightly different */ +@media (-ms-high-contrast) and (max-width: 1023px) { + .win-commandlayout { + min-height: 66px; + } + .win-appbar.win-top hr.win-command { + margin-bottom: 12px; + } + .win-appbar.win-bottom hr.win-command { + margin-top: 12px; + } +} + +/* +Flyout control. +*/ +.win-flyout { + z-index: 1008; + position: fixed; + padding: 25px 20px 20px 20px; + border-style: solid; + border-width: 2px; + margin: 5px; + min-width: 26px; /* 70px - padding - border = 26px */ + max-width: 466px; /* 510px - padding - border = 466px */ + min-height: 5px; /* 54px - padding - border = 5px */ + max-height: calc(100% - 59px); /* 768px - margin - padding - border = 709px */ + width: auto; + height: auto; + word-wrap: break-word; + overflow: auto; + -ms-touch-select: none; + font-size: 11pt; + font-weight: 400; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +.win-flyout.win-leftalign { + margin-left: 0; +} +.win-flyout.win-rightalign { + margin-right: 0; +} +.win-flyout.win-scrolls { + overflow: auto; +} + +/* +Menu control. +*/ +.win-menu { + padding: 5px 0 5px 0; + line-height: 33px; + text-align: left; /* Set explicitly in case our parent has different alignment, like appbar overflow. */ + min-height: 38px; /* 54px - padding - border = 38px */ + max-height: calc(100% - 26px); /* 768px - margin - padding - border = 742px */ + max-width: none; +} +.win-menu:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + text-align: right; /* Set explicitly in case our parent has different alignment, like appbar overflow. */ +} + +/* +Menu commands. +*/ +.win-menu button.win-command { + display: block; + border: none; + padding: 10px 20px 12px 20px; + margin-left: 0; + margin-right: 0; + float: none; + text-align: left; + width: 100%; + font-size: 11pt; + font-weight: 600; + line-height: 18px; /* 40px - 10px top padding - 12px bottom padding */ +} +.win-menu button.win-command:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + text-align: right; +} +.win-menu button.win-command:focus { + outline: none; +} +.win-menu hr.win-command { + display: block; + height: 1px; + width: auto; + border: 0; + + padding: 0; + margin: 9px 20px 10px 20px; +} + +/* +Menu toggle buttons. +*/ +.win-menu-toggle button.win-command::before { + font-family: 'Segoe UI Symbol'; + content: "\E0E7"; + visibility: hidden; + padding-left: 0px; + padding-right: 10px; +} +.win-menu-toggle button[aria-checked=true].win-command::before { + /* Display a checkbox if aria-checked is set */ + visibility: visible; +} +.win-menu-toggle button.win-command:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::before { + padding-right: 0; + padding-left: 10px; + float: right; +} + +/* +Settings Pane +*/ +.win-settingsflyout { + z-index: 1004; /* above appbar and below flyouts */ + border-left: 1px solid; + position: fixed; + top: 0; + right: 0; + height: 100%; + width: 345px; /* 346px - border (1px) */ + -ms-touch-select: none; +} +.win-settingsflyout.win-wide { + width: 645px; /* 646px - border (1px) */ +} +.win-settingsflyout .win-header { + padding-left: 40px; + padding-right: 40px; + padding-top: 32px; + height: 48px; +} +/* Settings back button is slightly smaller. */ +.win-settingsflyout .win-backbutton { + position: absolute; + width: 30px; + height: 30px; + font-size: 8pt; + line-height: 26px; + margin-top: 3px; +} +.win-settingsflyout .win-header .win-label { + display: inline-block; + padding-left: 40px; + font-size: 20pt; + line-height: 33px; +} +.win-settingsflyout .win-content { + overflow: auto; + padding-left: 40px; + padding-right: 40px; + padding-top: 33px; + height: calc(100% - 112px); +} + +.win-settingsflyout .win-label { + font-size: 20pt; + font-weight: 200; + line-height: normal; +} +.win-settingsflyout:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + right: auto; + left: 0; + border-left: none; + border-right: 1px solid; +} +.win-settingsflyout .win-header .win-label:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + padding-right: 40px; + padding-left: 0; +} + +.win-settingsflyout .win-content .win-settings-section { + padding-top: 0; + padding-bottom: 39px; +} + +.win-settingsflyout .win-content .win-settings-section p { + margin: 0; + padding-top: 0; + padding-bottom: 25px; +} + +.win-settingsflyout .win-content .win-settings-section a { + margin: 0; + padding-top: 0; + padding-bottom: 25px; + display:inline-block; +} + +.win-settingsflyout .win-content .win-settings-section .win-toggleswitch { + margin: 0; + padding-top: 0; + padding-bottom: 20px; +} + +.win-settingsflyout .win-content .win-settings-section label { + display:block; + padding-bottom: 7px; +} + +.win-settingsflyout .win-content .win-settings-section button, +.win-settingsflyout .win-content .win-settings-section select, +.win-settingsflyout .win-content .win-settings-section input[type=button], +.win-settingsflyout .win-content .win-settings-section input[type=text] { + margin-bottom: 25px; + margin-left: 0; + margin-right: 20px; +} + +.win-settingsflyout .win-content .win-settings-section input[type=radio] { + margin-top: 0; + margin-bottom: 0; + padding-bottom: 15px; +} + +.win-settingsflyout .win-content .win-settings-section button:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-bottom: 25px; + margin-left: 20px; + margin-right: 0; +} +.win-settingsflyout .win-content .win-settings-section select:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-bottom: 25px; + margin-left: 20px; + margin-right: 0; +} + +.win-settingsflyout .win-content .win-settings-section input[type=text]:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-bottom: 25px; + margin-left: 20px; + margin-right: 0; +} + +.win-settingsflyout .win-content .win-settings-section input[type=button]:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-bottom: 25px; + margin-left: 20px; + margin-right: 0; +} + +/* +Common AppBar/Flyout/SettingsFlyout styles +*/ + +.win-flyout input[type=text], +.win-settingsflyout input[type=text], +.win-appbar input[type=text] { + -ms-touch-select: grippers; +} + +/* +Rating control. +*/ +.win-rating { + display: -ms-inline-flexbox; + height: auto; + width: auto; + white-space: normal; + -ms-flex-align: stretch; + -ms-flex-pack: center; +} + +.win-rating .win-star { + -ms-flex: 1 1 auto; + height:28px; + width: 28px; + padding: 0 6px; + font-family: "Segoe UI Symbol"; + font-size: 28px; + overflow: hidden; + text-indent: 0; + line-height: 1; + cursor: default; + position: relative; + letter-spacing: 0; /* Use letter-spacing: 0 to make average star look like one glyph. */ + -ms-touch-action: none; +} +.win-rating.win-small .win-star { + width: 14px; + height: 14px; + font-size: 14px; + padding: 0 3px; +} +.win-rating .win-star:before { + content: "\E082"; +} +.win-rating .win-star.win-disabled { + cursor: default; + -ms-touch-action: auto; +} + +/* +DatePicker control. +*/ +.win-datepicker { + display: -ms-inline-flexbox; + height: auto; + width: auto; +} +.win-datepicker .win-datepicker-month { + margin-right: 20px; +} +.win-datepicker .win-datepicker-date.win-order0, +.win-datepicker .win-datepicker-date.win-order1 { + margin-right: 20px; +} +.win-datepicker .win-datepicker-year.win-order0 { + margin-right: 20px; +} +.win-datepicker .win-datepicker-month:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-right: 0; + margin-left: 20px; +} +.win-datepicker .win-datepicker-date.win-order0:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm), +.win-datepicker .win-datepicker-date.win-order1:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-right: 0; + margin-left: 20px; +} +.win-datepicker .win-datepicker-year.win-order0:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm){ + margin-right: 0; + margin-left: 20px; +} + +/* +TimePicker control. +*/ +.win-timepicker { + display: -ms-inline-flexbox; + height: auto; + width: auto; +} +.win-timepicker .win-timepicker-hour { + margin-right: 20px; +} +.win-timepicker .win-timepicker-period.win-order0 { + margin-right: 20px; +} +.win-timepicker .win-timepicker-minute.win-order1 { + margin-right: 20px; +} +.win-timepicker .win-timepicker-period.win-order0:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-right: 0; + margin-left: 20px; +} +.win-timepicker .win-timepicker-minute.win-order1:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm), +.win-timepicker .win-timepicker-minute.win-order0:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-left: 20px; + margin-right: 0; +} +.win-timepicker .win-timepicker-hour:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-right: 0; + margin-left: 20px; +} + +/* +Toggle control. +*/ +.win-toggleswitch { + padding: 0; + background-color: transparent; + vertical-align: top; + margin: 0; + display: block; +} +.win-toggleswitch .win-switch { + display: inline-block; + height: 19px; + width: 50px; + padding: 5px; + margin: 0; + background-color: transparent; + -ms-grid-column: 2; +} +.win-toggleswitch .win-switch::-ms-tooltip { + display: none; +} +.win-toggleswitch .win-switch::-ms-ticks-before, +.win-toggleswitch .win-switch::-ms-ticks-after { + display: none; +} +.win-toggleswitch .win-switch::-ms-track { + height: 15px; /* 19px - 2px borders */ + width: 46px; + padding: 0; + margin: 0; + border-width: 2px; + border-style: solid; +} +.win-toggleswitch .win-switch::-ms-fill-lower, +.win-toggleswitch .win-switch::-ms-fill-upper { + height: 13px; /* 19px - 2px borders - 1px margins */ + padding: 0; +} +.win-toggleswitch .win-switch::-ms-fill-lower { + margin-left: 1px; +} +.win-toggleswitch .win-switch:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-fill-lower { + margin-left: 0; + margin-right: 1px; +} +.win-toggleswitch .win-switch::-ms-fill-upper { + margin-right: 1px; +} +.win-toggleswitch .win-switch:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-fill-upper { + margin-right: 0; + margin-left: 1px; +} +.win-toggleswitch .win-switch::-ms-thumb { + height: 19px; + width: 12px; +} +.win-toggleswitch .win-title { + display: block; + vertical-align: top; + max-width:470px; + font-size: 11pt; + font-weight: 300; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +.win-toggleswitch .win-label { + display: inline-block; + vertical-align: top; + padding: 5px 20px 5px 0px; + min-width: 65px; + direction: inherit; + -ms-grid-column: 1; + -ms-grid-row: 1; + font-size: 11pt; + font-weight: 600; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +.win-toggleswitch .win-label:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + padding: 5px 0px 5px 20px; +} +.win-toggleswitch .win-label.win-hidden { + visibility: hidden; +} +.win-focus-hide { + outline: none; +} + +/* +Tooltip control. +*/ +.win-tooltip { + display: block; + position: fixed; + top: 30px; + left: 30px; + max-width: 380px; + box-sizing: border-box; + margin: 0; + padding: 6px 10px 7px 10px; + border-style: solid; + border-width: 2px; + z-index: 9999; + word-wrap: break-word; + animation-fill-mode: both; + font-size: 9pt; + font-weight: 400; + line-height: 1.6667; /* 20px when font-size is 9pt */ +} +.win-tooltip-phantom { + display: block; + position: fixed; + top: 30px; + left: 30px; + background-color: transparent; + border-width: 0; + margin: 0; + padding: 0; +} + +@media (-ms-view-state: snapped) { + .win-tooltip { + max-width: 310px; + box-sizing: border-box; + } +} + +.win-viewbox { + width: 100%; + height: 100%; + position: relative; +} + +.win-semanticzoom { + height: 400px; + -ms-touch-action: pan-x pan-y double-tap-zoom; +} +.win-semanticzoom * { + -ms-touch-action: inherit; +} + +.win-semanticzoom-button { + z-index: 100; + position: absolute; + min-width: 25px; + min-height: 25px; + width: 25px; + height: 25px; + padding: 0px; + bottom: 21px; + -ms-touch-action: none; +} + +.win-semanticzoom-button::before { + font-family: "Segoe UI Symbol"; + font-weight: normal; + font-size: 11pt; + content: "\E0B8"; /* minus sign */ +} + +.win-semanticzoom-button-location.rtl { + left: 4px; +} + +.win-semanticzoom-button-location.ltr{ + right: 4px; +} + +/* +ScrollView control. +*/ +.win-scrollview { + overflow-x: auto; + overflow-y: hidden; + height: 400px; + width: 100%; +} + +/* +ListView control. +*/ +.win-listview { + overflow: hidden; + height: 400px; +} +.win-listview .win-backdrop { + position: absolute; +} +.win-listview .win-surface { + position: relative; + overflow: visible; + width: 100%; + height: 100%; +} +.win-listview > .win-viewport { + position: relative; + width: 100%; + height: 100%; + z-index: 0; +} +.win-listview > .win-viewport.win-horizontal { + overflow-x: auto; + overflow-y: hidden; +} +.win-listview > .win-viewport.win-vertical { + overflow-x: hidden; + overflow-y: auto; +} +.win-listview > .win-horizontal .win-container { + margin: 5px; +} +.win-listview > .win-vertical .win-container { + margin: 5px 24px 5px 7px; +} +.win-listview.win-rtl > .win-vertical .win-container { + margin: 5px 7px 5px 24px; +} +.win-listview .win-container { + cursor: default; + position: absolute; + z-index: 0; +} +.win-listview .win-container.win-swipe { + z-index: 1; +} +.win-listview.win-swipeable .win-horizontal .win-container { + -ms-touch-action:pan-x pinch-zoom double-tap-zoom; +} +.win-listview.win-swipeable .win-vertical .win-container { + -ms-touch-action:pan-y pinch-zoom double-tap-zoom; +} +.win-semanticzoom .win-listview > .win-viewport * { + -ms-touch-action: auto; +} +.win-semanticzoom .win-listview > .win-viewport.win-zooming-x { + overflow-x: visible; +} +.win-semanticzoom .win-listview > .win-viewport.win-zooming-y { + overflow-y: visible; +} +.win-listview .win-item { + /* Used to place above .win-selectionbackground in ListView */ + z-index: 1; +} +.win-listview .win-item { + overflow: hidden; + position: absolute; +} +.win-listview > .win-vertical .win-item +{ + /* Allow flex box to fill win-item in list layout */ + width: 100%; +} +.win-listview .win-item:focus { + outline-style: none; +} +.win-listview .win-focusedoutline { + width: calc(100% + 4px); + height: calc(100% + 4px); + left: -2px; + top: -2px; + position: absolute; + z-index: 5; +} +.win-container.win-selected .win-selectionborder { + border-width: 4px; + border-style: solid; +} +.win-container.win-selected:hover .win-selectionborder { + border-width: 4px; + border-style: solid; +} +.win-listview .win-groupheader { + position: absolute; + top: 0; + padding: 10px 10px 10px 2px; + cursor: default; + overflow: hidden; + margin-left: 70px; + font-size: 20pt; + font-weight: 200; + line-height: 1.2; /* 32px when font-size is 20pt */ +} +.win-listview.win-rtl .win-groupheader { + margin-left: 0; + margin-right: 70px; + padding-left: 10px; + padding-right: 2px; +} +.win-listview.win-groups > .win-horizontal .win-surface { + margin-left: -70px; +} +.win-listview.win-groups.win-rtl > .win-horizontal .win-surface { + margin-left: 0; + margin-right: -70px; +} +.win-surface ._win-proxy { + position: absolute; + width: 0; + height: 0; + -ms-touch-action: none; +} +.win-selectionborder { + position: absolute; + opacity: inherit; + z-index: 2; +} +.win-container.win-selected .win-selectionbordertop { + top: 0; + left: 0; + right: 0; + height: 0; + border-right-style: hidden !important; + border-bottom-style: hidden !important; + border-left-style: hidden !important; +} +.win-container.win-selected .win-selectionborderright { + top: 0; + right: 0; + bottom: 0; + width: 0; + border-top-style: hidden !important; + border-bottom-style: hidden !important; + border-left-style: hidden !important; +} +.win-container.win-selected .win-selectionborderbottom { + left: 0; + right: 0; + bottom: 0; + height: 0; + border-top-style: hidden !important; + border-right-style: hidden !important; + border-left-style: hidden !important; +} +.win-container.win-selected .win-selectionborderleft { + top: 0; + left: 0; + bottom: 0; + width: 0; + border-top-style: hidden !important; + border-right-style: hidden !important; + border-bottom-style: hidden !important; +} +.win-selectionbackground { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + /* Used to place behind .win-item in the ListView */ + z-index: 0; +} +.win-selectioncheckmarkbackground { + position: absolute; + top: 0; + right: 0; + width: 0; + height: 0; + margin: 0; + padding: 0; + border-width: 20px; + border-style: solid; + z-index: 3; +} +.win-listview.win-rtl .win-selectioncheckmarkbackground { + left: 0; + right: auto; +} +.win-selectioncheckmark { + position: absolute; + margin: 0; + padding: 2px; + right: 0; + top: 0; + font-family: Segoe UI Symbol; + font-size: 11pt; + z-index: 4; +} +.win-listview.win-rtl .win-selectioncheckmark { + left: 0; + right: auto; +} +.win-selectionhint { + position: absolute; + margin: 0; + padding: 2px; + right: 0; + font-family: Segoe UI Symbol; + font-size: 11pt; + opacity: 0.5; +} +.win-selectionhint.win-revealed { + opacity: 1; +} +.win-listview.win-rtl .win-selectionhint { + left: 0; + right: auto; +} +.win-listview .win-progress { + left: 50%; + top: 50%; + width: 60px; + height: 60px; + margin-left: -30px; + margin-top: -30px; + z-index: 1; + position: absolute; +} +.win-listview .win-progress::-ms-fill { + animation-name: -ms-ring; +} + +/* +FlipView control. +*/ +.win-flipview { + overflow: hidden; + height: 400px; +} + +.win-flipview .win-surface { + -ms-scroll-chaining: none; +} + +.win-flipview .win-navleft { + left: 0%; + top: 50%; + margin-top: -19px; +} +.win-flipview .win-navright { + left: 100%; + top: 50%; + margin-left: -69px; + margin-top: -19px; +} +.win-flipview .win-navtop { + left: 50%; + top: 0%; + margin-left: -35px; +} +.win-flipview .win-navbottom { + left: 50%; + top: 100%; + margin-left: -35px; + margin-top: -39px; +} +.win-flipview .win-navbutton { + border: none; + width: 69px; + height: 39px; + z-index: 1; + position: absolute; + font-family: "Segoe UI Symbol"; + font-size: 16pt; + padding: 0; + min-width: 0; +} +.win-flipview .win-item, .win-flipview .win-item > .win-template { + display:-ms-flexbox; + height: 100%; + width: 100%; + -ms-flex-align:center; + -ms-flex-pack:center; +} + +/* +Typographic color definitions. +*/ + +body { + color: rgb(255, 255, 255); + background-color: rgb(29, 29, 29); +} + +.win-type-interactive:hover { + color: rgba(255, 255, 255, 0.8); +} +.win-type-interactive:hover:active { + color: rgba(255, 255, 255, 0.4); +} + +.win-type-interactive:-ms-keyboard-active { + color: rgba(255, 255, 255, 0.4); +} +.win-ui-light .win-type-interactive:hover { + color: rgba(0, 0, 0, 0.8); +} +.win-ui-light .win-type-interactive:hover:active { + color: rgba(0, 0, 0, 0.4); +} +.win-ui-light .win-type-interactive:-ms-keyboard-active { + color: rgba(0, 0, 0, 0.4); +} + +/* +These classes reverse the colors on the subtree to which they are applied. +*/ + +.win-ui-dark { + color: rgb(255, 255, 255); + background-color: rgb(29, 29, 29); +} +.win-ui-light { + color: rgb(0, 0, 0); + background-color: rgb(255, 255, 255); +} + +/* +Text selection color +*/ +::selection { + background-color: rgb(87, 41, 193); + color: rgb(255, 255, 255); +} + +/* +Text input, checkbox, radio, and select control colors. +*/ +input[type=text], input[type=password], +input[type=email], input[type=number], +input[type=tel], input[type=url], +input[type=search], textarea, .win-textarea, +select, input::-ms-check { + background-clip: border-box; + background-color: rgba(255, 255, 255, 0.8); + border-color: transparent; + color: rgb(0, 0, 0); +} +input[type=text]:hover, input[type=password]:hover, +input[type=email]:hover, input[type=number]:hover, +input[type=tel]:hover, input[type=url]:hover, +input[type=search]:hover, textarea:hover, +.win-textarea:hover, select:hover, input:hover::-ms-check { + background-clip: border-box; + background-color: rgba(255, 255, 255, 0.87); + border-color: transparent; + color: rgb(0, 0, 0); +} +input:hover:active::-ms-check { /* only checkbox and radio have press state */ + background-color: rgb(255, 255, 255); + color: rgb(0, 0, 0); +} +input:-ms-keyboard-active::-ms-check { /* only checkbox and radio have press state */ + background-color: rgb(255, 255, 255); +} +input[type=text]:focus, input[type=text]:active, +input[type=password]:focus, input[type=password]:active, +input[type=email]:focus, input[type=email]:active, +input[type=number]:focus, input[type=number]:active, +input[type=tel]:focus, input[type=tel]:active, +input[type=url]:focus, input[type=url]:active, +input[type=search]:focus, input[type=search]:active, +textarea:focus, textarea:active, +.win-textarea:focus, .win-textarea:active, +select:focus, select:active { + background-clip: border-box; + background-color: rgb(255, 255, 255); + border-color: transparent; + color: rgb(0, 0, 0); +} +input[type=text]:disabled, input[type=password]:disabled, +input[type=email]:disabled, input[type=number]:disabled, +input[type=tel]:disabled, input[type=url]:disabled, +input[type=search]:disabled, input[type=file]::-ms-value, +textarea:disabled, .win-textarea:disabled, select:disabled { + background-clip: border-box; + background-color: transparent; + border-color: rgba(255, 255, 255, 0.4); + color: rgba(255, 255, 255, 0.4); +} +input:disabled::-ms-check, input:disabled:active::-ms-check { /* checkbox and radio have filled disabled state in dark */ + background-clip: border-box; + background-color: rgba(255, 255, 255, 0.4); + border-color: transparent; + color: rgba(0, 0, 0, 0.4); +} +.win-ui-light input[type=text], .win-ui-light input[type=password], +.win-ui-light input[type=email], .win-ui-light input[type=number], +.win-ui-light input[type=tel], .win-ui-light input[type=url], +.win-ui-light input[type=search], .win-ui-light select, .win-ui-light textarea, +.win-ui-light .win-textarea, .win-ui-light input::-ms-check { + background-clip: padding-box; + background-color: rgba(255, 255, 255, 0.8); + border-color: rgba(0, 0, 0, 0.27); + color: rgb(0, 0, 0); +} +.win-ui-light input[type=text]:hover, .win-ui-light input[type=password]:hover, +.win-ui-light input[type=email]:hover, .win-ui-light input[type=number]:hover, +.win-ui-light input[type=tel]:hover, .win-ui-light input[type=url]:hover, +.win-ui-light input[type=search]:hover, .win-ui-light textarea:hover, +.win-ui-light select:hover, .win-ui-light .win-textarea:hover, .win-ui-light input:hover::-ms-check { + background-color: rgba(255, 255, 255, 0.87); + border-color: rgba(0, 0, 0, 0.44); + color: rgb(0, 0, 0); +} +.win-ui-light input[type=text]:focus, .win-ui-light input[type=text]:active, +.win-ui-light input[type=password]:focus, .win-ui-light input[type=password]:active, +.win-ui-light input[type=email]:focus, .win-ui-light input[type=email]:active, +.win-ui-light input[type=number]:focus, .win-ui-light input[type=number]:active, +.win-ui-light input[type=tel]:focus, .win-ui-light input[type=tel]:active, +.win-ui-light input[type=url]:focus, .win-ui-light input[type=url]:active, +.win-ui-light input[type=search]:focus, .win-ui-light input[type=search]:active, +.win-ui-light textarea:focus, .win-ui-light textarea:active, +.win-ui-light .win-textarea:focus, .win-ui-light .win-textarea:active, +.win-ui-light select:focus, .win-ui-light select:active { + background-color: rgb(255, 255, 255); + border-color: rgba(0, 0, 0, 0.6); + color: rgb(0, 0, 0); +} +.win-ui-light input:hover:active::-ms-check { /* only checkbox and radio have press state */ + background-clip: border-box; + background-color: rgb(0, 0, 0); + border-color: transparent; + color: rgb(255, 255, 255); +} +.win-ui-light input:-ms-keyboard-active::-ms-check { /* only checkbox and radio have press state */ + background-clip: border-box; + background-color: rgb(0, 0, 0); + border-color: transparent; + color: rgb(255, 255, 255); +} +.win-ui-light input[type=text]:disabled, .win-ui-light input[type=password]:disabled, +.win-ui-light input[type=email]:disabled, .win-ui-light input[type=number]:disabled, +.win-ui-light input[type=tel]:disabled, .win-ui-light input[type=url]:disabled, +.win-ui-light input[type=search]:disabled, .win-ui-light input[type=file]::-ms-value, +.win-ui-light textarea:disabled, .win-ui-light .win-textarea:disabled, .win-ui-light select:disabled, +.win-ui-light input:disabled::-ms-check, .win-ui-light input:disabled:active::-ms-check { + background-clip: padding-box; + background-color: rgba(202, 202, 202, 0.4); + border-color: rgba(0, 0, 0, 0.15); + color: rgba(0, 0, 0, 0.4); +} + +/* +Placeholder text style. +*/ +input[type]:-ms-input-placeholder, .win-ui-light input[type]:-ms-input-placeholder, +textarea:-ms-input-placeholder, .win-ui-light textarea:-ms-input-placeholder { + color: rgba(0, 0, 0, 0.6); /* same in dark and light */ +} +input[type]:disabled:-ms-input-placeholder, textarea:disabled:-ms-input-placeholder { + color: rgba(255, 255, 255, 0.22); +} +.win-ui-light input[type]:disabled:-ms-input-placeholder, .win-ui-light textarea:disabled:-ms-input-placeholder { + color: rgba(0, 0, 0, 0.22); +} + +/* +Invalid style. +*/ +input[type]:invalid { + outline-color: rgb(255, 128, 51); + outline-width: 2px; +} + +/* +Clear and reveal buttons. +*/ +input::-ms-clear, input::-ms-reveal { + background-color: rgb(255, 255, 255); + color: rgb(0, 0, 0); +} +input::-ms-clear:hover, input::-ms-reveal:hover { + background-color: rgb(222, 222, 222); +} +input::-ms-clear:hover:active, input::-ms-reveal:hover:active { + background-color: rgb(0, 0, 0); + color: rgb(255, 255, 255); +} + +/* +Option for select control (all colors are same for both light and dark). +*/ +option:checked, select:focus::-ms-value { + color: rgb(255, 255, 255); + background-color: rgb(70, 23, 180); +} +option:hover { + color: rgb(0, 0, 0); + background-color: rgb(197, 197, 197); +} +select:focus option:hover { + color: rgb(0, 0, 0); + background-color: rgb(222, 222, 222); +} +option:checked:hover, select:focus option:checked:hover { + color: rgb(255, 255, 255); + background-color: rgb(95, 55, 190); +} +option:hover:active, select:focus option:hover:active{ + color: rgb(0, 0, 0); + background-color: rgb(211, 211, 211); +} +option:checked:disabled, option:checked:disabled:active, +optgroup:disabled option:checked, optgroup:disabled option:checked:active, +select:disabled option:checked, select:disabled option:checked:active, +select:disabled:focus::-ms-value { + background-color: rgba(255, 255, 255, 0.4); + color: rgba(0, 0, 0, 0.6); +} +.win-ui-light option:checked:disabled, .win-ui-light option:checked:disabled:active, +.win-ui-light optgroup:disabled option:checked, .win-ui-light optgroup:disabled option:checked:active, +.win-ui-light select:disabled option:checked, .win-ui-light select:disabled option:checked:active, +.win-ui-light select:disabled:focus::-ms-value { + background-color: rgba(0, 0, 0, 0.55); + color: rgba(255, 255, 255, 0.6); +} + +/* +Button control colors. +*/ +button, input[type=button], input[type=submit], +input[type=reset], input[type=file]::-ms-browse { + background-color: transparent; + border-color: rgb(255, 255, 255); + color: rgb(255, 255, 255); +} +button[type=submit], input[type=submit] { + background-clip: padding-box; + background-color: rgb(70, 23, 180); +} +button:hover, input[type=button]:hover, input[type=reset]:hover, +input[type=file]::-ms-browse:hover { + background-color: rgba(255, 255, 255, 0.13); + border-color: rgb(255, 255, 255); +} +button[type=submit]:hover, input[type=submit]:hover { + background-clip: padding-box; + background-color: rgb(95, 55, 190); + border-color: rgb(255, 255, 255); +} +button:hover:active, button[type=submit]:hover:active, +input[type=button]:hover:active, input[type=reset]:hover:active, +input[type=submit]:hover:active, input[type=file]::-ms-browse:hover:active { + background-clip: border-box; + background-color: rgb(255, 255, 255); + border-color: transparent; + color: rgb(0, 0, 0); +} +button:-ms-keyboard-active, button[type=submit]:-ms-keyboard-active, +input[type=button]:-ms-keyboard-active, input[type=reset]:-ms-keyboard-active, +input[type=submit]:-ms-keyboard-active { + background-clip: border-box; + background-color: rgb(255, 255, 255); + border-color: transparent; + color: rgb(0, 0, 0); +} +button:disabled, button[type=submit]:disabled, +input[type=button]:disabled, input[type=reset]:disabled, +input[type=submit]:disabled, input[type=file]:disabled::-ms-browse, +button:disabled:active, button[type=submit]:disabled:active, +input[type=button]:disabled:active, input[type=reset]:disabled:active, +input[type=submit]:disabled:active, input[type=file]:disabled::-ms-browse:active { + background-color: transparent; + border-color: rgba(255, 255, 255, 0.4); + color: rgba(255, 255, 255, 0.4); +} +.win-ui-light button, .win-ui-light input[type=button], .win-ui-light input[type=submit], +.win-ui-light input[type=reset], .win-ui-light input[type=file]::-ms-browse { + background-color: rgba(182, 182, 182, 0.7); + border-color: rgba(0, 0, 0, 0.2); + color: rgb(0, 0, 0); +} +.win-ui-light button[type=submit], .win-ui-light input[type=submit]{ + background-clip: border-box; + background-color: rgb(70, 23, 180); + border-color: transparent; + color: rgb(255, 255, 255); +} +.win-ui-light button:hover, .win-ui-light input[type=button]:hover, .win-ui-light input[type=reset]:hover, +.win-ui-light input[type=file]::-ms-browse:hover { + background-color: rgba(205, 205, 205, 0.82); + border-color: rgba(164, 164, 164, 0.45); +} +.win-ui-light button[type=submit]:hover, .win-ui-light input[type=submit]:hover { + background-clip: border-box; + background-color: rgb(95, 55, 190); + border-color: transparent; +} +.win-ui-light button:hover:active, .win-ui-light button[type=submit]:hover:active, +.win-ui-light input[type=button]:hover:active, .win-ui-light input[type=reset]:hover:active, +.win-ui-light input[type=submit]:hover:active, .win-ui-light input[type=file]::-ms-browse:hover:active { + background-clip: border-box; + background-color: rgb(0, 0, 0); + border-color: transparent; + color: rgb(255, 255, 255); +} +.win-ui-light button:-ms-keyboard-active, .win-ui-light button[type=submit]:-ms-keyboard-active, +.win-ui-light input[type=button]:-ms-keyboard-active, .win-ui-light input[type=reset]:-ms-keyboard-active, +.win-ui-light input[type=submit]:-ms-keyboard-active { + background-clip: border-box; + background-color: rgb(0, 0, 0); + border-color: transparent; + color: rgb(255, 255, 255); +} +.win-ui-light button:disabled, .win-ui-light button[type=submit]:disabled, +.win-ui-light input[type=button]:disabled, .win-ui-light input[type=reset]:disabled, +.win-ui-light input[type=submit]:disabled, .win-ui-light input[type=file]:disabled::-ms-browse, +.win-ui-light button:disabled:active, .win-ui-light button[type=submit]:disabled:active, +.win-ui-light input[type=button]:disabled:active, .win-ui-light input[type=reset]:disabled:active, +.win-ui-light input[type=submit]:disabled:active, .win-ui-light input[type=file]:disabled::-ms-browse:active { + background-color: rgba(202, 202, 202, 0.4); + border-color: rgba(0, 0, 0, 0.08); + color: rgba(0, 0, 0, 0.4); +} +.win-ui-light input[type=file]:disabled::-ms-browse, .win-ui-light input[type=file]:disabled::-ms-browse:active { + border-color: rgba(0, 0, 0, 0.15); +} + +/* +File upload control colors. +*/ +input[type=file] { + background-color: transparent; + border-color: transparent; + color: transparent; +} + +/* +Link colors. +*/ +a { + color: rgb(156, 114, 255); +} +a:hover:active { + color: rgba(156, 114, 255, 0.6); +} +a:hover { + color: rgba(156, 114, 255, 0.8); +} +a[disabled], a[disabled]:active { /* :disabled pseudo-class doesn't apply to a, even though disabled attribute stops navigation */ + color: rgba(255, 255, 255, 0.4); +} +.win-ui-light a { + color: rgb(79, 26, 203); +} +.win-ui-light a:hover:active { + color: rgba(79, 26, 203, 0.6); +} +.win-ui-light a:hover { + color: rgba(79, 26, 203, 0.8); +} +.win-ui-light a[disabled], .win-ui-light a[disabled]:active { /* :disabled pseudo-class doesn't apply to a, even though disabled attribute stops navigation */ + color: rgba(0, 0, 0, 0.4); +} + +/* +Range control colors. +*/ +input[type=range], input[type=range]::-ms-track { + background-color: transparent; +} + +input[type=range]::-ms-fill-lower { + background-color: rgb(91, 46, 197); +} +input[type=range]:hover::-ms-fill-lower { + background-color: rgb(114, 75, 205); +} +input[type=range]:active::-ms-fill-lower { + background-color: rgb(129, 82, 239); +} +input[type=range]:disabled::-ms-fill-lower { + background-color: rgba(255, 255, 255, 0.23); +} +.win-ui-light input[type=range]::-ms-fill-lower { + background-color: rgb(70, 23, 180); +} +.win-ui-light input[type=range]:hover::-ms-fill-lower { + background-color: rgb(95, 55, 190); +} +.win-ui-light input[type=range]:active::-ms-fill-lower { + background-color: rgb(114, 65, 228); +} +.win-ui-light input[type=range]:disabled::-ms-fill-lower { + background-color: rgba(0, 0, 0, 0.2); +} + +input[type=range]::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.16); +} +input[type=range]:hover::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.18); +} +input[type=range]:active::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.23); +} +input[type=range]:disabled::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.16); +} +.win-ui-light input[type=range]::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.1); +} +.win-ui-light input[type=range]:hover::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.15); +} +.win-ui-light input[type=range]:active::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.2); +} +.win-ui-light input[type=range]:disabled::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.1); +} + +input[type=range]::-ms-thumb { + background-color: rgb(255, 255, 255); +} +input[type=range]:disabled::-ms-thumb { + background-color: rgb(126, 126, 126); +} +.win-ui-light input[type=range]::-ms-thumb { + background-color: rgb(0, 0, 0); +} +.win-ui-light input[type=range]:disabled::-ms-thumb { + background-color: rgb(146, 146, 146); +} + +input[type=range]::-ms-ticks-before, +input[type=range]::-ms-ticks-after { + color: rgba(255, 255, 255, 0.5); +} +.win-ui-light input[type=range]::-ms-ticks-before, +.win-ui-light input[type=range]::-ms-ticks-after { + color: rgba(0, 0, 0, 0.5); +} + +/* +Progress control colors. +*/ +progress { + background-color: rgba(255,255,255,0.35); + color: rgb(91, 46, 197); +} +.win-ui-light progress { + background-color: rgba(0, 0, 0, 0.2); + color: rgb(70, 23, 180); +} +progress:indeterminate { + color: rgb(138, 87, 255); +} +.win-ui-light progress:indeterminate { + color: rgb(70, 23, 180); +} +progress::-ms-fill { + background-color: currentColor; +} +/* +Explicitly define indeterminate background transparent for dark and light +because ".win-ui-light progress" has higher specificity than "progress:indeterminate". +*/ +progress:indeterminate, .win-ui-light progress:indeterminate { + background-color: transparent; +} + +/* +FlipView control colors. +*/ +.win-flipview .win-navbutton { + background-color: rgba(213, 213, 213, 0.35); + color: rgba(0, 0, 0, 0.60); +} +.win-flipview .win-navbutton:hover { + background-color: rgba(215, 215, 215, 0.94); + color: rgba(0, 0, 0, 1.0); +} +.win-flipview .win-navbutton:hover:active { + background-color: rgba(41, 41, 41, 0.74); + color: rgba(255, 255, 255, 1.0); +} + +/* +ListView control colors. +*/ +.win-listview .win-container:not(.win-footprint) { + background-color: rgb(29, 29, 29); +} +.win-ui-light .win-listview .win-container:not(.win-footprint) { + background-color: rgb(255, 255, 255); +} +.win-listview .win-container:hover { + outline: rgba(255, 255, 255, 0.3) solid 3px; +} +.win-listview.win-selectionstylefilled .win-container:hover { + background-color: rgba(255, 255, 255, 0.3); +} +.win-listview .win-focusedoutline { + outline: rgb(255, 255, 255) solid 2px; +} +.win-ui-light .win-listview .win-container:hover { + outline: rgba(0, 0, 0, 0.3) solid 3px; +} +.win-ui-light .win-listview.win-selectionstylefilled .win-container:hover { + background-color: rgba(0, 0, 0, 0.3); +} +.win-ui-light .win-listview .win-focusedoutline { + outline: rgb(0, 0, 0) solid 2px; +} +.win-listview.win-selectionstylefilled .win-container.win-selected:hover { + background-color: rgb(95, 55, 190); +} +.win-listview .win-container.win-swipe:hover { + background-color: rgb(29, 29, 29); + outline: none; +} +.win-ui-light .win-listview .win-container.win-swipe:hover { + background-color: rgb(255, 255, 255); + outline: none; +} +.win-listview.win-selectionstylefilled .win-selected { + color: rgb(255, 255, 255); +} +.win-listview:not(.win-selectionstylefilled) .win-container.win-selected .win-selectionborder { + border-color: rgb(70, 23, 180); +} +.win-listview.win-selectionstylefilled .win-container.win-selected .win-selectionborder { + border-color: transparent; +} +.win-listview:not(.win-selectionstylefilled) .win-container.win-selected:hover .win-selectionborder { + border-color: rgb(95, 55, 190); +} +.win-listview.win-selectionstylefilled .win-selected .win-selectionbackground { + background-color: rgb(70, 23, 180); +} +.win-listview.win-selectionstylefilled .win-selected:hover .win-selectionbackground { + background-color: rgb(95, 55, 190); +} +.win-selectioncheckmark { + color: rgb(255, 255, 255); +} +.win-selectionhint { + color: rgb(255, 255, 255); +} +.win-ui-light .win-selectionhint { + color: rgb(70, 23, 180); +} +.win-listview:not(.win-selectionstylefilled) .win-selectioncheckmarkbackground { + border-top-color: rgb(70, 23, 180); + border-right-color: rgb(70, 23, 180); + border-left-color: transparent; + border-bottom-color: transparent; +} +.win-listview.win-selectionstylefilled .win-selectioncheckmarkbackground { + border-color: transparent; +} +.win-listview:not(.win-selectionstylefilled) .win-container.win-selected:hover .win-selectioncheckmarkbackground { + border-top-color: rgb(95, 55, 190); + border-right-color: rgb(95, 55, 190); + border-left-color: transparent; + border-bottom-color: transparent; +} +.win-listview.win-rtl:not(.win-selectionstylefilled) .win-selectioncheckmarkbackground { + border-left-color: rgb(70, 23, 180); + border-right-color: transparent; +} +.win-listview.win-rtl:not(.win-selectionstylefilled) .win-container.win-selected:hover .win-selectioncheckmarkbackground { + border-left-color: rgb(95, 55, 190); + border-right-color: transparent; +} + +.win-listview.win-selectionstylefilled .win-selected a, +.win-listview.win-selectionstylefilled .win-selected progress, +.win-listview.win-selectionstylefilled .win-selected .win-rating .win-star.win-full { + color: rgb(255, 255, 255); +} +.win-listview.win-selectionstylefilled .win-selected a:hover:active { + color: rgba(255, 255, 255, 0.6); +} +.win-listview.win-selectionstylefilled .win-selected a:hover { + color: rgba(255, 255, 255, 0.8); +} +.win-listview.win-selectionstylefilled .win-selected button, +.win-listview.win-selectionstylefilled .win-selected input[type=button], +.win-listview.win-selectionstylefilled .win-selected input[type=reset], +.win-listview.win-selectionstylefilled .win-selected input[type=text], +.win-listview.win-selectionstylefilled .win-selected input[type=password], +.win-listview.win-selectionstylefilled .win-selected input[type=email], +.win-listview.win-selectionstylefilled .win-selected input[type=number], +.win-listview.win-selectionstylefilled .win-selected input[type=tel], +.win-listview.win-selectionstylefilled .win-selected input[type=url], +.win-listview.win-selectionstylefilled .win-selected input[type=search], +.win-listview.win-selectionstylefilled .win-selected input::-ms-check, +.win-listview.win-selectionstylefilled .win-selected textarea, +.win-listview.win-selectionstylefilled .win-selected .win-textarea, +.win-listview.win-selectionstylefilled .win-selected select { + background-clip: border-box; + background-color: rgba(255, 255, 255, 0.8); + border-color: transparent; + color: rgb(0, 0, 0); +} +.win-listview.win-selectionstylefilled .win-selected button[type=submit], +.win-listview.win-selectionstylefilled .win-selected input[type=submit] { + border-color: rgb(255, 255, 255); +} +.win-listview.win-selectionstylefilled .win-selected input[type=range]::-ms-fill-lower { + background-color: rgb(255, 255, 255); +} +.win-listview.win-selectionstylefilled .win-selected input[type=range]::-ms-thumb { + background-color: rgb(0,0,0); +} +.win-listview.win-selectionstylefilled .win-selected input[type=range]::-ms-fill-upper, +.win-listview.win-selectionstylefilled .win-selected progress { + background-color: rgba(255, 255, 255, 0.16); +} +.win-listview.win-selectionstylefilled .win-selected progress:indeterminate { + background-color: transparent; +} +.win-listview.win-selectionstylefilled .win-selected .win-rating .win-star.win-empty { + color: rgba(255, 255, 255, 0.16); +} + +/* +Back button control colors. +*/ +.win-backbutton { + background-color: transparent; + border-color: rgb(255, 255, 255); + color: rgb(255, 255, 255); +} +.win-backbutton:hover { + background-color: rgba(255, 255, 255, 0.13); + border-color: rgb(255, 255, 255); +} +.win-backbutton:hover:active { + background-color: rgb(255, 255, 255); + border-color: rgb(255, 255, 255); + color: rgb(0, 0, 0); +} +.win-backbutton:-ms-keyboard-active { + background-color: rgb(255, 255, 255); + border-color: rgb(255, 255, 255); + color: rgb(0, 0, 0); +} +.win-backbutton:disabled, .win-backbutton:disabled:active { + background-clip: padding-box; + background-color: transparent; + border-color: rgba(255, 255, 255, 0.4); + color: rgba(255, 255, 255, 0.4); +} +.win-ui-light .win-backbutton { + background-color: transparent; + border-color: rgb(0, 0, 0); + color: rgb(0, 0, 0); +} +.win-ui-light .win-backbutton:hover { + background-color: rgba(0, 0, 0, 0.13); + border-color: rgb(0, 0, 0); +} +.win-ui-light .win-backbutton:hover:active { + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); + color: rgb(255, 255, 255); +} +.win-ui-light .win-backbutton:-ms-keyboard-active { + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); + color: rgb(255, 255, 255); +} +.win-ui-light .win-backbutton:disabled, .win-ui-light .win-backbutton:disabled:active { + background-clip: padding-box; + background-color: transparent; + border-color: rgba(0, 0, 0, 0.4); + color: rgba(0, 0, 0, 0.4); +} + +/* +Command button colors. +*/ +button.win-command { + background-color: transparent; + border-color: transparent; +} +button.win-command:hover { + background-color: transparent; + border-color: transparent; +} +button.win-command:active { + background-color: transparent; + color: inherit; +} +button.win-command:hover:active { + background-color: transparent; + color: inherit; +} +button.win-command:-ms-keyboard-active { + background-color: transparent; + color: inherit; +} +button.win-command:disabled { + border-color: transparent; + background-color: transparent; +} +button.win-command:focus { + border-color: rgb(255, 255, 255); +} +.win-ui-light button.win-command:focus { + border-color: rgb(0, 0, 0); +} +.win-hidefocus:focus { + outline: none; +} +/* + win-hide-focus needs the qualifier for win-ui-light or dark, + otherwise .win-ui-dark button.win-command:focus is more + qualified and stomps on the transparent that doesn't have .win-ui-dark +*/ +.win-command.win-hidefocus:focus, +.win-ui-light .win-command.win-hidefocus:focus { + border-color: transparent; +} +.win-commandimage { + color: rgb(255, 255, 255); +} +button:hover:active .win-commandimage { + color: rgb(0, 0, 0); +} +button:-ms-keyboard-active .win-commandimage { + color: rgb(0, 0, 0); +} +button:disabled .win-commandimage, +button:disabled:active .win-commandimage { + color: rgba(255, 255, 255, 0.4); +} +button[aria-checked=true] .win-commandimage, +button[aria-checked=true]:active .win-commandimage { + color: rgb(0, 0, 0); +} + +button[aria-checked=true]:hover .win-commandimage { + color: rgb(0, 0, 0); +} + +button[aria-checked=true]:hover:active .win-commandimage { + color: rgb(255, 255, 255); +} +button[aria-checked=true]:-ms-keyboard-active .win-commandimage { + color: rgb(255, 255, 255); +} +button[aria-checked=true]:disabled .win-commandimage, +button[aria-checked=true]:disabled:active .win-commandimage { + color: rgb(0, 0, 0); +} +.win-ui-light .win-commandimage { + color: rgb(0, 0, 0); +} +.win-ui-light button:hover:active .win-commandimage { + color: rgb(255, 255, 255); +} +.win-ui-light button:-ms-keyboard-active .win-commandimage { + color: rgb(255, 255, 255); +} +.win-ui-light button:disabled .win-commandimage, +.win-ui-light button:disabled:active .win-commandimage { + color: rgba(0, 0, 0, 0.4); +} +.win-ui-light button[aria-checked=true] .win-commandimage, +.win-ui-light button[aria-checked=true]:active .win-commandimage { + color: rgb(255, 255, 255); +} +.win-ui-light button[aria-checked=true]:hover .win-commandimage { + color: rgb(255, 255, 255); +} +.win-ui-light button[aria-checked=true]:hover:active .win-commandimage { + color: rgb(0, 0, 0); +} +.win-ui-light button[aria-checked=true]:-ms-keyboard-active .win-commandimage { + color: rgb(0, 0, 0); +} +.win-ui-light button[aria-checked=true]:disabled .win-commandimage, +.win-ui-light button[aria-checked=true]:disabled:active .win-commandimage { + color: rgb(255, 255, 255); +} + +/* +Command ring colors. +*/ +.win-commandring, button:active .win-commandring { + background-color: transparent; + border-color: rgb(255, 255, 255); +} +button:hover .win-commandring { + background-color: rgba(255, 255, 255, 0.13); + border-color: rgb(255, 255, 255); +} +button:hover:active .win-commandring { + background-color: rgb(255, 255, 255); + border-color: rgb(255, 255, 255); +} +button:-ms-keyboard-active .win-commandring { + background-color: rgb(255, 255, 255); + border-color: rgb(255, 255, 255); +} +button:disabled .win-commandring, +button:disabled:active .win-commandring { + background-color: transparent; + border-color: rgba(255, 255, 255, 0.4); +} +button[aria-checked=true] .win-commandring, +button[aria-checked=true]:active .win-commandring { + background-color: rgb(255, 255, 255); + border-color: rgb(255, 255, 255); +} +button[aria-checked=true]:hover .win-commandring { + background-color: rgb(222, 222, 222); + border-color: rgb(222, 222, 222); +} +button[aria-checked=true]:hover:active .win-commandring { + background-color: transparent; + border-color: rgb(255, 255, 255); +} +button[aria-checked=true]:-ms-keyboard-active .win-commandring { + background-color: transparent; + border-color: rgb(255, 255, 255); +} +button[aria-checked=true]:disabled .win-commandring, +button[aria-checked=true]:disabled:active .win-commandring { + background-color: rgba(255, 255, 255, 0.4); + border-color: rgba(255, 255, 255, 0.4); +} +.win-ui-light .win-commandring, .win-ui-light button:active .win-commandring { + background-color: transparent; + border-color: rgb(0, 0, 0); +} +.win-ui-light button:hover .win-commandring { + background-color: rgba(0, 0, 0, 0.13); + border-color: rgb(0, 0, 0); +} +.win-ui-light button:hover:active .win-commandring { + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); +} +.win-ui-light button:-ms-keyboard-active .win-commandring { + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); +} +.win-ui-light button:disabled .win-commandring, +.win-ui-light button:disabled:active .win-commandring { + background-color: transparent; + border-color: rgba(0, 0, 0, 0.4); +} +.win-ui-light button[aria-checked=true] .win-commandring, +.win-ui-light button[aria-checked=true]:active .win-commandring { + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); +} +.win-ui-light button[aria-checked=true]:hover .win-commandring { + background-color: rgb(33, 33, 33); + border-color: rgb(33, 33, 33); +} +.win-ui-light button[aria-checked=true]:hover:active .win-commandring { + background-color: transparent; + border-color: rgb(0, 0, 0); +} +.win-ui-light button[aria-checked=true]:-ms-keyboard-active .win-commandring { + background-color: transparent; + border-color: rgb(0, 0, 0); +} +.win-ui-light button[aria-checked=true]:disabled .win-commandring, +.win-ui-light button[aria-checked=true]:disabled:active .win-commandring { + background-color: rgba(0, 0, 0, 0.4); + border-color: rgba(0, 0, 0, 0.4); +} + +/* +Command button labels colors. +*/ +button.win-command:disabled .win-label, +button.win-command:disabled:active .win-label { + color: rgba(255, 255, 255, 0.4); +} +.win-ui-light button.win-command:disabled .win-label, +.win-ui-light button.win-command:disabled:active .win-label { + color: rgba(0, 0, 0, 0.4); +} + +/* +AppBarCommand (and MenuCommand) separator. +*/ +hr.win-command { + /* Same color in both light and dark. */ + background-color: rgb(123, 123, 123); +} + +/* +AppBar control colors. +*/ +.win-appbar { + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); +} +.win-ui-light.win-appbar, .win-ui-light .win-appbar { + background-color: rgb(255, 255, 255); + border-color: rgb(255, 255, 255); +} + +/* +Flyout control colors, flyout normally has light theme. +*/ +.win-ui-light.win-flyout, .win-ui-light .win-flyout { + border-color: rgb(0, 0, 0); + background-color: rgb(255, 255, 255); +} + +.win-ui-light.win-settingsflyout, .win-ui-light .win-settingsflyout { + border-color: rgba(0, 0, 0, 0.24); +} + +.win-flyout { + border-color: rgb(255, 255, 255); + background-color: rgb(0, 0, 0); +} + +.win-settingsflyout { + border-color: rgba(255, 255, 255, 0.24); +} + +/* +Settings flyout is light in both themes unless the app explicitly overrides it. +*/ +.win-settingsflyout { + background-color: rgb(255, 255, 255); +} +.win-settingsflyout.win-ui-dark { + background-color: rgb(0, 0, 0); +} + +/* +Settings flyout will be white in dark theme so we need to have the header stay +aligned with the theme unless the app explicitly overrides it. +*/ + +.win-settingsflyout .win-header { + background-color: rgb(0, 0, 0); +} +.win-settingsflyout .win-header.win-ui-light { + background-color: rgb(255, 255, 255); +} + +/* +Menu button colors, menu is always light theme. +*/ +.win-ui-light.win-menu button, .win-ui-light .win-menu button { + background-color: transparent; + color: rgb(0, 0, 0); +} +.win-ui-light.win-menu button:focus, .win-ui-light .win-menu button:focus, +.win-ui-light.win-menu button:active, .win-ui-light .win-menu button:active { + background-color: rgb(222, 222, 222); +} +.win-ui-light.win-menu button:hover:active, .win-ui-light .win-menu button:hover:active { + color: rgb(255, 255, 255); + background-color: rgb(0, 0, 0); +} +.win-ui-light.win-menu button:-ms-keyboard-active, .win-ui-light .win-menu button:-ms-keyboard-active { + color: rgb(255, 255, 255); + background-color: rgb(0, 0, 0); +} +.win-ui-light.win-menu button:disabled, .win-ui-light .win-menu button:disabled, +.win-ui-light.win-menu button:disabled:active, .win-ui-light .win-menu button:disabled:active { + background-color: transparent; + color: rgba(0, 0, 0, 0.1); +} +.win-menu button { + background-color: transparent; + color: rgb(255, 255, 255); +} +.win-menu button:focus, +.win-menu button:active { + background-color: rgb(222, 222, 222); +} +.win-menu button:hover:active { + color: rgb(0, 0, 0); + background-color: rgb(255, 255, 255); +} +.win-menu button:-ms-keyboard-active { + color: rgb(0, 0, 0); + background-color: rgb(255, 255, 255); +} +.win-menu button:disabled, +.win-menu button:disabled:active { + background-color: transparent; + color: rgba(255, 255, 255, 0.1); +} + +/* +Rating control colors. +*/ +.win-rating .win-star.win-user.win-full, +.win-rating .win-star.win-user.win-full.win-disabled { + color: rgb(91, 46, 197); +} +.win-rating .win-star.win-tentative.win-full { + color: rgb(129, 82, 239); +} +.win-rating .win-star.win-average.win-full, +.win-rating .win-star.win-average.win-full.win-disabled { + color: rgb(255, 255, 255); +} +.win-rating .win-star.win-empty { + color: rgba(255, 255, 255, 0.35); +} +.win-ui-light .win-rating .win-star.win-user.win-full, +.win-ui-light .win-rating .win-star.win-user.win-full.win-disabled { + color: rgb(70, 23, 180); +} +.win-ui-light .win-rating .win-star.win-tentative.win-full { + color: rgb(114, 65, 228); +} +.win-ui-light .win-rating .win-star.win-average.win-full, +.win-ui-light .win-rating .win-star.win-average.win-full.win-disabled { + color: rgb(0, 0, 0); +} +.win-ui-light .win-rating .win-star.win-empty { + color: rgba(0, 0, 0, 0.35); +} + +/* +SemanticZoom button colors. +*/ +.win-semanticzoom-button { + background-color: rgba(216, 216, 216, 0.33); + border-color: transparent; +} +button:hover.win-semanticzoom-button { + background-color: rgba(216, 216, 216, 1.0); +} +button.win-semanticzoom-button:active { + background-color: rgb(255, 255, 255); +} +.win-ui-light button.win-semanticzoom-button:active { + background-color: rgb(0, 0, 0); +} + +/* +Toggleswitch control colors. +*/ + +.win-toggleswitch .win-title { + color: rgb(255, 255, 255); +} +.win-toggleswitch .win-title.win-disabled { + color: rgba(255, 255, 255, 0.4); +} +.win-ui-light .win-toggleswitch .win-title { + color: rgb(0, 0, 0); +} +.win-ui-light .win-toggleswitch .win-title.win-disabled { + color: rgba(0, 0, 0, 0.4); +} + +.win-toggleswitch .win-label { + color: rgb(255, 255, 255); +} +.win-toggleswitch .win-label.win-disabled { + color: rgba(255, 255, 255, 0.4); +} +.win-ui-light .win-toggleswitch .win-label { + color: rgb(0, 0, 0); +} +.win-ui-light .win-toggleswitch .win-label.win-disabled { + color: rgba(0, 0, 0, 0.4); +} + +.win-toggleswitch .win-switch::-ms-track { + background-color: transparent; +} +.win-toggleswitch .win-switch::-ms-track { + border-color: rgba(255, 255, 255, 0.35); +} +.win-toggleswitch .win-switch::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.26); +} +.win-toggleswitch .win-switch:hover::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.29); +} +.win-toggleswitch .win-switch:active::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.35); +} +.win-toggleswitch .win-switch:disabled::-ms-track { + border-color: rgba(255, 255, 255, 0.2); +} +.win-toggleswitch .win-switch:disabled::-ms-fill-lower, +.win-toggleswitch .win-switch:disabled::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.12); +} +.win-ui-light .win-toggleswitch .win-switch::-ms-track { + border-color: rgba(0, 0, 0, 0.35); +} +.win-ui-light .win-toggleswitch .win-switch::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.26); +} +.win-ui-light .win-toggleswitch .win-switch:hover::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.29); +} +.win-ui-light .win-toggleswitch .win-switch:active::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.35); +} +.win-ui-light .win-toggleswitch .win-switch:disabled::-ms-track { + border-color: rgba(0, 0, 0, 0.2); +} +.win-ui-light .win-toggleswitch .win-switch:disabled::-ms-fill-lower, +.win-ui-light .win-toggleswitch .win-switch:disabled::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.12); +} + +/* +Tooltip control colors. +*/ +.win-tooltip { + background-color: rgb(255, 255, 255); + border-color: rgb(128, 128, 128); + color: rgba(0, 0, 0, 0.6); +} + +/* +High contrast colors. +*/ +@media (-ms-high-contrast) +{ + /* + Typographic color definitions. + */ + .win-type-interactive { + color: WindowText; + } + .win-type-interactive:hover, .win-type-interactive:hover:active { + color: -ms-hotlight; + } + .win-type-interactive:-ms-keyboard-active { + color: -ms-hotlight; + } + + /* + Text selection high contrast color. + */ + ::selection, select:focus::-ms-value { + background-color: Highlight; + color: HighlightText; + } + + /* + Button high contrast colors. + */ + button, input[type=button], input[type=reset], + input[type=file]::-ms-browse, input::-ms-clear, + input::-ms-reveal, select::-ms-expand, .win-backbutton, .win-semanticzoom-button { + background-color: ButtonFace; + border-color: ButtonText; + color: ButtonText; + } + button[type=submit], input[type=submit] { + background-color: Highlight; + border-color: ButtonText; + color: HighlightText; + } + button:hover, input[type=button]:hover, input[type=reset]:hover, + input::-ms-clear:hover, input::-ms-reveal:hover, + input[type=file]::-ms-browse:hover, .win-backbutton:hover, .win-semanticzoom-button:hover { + background-color: Highlight; + border-color: ButtonText; + color: HighlightText; + } + button:hover:active, button[type=submit]:hover:active, input[type=button]:hover:active, + input[type=submit]:hover:active, input[type=reset]:hover:active, + input::-ms-clear:hover:active, input::-ms-reveal:hover:active, + input[type=file]::-ms-browse:hover:active, .win-backbutton:hover:active, + .win-semanticzoom-button:hover:active { + background-clip: border-box; + background-color: ButtonText; + border-color: transparent; + color: ButtonFace; + } + button:-ms-keyboard-active, button[type=submit]:-ms-keyboard-active, input[type=button]:-ms-keyboard-active, + input[type=submit]:-ms-keyboard-active, input[type=reset]:-ms-keyboard-active, + .win-backbutton:-ms-keyboard-active, .win-semanticzoom-button:-ms-keyboard-active { + background-clip: border-box; + background-color: ButtonText; + border-color: transparent; + color: ButtonFace; + } + button:disabled, button[type=submit]:disabled, input[type=button]:disabled, + input[type=submit]:disabled, input[type=reset]:disabled, + input[type=file]:disabled::-ms-browse, input:disabled::-ms-clear, input:disabled::-ms-reveal, + select:disabled::-ms-expand, .win-backbutton:disabled, + button:disabled:active, button[type=submit]:disabled:active, input[type=button]:disabled:active, + input[type=submit]:disabled:active, input[type=reset]:disabled:active, + input[type=file]:disabled::-ms-browse:active, .win-backbutton:disabled:active { + background-color: ButtonFace; + border-color: GrayText; + color: GrayText; + } + + /* + Text input, checkbox, radio, and select high contrast colors. + */ + input[type=text], input[type=password], + input[type=email], input[type=number], + input[type=tel], input[type=url], + input[type=search], textarea, .win-textarea, select, + input::-ms-check { + background-color: ButtonFace; + border-color: ButtonText; + color: ButtonText; + } + input:hover::-ms-check { /* only checkbox and radio have hover state */ + background-color: Highlight; + color: HighlightText; + } + input:hover:active::-ms-check { /* only checkbox and radio have active state */ + background-clip: border-box; + background-color: ButtonText; + border-color: transparent; + color: ButtonFace; + } + input:-ms-keyboard-active::-ms-check { /* only checkbox and radio have active state */ + background-clip: border-box; + background-color: ButtonText; + border-color: transparent; + color: ButtonFace; + } + input[type=text]:disabled, input[type=password]:disabled, + input[type=email]:disabled, input[type=number]:disabled, + input[type=tel]:disabled, input[type=url]:disabled, + input[type=search]:disabled, input[type=file]::-ms-value, + textarea:disabled, .win-textarea:disabled, select:disabled, + input:disabled::-ms-check, input:disabled:active::-ms-check { + border-color: GrayText; + color: ButtonText; + background-color: ButtonFace; + } + input:disabled::-ms-check, input:disabled:active::-ms-check, + input[type]:-ms-input-placeholder, textarea:-ms-input-placeholder, + select:disabled::-ms-value { + color: GrayText; + } + + /* + Select option high contrast colors. + */ + option:checked, select:focus::-ms-value { + color: HighlightText; + background-color: Highlight; + } + select:active:not(:disabled)::-ms-value, select:active:not(:disabled)::-ms-expand, + option:hover:active, option:checked:hover:active { + color: ButtonFace; + background-color: ButtonText; + } + option:checked:disabled, option:checked:disabled:active, + optgroup:disabled option:checked, optgroup:disabled option:checked:active, + select:disabled option:checked, select:disabled option:checked:active, + select:disabled:focus::-ms-value { + color: ButtonFace; + background-color: GrayText; + } + + /* + Link high contrast colors. + */ + a { + color: -ms-hotlight; + } + a[disabled] { + color: GrayText; + } + + /* + Progress high contrast colors. + */ + progress { + border: 1px solid ButtonText; + background-color: ButtonFace; + color: Highlight; + } + progress:indeterminate { + border: transparent; + } + progress::-ms-fill { + border-right: 1px solid ButtonText; + background-color: Highlight; + } + progress.win-paused:not(:indeterminate)::-ms-fill { + background-color: GrayText; + } + progress.win-paused:not(:indeterminate) { + opacity: 1.0; + } + + /* + Range control high contrast colors. + */ + input[type=range]::-ms-track { + border: 1px solid ButtonText; + background-color: ButtonFace; + color: transparent; + } + input[type=range]:disabled::-ms-track { + border-color: GrayText; + } + input[type=range]::-ms-fill-lower { + background-color: Highlight; + } + input[type=range]:disabled::-ms-fill-lower { + background-color: transparent; + } + input[type=range]::-ms-ticks-before, + input[type=range]::-ms-ticks-after { + color: ButtonText; + } + input[type=range]:disabled::-ms-ticks-before, + input[type=range]:disabled::-ms-ticks-after { + color: GrayText; + } + input[type=range]::-ms-thumb { + background-color: ButtonText; + border-left: 1px solid HighlightText; + border-right: 1px solid HighlightText; + } + input[type=range].win-vertical::-ms-thumb { + border-left-style: none; + border-right-style: none; + border-top: 1px solid HighlightText; + border-bottom: 1px solid HighlightText; + } + input[type=range]:hover::-ms-thumb { + background-color: Highlight; + } + input[type=range]:active::-ms-thumb { + background-color: HighlightText; + border-color: ButtonText; + } + input[type=range]:disabled::-ms-thumb, + input[type=range]:disabled:active::-ms-thumb { + background-color: GrayText; + border-color: GrayText; + } + + /* + AppBar high contrast colors. + */ + .win-appbar { + background-color: ButtonFace; + border-color: Highlight; + } + + /* + Command button label high contrast colors. + */ + button.win-command, + button.win-command:active, + button.win-command:hover:active { + background-color: transparent; + border-color: transparent; + } + hr.win-command { + background-color: ButtonText; + } + .win-label { + background-color: transparent; + color: ButtonText; + } + button:disabled .win-label, + button:disabled:active .win-label { + color: GrayText; + } + button.win-command:focus { + border-color: ButtonText; + } + button.win-hidefocus:focus { + outline: none; + } + button.win-command.win-hidefocus:focus { + border-color: transparent; + } + + /* + Command Image high contrast colors with states + */ + .win-commandimage { + background-color: transparent; + color: ButtonText; + } + button:active .win-commandimage, + button:hover:active .win-commandimage { + color: ButtonFace; + } + button:hover .win-commandimage { + color: HighlightText; + } + button:disabled .win-commandimage, + button:disabled:active .win-commandimage { + color: GrayText; + } + button[aria-checked=true] .win-commandimage { + color: ButtonFace; + } + button[aria-checked=true]:active .win-commandimage, + button[aria-checked=true]:hover:active .win-commandimage { + color: ButtonText; + } + button[aria-checked=true]:hover .win-commandimage { + color: HighlightText; + } + button[aria-checked=true]:disabled .win-commandimage, + button[aria-checked=true]:disabled:active .win-commandimage { + color: ButtonFace; + } + + /* + Command Image high contrast colors with states + */ + .win-commandring { + background-color: transparent; + border-color: ButtonText; + } + button:active .win-commandring, + button:hover:active .win-commandring { + background-color: ButtonText; + border-color: ButtonText; + } + button:hover .win-commandring { + background-color: Highlight; + border-color: ButtonText; + } + button:disabled .win-commandring, + button:disabled:active .win-commandring { + background-color: transparent; + border-color: GrayText; + } + button[aria-checked=true] .win-commandring { + background-color: ButtonText; + border-color: ButtonText; + } + button[aria-checked=true]:active .win-commandring, + button[aria-checked=true]:hover:active .win-commandring { + background-color: ButtonFace; + border-color: ButtonText; + } + button[aria-checked=true]:hover .win-commandring { + background-color: Highlight; + border-color: ButtonText; + } + button[aria-checked=true]:disabled .win-commandring, + button[aria-checked=true]:disabled:active .win-commandring { + background-color: GrayText; + border-color: GrayText; + } + + /* + Menu command high contrast colors + */ + .win-menu hr.win-command { + background-color: ButtonText; + } + .win-menu button.win-command { + background-color: ButtonFace; + color: ButtonText; + } + .win-menu button.win-command:focus { + outline: none; + background-color: Highlight; + color: HighlightText; + } + .win-menu button.win-command:active { + background-color: Highlight; + color: HighlightText; + } + .win-menu button.win-command:hover:active { + background-color: ButtonText; + color: ButtonFace; + } + .win-menu button.win-command:-ms-keyboard-active { + background-color: ButtonText; + color: ButtonFace; + } + .win-menu button.win-command:disabled, + .win-menu button.win-command:disabled:active { + background-color: ButtonFace; + color: GrayText; + } + + /* + FlipView high contrast colors. + */ + .win-flipview .win-navbottom { + left: 50%; + top: 100%; + margin-left: -35px; + margin-top: -35px; + } + .win-flipview .win-navbutton { + background-color: ButtonFace; + color: ButtonText; + border: 2px solid ButtonText; + width: 65px; + height: 35px; + } + .win-flipview .win-navbutton:hover { + background-color: Highlight; + color: HighlightText; + } + .win-flipview .win-navbutton:active { + background-color: ButtonText; + color: ButtonFace; + } + .win-flipview .win-navright { + margin-left: -65px; + } + + /* + Flyout control high contrast colors. + */ + .win-flyout, .win-settingsflyout { + background-color: Window; + border-color: WindowText; + color: WindowText; + } + + /* + ListView high contrast colors. + */ + .win-listview.win-selectionstylefilled .win-container { + background-color: Window; + color: WindowText; + } + .win-listview .win-container:hover { + outline: Highlight solid 3px; + } + .win-listview.win-selectionstylefilled .win-container:hover { + background-color: Highlight; + color: HighlightText; + } + .win-listview .win-focusedoutline { + outline-color: WindowText; + } + .win-listview.win-selectionstylefilled .win-container.win-selected, + .win-listview.win-selectionstylefilled .win-container.win-selected:hover { + background-color: Highlight; + color: HighlightText; + } + .win-listview .win-groupheader { + color: WindowText; + } + .win-listview:not(.win-selectionstylefilled) .win-container.win-selected .win-selectionborder { + border-color: Highlight; + } + .win-listview.win-selectionstylefilled .win-container.win-selected .win-selectionborder { + border-color: transparent; + } + .win-listview:not(.win-selectionstylefilled) .win-container.win-selected:hover .win-selectionborder { + border-color: Highlight; + } + .win-listview.win-selectionstylefilled .win-selected .win-selectionbackground, + .win-listview.win-selectionstylefilled .win-selected:hover .win-selectionbackground + { + background-color: Highlight; + color: HighlightText; + } + .win-selectioncheckmark { + color: HighlightText; + } + .win-listview.win-selectionstylefilled .win-selectioncheckmarkbackground { + border-color: transparent; + } + .win-listview:not(.win-selectionstylefilled) .win-selectioncheckmarkbackground { + border-top-color: Highlight; + border-right-color: Highlight; + border-left-color: transparent; + border-bottom-color: transparent; + } + .win-listview:not(.win-selectionstylefilled) .win-selectioncheckmarkbackground:hover { + border-top-color: Highlight; + border-right-color: Highlight; + } + .win-listview.win-rtl:not(.win-selectionstylefilled) .win-selectioncheckmarkbackground { + border-top-color: Highlight; + border-left-color: Highlight; + border-right-color: transparent; + border-bottom-color: transparent; + } + .win-listview .win-selectionhint { + color: WindowText; + } + .win-listview.win-selectionstylefilled .win-selected a, + .win-listview.win-selectionstylefilled .win-container:hover a, + .win-listview.win-selectionstylefilled .win-selected progress, + .win-listview.win-selectionstylefilled .win-container:hover progress, + .win-listview.win-selectionstylefilled .win-selected .win-rating .win-star:after, + .win-listview.win-selectionstylefilled .win-container:hover .win-rating .win-star:after{ + color: HighlightText; + } + .win-listview.win-selectionstylefilled .win-selected input, + .win-listview.win-selectionstylefilled .win-container:hover input, + .win-listview.win-selectionstylefilled .win-selected input::-ms-check, + .win-listview.win-selectionstylefilled .win-container:hover input::-ms-check, + .win-listview.win-selectionstylefilled .win-selected input::-ms-value, + .win-listview.win-selectionstylefilled .win-container:hover input::-ms-value, + .win-listview.win-selectionstylefilled .win-selected input::-ms-track, + .win-listview.win-selectionstylefilled .win-container:hover input::-ms-track, + .win-listview.win-selectionstylefilled .win-selected button, + .win-listview.win-selectionstylefilled .win-container:hover button, + .win-listview.win-selectionstylefilled .win-selected progress, + .win-listview.win-selectionstylefilled .win-container:hover progress, + .win-listview.win-selectionstylefilled .win-selected progress::-ms-fill, + .win-listview.win-selectionstylefilled .win-container:hover progress::-ms-fill, + .win-listview.win-selectionstylefilled .win-selected select, + .win-listview.win-selectionstylefilled .win-container:hover select, + .win-listview.win-selectionstylefilled .win-selected textarea, + .win-listview.win-selectionstylefilled .win-container:hover textarea { + border-color: HighlightText; + } + .win-listview.win-selectionstylefilled .win-selected input[type=range]::-ms-fill-lower, + .win-listview.win-selectionstylefilled .win-container:hover input[type=range]::-ms-fill-lower, + .win-listview.win-selectionstylefilled .win-selected progress::-ms-fill, + .win-listview.win-selectionstylefilled .win-container:hover progress::-ms-fill { + background-color: HighlightText; + } + .win-listview.win-selectionstylefilled .win-selected input[type=range]::-ms-fill-upper, + .win-listview.win-selectionstylefilled .win-container:hover input[type=range]::-ms-fill-upper, + .win-listview.win-selectionstylefilled .win-selected progress, + .win-listview.win-selectionstylefilled .win-container:hover progress { + background-color: Highlight; + } + .win-listview.win-selectionstylefilled .win-selected .win-rating .win-star.win-full:before, + .win-listview.win-selectionstylefilled .win-container:hover .win-rating .win-star.win-full:before { + color: ButtonFace; + } + .win-listview.win-selectionstylefilled .win-selected .win-rating .win-star.win-empty:before, + .win-listview.win-selectionstylefilled .win-container:hover .win-rating .win-star.win-empty:before { + color: HighLight; + } + + /* + Rating control high contrast colors. + */ + .win-rating .win-star:before { + content: "\E082" !important; + } + .win-rating .win-star.win-full { + color: HighLight; + } + .win-rating .win-star.win-tentative.win-full { + color: ButtonText; + } + .win-rating .win-star.win-empty { + color: ButtonFace; + } + .win-rating .win-star:after { + content: "\E224" !important; + position: relative; + top: -100%; + color: ButtonText; + } + + /* + Toggle control high contrast colors. + */ + .win-toggleswitch .win-switch::-ms-thumb { + height: 17px; /* 19px - 1px borders */ + } + .win-toggleswitch .win-switch:disabled::-ms-fill-lower { + background-color: GrayText; + } + + /* + Tooltip high contrast colors. + */ + .win-tooltip { + background-color: Window; + border-color: WindowText; + color: WindowText; + } + } + +@keyframes WinJS-opacity-in { from { opacity: 0; } to { opacity: 1; } } +@keyframes WinJS-opacity-out { from { opacity: 1; } to { opacity: 0; } } + +@keyframes WinJS-scale-up { from { transform: scale(0.85); } to { transform: scale(1); } } +@keyframes WinJS-scale-down { from { transform: scale(1); } to { transform: scale(0.85); } } + +@keyframes WinJS-default-remove { from { transform: translateX( 11px); } to { transform: none; } } +@keyframes WinJS-default-remove-rtl { from { transform: translateX(-11px); } to { transform: none; } } + +@keyframes WinJS-default-apply { from { transform: none; } to { transform: translateX( 11px); } } +@keyframes WinJS-default-apply-rtl { from { transform: none; } to { transform: translateX(-11px); } } + +@keyframes WinJS-showEdgeUI { from { transform: translateY(-70px); } to { transform: none; } } +@keyframes WinJS-showPanel { from { transform: translateX( 364px); } to { transform: none; } } +@keyframes WinJS-showPanel-rtl { from { transform: translateX(-364px); } to { transform: none; } } +@keyframes WinJS-hideEdgeUI { from { transform: none; } to { transform: translateY(-70px); } } +@keyframes WinJS-hidePanel { from { transform: none; } to { transform: translateX( 364px); } } +@keyframes WinJS-hidePanel-rtl { from { transform: none; } to { transform: translateX(-364px); } } + +@keyframes WinJS-showPopup { from { transform: translateY(50px); } to { transform: none; } } + +@keyframes WinJS-dragSourceEnd { from { transform: translateX( 11px) scale(1.05); } to { transform: none; } } +@keyframes WinJS-dragSourceEnd-rtl { from { transform: translateX(-11px) scale(1.05); } to { transform: none; } } + +@keyframes WinJS-enterContent { from { transform: translateX( 40px); } to { transform: none; } } +@keyframes WinJS-enterContent-rtl { from { transform: translateX(-40px); } to { transform: none; } } +@keyframes WinJS-enterContent-snapped { from { transform: translateX( 20px); } to { transform: none; } } +@keyframes WinJS-enterContent-snapped-rtl { from { transform: translateX(-20px); } to { transform: none; } } + +@keyframes WinJS-exit { from { transform: none; } to { transform: none; } } + +@keyframes WinJS-enterPage { from { transform: translateX( 100px); } to { transform: none; } } +@keyframes WinJS-enterPage-rtl { from { transform: translateX(-100px); } to { transform: none; } } +@keyframes WinJS-enterPage-snapped { from { transform: translateX( 40px); } to { transform: none; } } +@keyframes WinJS-enterPage-snapped-rtl { from { transform: translateX(-40px); } to { transform: none; } } + +@keyframes WinJS-updateBadge { from { transform: translateY(24px); } to { transform: none; } } + +body, +.win-type-xx-large, +.win-type-x-large, +.win-type-large, +.win-type-medium, +.win-type-small, +.win-type-x-small, +.win-type-xx-small, +input, +textarea, +.win-textarea, +button, +select, +option { + font-family: + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:-ms-lang(am, ti), +.win-type-xx-large:-ms-lang(am, ti), +.win-type-x-large:-ms-lang(am, ti), +.win-type-large:-ms-lang(am, ti), +.win-type-medium:-ms-lang(am, ti), +.win-type-small:-ms-lang(am, ti), +.win-type-x-small:-ms-lang(am, ti), +.win-type-xx-small:-ms-lang(am, ti), +input:-ms-lang(am, ti), +textarea:-ms-lang(am, ti), +.win-textarea:-ms-lang(am, ti), +button:-ms-lang(am, ti), +select:-ms-lang(am, ti), +option:-ms-lang(am, ti) { + font-family: + "Ebrima", + "Segoe UI", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-xx-large:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-x-large:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-large:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-medium:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-small:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-x-small:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-xx-small:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +input:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +textarea:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-textarea:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +button:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +select:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +option:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te) { + font-family: + "Nirmala UI", + "Segoe UI", + "Ebrima", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:lang(chr-CHER-US), +.win-type-xx-large:lang(chr-CHER-US), +.win-type-x-large:lang(chr-CHER-US), +.win-type-large:lang(chr-CHER-US), +.win-type-medium:lang(chr-CHER-US), +.win-type-small:lang(chr-CHER-US), +.win-type-x-small:lang(chr-CHER-US), +.win-type-xx-small:lang(chr-CHER-US), +input:lang(chr-CHER-US), +textarea:lang(chr-CHER-US), +.win-textarea:lang(chr-CHER-US), +button:lang(chr-CHER-US), +select:lang(chr-CHER-US), +option:lang(chr-CHER-US) { + font-family: + "Gadugi", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:lang(ja), +.win-type-xx-large:lang(ja), +.win-type-x-large:lang(ja), +.win-type-large:lang(ja), +.win-type-medium:lang(ja), +.win-type-small:lang(ja), +.win-type-x-small:lang(ja), +.win-type-xx-small:lang(ja), +input:lang(ja), +textarea:lang(ja), +.win-textarea:lang(ja), +button:lang(ja), +select:lang(ja), +option:lang(ja) { + font-family: + "Meiryo UI", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:lang(km), +.win-type-xx-large:lang(km), +.win-type-x-large:lang(km), +.win-type-large:lang(km), +.win-type-medium:lang(km), +.win-type-small:lang(km), +.win-type-x-small:lang(km), +.win-type-xx-small:lang(km), +input:lang(km), +textarea:lang(km), +.win-textarea:lang(km), +button:lang(km), +select:lang(km), +option:lang(km) { + font-family: + "Khmer UI", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:lang(ko), +.win-type-xx-large:lang(ko), +.win-type-x-large:lang(ko), +.win-type-large:lang(ko), +.win-type-medium:lang(ko), +.win-type-small:lang(ko), +.win-type-x-small:lang(ko), +.win-type-xx-small:lang(ko), +input:lang(ko), +textarea:lang(ko), +.win-textarea:lang(ko), +button:lang(ko), +select:lang(ko), +option:lang(ko) { + font-family: + "Malgun Gothic", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:lang(lo), +.win-type-xx-large:lang(lo), +.win-type-x-large:lang(lo), +.win-type-large:lang(lo), +.win-type-medium:lang(lo), +.win-type-small:lang(lo), +.win-type-x-small:lang(lo), +.win-type-xx-small:lang(lo), +input:lang(lo), +textarea:lang(lo), +.win-textarea:lang(lo), +button:lang(lo), +select:lang(lo), +option:lang(lo) { + font-family: + "Lao UI", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:lang(th), +.win-type-xx-large:lang(th), +.win-type-x-large:lang(th), +.win-type-large:lang(th), +.win-type-medium:lang(th), +.win-type-small:lang(th), +.win-type-x-small:lang(th), +.win-type-xx-small:lang(th), +input:lang(th), +textarea:lang(th), +.win-textarea:lang(th), +button:lang(th), +select:lang(th), +option:lang(th) { + font-family: + "Leelawadee", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-xx-large:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-x-large:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-large:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-medium:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-small:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-x-small:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-xx-small:-ms-lang(zh-CN, zh-Hans, zh-SG), +input:-ms-lang(zh-CN, zh-Hans, zh-SG), +textarea:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-textarea:-ms-lang(zh-CN, zh-Hans, zh-SG), +button:-ms-lang(zh-CN, zh-Hans, zh-SG), +select:-ms-lang(zh-CN, zh-Hans, zh-SG), +option:-ms-lang(zh-CN, zh-Hans, zh-SG) { + font-family: + "Microsoft YaHei UI", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-xx-large:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-x-large:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-large:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-medium:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-small:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-x-small:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-xx-small:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +input:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +textarea:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-textarea:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +button:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +select:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +option:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO) { + font-family: + "Microsoft JhengHei UI", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + diff --git a/shared/html/libs/winjs/1.0/css/ui-light.css b/shared/html/libs/winjs/1.0/css/ui-light.css new file mode 100644 index 0000000..f8c5855 --- /dev/null +++ b/shared/html/libs/winjs/1.0/css/ui-light.css @@ -0,0 +1,3915 @@ +/*! + © Microsoft. All rights reserved. + + This library is supported for use in Windows Store apps only. + + Build: 1.0.9200.20602.win8_ldr.130108-1504 + + Version: Microsoft.WinJS.1.0 +*/ + +/* +Modifications for top-level elements html, body and iframe. +*/ +html, body { + height: 100%; + width: 100%; + margin: 0; + -ms-user-select: none; + cursor: default; + -ms-scroll-translation: vertical-to-horizontal; +} +html { + overflow: hidden; +} +body { + -ms-content-zooming: none; +} +iframe { + border: 0; +} +html:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + direction: rtl; +} + +/* +Explicitly define a Segoe UI font-family so that we can assign Segoe UI +Semilight to an appropriate font-weight. +*/ +@font-face { + font-family: "Segoe UI"; + font-weight: 200; + src: local("Segoe UI Light"); +} +@font-face { + font-family: "Segoe UI"; + font-weight: 300; + src: local("Segoe UI Semilight"); +} +@font-face { + font-family: "Segoe UI"; + font-weight: 400; + src: local("Segoe UI"); +} +@font-face { + font-family: "Segoe UI"; + font-weight: 600; + src: local("Segoe UI Semibold"); +} +@font-face { + font-family: "Segoe UI"; + font-weight: 700; + src: local("Segoe UI Bold"); +} +@font-face { + font-family: "Segoe UI"; + font-style: italic; + font-weight: 400; + src: local("Segoe UI Italic"); +} +@font-face { + font-family: "Segoe UI"; + font-style: italic; + font-weight: 700; + src: local("Segoe UI Bold Italic"); +} + +/* +Weight and size definitions for typographic classes and elements. +*/ +h1, .win-type-xx-large { + font-size: 42pt; + font-weight: 200; + letter-spacing: 0; + line-height: 1.1429; /* 64px when font-size is 42pt */ +} +h2, .win-type-x-large { + font-size: 20pt; + font-weight: 200; + line-height: 1.2; /* 32px when font-size is 20pt */ +} +h3, .win-type-large { + font-size: 11pt; + font-weight: 600; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +h4, .win-type-medium, code, pre, samp { + font-size: 11pt; + font-weight: 400; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +body, h5, .win-type-small, legend { + font-size: 11pt; + font-weight: 300; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +.win-type-x-small { + font-size: 11pt; + font-weight: 300; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +h6, caption, figcaption, small, .win-type-xx-small { + font-size: 9pt; + font-weight: 400; + line-height: 1.6667; /* 20px when font-size is 9pt */ +} +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0; +} +.win-type-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +h1.win-type-ellipsis, .win-type-xx-large.win-type-ellipsis { + line-height: 1.4286; /* 80px when font-size is 42pt */ +} +h2.win-type-ellipsis, .win-type-x-large.win-type-ellipsis { + line-height: 1.5; /* 40px when font-size is 20pt */ +} +dt, th { + font-size: 11pt; + font-weight: 700; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +abbr, acronym, address, blockquote, cite, dl, dd, li, ol, p, q, td, tr { + font-weight: 300; +} +b, strong { + font-weight: 700; +} +em { + font-style: italic; +} + +@media (-ms-view-state: snapped) { + h1, .win-type-xx-large { + font-size: 20pt; + font-weight: 200; + line-height: 1.2; /* 32px when font-size is 20pt */ + } + h2, .win-type-x-large { + font-size: 11pt; + font-weight: 600; + line-height: 1.3636; /* 20px when font-size is 11pt */ + } + h3, .win-type-large { + font-size: 11pt; + font-weight: 400; + line-height: 1.3636; /* 20px when font-size is 11pt */ + } + h1.win-type-ellipsis, .win-type-xx-large.win-type-ellipsis { + line-height: 1.5; /* 40px when font-size is 20pt */ + } + h2.win-type-ellipsis, .win-type-x-large.win-type-ellipsis { + line-height: 1.3636; /* 20px when font-size is 11pt */ + } +} + +/* +Additional letter-spacing for the document and its controls. +*/ +body, +button, input, +textarea, .win-textarea, +select, option { + letter-spacing: 0.02em; +} + +/* +Fixed font-family rules. +*/ +code, pre, samp { + font-family: "Consolas"; +} + +/* +Text input controls. +*/ + +input[type=text], input[type=password], input[type=email], input[type=number], +input[type=tel], input[type=url], input[type=search], textarea, .win-textarea { + -ms-user-select: element; + min-height: 28px; + min-width: 64px; + margin: 4px 0; + border-width: 2px; + border-style: solid; + font-size: 11pt; + font-weight: 400; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} + +input[type=text], input[type=password], input[type=email], input[type=number], +input[type=tel], input[type=url], input[type=search] { + width: 276px; + padding: 0; +} +input::-ms-value { + margin: 4px 8px; +} + +textarea, .win-textarea { + overflow-y: scroll; + word-wrap: break-word; + padding: 4px 8px; + /* Leave space for autohiding scrollbar on elements that enable text selection. */ + padding-right: 17px; +} +textarea:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm), .win-textarea:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + /* Flip sides for autohiding scrollbar padding on RTL layouts. */ + padding-right: 8px; + padding-left: 17px; +} +textarea { + min-width: 260px; + min-height: 39px; +} +.win-textarea { + width: 260px; + height: 39px; +} + +/* +Clear and reveal button styles. +*/ + +input::-ms-clear, input::-ms-reveal { + margin-left: 2px; +} +input:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-clear, +input:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-reveal { + margin-left: 0; + margin-right: 2px; +} + +/* +Button control. +*/ +button, input[type=button], input[type=reset], input[type=submit] { + min-height: 32px; /* content (20px) + padding + border */ + min-width: 90px; + padding: 4px 8px; + border-width: 2px; + border-style: solid; + background-clip: padding-box; + font-size: 11pt; + font-weight: 600; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} + +/* +File upload control. +*/ +input[type=file] { + border: none; + min-width: 100px; + min-height: 20px; + width: 340px; + height: 32px; + padding: 0; + margin: 7px 8px 21px 8px; + background-clip: padding-box; +} +input[type=file]::-ms-value { + margin: 0; + border-width: 2px; + border-style: solid; + border-right-style: none; + border-radius: 0; + background-clip: padding-box; + font-size: 11pt; + font-weight: 400; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +input[type=file]:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-value { + border-left-style: none; + border-right-style: solid; +} +input[type=file]::-ms-browse { + margin: 0; + padding: 0 18px; + border-width: 2px; + border-style: solid; + background-clip: padding-box; + font-size: 11pt; + font-weight: 600; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} + +/* +No underline or special cursor on links. +*/ +a { + text-decoration: none; + cursor: default; +} + +/* +No border on images when in a link. +*/ +img { + border-style: none; +} + +/* +Select control. +*/ +select { + min-height: 32px; + min-width: 80px; + border-width: 2px; + border-style: solid; + margin: 4px 0; + font-size: 11pt; + font-weight: 400; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +select::-ms-value { + padding: 4px 8px; + margin: 0; +} +select::-ms-expand { + border: 0; + padding-left: 6px; + padding-right: 6px; + background-color: transparent; +} + +/* +Radio button and checkbox shared styles. +*/ +input::-ms-check { + border-width: 2px; + border-style: solid; + display: inline-block; +} + +/* +Radio button. +*/ +input[type=radio] { + width: 23px; + height: 23px; + margin-left: -1px; + margin-right: 4px; + margin-bottom: -2px; +} +input[type=radio]:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-left: 4px; + margin-right: -1px; +} + +/* +Checkbox. +*/ +input[type=checkbox] { + width: 21px; + height: 21px; + margin-right: 5px; +} +input[type=checkbox]:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-right: 0; + margin-left: 5px; +} + +/* +Range control. +*/ +input[type=range] { + width: 280px; + height: auto; + padding: 17px 0 32px 0; +} +input[type=range]::-ms-track { + padding: 0; + width: auto; + height: 11px; + border-style: none; + color: transparent; /* ticks hidden by default */ +} +input[type=range]::-ms-thumb { + width: 11px; + height: 11px; + border-style: none; +} +input[type=range]::-ms-ticks-before, input[type=range]::-ms-ticks-after { + width: 100%; + height: 5px; + display: none; +} +input[type=range]:disabled::-ms-fill-lower { + margin-right: 5px; +} +input[type=range]:disabled::-ms-fill-upper { + margin-left: 6px; +} +input[type=range]:disabled:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-fill-lower { + margin-right: 0; + margin-left: 5px; +} +input[type=range]:disabled:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-fill-upper { + margin-left: 0; + margin-right: 6px; +} + +/* +Vertical range control. +*/ +input[type=range].win-vertical { + width: auto; + height: 191px; + padding: 0 17px; + writing-mode: bt-lr; +} +input[type=range].win-vertical:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + writing-mode: bt-rl; +} +input[type=range].win-vertical::-ms-track { + width: 11px; + height: auto; +} +input[type=range].win-vertical:disabled::-ms-fill-lower { + margin-right: 0; + margin-top: 6px; +} +input[type=range].win-vertical:disabled::-ms-fill-upper { + margin-left: 0; + margin-bottom: 5px; +} +input[type=range].win-vertical:disabled:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-fill-lower { + margin-left: 0; +} +input[type=range].win-vertical:disabled:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-fill-upper { + margin-right: 0; +} + +input[type=range].win-vertical::-ms-ticks-before, +input[type=range].win-vertical::-ms-ticks-after { + width: 5px; + height: 100%; + display: none; +} + +/* +Progress control. +*/ +progress { + width: 180px; + height: 6px; + border-style: none; +} +progress.win-medium { + width: 280px; +} +progress.win-large { + width: 100%; +} +progress::-ms-fill { + border-style: none; +} +progress:indeterminate { + height: 4px; + padding: 1px 0; /* Indeterminate dots should be 4px tall, but overall control stays 6px. */ +} +@keyframes win-progress-fade-out { + from { + opacity: 1.0; + } + to { + opacity: 0.5; + } +} +progress.win-paused:not(:indeterminate) { + animation-name: win-progress-fade-out; + animation-duration: 3s; + animation-timing-function: cubic-bezier(0.03, 0.76, 0.31, 1.0); + opacity: 0.5; +} +progress.win-error::-ms-fill { + opacity: 0; +} +progress.win-ring:indeterminate::-ms-fill { + animation-name: -ms-ring; +} +progress.win-ring { + width: 20px; + height: 20px; +} +progress.win-medium.win-ring { + width: 40px; + height: 40px; +} +progress.win-large.win-ring { + width: 60px; + height: 60px; +} + +form { + margin: 0; + padding: 0; +} +legend { + margin: 0 0 10px 0; + padding: 0; + color: inherit; +} + +/* +AppBar/Flyout z-index values: + 1000 - AppBar/settings click-eating div + 1002 - AppBar + 1004 - Settings Flyout + 1006 - Flyout click-eating div + 1008 - Flyout +*/ + +@font-face { + font-family: "Segoe UI Command"; + src: local("Segoe UI Symbol"); + font-weight: normal; + font-style: normal; +} + +.win-appbarclickeater { + z-index: 1000; + opacity: 0; + background-color: Purple; + display: none; + width: 100%; + height: 100%; + left: 0; + top: 0; + position: fixed; + -ms-touch-action: none; +} +.win-flyoutmenuclickeater { + z-index: 1006; + opacity: 0; + background-color: Lime; + display: none; + width: 100%; + height: 100%; + left: 0; + top: 0; + position: fixed; + -ms-touch-action: none; +} + +/* +Back button. +*/ +.win-backbutton { + display: inline-block; + min-width: 0; + min-height: 0; + background-clip: border-box; + box-sizing: border-box; + border-radius: 50%; + border-width: 2px; + border-style: solid; + padding: 0; + text-align: center; + + /* Normal sizing. */ + width: 41px; + height: 41px; + font-size: 14pt; + line-height: 37px; /* line-height must match the content box height. */ + vertical-align: baseline; +} +.win-backbutton:hover, .win-backbutton:hover:active { + border-width: 2px; + border-style: solid; + border-radius: 50%; +} +.win-backbutton::before { + font-family: "Segoe UI Symbol"; + font-weight: normal; + content: "\E0D5"; + vertical-align: 50%; +} +.win-backbutton:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::before { + content: "\E0AE"; +} +.win-backbutton:disabled, .win-backbutton:disabled:active { + visibility: hidden; +} + +/* +Back button snapped view sizing. +*/ +@media (-ms-view-state: snapped) { + .win-backbutton { + width: 30px; + height: 30px; + font-size: 10pt; + line-height: 26px; /* line-height must match the content box height */ + vertical-align: -0.37em; + } +} + +/* +Command buttons. +*/ +button.win-command { + background: none; + background-clip: border-box; + height: auto; /* height is 88px (label) or 68px (no label) */ + padding: 12px 0; /* bottom dependent on label/img, 2px margin */ + margin: 0; + border: 2px solid; /* reserve focus rect */ + min-width: 40px; + text-align: center; + font-size: 9pt; + line-height: 16px; + font-weight: normal; + + /* Commands are lrtb */ + writing-mode: lr-tb; +} +button.win-command:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + writing-mode: rl-tb; +} +button.win-command:focus { + outline: none; +} + +/* +Command button icons. +*/ +.win-commandicon { + display: inline-block; + margin: 0 28px; /* padding for command buttons, 2px/side for focus */ + min-width: 0; + min-height: 0; + padding: 0; + + /* Normal sizing */ + width: 40px; + height: 40px; + box-sizing: border-box; + cursor: default; +} +.win-commandimage { + /* Default font for glyphs. */ + font-family: "Segoe UI Command"; + letter-spacing: 0; + + /* Applications provide their own content, like . */ + vertical-align: middle; + font-size: 14pt; + margin: -2px; + line-height: 40px; /* line-height must match the content box height */ + background-position: 0 0; + background-origin: border-box; + display: inline-block; + /* Still 40px */ + width: 40px; + height: 40px; + background-size: 160px 80px; +} + +/* +Offsets for sprite versions. +*/ +button:hover .win-commandimage, button:active .win-commandimage { + background-position: -40px 0; +} +button:hover:active .win-commandimage { + background-position: -80px 0; +} + +button:-ms-keyboard-active .win-commandimage { + background-position: -80px 0; +} +button:disabled .win-commandimage, +button:disabled:active .win-commandimage { + background-position: -120px 0; +} + +/* +Offsets for sprite versions in selected state. +*/ +button[aria-checked=true] .win-commandimage { + background-position: 0 -40px; +} +button[aria-checked=true]:hover .win-commandimage, +button[aria-checked=true]:active .win-commandimage { + background-position: -40px -40px; +} +button[aria-checked=true]:hover:active .win-commandimage { + background-position: -80px -40px; +} +button[aria-checked=true]:-ms-keyboard-active .win-commandimage { + background-position: -80px -40px; +} +button[aria-checked=true]:disabled .win-commandimage, +button[aria-checked=true]:disabled:active .win-commandimage { + background-position: -120px -40px; +} + +/* +Command button "ring". +*/ +.win-commandring, +button:hover .win-commandring, +button:active .win-commandring, +button.win-command:disabled .win-commandring, +button.win-command:disabled:active .win-commandring { + border-width: 2px; + border-style: solid; + border-radius: 50%; + background-clip: border-box; +} +button:hover:active .win-commandring, +button[aria-checked=true] .win-commandring, +button[aria-checked=true]:active .win-commandring, +button[aria-checked=true]:disabled .win-commandring, +button[aria-checked=true]:disabled:active .win-commandring { + background-clip: border-box; +} + +/* +Command button labels. +*/ +button.win-command .win-label { + position: relative; + line-height: 16px; + display: block; + max-width: 88px; /* 100px button, but allow for 2px margins and 4px padding on each side */ + margin-top: 5px; + margin-bottom: -1px; + padding-left: 4px; /* 12px between buttons, 6px per side, minus 2px margins */ + padding-right: 4px; + overflow: hidden; + word-wrap: break-word; + word-break: keep-all; +} + +/* +AppBarCommand separator. +*/ +hr.win-command { + display: inline-block; + padding: 0; + margin: 14px 29px 34px 30px; + width: 1px; + height: 40px; + border: 0; + vertical-align: top; +} +hr.win-command:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-left: 29px; + margin-right: 30px; +} + +/* +AppBar Edgy Container +*/ +.win-appbar { + z-index: 1002; + border-width: 0; + width: 100%; + height: auto; + left: 0; + position: fixed; + -ms-touch-select: none; +} + +/* +AppBar control. +*/ +.win-commandlayout { + text-align: right; + padding-left: 10px; + padding-right: calc(((100% + 5px) mod 20px) + 5px); + width: calc(100% - 10px - (((100% + 5px) mod 20px) + 5px)); + + /* Hide whitespace between buttons. */ + font-size: 0; + + min-height: 88px; +} +.win-commandlayout .win-selection { + float: left; +} +.win-commandlayout:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + text-align: left; +} +.win-commandlayout .win-selection:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + float: right; +} + +/* Narrow buttons and no labels when < 1024px. */ +@media (max-width: 1023px) { + button.win-command .win-label { + display: none; + } + button.win-command .win-commandicon { + margin: 0 8px; /* 2px margin for focus */ + } + .win-commandlayout { + min-height: 68px; + } + hr.win-command { + margin-bottom: 14px; + margin-top: 14px; + } +} + +@media (min-width: 321px) and (max-width: 1023px) { + hr.win-command { + margin-left: 10px; + margin-right: 9px; + } + hr.win-command:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-left: 9px; + margin-right: 10px; + } +} + +/* Snapped AppBar reduced margins. */ +@media (max-width: 320px) { + .win-commandlayout { + padding-right: 10px; + width: calc(100% - 20px); + } + + .win-commandlayout.win-bottom { + /* Overflow buttons up instead of down */ + writing-mode: lr-bt; + } + + .win-commandlayout.win-bottom:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + writing-mode: rl-bt; + } +} + +/* +High contrast AppBar needs a border +*/ +@media (-ms-high-contrast) { + /* + AppBar Borders + */ + .win-appbar { + border: solid 2px; + } + .win-appbar.win-top { + border-top: none; + border-left: none; + border-right: none; + } + .win-appbar.win-bottom { + border-bottom: none; + border-left: none; + border-right: none; + } + .win-appbar.win-top button.win-command { + padding-bottom: 10px; + } + .win-appbar.win-bottom button.win-command { + padding-top: 10px; + } + .win-appbar.win-top hr.win-command { + margin-bottom: 32px; + } + .win-appbar.win-bottom hr.win-command { + margin-top: 12px; + } + .win-commandlayout { + min-height: 66px; + } +} +/* Portrait and snapped slightly different */ +@media (-ms-high-contrast) and (max-width: 1023px) { + .win-commandlayout { + min-height: 66px; + } + .win-appbar.win-top hr.win-command { + margin-bottom: 12px; + } + .win-appbar.win-bottom hr.win-command { + margin-top: 12px; + } +} + +/* +Flyout control. +*/ +.win-flyout { + z-index: 1008; + position: fixed; + padding: 25px 20px 20px 20px; + border-style: solid; + border-width: 2px; + margin: 5px; + min-width: 26px; /* 70px - padding - border = 26px */ + max-width: 466px; /* 510px - padding - border = 466px */ + min-height: 5px; /* 54px - padding - border = 5px */ + max-height: calc(100% - 59px); /* 768px - margin - padding - border = 709px */ + width: auto; + height: auto; + word-wrap: break-word; + overflow: auto; + -ms-touch-select: none; + font-size: 11pt; + font-weight: 400; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +.win-flyout.win-leftalign { + margin-left: 0; +} +.win-flyout.win-rightalign { + margin-right: 0; +} +.win-flyout.win-scrolls { + overflow: auto; +} + +/* +Menu control. +*/ +.win-menu { + padding: 5px 0 5px 0; + line-height: 33px; + text-align: left; /* Set explicitly in case our parent has different alignment, like appbar overflow. */ + min-height: 38px; /* 54px - padding - border = 38px */ + max-height: calc(100% - 26px); /* 768px - margin - padding - border = 742px */ + max-width: none; +} +.win-menu:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + text-align: right; /* Set explicitly in case our parent has different alignment, like appbar overflow. */ +} + +/* +Menu commands. +*/ +.win-menu button.win-command { + display: block; + border: none; + padding: 10px 20px 12px 20px; + margin-left: 0; + margin-right: 0; + float: none; + text-align: left; + width: 100%; + font-size: 11pt; + font-weight: 600; + line-height: 18px; /* 40px - 10px top padding - 12px bottom padding */ +} +.win-menu button.win-command:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + text-align: right; +} +.win-menu button.win-command:focus { + outline: none; +} +.win-menu hr.win-command { + display: block; + height: 1px; + width: auto; + border: 0; + + padding: 0; + margin: 9px 20px 10px 20px; +} + +/* +Menu toggle buttons. +*/ +.win-menu-toggle button.win-command::before { + font-family: 'Segoe UI Symbol'; + content: "\E0E7"; + visibility: hidden; + padding-left: 0px; + padding-right: 10px; +} +.win-menu-toggle button[aria-checked=true].win-command::before { + /* Display a checkbox if aria-checked is set */ + visibility: visible; +} +.win-menu-toggle button.win-command:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::before { + padding-right: 0; + padding-left: 10px; + float: right; +} + +/* +Settings Pane +*/ +.win-settingsflyout { + z-index: 1004; /* above appbar and below flyouts */ + border-left: 1px solid; + position: fixed; + top: 0; + right: 0; + height: 100%; + width: 345px; /* 346px - border (1px) */ + -ms-touch-select: none; +} +.win-settingsflyout.win-wide { + width: 645px; /* 646px - border (1px) */ +} +.win-settingsflyout .win-header { + padding-left: 40px; + padding-right: 40px; + padding-top: 32px; + height: 48px; +} +/* Settings back button is slightly smaller. */ +.win-settingsflyout .win-backbutton { + position: absolute; + width: 30px; + height: 30px; + font-size: 8pt; + line-height: 26px; + margin-top: 3px; +} +.win-settingsflyout .win-header .win-label { + display: inline-block; + padding-left: 40px; + font-size: 20pt; + line-height: 33px; +} +.win-settingsflyout .win-content { + overflow: auto; + padding-left: 40px; + padding-right: 40px; + padding-top: 33px; + height: calc(100% - 112px); +} + +.win-settingsflyout .win-label { + font-size: 20pt; + font-weight: 200; + line-height: normal; +} +.win-settingsflyout:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + right: auto; + left: 0; + border-left: none; + border-right: 1px solid; +} +.win-settingsflyout .win-header .win-label:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + padding-right: 40px; + padding-left: 0; +} + +.win-settingsflyout .win-content .win-settings-section { + padding-top: 0; + padding-bottom: 39px; +} + +.win-settingsflyout .win-content .win-settings-section p { + margin: 0; + padding-top: 0; + padding-bottom: 25px; +} + +.win-settingsflyout .win-content .win-settings-section a { + margin: 0; + padding-top: 0; + padding-bottom: 25px; + display:inline-block; +} + +.win-settingsflyout .win-content .win-settings-section .win-toggleswitch { + margin: 0; + padding-top: 0; + padding-bottom: 20px; +} + +.win-settingsflyout .win-content .win-settings-section label { + display:block; + padding-bottom: 7px; +} + +.win-settingsflyout .win-content .win-settings-section button, +.win-settingsflyout .win-content .win-settings-section select, +.win-settingsflyout .win-content .win-settings-section input[type=button], +.win-settingsflyout .win-content .win-settings-section input[type=text] { + margin-bottom: 25px; + margin-left: 0; + margin-right: 20px; +} + +.win-settingsflyout .win-content .win-settings-section input[type=radio] { + margin-top: 0; + margin-bottom: 0; + padding-bottom: 15px; +} + +.win-settingsflyout .win-content .win-settings-section button:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-bottom: 25px; + margin-left: 20px; + margin-right: 0; +} +.win-settingsflyout .win-content .win-settings-section select:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-bottom: 25px; + margin-left: 20px; + margin-right: 0; +} + +.win-settingsflyout .win-content .win-settings-section input[type=text]:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-bottom: 25px; + margin-left: 20px; + margin-right: 0; +} + +.win-settingsflyout .win-content .win-settings-section input[type=button]:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-bottom: 25px; + margin-left: 20px; + margin-right: 0; +} + +/* +Common AppBar/Flyout/SettingsFlyout styles +*/ + +.win-flyout input[type=text], +.win-settingsflyout input[type=text], +.win-appbar input[type=text] { + -ms-touch-select: grippers; +} + +/* +Rating control. +*/ +.win-rating { + display: -ms-inline-flexbox; + height: auto; + width: auto; + white-space: normal; + -ms-flex-align: stretch; + -ms-flex-pack: center; +} + +.win-rating .win-star { + -ms-flex: 1 1 auto; + height:28px; + width: 28px; + padding: 0 6px; + font-family: "Segoe UI Symbol"; + font-size: 28px; + overflow: hidden; + text-indent: 0; + line-height: 1; + cursor: default; + position: relative; + letter-spacing: 0; /* Use letter-spacing: 0 to make average star look like one glyph. */ + -ms-touch-action: none; +} +.win-rating.win-small .win-star { + width: 14px; + height: 14px; + font-size: 14px; + padding: 0 3px; +} +.win-rating .win-star:before { + content: "\E082"; +} +.win-rating .win-star.win-disabled { + cursor: default; + -ms-touch-action: auto; +} + +/* +DatePicker control. +*/ +.win-datepicker { + display: -ms-inline-flexbox; + height: auto; + width: auto; +} +.win-datepicker .win-datepicker-month { + margin-right: 20px; +} +.win-datepicker .win-datepicker-date.win-order0, +.win-datepicker .win-datepicker-date.win-order1 { + margin-right: 20px; +} +.win-datepicker .win-datepicker-year.win-order0 { + margin-right: 20px; +} +.win-datepicker .win-datepicker-month:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-right: 0; + margin-left: 20px; +} +.win-datepicker .win-datepicker-date.win-order0:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm), +.win-datepicker .win-datepicker-date.win-order1:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-right: 0; + margin-left: 20px; +} +.win-datepicker .win-datepicker-year.win-order0:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm){ + margin-right: 0; + margin-left: 20px; +} + +/* +TimePicker control. +*/ +.win-timepicker { + display: -ms-inline-flexbox; + height: auto; + width: auto; +} +.win-timepicker .win-timepicker-hour { + margin-right: 20px; +} +.win-timepicker .win-timepicker-period.win-order0 { + margin-right: 20px; +} +.win-timepicker .win-timepicker-minute.win-order1 { + margin-right: 20px; +} +.win-timepicker .win-timepicker-period.win-order0:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-right: 0; + margin-left: 20px; +} +.win-timepicker .win-timepicker-minute.win-order1:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm), +.win-timepicker .win-timepicker-minute.win-order0:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-left: 20px; + margin-right: 0; +} +.win-timepicker .win-timepicker-hour:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + margin-right: 0; + margin-left: 20px; +} + +/* +Toggle control. +*/ +.win-toggleswitch { + padding: 0; + background-color: transparent; + vertical-align: top; + margin: 0; + display: block; +} +.win-toggleswitch .win-switch { + display: inline-block; + height: 19px; + width: 50px; + padding: 5px; + margin: 0; + background-color: transparent; + -ms-grid-column: 2; +} +.win-toggleswitch .win-switch::-ms-tooltip { + display: none; +} +.win-toggleswitch .win-switch::-ms-ticks-before, +.win-toggleswitch .win-switch::-ms-ticks-after { + display: none; +} +.win-toggleswitch .win-switch::-ms-track { + height: 15px; /* 19px - 2px borders */ + width: 46px; + padding: 0; + margin: 0; + border-width: 2px; + border-style: solid; +} +.win-toggleswitch .win-switch::-ms-fill-lower, +.win-toggleswitch .win-switch::-ms-fill-upper { + height: 13px; /* 19px - 2px borders - 1px margins */ + padding: 0; +} +.win-toggleswitch .win-switch::-ms-fill-lower { + margin-left: 1px; +} +.win-toggleswitch .win-switch:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-fill-lower { + margin-left: 0; + margin-right: 1px; +} +.win-toggleswitch .win-switch::-ms-fill-upper { + margin-right: 1px; +} +.win-toggleswitch .win-switch:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm)::-ms-fill-upper { + margin-right: 0; + margin-left: 1px; +} +.win-toggleswitch .win-switch::-ms-thumb { + height: 19px; + width: 12px; +} +.win-toggleswitch .win-title { + display: block; + vertical-align: top; + max-width:470px; + font-size: 11pt; + font-weight: 300; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +.win-toggleswitch .win-label { + display: inline-block; + vertical-align: top; + padding: 5px 20px 5px 0px; + min-width: 65px; + direction: inherit; + -ms-grid-column: 1; + -ms-grid-row: 1; + font-size: 11pt; + font-weight: 600; + line-height: 1.3636; /* 20px when font-size is 11pt */ +} +.win-toggleswitch .win-label:-ms-lang(ar, dv, fa, he, ku-Arab, pa-Arab, prs, ps, sd-Arab, syr, ug, ur, qps-plocm) { + padding: 5px 0px 5px 20px; +} +.win-toggleswitch .win-label.win-hidden { + visibility: hidden; +} +.win-focus-hide { + outline: none; +} + +/* +Tooltip control. +*/ +.win-tooltip { + display: block; + position: fixed; + top: 30px; + left: 30px; + max-width: 380px; + box-sizing: border-box; + margin: 0; + padding: 6px 10px 7px 10px; + border-style: solid; + border-width: 2px; + z-index: 9999; + word-wrap: break-word; + animation-fill-mode: both; + font-size: 9pt; + font-weight: 400; + line-height: 1.6667; /* 20px when font-size is 9pt */ +} +.win-tooltip-phantom { + display: block; + position: fixed; + top: 30px; + left: 30px; + background-color: transparent; + border-width: 0; + margin: 0; + padding: 0; +} + +@media (-ms-view-state: snapped) { + .win-tooltip { + max-width: 310px; + box-sizing: border-box; + } +} + +.win-viewbox { + width: 100%; + height: 100%; + position: relative; +} + +.win-semanticzoom { + height: 400px; + -ms-touch-action: pan-x pan-y double-tap-zoom; +} +.win-semanticzoom * { + -ms-touch-action: inherit; +} + +.win-semanticzoom-button { + z-index: 100; + position: absolute; + min-width: 25px; + min-height: 25px; + width: 25px; + height: 25px; + padding: 0px; + bottom: 21px; + -ms-touch-action: none; +} + +.win-semanticzoom-button::before { + font-family: "Segoe UI Symbol"; + font-weight: normal; + font-size: 11pt; + content: "\E0B8"; /* minus sign */ +} + +.win-semanticzoom-button-location.rtl { + left: 4px; +} + +.win-semanticzoom-button-location.ltr{ + right: 4px; +} + +/* +ScrollView control. +*/ +.win-scrollview { + overflow-x: auto; + overflow-y: hidden; + height: 400px; + width: 100%; +} + +/* +ListView control. +*/ +.win-listview { + overflow: hidden; + height: 400px; +} +.win-listview .win-backdrop { + position: absolute; +} +.win-listview .win-surface { + position: relative; + overflow: visible; + width: 100%; + height: 100%; +} +.win-listview > .win-viewport { + position: relative; + width: 100%; + height: 100%; + z-index: 0; +} +.win-listview > .win-viewport.win-horizontal { + overflow-x: auto; + overflow-y: hidden; +} +.win-listview > .win-viewport.win-vertical { + overflow-x: hidden; + overflow-y: auto; +} +.win-listview > .win-horizontal .win-container { + margin: 5px; +} +.win-listview > .win-vertical .win-container { + margin: 5px 24px 5px 7px; +} +.win-listview.win-rtl > .win-vertical .win-container { + margin: 5px 7px 5px 24px; +} +.win-listview .win-container { + cursor: default; + position: absolute; + z-index: 0; +} +.win-listview .win-container.win-swipe { + z-index: 1; +} +.win-listview.win-swipeable .win-horizontal .win-container { + -ms-touch-action:pan-x pinch-zoom double-tap-zoom; +} +.win-listview.win-swipeable .win-vertical .win-container { + -ms-touch-action:pan-y pinch-zoom double-tap-zoom; +} +.win-semanticzoom .win-listview > .win-viewport * { + -ms-touch-action: auto; +} +.win-semanticzoom .win-listview > .win-viewport.win-zooming-x { + overflow-x: visible; +} +.win-semanticzoom .win-listview > .win-viewport.win-zooming-y { + overflow-y: visible; +} +.win-listview .win-item { + /* Used to place above .win-selectionbackground in ListView */ + z-index: 1; +} +.win-listview .win-item { + overflow: hidden; + position: absolute; +} +.win-listview > .win-vertical .win-item +{ + /* Allow flex box to fill win-item in list layout */ + width: 100%; +} +.win-listview .win-item:focus { + outline-style: none; +} +.win-listview .win-focusedoutline { + width: calc(100% + 4px); + height: calc(100% + 4px); + left: -2px; + top: -2px; + position: absolute; + z-index: 5; +} +.win-container.win-selected .win-selectionborder { + border-width: 4px; + border-style: solid; +} +.win-container.win-selected:hover .win-selectionborder { + border-width: 4px; + border-style: solid; +} +.win-listview .win-groupheader { + position: absolute; + top: 0; + padding: 10px 10px 10px 2px; + cursor: default; + overflow: hidden; + margin-left: 70px; + font-size: 20pt; + font-weight: 200; + line-height: 1.2; /* 32px when font-size is 20pt */ +} +.win-listview.win-rtl .win-groupheader { + margin-left: 0; + margin-right: 70px; + padding-left: 10px; + padding-right: 2px; +} +.win-listview.win-groups > .win-horizontal .win-surface { + margin-left: -70px; +} +.win-listview.win-groups.win-rtl > .win-horizontal .win-surface { + margin-left: 0; + margin-right: -70px; +} +.win-surface ._win-proxy { + position: absolute; + width: 0; + height: 0; + -ms-touch-action: none; +} +.win-selectionborder { + position: absolute; + opacity: inherit; + z-index: 2; +} +.win-container.win-selected .win-selectionbordertop { + top: 0; + left: 0; + right: 0; + height: 0; + border-right-style: hidden !important; + border-bottom-style: hidden !important; + border-left-style: hidden !important; +} +.win-container.win-selected .win-selectionborderright { + top: 0; + right: 0; + bottom: 0; + width: 0; + border-top-style: hidden !important; + border-bottom-style: hidden !important; + border-left-style: hidden !important; +} +.win-container.win-selected .win-selectionborderbottom { + left: 0; + right: 0; + bottom: 0; + height: 0; + border-top-style: hidden !important; + border-right-style: hidden !important; + border-left-style: hidden !important; +} +.win-container.win-selected .win-selectionborderleft { + top: 0; + left: 0; + bottom: 0; + width: 0; + border-top-style: hidden !important; + border-right-style: hidden !important; + border-bottom-style: hidden !important; +} +.win-selectionbackground { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + /* Used to place behind .win-item in the ListView */ + z-index: 0; +} +.win-selectioncheckmarkbackground { + position: absolute; + top: 0; + right: 0; + width: 0; + height: 0; + margin: 0; + padding: 0; + border-width: 20px; + border-style: solid; + z-index: 3; +} +.win-listview.win-rtl .win-selectioncheckmarkbackground { + left: 0; + right: auto; +} +.win-selectioncheckmark { + position: absolute; + margin: 0; + padding: 2px; + right: 0; + top: 0; + font-family: Segoe UI Symbol; + font-size: 11pt; + z-index: 4; +} +.win-listview.win-rtl .win-selectioncheckmark { + left: 0; + right: auto; +} +.win-selectionhint { + position: absolute; + margin: 0; + padding: 2px; + right: 0; + font-family: Segoe UI Symbol; + font-size: 11pt; + opacity: 0.5; +} +.win-selectionhint.win-revealed { + opacity: 1; +} +.win-listview.win-rtl .win-selectionhint { + left: 0; + right: auto; +} +.win-listview .win-progress { + left: 50%; + top: 50%; + width: 60px; + height: 60px; + margin-left: -30px; + margin-top: -30px; + z-index: 1; + position: absolute; +} +.win-listview .win-progress::-ms-fill { + animation-name: -ms-ring; +} + +/* +FlipView control. +*/ +.win-flipview { + overflow: hidden; + height: 400px; +} + +.win-flipview .win-surface { + -ms-scroll-chaining: none; +} + +.win-flipview .win-navleft { + left: 0%; + top: 50%; + margin-top: -19px; +} +.win-flipview .win-navright { + left: 100%; + top: 50%; + margin-left: -69px; + margin-top: -19px; +} +.win-flipview .win-navtop { + left: 50%; + top: 0%; + margin-left: -35px; +} +.win-flipview .win-navbottom { + left: 50%; + top: 100%; + margin-left: -35px; + margin-top: -39px; +} +.win-flipview .win-navbutton { + border: none; + width: 69px; + height: 39px; + z-index: 1; + position: absolute; + font-family: "Segoe UI Symbol"; + font-size: 16pt; + padding: 0; + min-width: 0; +} +.win-flipview .win-item, .win-flipview .win-item > .win-template { + display:-ms-flexbox; + height: 100%; + width: 100%; + -ms-flex-align:center; + -ms-flex-pack:center; +} + +/* +Typographic color definitions. +*/ + +body { + color: rgb(0, 0, 0); + background-color: rgb(255, 255, 255); +} + +.win-ui-dark .win-type-interactive:hover { + color: rgba(255, 255, 255, 0.8); +} +.win-ui-dark .win-type-interactive:hover:active { + color: rgba(255, 255, 255, 0.4); +} + +.win-ui-dark .win-type-interactive:-ms-keyboard-active { + color: rgba(255, 255, 255, 0.4); +} +.win-type-interactive:hover { + color: rgba(0, 0, 0, 0.8); +} +.win-type-interactive:hover:active { + color: rgba(0, 0, 0, 0.4); +} +.win-type-interactive:-ms-keyboard-active { + color: rgba(0, 0, 0, 0.4); +} + +/* +These classes reverse the colors on the subtree to which they are applied. +*/ + +.win-ui-light { + color: rgb(0, 0, 0); + background-color: rgb(255, 255, 255); +} +.win-ui-dark { + color: rgb(255, 255, 255); + background-color: rgb(29, 29, 29); +} + +/* +Text selection color +*/ +::selection { + background-color: rgb(87, 41, 193); + color: rgb(255, 255, 255); +} + +/* +Text input, checkbox, radio, and select control colors. +*/ +.win-ui-dark input[type=text], .win-ui-dark input[type=password], +.win-ui-dark input[type=email], .win-ui-dark input[type=number], +.win-ui-dark input[type=tel], .win-ui-dark input[type=url], +.win-ui-dark input[type=search], .win-ui-dark textarea, .win-ui-dark .win-textarea, +.win-ui-dark select, .win-ui-dark input::-ms-check { + background-clip: border-box; + background-color: rgba(255, 255, 255, 0.8); + border-color: transparent; + color: rgb(0, 0, 0); +} +.win-ui-dark input[type=text]:hover, .win-ui-dark input[type=password]:hover, +.win-ui-dark input[type=email]:hover, .win-ui-dark input[type=number]:hover, +.win-ui-dark input[type=tel]:hover, .win-ui-dark input[type=url]:hover, +.win-ui-dark input[type=search]:hover, .win-ui-dark textarea:hover, +.win-ui-dark .win-textarea:hover, .win-ui-dark select:hover, .win-ui-dark input:hover::-ms-check { + background-clip: border-box; + background-color: rgba(255, 255, 255, 0.87); + border-color: transparent; + color: rgb(0, 0, 0); +} +.win-ui-dark input:hover:active::-ms-check { /* only checkbox and radio have press state */ + background-color: rgb(255, 255, 255); + color: rgb(0, 0, 0); +} +.win-ui-dark input:-ms-keyboard-active::-ms-check { /* only checkbox and radio have press state */ + background-color: rgb(255, 255, 255); +} +.win-ui-dark input[type=text]:focus, .win-ui-dark input[type=text]:active, +.win-ui-dark input[type=password]:focus, .win-ui-dark input[type=password]:active, +.win-ui-dark input[type=email]:focus, .win-ui-dark input[type=email]:active, +.win-ui-dark input[type=number]:focus, .win-ui-dark input[type=number]:active, +.win-ui-dark input[type=tel]:focus, .win-ui-dark input[type=tel]:active, +.win-ui-dark input[type=url]:focus, .win-ui-dark input[type=url]:active, +.win-ui-dark input[type=search]:focus, .win-ui-dark input[type=search]:active, +.win-ui-dark textarea:focus, .win-ui-dark textarea:active, +.win-ui-dark .win-textarea:focus, .win-ui-dark .win-textarea:active, +.win-ui-dark select:focus, .win-ui-dark select:active { + background-clip: border-box; + background-color: rgb(255, 255, 255); + border-color: transparent; + color: rgb(0, 0, 0); +} +.win-ui-dark input[type=text]:disabled, .win-ui-dark input[type=password]:disabled, +.win-ui-dark input[type=email]:disabled, .win-ui-dark input[type=number]:disabled, +.win-ui-dark input[type=tel]:disabled, .win-ui-dark input[type=url]:disabled, +.win-ui-dark input[type=search]:disabled, .win-ui-dark input[type=file]::-ms-value, +.win-ui-dark textarea:disabled, .win-ui-dark .win-textarea:disabled, .win-ui-dark select:disabled { + background-clip: border-box; + background-color: transparent; + border-color: rgba(255, 255, 255, 0.4); + color: rgba(255, 255, 255, 0.4); +} +.win-ui-dark input:disabled::-ms-check, .win-ui-dark input:disabled:active::-ms-check { /* checkbox and radio have filled disabled state in dark */ + background-clip: border-box; + background-color: rgba(255, 255, 255, 0.4); + border-color: transparent; + color: rgba(0, 0, 0, 0.4); +} +input[type=text], input[type=password], +input[type=email], input[type=number], +input[type=tel], input[type=url], +input[type=search], select, textarea, +.win-textarea, input::-ms-check { + background-clip: padding-box; + background-color: rgba(255, 255, 255, 0.8); + border-color: rgba(0, 0, 0, 0.27); + color: rgb(0, 0, 0); +} +input[type=text]:hover, input[type=password]:hover, +input[type=email]:hover, input[type=number]:hover, +input[type=tel]:hover, input[type=url]:hover, +input[type=search]:hover, textarea:hover, +select:hover, .win-textarea:hover, input:hover::-ms-check { + background-color: rgba(255, 255, 255, 0.87); + border-color: rgba(0, 0, 0, 0.44); + color: rgb(0, 0, 0); +} +input[type=text]:focus, input[type=text]:active, +input[type=password]:focus, input[type=password]:active, +input[type=email]:focus, input[type=email]:active, +input[type=number]:focus, input[type=number]:active, +input[type=tel]:focus, input[type=tel]:active, +input[type=url]:focus, input[type=url]:active, +input[type=search]:focus, input[type=search]:active, +textarea:focus, textarea:active, +.win-textarea:focus, .win-textarea:active, +select:focus, select:active { + background-color: rgb(255, 255, 255); + border-color: rgba(0, 0, 0, 0.6); + color: rgb(0, 0, 0); +} +input:hover:active::-ms-check { /* only checkbox and radio have press state */ + background-clip: border-box; + background-color: rgb(0, 0, 0); + border-color: transparent; + color: rgb(255, 255, 255); +} +input:-ms-keyboard-active::-ms-check { /* only checkbox and radio have press state */ + background-clip: border-box; + background-color: rgb(0, 0, 0); + border-color: transparent; + color: rgb(255, 255, 255); +} +input[type=text]:disabled, input[type=password]:disabled, +input[type=email]:disabled, input[type=number]:disabled, +input[type=tel]:disabled, input[type=url]:disabled, +input[type=search]:disabled, input[type=file]::-ms-value, +textarea:disabled, .win-textarea:disabled, select:disabled, +input:disabled::-ms-check, input:disabled:active::-ms-check { + background-clip: padding-box; + background-color: rgba(202, 202, 202, 0.4); + border-color: rgba(0, 0, 0, 0.15); + color: rgba(0, 0, 0, 0.4); +} + +/* +Placeholder text style. +*/ +.win-ui-dark input[type]:-ms-input-placeholder, input[type]:-ms-input-placeholder, +.win-ui-dark textarea:-ms-input-placeholder, textarea:-ms-input-placeholder { + color: rgba(0, 0, 0, 0.6); /* same in dark and light */ +} +.win-ui-dark input[type]:disabled:-ms-input-placeholder, .win-ui-dark textarea:disabled:-ms-input-placeholder { + color: rgba(255, 255, 255, 0.22); +} +input[type]:disabled:-ms-input-placeholder, textarea:disabled:-ms-input-placeholder { + color: rgba(0, 0, 0, 0.22); +} + +/* +Invalid style. +*/ +input[type]:invalid { + outline-color: rgb(255, 128, 51); + outline-width: 2px; +} + +/* +Clear and reveal buttons. +*/ +input::-ms-clear, input::-ms-reveal { + background-color: rgb(255, 255, 255); + color: rgb(0, 0, 0); +} +input::-ms-clear:hover, input::-ms-reveal:hover { + background-color: rgb(222, 222, 222); +} +input::-ms-clear:hover:active, input::-ms-reveal:hover:active { + background-color: rgb(0, 0, 0); + color: rgb(255, 255, 255); +} + +/* +Option for select control (all colors are same for both light and dark). +*/ +option:checked, select:focus::-ms-value { + color: rgb(255, 255, 255); + background-color: rgb(70, 23, 180); +} +option:hover { + color: rgb(0, 0, 0); + background-color: rgb(197, 197, 197); +} +select:focus option:hover { + color: rgb(0, 0, 0); + background-color: rgb(222, 222, 222); +} +option:checked:hover, select:focus option:checked:hover { + color: rgb(255, 255, 255); + background-color: rgb(95, 55, 190); +} +option:hover:active, select:focus option:hover:active{ + color: rgb(0, 0, 0); + background-color: rgb(211, 211, 211); +} +.win-ui-dark option:checked:disabled, .win-ui-dark option:checked:disabled:active, +.win-ui-dark optgroup:disabled option:checked, .win-ui-dark optgroup:disabled option:checked:active, +.win-ui-dark select:disabled option:checked, .win-ui-dark select:disabled option:checked:active, +.win-ui-dark select:disabled:focus::-ms-value { + background-color: rgba(255, 255, 255, 0.4); + color: rgba(0, 0, 0, 0.6); +} +option:checked:disabled, option:checked:disabled:active, +optgroup:disabled option:checked, optgroup:disabled option:checked:active, +select:disabled option:checked, select:disabled option:checked:active, +select:disabled:focus::-ms-value { + background-color: rgba(0, 0, 0, 0.55); + color: rgba(255, 255, 255, 0.6); +} + +/* +Button control colors. +*/ +.win-ui-dark button, .win-ui-dark input[type=button], .win-ui-dark input[type=submit], +.win-ui-dark input[type=reset], .win-ui-dark input[type=file]::-ms-browse { + background-color: transparent; + border-color: rgb(255, 255, 255); + color: rgb(255, 255, 255); +} +.win-ui-dark button[type=submit], .win-ui-dark input[type=submit] { + background-clip: padding-box; + background-color: rgb(70, 23, 180); +} +.win-ui-dark button:hover, .win-ui-dark input[type=button]:hover, .win-ui-dark input[type=reset]:hover, +.win-ui-dark input[type=file]::-ms-browse:hover { + background-color: rgba(255, 255, 255, 0.13); + border-color: rgb(255, 255, 255); +} +.win-ui-dark button[type=submit]:hover, .win-ui-dark input[type=submit]:hover { + background-clip: padding-box; + background-color: rgb(95, 55, 190); + border-color: rgb(255, 255, 255); +} +.win-ui-dark button:hover:active, .win-ui-dark button[type=submit]:hover:active, +.win-ui-dark input[type=button]:hover:active, .win-ui-dark input[type=reset]:hover:active, +.win-ui-dark input[type=submit]:hover:active, .win-ui-dark input[type=file]::-ms-browse:hover:active { + background-clip: border-box; + background-color: rgb(255, 255, 255); + border-color: transparent; + color: rgb(0, 0, 0); +} +.win-ui-dark button:-ms-keyboard-active, .win-ui-dark button[type=submit]:-ms-keyboard-active, +.win-ui-dark input[type=button]:-ms-keyboard-active, .win-ui-dark input[type=reset]:-ms-keyboard-active, +.win-ui-dark input[type=submit]:-ms-keyboard-active { + background-clip: border-box; + background-color: rgb(255, 255, 255); + border-color: transparent; + color: rgb(0, 0, 0); +} +.win-ui-dark button:disabled, .win-ui-dark button[type=submit]:disabled, +.win-ui-dark input[type=button]:disabled, .win-ui-dark input[type=reset]:disabled, +.win-ui-dark input[type=submit]:disabled, .win-ui-dark input[type=file]:disabled::-ms-browse, +.win-ui-dark button:disabled:active, .win-ui-dark button[type=submit]:disabled:active, +.win-ui-dark input[type=button]:disabled:active, .win-ui-dark input[type=reset]:disabled:active, +.win-ui-dark input[type=submit]:disabled:active, .win-ui-dark input[type=file]:disabled::-ms-browse:active { + background-color: transparent; + border-color: rgba(255, 255, 255, 0.4); + color: rgba(255, 255, 255, 0.4); +} +button, input[type=button], input[type=submit], +input[type=reset], input[type=file]::-ms-browse { + background-color: rgba(182, 182, 182, 0.7); + border-color: rgba(0, 0, 0, 0.2); + color: rgb(0, 0, 0); +} +button[type=submit], input[type=submit]{ + background-clip: border-box; + background-color: rgb(70, 23, 180); + border-color: transparent; + color: rgb(255, 255, 255); +} +button:hover, input[type=button]:hover, input[type=reset]:hover, +input[type=file]::-ms-browse:hover { + background-color: rgba(205, 205, 205, 0.82); + border-color: rgba(164, 164, 164, 0.45); +} +button[type=submit]:hover, input[type=submit]:hover { + background-clip: border-box; + background-color: rgb(95, 55, 190); + border-color: transparent; +} +button:hover:active, button[type=submit]:hover:active, +input[type=button]:hover:active, input[type=reset]:hover:active, +input[type=submit]:hover:active, input[type=file]::-ms-browse:hover:active { + background-clip: border-box; + background-color: rgb(0, 0, 0); + border-color: transparent; + color: rgb(255, 255, 255); +} +button:-ms-keyboard-active, button[type=submit]:-ms-keyboard-active, +input[type=button]:-ms-keyboard-active, input[type=reset]:-ms-keyboard-active, +input[type=submit]:-ms-keyboard-active { + background-clip: border-box; + background-color: rgb(0, 0, 0); + border-color: transparent; + color: rgb(255, 255, 255); +} +button:disabled, button[type=submit]:disabled, +input[type=button]:disabled, input[type=reset]:disabled, +input[type=submit]:disabled, input[type=file]:disabled::-ms-browse, +button:disabled:active, button[type=submit]:disabled:active, +input[type=button]:disabled:active, input[type=reset]:disabled:active, +input[type=submit]:disabled:active, input[type=file]:disabled::-ms-browse:active { + background-color: rgba(202, 202, 202, 0.4); + border-color: rgba(0, 0, 0, 0.08); + color: rgba(0, 0, 0, 0.4); +} +input[type=file]:disabled::-ms-browse, input[type=file]:disabled::-ms-browse:active { + border-color: rgba(0, 0, 0, 0.15); +} + +/* +File upload control colors. +*/ +input[type=file] { + background-color: transparent; + border-color: transparent; + color: transparent; +} + +/* +Link colors. +*/ +.win-ui-dark a { + color: rgb(156, 114, 255); +} +.win-ui-dark a:hover:active { + color: rgba(156, 114, 255, 0.6); +} +.win-ui-dark a:hover { + color: rgba(156, 114, 255, 0.8); +} +.win-ui-dark a[disabled], .win-ui-dark a[disabled]:active { /* :disabled pseudo-class doesn't apply to a, even though disabled attribute stops navigation */ + color: rgba(255, 255, 255, 0.4); +} +a { + color: rgb(79, 26, 203); +} +a:hover:active { + color: rgba(79, 26, 203, 0.6); +} +a:hover { + color: rgba(79, 26, 203, 0.8); +} +a[disabled], a[disabled]:active { /* :disabled pseudo-class doesn't apply to a, even though disabled attribute stops navigation */ + color: rgba(0, 0, 0, 0.4); +} + +/* +Range control colors. +*/ +input[type=range], input[type=range]::-ms-track { + background-color: transparent; +} + +.win-ui-dark input[type=range]::-ms-fill-lower { + background-color: rgb(91, 46, 197); +} +.win-ui-dark input[type=range]:hover::-ms-fill-lower { + background-color: rgb(114, 75, 205); +} +.win-ui-dark input[type=range]:active::-ms-fill-lower { + background-color: rgb(129, 82, 239); +} +.win-ui-dark input[type=range]:disabled::-ms-fill-lower { + background-color: rgba(255, 255, 255, 0.23); +} +input[type=range]::-ms-fill-lower { + background-color: rgb(70, 23, 180); +} +input[type=range]:hover::-ms-fill-lower { + background-color: rgb(95, 55, 190); +} +input[type=range]:active::-ms-fill-lower { + background-color: rgb(114, 65, 228); +} +input[type=range]:disabled::-ms-fill-lower { + background-color: rgba(0, 0, 0, 0.2); +} + +.win-ui-dark input[type=range]::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.16); +} +.win-ui-dark input[type=range]:hover::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.18); +} +.win-ui-dark input[type=range]:active::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.23); +} +.win-ui-dark input[type=range]:disabled::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.16); +} +input[type=range]::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.1); +} +input[type=range]:hover::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.15); +} +input[type=range]:active::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.2); +} +input[type=range]:disabled::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.1); +} + +.win-ui-dark input[type=range]::-ms-thumb { + background-color: rgb(255, 255, 255); +} +.win-ui-dark input[type=range]:disabled::-ms-thumb { + background-color: rgb(126, 126, 126); +} +input[type=range]::-ms-thumb { + background-color: rgb(0, 0, 0); +} +input[type=range]:disabled::-ms-thumb { + background-color: rgb(146, 146, 146); +} + +.win-ui-dark input[type=range]::-ms-ticks-before, +.win-ui-dark input[type=range]::-ms-ticks-after { + color: rgba(255, 255, 255, 0.5); +} +input[type=range]::-ms-ticks-before, +input[type=range]::-ms-ticks-after { + color: rgba(0, 0, 0, 0.5); +} + +/* +Progress control colors. +*/ +.win-ui-dark progress { + background-color: rgba(255,255,255,0.35); + color: rgb(91, 46, 197); +} +progress { + background-color: rgba(0, 0, 0, 0.2); + color: rgb(70, 23, 180); +} +.win-ui-dark progress:indeterminate { + color: rgb(138, 87, 255); +} +progress:indeterminate { + color: rgb(70, 23, 180); +} +progress::-ms-fill { + background-color: currentColor; +} +/* +Explicitly define indeterminate background transparent for dark and light +because ".win-ui-light progress" has higher specificity than "progress:indeterminate". +*/ +.win-ui-dark progress:indeterminate, progress:indeterminate { + background-color: transparent; +} + +/* +FlipView control colors. +*/ +.win-flipview .win-navbutton { + background-color: rgba(213, 213, 213, 0.35); + color: rgba(0, 0, 0, 0.60); +} +.win-flipview .win-navbutton:hover { + background-color: rgba(215, 215, 215, 0.94); + color: rgba(0, 0, 0, 1.0); +} +.win-flipview .win-navbutton:hover:active { + background-color: rgba(41, 41, 41, 0.74); + color: rgba(255, 255, 255, 1.0); +} + +/* +ListView control colors. +*/ +.win-ui-dark .win-listview .win-container:not(.win-footprint) { + background-color: rgb(29, 29, 29); +} +.win-listview .win-container:not(.win-footprint) { + background-color: rgb(255, 255, 255); +} +.win-ui-dark .win-listview .win-container:hover { + outline: rgba(255, 255, 255, 0.3) solid 3px; +} +.win-ui-dark .win-listview.win-selectionstylefilled .win-container:hover { + background-color: rgba(255, 255, 255, 0.3); +} +.win-ui-dark .win-listview .win-focusedoutline { + outline: rgb(255, 255, 255) solid 2px; +} +.win-listview .win-container:hover { + outline: rgba(0, 0, 0, 0.3) solid 3px; +} +.win-listview.win-selectionstylefilled .win-container:hover { + background-color: rgba(0, 0, 0, 0.3); +} +.win-listview .win-focusedoutline { + outline: rgb(0, 0, 0) solid 2px; +} +.win-listview.win-selectionstylefilled .win-container.win-selected:hover { + background-color: rgb(95, 55, 190); +} +.win-ui-dark .win-listview .win-container.win-swipe:hover { + background-color: rgb(29, 29, 29); + outline: none; +} +.win-listview .win-container.win-swipe:hover { + background-color: rgb(255, 255, 255); + outline: none; +} +.win-listview.win-selectionstylefilled .win-selected { + color: rgb(255, 255, 255); +} +.win-listview:not(.win-selectionstylefilled) .win-container.win-selected .win-selectionborder { + border-color: rgb(70, 23, 180); +} +.win-listview.win-selectionstylefilled .win-container.win-selected .win-selectionborder { + border-color: transparent; +} +.win-listview:not(.win-selectionstylefilled) .win-container.win-selected:hover .win-selectionborder { + border-color: rgb(95, 55, 190); +} +.win-listview.win-selectionstylefilled .win-selected .win-selectionbackground { + background-color: rgb(70, 23, 180); +} +.win-listview.win-selectionstylefilled .win-selected:hover .win-selectionbackground { + background-color: rgb(95, 55, 190); +} +.win-selectioncheckmark { + color: rgb(255, 255, 255); +} +.win-ui-dark .win-selectionhint { + color: rgb(255, 255, 255); +} +.win-selectionhint { + color: rgb(70, 23, 180); +} +.win-listview:not(.win-selectionstylefilled) .win-selectioncheckmarkbackground { + border-top-color: rgb(70, 23, 180); + border-right-color: rgb(70, 23, 180); + border-left-color: transparent; + border-bottom-color: transparent; +} +.win-listview.win-selectionstylefilled .win-selectioncheckmarkbackground { + border-color: transparent; +} +.win-listview:not(.win-selectionstylefilled) .win-container.win-selected:hover .win-selectioncheckmarkbackground { + border-top-color: rgb(95, 55, 190); + border-right-color: rgb(95, 55, 190); + border-left-color: transparent; + border-bottom-color: transparent; +} +.win-listview.win-rtl:not(.win-selectionstylefilled) .win-selectioncheckmarkbackground { + border-left-color: rgb(70, 23, 180); + border-right-color: transparent; +} +.win-listview.win-rtl:not(.win-selectionstylefilled) .win-container.win-selected:hover .win-selectioncheckmarkbackground { + border-left-color: rgb(95, 55, 190); + border-right-color: transparent; +} + +.win-listview.win-selectionstylefilled .win-selected a, +.win-listview.win-selectionstylefilled .win-selected progress, +.win-listview.win-selectionstylefilled .win-selected .win-rating .win-star.win-full { + color: rgb(255, 255, 255); +} +.win-listview.win-selectionstylefilled .win-selected a:hover:active { + color: rgba(255, 255, 255, 0.6); +} +.win-listview.win-selectionstylefilled .win-selected a:hover { + color: rgba(255, 255, 255, 0.8); +} +.win-listview.win-selectionstylefilled .win-selected button, +.win-listview.win-selectionstylefilled .win-selected input[type=button], +.win-listview.win-selectionstylefilled .win-selected input[type=reset], +.win-listview.win-selectionstylefilled .win-selected input[type=text], +.win-listview.win-selectionstylefilled .win-selected input[type=password], +.win-listview.win-selectionstylefilled .win-selected input[type=email], +.win-listview.win-selectionstylefilled .win-selected input[type=number], +.win-listview.win-selectionstylefilled .win-selected input[type=tel], +.win-listview.win-selectionstylefilled .win-selected input[type=url], +.win-listview.win-selectionstylefilled .win-selected input[type=search], +.win-listview.win-selectionstylefilled .win-selected input::-ms-check, +.win-listview.win-selectionstylefilled .win-selected textarea, +.win-listview.win-selectionstylefilled .win-selected .win-textarea, +.win-listview.win-selectionstylefilled .win-selected select { + background-clip: border-box; + background-color: rgba(255, 255, 255, 0.8); + border-color: transparent; + color: rgb(0, 0, 0); +} +.win-listview.win-selectionstylefilled .win-selected button[type=submit], +.win-listview.win-selectionstylefilled .win-selected input[type=submit] { + border-color: rgb(255, 255, 255); +} +.win-listview.win-selectionstylefilled .win-selected input[type=range]::-ms-fill-lower { + background-color: rgb(255, 255, 255); +} +.win-listview.win-selectionstylefilled .win-selected input[type=range]::-ms-thumb { + background-color: rgb(0,0,0); +} +.win-listview.win-selectionstylefilled .win-selected input[type=range]::-ms-fill-upper, +.win-listview.win-selectionstylefilled .win-selected progress { + background-color: rgba(255, 255, 255, 0.16); +} +.win-listview.win-selectionstylefilled .win-selected progress:indeterminate { + background-color: transparent; +} +.win-listview.win-selectionstylefilled .win-selected .win-rating .win-star.win-empty { + color: rgba(255, 255, 255, 0.16); +} + +/* +Back button control colors. +*/ +.win-ui-dark .win-backbutton { + background-color: transparent; + border-color: rgb(255, 255, 255); + color: rgb(255, 255, 255); +} +.win-ui-dark .win-backbutton:hover { + background-color: rgba(255, 255, 255, 0.13); + border-color: rgb(255, 255, 255); +} +.win-ui-dark .win-backbutton:hover:active { + background-color: rgb(255, 255, 255); + border-color: rgb(255, 255, 255); + color: rgb(0, 0, 0); +} +.win-ui-dark .win-backbutton:-ms-keyboard-active { + background-color: rgb(255, 255, 255); + border-color: rgb(255, 255, 255); + color: rgb(0, 0, 0); +} +.win-ui-dark .win-backbutton:disabled, .win-ui-dark .win-backbutton:disabled:active { + background-clip: padding-box; + background-color: transparent; + border-color: rgba(255, 255, 255, 0.4); + color: rgba(255, 255, 255, 0.4); +} +.win-backbutton { + background-color: transparent; + border-color: rgb(0, 0, 0); + color: rgb(0, 0, 0); +} +.win-backbutton:hover { + background-color: rgba(0, 0, 0, 0.13); + border-color: rgb(0, 0, 0); +} +.win-backbutton:hover:active { + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); + color: rgb(255, 255, 255); +} +.win-backbutton:-ms-keyboard-active { + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); + color: rgb(255, 255, 255); +} +.win-backbutton:disabled, .win-backbutton:disabled:active { + background-clip: padding-box; + background-color: transparent; + border-color: rgba(0, 0, 0, 0.4); + color: rgba(0, 0, 0, 0.4); +} + +/* +Command button colors. +*/ +button.win-command { + background-color: transparent; + border-color: transparent; +} +button.win-command:hover { + background-color: transparent; + border-color: transparent; +} +button.win-command:active { + background-color: transparent; + color: inherit; +} +button.win-command:hover:active { + background-color: transparent; + color: inherit; +} +button.win-command:-ms-keyboard-active { + background-color: transparent; + color: inherit; +} +button.win-command:disabled { + border-color: transparent; + background-color: transparent; +} +.win-ui-dark button.win-command:focus { + border-color: rgb(255, 255, 255); +} +button.win-command:focus { + border-color: rgb(0, 0, 0); +} +.win-hidefocus:focus { + outline: none; +} +/* + win-hide-focus needs the qualifier for win-ui-light or dark, + otherwise .win-ui-dark button.win-command:focus is more + qualified and stomps on the transparent that doesn't have .win-ui-dark +*/ +.win-ui-dark .win-command.win-hidefocus:focus, +.win-command.win-hidefocus:focus { + border-color: transparent; +} +.win-ui-dark .win-commandimage { + color: rgb(255, 255, 255); +} +.win-ui-dark button:hover:active .win-commandimage { + color: rgb(0, 0, 0); +} +.win-ui-dark button:-ms-keyboard-active .win-commandimage { + color: rgb(0, 0, 0); +} +.win-ui-dark button:disabled .win-commandimage, +.win-ui-dark button:disabled:active .win-commandimage { + color: rgba(255, 255, 255, 0.4); +} +.win-ui-dark button[aria-checked=true] .win-commandimage, +.win-ui-dark button[aria-checked=true]:active .win-commandimage { + color: rgb(0, 0, 0); +} + +.win-ui-dark button[aria-checked=true]:hover .win-commandimage { + color: rgb(0, 0, 0); +} + +.win-ui-dark button[aria-checked=true]:hover:active .win-commandimage { + color: rgb(255, 255, 255); +} +.win-ui-dark button[aria-checked=true]:-ms-keyboard-active .win-commandimage { + color: rgb(255, 255, 255); +} +.win-ui-dark button[aria-checked=true]:disabled .win-commandimage, +.win-ui-dark button[aria-checked=true]:disabled:active .win-commandimage { + color: rgb(0, 0, 0); +} +.win-commandimage { + color: rgb(0, 0, 0); +} +button:hover:active .win-commandimage { + color: rgb(255, 255, 255); +} +button:-ms-keyboard-active .win-commandimage { + color: rgb(255, 255, 255); +} +button:disabled .win-commandimage, +button:disabled:active .win-commandimage { + color: rgba(0, 0, 0, 0.4); +} +button[aria-checked=true] .win-commandimage, +button[aria-checked=true]:active .win-commandimage { + color: rgb(255, 255, 255); +} +button[aria-checked=true]:hover .win-commandimage { + color: rgb(255, 255, 255); +} +button[aria-checked=true]:hover:active .win-commandimage { + color: rgb(0, 0, 0); +} +button[aria-checked=true]:-ms-keyboard-active .win-commandimage { + color: rgb(0, 0, 0); +} +button[aria-checked=true]:disabled .win-commandimage, +button[aria-checked=true]:disabled:active .win-commandimage { + color: rgb(255, 255, 255); +} + +/* +Command ring colors. +*/ +.win-ui-dark .win-commandring, .win-ui-dark button:active .win-commandring { + background-color: transparent; + border-color: rgb(255, 255, 255); +} +.win-ui-dark button:hover .win-commandring { + background-color: rgba(255, 255, 255, 0.13); + border-color: rgb(255, 255, 255); +} +.win-ui-dark button:hover:active .win-commandring { + background-color: rgb(255, 255, 255); + border-color: rgb(255, 255, 255); +} +.win-ui-dark button:-ms-keyboard-active .win-commandring { + background-color: rgb(255, 255, 255); + border-color: rgb(255, 255, 255); +} +.win-ui-dark button:disabled .win-commandring, +.win-ui-dark button:disabled:active .win-commandring { + background-color: transparent; + border-color: rgba(255, 255, 255, 0.4); +} +.win-ui-dark button[aria-checked=true] .win-commandring, +.win-ui-dark button[aria-checked=true]:active .win-commandring { + background-color: rgb(255, 255, 255); + border-color: rgb(255, 255, 255); +} +.win-ui-dark button[aria-checked=true]:hover .win-commandring { + background-color: rgb(222, 222, 222); + border-color: rgb(222, 222, 222); +} +.win-ui-dark button[aria-checked=true]:hover:active .win-commandring { + background-color: transparent; + border-color: rgb(255, 255, 255); +} +.win-ui-dark button[aria-checked=true]:-ms-keyboard-active .win-commandring { + background-color: transparent; + border-color: rgb(255, 255, 255); +} +.win-ui-dark button[aria-checked=true]:disabled .win-commandring, +.win-ui-dark button[aria-checked=true]:disabled:active .win-commandring { + background-color: rgba(255, 255, 255, 0.4); + border-color: rgba(255, 255, 255, 0.4); +} +.win-commandring, button:active .win-commandring { + background-color: transparent; + border-color: rgb(0, 0, 0); +} +button:hover .win-commandring { + background-color: rgba(0, 0, 0, 0.13); + border-color: rgb(0, 0, 0); +} +button:hover:active .win-commandring { + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); +} +button:-ms-keyboard-active .win-commandring { + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); +} +button:disabled .win-commandring, +button:disabled:active .win-commandring { + background-color: transparent; + border-color: rgba(0, 0, 0, 0.4); +} +button[aria-checked=true] .win-commandring, +button[aria-checked=true]:active .win-commandring { + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); +} +button[aria-checked=true]:hover .win-commandring { + background-color: rgb(33, 33, 33); + border-color: rgb(33, 33, 33); +} +button[aria-checked=true]:hover:active .win-commandring { + background-color: transparent; + border-color: rgb(0, 0, 0); +} +button[aria-checked=true]:-ms-keyboard-active .win-commandring { + background-color: transparent; + border-color: rgb(0, 0, 0); +} +button[aria-checked=true]:disabled .win-commandring, +button[aria-checked=true]:disabled:active .win-commandring { + background-color: rgba(0, 0, 0, 0.4); + border-color: rgba(0, 0, 0, 0.4); +} + +/* +Command button labels colors. +*/ +.win-ui-dark button.win-command:disabled .win-label, +.win-ui-dark button.win-command:disabled:active .win-label { + color: rgba(255, 255, 255, 0.4); +} +button.win-command:disabled .win-label, +button.win-command:disabled:active .win-label { + color: rgba(0, 0, 0, 0.4); +} + +/* +AppBarCommand (and MenuCommand) separator. +*/ +hr.win-command { + /* Same color in both light and dark. */ + background-color: rgb(123, 123, 123); +} + +/* +AppBar control colors. +*/ +.win-ui-dark.win-appbar, .win-ui-dark .win-appbar { + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); +} +.win-appbar { + background-color: rgb(255, 255, 255); + border-color: rgb(255, 255, 255); +} + +/* +Flyout control colors, flyout normally has light theme. +*/ +.win-flyout { + border-color: rgb(0, 0, 0); + background-color: rgb(255, 255, 255); +} + +.win-settingsflyout { + border-color: rgba(0, 0, 0, 0.24); +} + +.win-ui-dark.win-flyout, .win-ui-dark .win-flyout { + border-color: rgb(255, 255, 255); + background-color: rgb(0, 0, 0); +} + +.win-ui-dark.win-settingsflyout, .win-ui-dark .win-settingsflyout { + border-color: rgba(255, 255, 255, 0.24); +} + +/* +Settings flyout is light in both themes unless the app explicitly overrides it. +*/ +.win-settingsflyout { + background-color: rgb(255, 255, 255); +} +.win-settingsflyout.win-ui-dark { + background-color: rgb(0, 0, 0); +} + +/* +Settings flyout will be white in dark theme so we need to have the header stay +aligned with the theme unless the app explicitly overrides it. +*/ + +/* +Menu button colors, menu is always light theme. +*/ +.win-menu button { + background-color: transparent; + color: rgb(0, 0, 0); +} +.win-menu button:focus, +.win-menu button:active { + background-color: rgb(222, 222, 222); +} +.win-menu button:hover:active { + color: rgb(255, 255, 255); + background-color: rgb(0, 0, 0); +} +.win-menu button:-ms-keyboard-active { + color: rgb(255, 255, 255); + background-color: rgb(0, 0, 0); +} +.win-menu button:disabled, +.win-menu button:disabled:active { + background-color: transparent; + color: rgba(0, 0, 0, 0.1); +} +.win-ui-dark.win-menu button, .win-ui-dark .win-menu button { + background-color: transparent; + color: rgb(255, 255, 255); +} +.win-ui-dark.win-menu button:focus, .win-ui-dark .win-menu button:focus, +.win-ui-dark.win-menu button:active, .win-ui-dark .win-menu button:active { + background-color: rgb(222, 222, 222); +} +.win-ui-dark.win-menu button:hover:active, .win-ui-dark .win-menu button:hover:active { + color: rgb(0, 0, 0); + background-color: rgb(255, 255, 255); +} +.win-ui-dark.win-menu button:-ms-keyboard-active, .win-ui-dark .win-menu button:-ms-keyboard-active { + color: rgb(0, 0, 0); + background-color: rgb(255, 255, 255); +} +.win-ui-dark.win-menu button:disabled, .win-ui-dark .win-menu button:disabled, +.win-ui-dark.win-menu button:disabled:active, .win-ui-dark .win-menu button:disabled:active { + background-color: transparent; + color: rgba(255, 255, 255, 0.1); +} + +/* +Rating control colors. +*/ +.win-ui-dark .win-rating .win-star.win-user.win-full, +.win-ui-dark .win-rating .win-star.win-user.win-full.win-disabled { + color: rgb(91, 46, 197); +} +.win-ui-dark .win-rating .win-star.win-tentative.win-full { + color: rgb(129, 82, 239); +} +.win-ui-dark .win-rating .win-star.win-average.win-full, +.win-ui-dark .win-rating .win-star.win-average.win-full.win-disabled { + color: rgb(255, 255, 255); +} +.win-ui-dark .win-rating .win-star.win-empty { + color: rgba(255, 255, 255, 0.35); +} +.win-rating .win-star.win-user.win-full, +.win-rating .win-star.win-user.win-full.win-disabled { + color: rgb(70, 23, 180); +} +.win-rating .win-star.win-tentative.win-full { + color: rgb(114, 65, 228); +} +.win-rating .win-star.win-average.win-full, +.win-rating .win-star.win-average.win-full.win-disabled { + color: rgb(0, 0, 0); +} +.win-rating .win-star.win-empty { + color: rgba(0, 0, 0, 0.35); +} + +/* +SemanticZoom button colors. +*/ +.win-semanticzoom-button { + background-color: rgba(216, 216, 216, 0.33); + border-color: transparent; +} +button:hover.win-semanticzoom-button { + background-color: rgba(216, 216, 216, 1.0); +} +.win-ui-dark button.win-semanticzoom-button:active { + background-color: rgb(255, 255, 255); +} +button.win-semanticzoom-button:active { + background-color: rgb(0, 0, 0); +} + +/* +Toggleswitch control colors. +*/ + +.win-ui-dark .win-toggleswitch .win-title { + color: rgb(255, 255, 255); +} +.win-ui-dark .win-toggleswitch .win-title.win-disabled { + color: rgba(255, 255, 255, 0.4); +} +.win-toggleswitch .win-title { + color: rgb(0, 0, 0); +} +.win-toggleswitch .win-title.win-disabled { + color: rgba(0, 0, 0, 0.4); +} + +.win-ui-dark .win-toggleswitch .win-label { + color: rgb(255, 255, 255); +} +.win-ui-dark .win-toggleswitch .win-label.win-disabled { + color: rgba(255, 255, 255, 0.4); +} +.win-toggleswitch .win-label { + color: rgb(0, 0, 0); +} +.win-toggleswitch .win-label.win-disabled { + color: rgba(0, 0, 0, 0.4); +} + +.win-toggleswitch .win-switch::-ms-track { + background-color: transparent; +} +.win-ui-dark .win-toggleswitch .win-switch::-ms-track { + border-color: rgba(255, 255, 255, 0.35); +} +.win-ui-dark .win-toggleswitch .win-switch::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.26); +} +.win-ui-dark .win-toggleswitch .win-switch:hover::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.29); +} +.win-ui-dark .win-toggleswitch .win-switch:active::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.35); +} +.win-ui-dark .win-toggleswitch .win-switch:disabled::-ms-track { + border-color: rgba(255, 255, 255, 0.2); +} +.win-ui-dark .win-toggleswitch .win-switch:disabled::-ms-fill-lower, +.win-ui-dark .win-toggleswitch .win-switch:disabled::-ms-fill-upper { + background-color: rgba(255, 255, 255, 0.12); +} +.win-toggleswitch .win-switch::-ms-track { + border-color: rgba(0, 0, 0, 0.35); +} +.win-toggleswitch .win-switch::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.26); +} +.win-toggleswitch .win-switch:hover::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.29); +} +.win-toggleswitch .win-switch:active::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.35); +} +.win-toggleswitch .win-switch:disabled::-ms-track { + border-color: rgba(0, 0, 0, 0.2); +} +.win-toggleswitch .win-switch:disabled::-ms-fill-lower, +.win-toggleswitch .win-switch:disabled::-ms-fill-upper { + background-color: rgba(0, 0, 0, 0.12); +} + +/* +Tooltip control colors. +*/ +.win-tooltip { + background-color: rgb(255, 255, 255); + border-color: rgb(128, 128, 128); + color: rgba(0, 0, 0, 0.6); +} + +/* +High contrast colors. +*/ +@media (-ms-high-contrast) +{ + /* + Typographic color definitions. + */ + .win-type-interactive { + color: WindowText; + } + .win-type-interactive:hover, .win-type-interactive:hover:active { + color: -ms-hotlight; + } + .win-type-interactive:-ms-keyboard-active { + color: -ms-hotlight; + } + + /* + Text selection high contrast color. + */ + ::selection, select:focus::-ms-value { + background-color: Highlight; + color: HighlightText; + } + + /* + Button high contrast colors. + */ + button, input[type=button], input[type=reset], + input[type=file]::-ms-browse, input::-ms-clear, + input::-ms-reveal, select::-ms-expand, .win-backbutton, .win-semanticzoom-button { + background-color: ButtonFace; + border-color: ButtonText; + color: ButtonText; + } + button[type=submit], input[type=submit] { + background-color: Highlight; + border-color: ButtonText; + color: HighlightText; + } + button:hover, input[type=button]:hover, input[type=reset]:hover, + input::-ms-clear:hover, input::-ms-reveal:hover, + input[type=file]::-ms-browse:hover, .win-backbutton:hover, .win-semanticzoom-button:hover { + background-color: Highlight; + border-color: ButtonText; + color: HighlightText; + } + button:hover:active, button[type=submit]:hover:active, input[type=button]:hover:active, + input[type=submit]:hover:active, input[type=reset]:hover:active, + input::-ms-clear:hover:active, input::-ms-reveal:hover:active, + input[type=file]::-ms-browse:hover:active, .win-backbutton:hover:active, + .win-semanticzoom-button:hover:active { + background-clip: border-box; + background-color: ButtonText; + border-color: transparent; + color: ButtonFace; + } + button:-ms-keyboard-active, button[type=submit]:-ms-keyboard-active, input[type=button]:-ms-keyboard-active, + input[type=submit]:-ms-keyboard-active, input[type=reset]:-ms-keyboard-active, + .win-backbutton:-ms-keyboard-active, .win-semanticzoom-button:-ms-keyboard-active { + background-clip: border-box; + background-color: ButtonText; + border-color: transparent; + color: ButtonFace; + } + button:disabled, button[type=submit]:disabled, input[type=button]:disabled, + input[type=submit]:disabled, input[type=reset]:disabled, + input[type=file]:disabled::-ms-browse, input:disabled::-ms-clear, input:disabled::-ms-reveal, + select:disabled::-ms-expand, .win-backbutton:disabled, + button:disabled:active, button[type=submit]:disabled:active, input[type=button]:disabled:active, + input[type=submit]:disabled:active, input[type=reset]:disabled:active, + input[type=file]:disabled::-ms-browse:active, .win-backbutton:disabled:active { + background-color: ButtonFace; + border-color: GrayText; + color: GrayText; + } + + /* + Text input, checkbox, radio, and select high contrast colors. + */ + input[type=text], input[type=password], + input[type=email], input[type=number], + input[type=tel], input[type=url], + input[type=search], textarea, .win-textarea, select, + input::-ms-check { + background-color: ButtonFace; + border-color: ButtonText; + color: ButtonText; + } + input:hover::-ms-check { /* only checkbox and radio have hover state */ + background-color: Highlight; + color: HighlightText; + } + input:hover:active::-ms-check { /* only checkbox and radio have active state */ + background-clip: border-box; + background-color: ButtonText; + border-color: transparent; + color: ButtonFace; + } + input:-ms-keyboard-active::-ms-check { /* only checkbox and radio have active state */ + background-clip: border-box; + background-color: ButtonText; + border-color: transparent; + color: ButtonFace; + } + input[type=text]:disabled, input[type=password]:disabled, + input[type=email]:disabled, input[type=number]:disabled, + input[type=tel]:disabled, input[type=url]:disabled, + input[type=search]:disabled, input[type=file]::-ms-value, + textarea:disabled, .win-textarea:disabled, select:disabled, + input:disabled::-ms-check, input:disabled:active::-ms-check { + border-color: GrayText; + color: ButtonText; + background-color: ButtonFace; + } + input:disabled::-ms-check, input:disabled:active::-ms-check, + input[type]:-ms-input-placeholder, textarea:-ms-input-placeholder, + select:disabled::-ms-value { + color: GrayText; + } + + /* + Select option high contrast colors. + */ + option:checked, select:focus::-ms-value { + color: HighlightText; + background-color: Highlight; + } + select:active:not(:disabled)::-ms-value, select:active:not(:disabled)::-ms-expand, + option:hover:active, option:checked:hover:active { + color: ButtonFace; + background-color: ButtonText; + } + option:checked:disabled, option:checked:disabled:active, + optgroup:disabled option:checked, optgroup:disabled option:checked:active, + select:disabled option:checked, select:disabled option:checked:active, + select:disabled:focus::-ms-value { + color: ButtonFace; + background-color: GrayText; + } + + /* + Link high contrast colors. + */ + a { + color: -ms-hotlight; + } + a[disabled] { + color: GrayText; + } + + /* + Progress high contrast colors. + */ + progress { + border: 1px solid ButtonText; + background-color: ButtonFace; + color: Highlight; + } + progress:indeterminate { + border: transparent; + } + progress::-ms-fill { + border-right: 1px solid ButtonText; + background-color: Highlight; + } + progress.win-paused:not(:indeterminate)::-ms-fill { + background-color: GrayText; + } + progress.win-paused:not(:indeterminate) { + opacity: 1.0; + } + + /* + Range control high contrast colors. + */ + input[type=range]::-ms-track { + border: 1px solid ButtonText; + background-color: ButtonFace; + color: transparent; + } + input[type=range]:disabled::-ms-track { + border-color: GrayText; + } + input[type=range]::-ms-fill-lower { + background-color: Highlight; + } + input[type=range]:disabled::-ms-fill-lower { + background-color: transparent; + } + input[type=range]::-ms-ticks-before, + input[type=range]::-ms-ticks-after { + color: ButtonText; + } + input[type=range]:disabled::-ms-ticks-before, + input[type=range]:disabled::-ms-ticks-after { + color: GrayText; + } + input[type=range]::-ms-thumb { + background-color: ButtonText; + border-left: 1px solid HighlightText; + border-right: 1px solid HighlightText; + } + input[type=range].win-vertical::-ms-thumb { + border-left-style: none; + border-right-style: none; + border-top: 1px solid HighlightText; + border-bottom: 1px solid HighlightText; + } + input[type=range]:hover::-ms-thumb { + background-color: Highlight; + } + input[type=range]:active::-ms-thumb { + background-color: HighlightText; + border-color: ButtonText; + } + input[type=range]:disabled::-ms-thumb, + input[type=range]:disabled:active::-ms-thumb { + background-color: GrayText; + border-color: GrayText; + } + + /* + AppBar high contrast colors. + */ + .win-appbar { + background-color: ButtonFace; + border-color: Highlight; + } + + /* + Command button label high contrast colors. + */ + button.win-command, + button.win-command:active, + button.win-command:hover:active { + background-color: transparent; + border-color: transparent; + } + hr.win-command { + background-color: ButtonText; + } + .win-label { + background-color: transparent; + color: ButtonText; + } + button:disabled .win-label, + button:disabled:active .win-label { + color: GrayText; + } + button.win-command:focus { + border-color: ButtonText; + } + button.win-hidefocus:focus { + outline: none; + } + button.win-command.win-hidefocus:focus { + border-color: transparent; + } + + /* + Command Image high contrast colors with states + */ + .win-commandimage { + background-color: transparent; + color: ButtonText; + } + button:active .win-commandimage, + button:hover:active .win-commandimage { + color: ButtonFace; + } + button:hover .win-commandimage { + color: HighlightText; + } + button:disabled .win-commandimage, + button:disabled:active .win-commandimage { + color: GrayText; + } + button[aria-checked=true] .win-commandimage { + color: ButtonFace; + } + button[aria-checked=true]:active .win-commandimage, + button[aria-checked=true]:hover:active .win-commandimage { + color: ButtonText; + } + button[aria-checked=true]:hover .win-commandimage { + color: HighlightText; + } + button[aria-checked=true]:disabled .win-commandimage, + button[aria-checked=true]:disabled:active .win-commandimage { + color: ButtonFace; + } + + /* + Command Image high contrast colors with states + */ + .win-commandring { + background-color: transparent; + border-color: ButtonText; + } + button:active .win-commandring, + button:hover:active .win-commandring { + background-color: ButtonText; + border-color: ButtonText; + } + button:hover .win-commandring { + background-color: Highlight; + border-color: ButtonText; + } + button:disabled .win-commandring, + button:disabled:active .win-commandring { + background-color: transparent; + border-color: GrayText; + } + button[aria-checked=true] .win-commandring { + background-color: ButtonText; + border-color: ButtonText; + } + button[aria-checked=true]:active .win-commandring, + button[aria-checked=true]:hover:active .win-commandring { + background-color: ButtonFace; + border-color: ButtonText; + } + button[aria-checked=true]:hover .win-commandring { + background-color: Highlight; + border-color: ButtonText; + } + button[aria-checked=true]:disabled .win-commandring, + button[aria-checked=true]:disabled:active .win-commandring { + background-color: GrayText; + border-color: GrayText; + } + + /* + Menu command high contrast colors + */ + .win-menu hr.win-command { + background-color: ButtonText; + } + .win-menu button.win-command { + background-color: ButtonFace; + color: ButtonText; + } + .win-menu button.win-command:focus { + outline: none; + background-color: Highlight; + color: HighlightText; + } + .win-menu button.win-command:active { + background-color: Highlight; + color: HighlightText; + } + .win-menu button.win-command:hover:active { + background-color: ButtonText; + color: ButtonFace; + } + .win-menu button.win-command:-ms-keyboard-active { + background-color: ButtonText; + color: ButtonFace; + } + .win-menu button.win-command:disabled, + .win-menu button.win-command:disabled:active { + background-color: ButtonFace; + color: GrayText; + } + + /* + FlipView high contrast colors. + */ + .win-flipview .win-navbottom { + left: 50%; + top: 100%; + margin-left: -35px; + margin-top: -35px; + } + .win-flipview .win-navbutton { + background-color: ButtonFace; + color: ButtonText; + border: 2px solid ButtonText; + width: 65px; + height: 35px; + } + .win-flipview .win-navbutton:hover { + background-color: Highlight; + color: HighlightText; + } + .win-flipview .win-navbutton:active { + background-color: ButtonText; + color: ButtonFace; + } + .win-flipview .win-navright { + margin-left: -65px; + } + + /* + Flyout control high contrast colors. + */ + .win-flyout, .win-settingsflyout { + background-color: Window; + border-color: WindowText; + color: WindowText; + } + + /* + ListView high contrast colors. + */ + .win-listview.win-selectionstylefilled .win-container { + background-color: Window; + color: WindowText; + } + .win-listview .win-container:hover { + outline: Highlight solid 3px; + } + .win-listview.win-selectionstylefilled .win-container:hover { + background-color: Highlight; + color: HighlightText; + } + .win-listview .win-focusedoutline { + outline-color: WindowText; + } + .win-listview.win-selectionstylefilled .win-container.win-selected, + .win-listview.win-selectionstylefilled .win-container.win-selected:hover { + background-color: Highlight; + color: HighlightText; + } + .win-listview .win-groupheader { + color: WindowText; + } + .win-listview:not(.win-selectionstylefilled) .win-container.win-selected .win-selectionborder { + border-color: Highlight; + } + .win-listview.win-selectionstylefilled .win-container.win-selected .win-selectionborder { + border-color: transparent; + } + .win-listview:not(.win-selectionstylefilled) .win-container.win-selected:hover .win-selectionborder { + border-color: Highlight; + } + .win-listview.win-selectionstylefilled .win-selected .win-selectionbackground, + .win-listview.win-selectionstylefilled .win-selected:hover .win-selectionbackground + { + background-color: Highlight; + color: HighlightText; + } + .win-selectioncheckmark { + color: HighlightText; + } + .win-listview.win-selectionstylefilled .win-selectioncheckmarkbackground { + border-color: transparent; + } + .win-listview:not(.win-selectionstylefilled) .win-selectioncheckmarkbackground { + border-top-color: Highlight; + border-right-color: Highlight; + border-left-color: transparent; + border-bottom-color: transparent; + } + .win-listview:not(.win-selectionstylefilled) .win-selectioncheckmarkbackground:hover { + border-top-color: Highlight; + border-right-color: Highlight; + } + .win-listview.win-rtl:not(.win-selectionstylefilled) .win-selectioncheckmarkbackground { + border-top-color: Highlight; + border-left-color: Highlight; + border-right-color: transparent; + border-bottom-color: transparent; + } + .win-listview .win-selectionhint { + color: WindowText; + } + .win-listview.win-selectionstylefilled .win-selected a, + .win-listview.win-selectionstylefilled .win-container:hover a, + .win-listview.win-selectionstylefilled .win-selected progress, + .win-listview.win-selectionstylefilled .win-container:hover progress, + .win-listview.win-selectionstylefilled .win-selected .win-rating .win-star:after, + .win-listview.win-selectionstylefilled .win-container:hover .win-rating .win-star:after{ + color: HighlightText; + } + .win-listview.win-selectionstylefilled .win-selected input, + .win-listview.win-selectionstylefilled .win-container:hover input, + .win-listview.win-selectionstylefilled .win-selected input::-ms-check, + .win-listview.win-selectionstylefilled .win-container:hover input::-ms-check, + .win-listview.win-selectionstylefilled .win-selected input::-ms-value, + .win-listview.win-selectionstylefilled .win-container:hover input::-ms-value, + .win-listview.win-selectionstylefilled .win-selected input::-ms-track, + .win-listview.win-selectionstylefilled .win-container:hover input::-ms-track, + .win-listview.win-selectionstylefilled .win-selected button, + .win-listview.win-selectionstylefilled .win-container:hover button, + .win-listview.win-selectionstylefilled .win-selected progress, + .win-listview.win-selectionstylefilled .win-container:hover progress, + .win-listview.win-selectionstylefilled .win-selected progress::-ms-fill, + .win-listview.win-selectionstylefilled .win-container:hover progress::-ms-fill, + .win-listview.win-selectionstylefilled .win-selected select, + .win-listview.win-selectionstylefilled .win-container:hover select, + .win-listview.win-selectionstylefilled .win-selected textarea, + .win-listview.win-selectionstylefilled .win-container:hover textarea { + border-color: HighlightText; + } + .win-listview.win-selectionstylefilled .win-selected input[type=range]::-ms-fill-lower, + .win-listview.win-selectionstylefilled .win-container:hover input[type=range]::-ms-fill-lower, + .win-listview.win-selectionstylefilled .win-selected progress::-ms-fill, + .win-listview.win-selectionstylefilled .win-container:hover progress::-ms-fill { + background-color: HighlightText; + } + .win-listview.win-selectionstylefilled .win-selected input[type=range]::-ms-fill-upper, + .win-listview.win-selectionstylefilled .win-container:hover input[type=range]::-ms-fill-upper, + .win-listview.win-selectionstylefilled .win-selected progress, + .win-listview.win-selectionstylefilled .win-container:hover progress { + background-color: Highlight; + } + .win-listview.win-selectionstylefilled .win-selected .win-rating .win-star.win-full:before, + .win-listview.win-selectionstylefilled .win-container:hover .win-rating .win-star.win-full:before { + color: ButtonFace; + } + .win-listview.win-selectionstylefilled .win-selected .win-rating .win-star.win-empty:before, + .win-listview.win-selectionstylefilled .win-container:hover .win-rating .win-star.win-empty:before { + color: HighLight; + } + + /* + Rating control high contrast colors. + */ + .win-rating .win-star:before { + content: "\E082" !important; + } + .win-rating .win-star.win-full { + color: HighLight; + } + .win-rating .win-star.win-tentative.win-full { + color: ButtonText; + } + .win-rating .win-star.win-empty { + color: ButtonFace; + } + .win-rating .win-star:after { + content: "\E224" !important; + position: relative; + top: -100%; + color: ButtonText; + } + + /* + Toggle control high contrast colors. + */ + .win-toggleswitch .win-switch::-ms-thumb { + height: 17px; /* 19px - 1px borders */ + } + .win-toggleswitch .win-switch:disabled::-ms-fill-lower { + background-color: GrayText; + } + + /* + Tooltip high contrast colors. + */ + .win-tooltip { + background-color: Window; + border-color: WindowText; + color: WindowText; + } + } + +@keyframes WinJS-opacity-in { from { opacity: 0; } to { opacity: 1; } } +@keyframes WinJS-opacity-out { from { opacity: 1; } to { opacity: 0; } } + +@keyframes WinJS-scale-up { from { transform: scale(0.85); } to { transform: scale(1); } } +@keyframes WinJS-scale-down { from { transform: scale(1); } to { transform: scale(0.85); } } + +@keyframes WinJS-default-remove { from { transform: translateX( 11px); } to { transform: none; } } +@keyframes WinJS-default-remove-rtl { from { transform: translateX(-11px); } to { transform: none; } } + +@keyframes WinJS-default-apply { from { transform: none; } to { transform: translateX( 11px); } } +@keyframes WinJS-default-apply-rtl { from { transform: none; } to { transform: translateX(-11px); } } + +@keyframes WinJS-showEdgeUI { from { transform: translateY(-70px); } to { transform: none; } } +@keyframes WinJS-showPanel { from { transform: translateX( 364px); } to { transform: none; } } +@keyframes WinJS-showPanel-rtl { from { transform: translateX(-364px); } to { transform: none; } } +@keyframes WinJS-hideEdgeUI { from { transform: none; } to { transform: translateY(-70px); } } +@keyframes WinJS-hidePanel { from { transform: none; } to { transform: translateX( 364px); } } +@keyframes WinJS-hidePanel-rtl { from { transform: none; } to { transform: translateX(-364px); } } + +@keyframes WinJS-showPopup { from { transform: translateY(50px); } to { transform: none; } } + +@keyframes WinJS-dragSourceEnd { from { transform: translateX( 11px) scale(1.05); } to { transform: none; } } +@keyframes WinJS-dragSourceEnd-rtl { from { transform: translateX(-11px) scale(1.05); } to { transform: none; } } + +@keyframes WinJS-enterContent { from { transform: translateX( 40px); } to { transform: none; } } +@keyframes WinJS-enterContent-rtl { from { transform: translateX(-40px); } to { transform: none; } } +@keyframes WinJS-enterContent-snapped { from { transform: translateX( 20px); } to { transform: none; } } +@keyframes WinJS-enterContent-snapped-rtl { from { transform: translateX(-20px); } to { transform: none; } } + +@keyframes WinJS-exit { from { transform: none; } to { transform: none; } } + +@keyframes WinJS-enterPage { from { transform: translateX( 100px); } to { transform: none; } } +@keyframes WinJS-enterPage-rtl { from { transform: translateX(-100px); } to { transform: none; } } +@keyframes WinJS-enterPage-snapped { from { transform: translateX( 40px); } to { transform: none; } } +@keyframes WinJS-enterPage-snapped-rtl { from { transform: translateX(-40px); } to { transform: none; } } + +@keyframes WinJS-updateBadge { from { transform: translateY(24px); } to { transform: none; } } + +body, +.win-type-xx-large, +.win-type-x-large, +.win-type-large, +.win-type-medium, +.win-type-small, +.win-type-x-small, +.win-type-xx-small, +input, +textarea, +.win-textarea, +button, +select, +option { + font-family: + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:-ms-lang(am, ti), +.win-type-xx-large:-ms-lang(am, ti), +.win-type-x-large:-ms-lang(am, ti), +.win-type-large:-ms-lang(am, ti), +.win-type-medium:-ms-lang(am, ti), +.win-type-small:-ms-lang(am, ti), +.win-type-x-small:-ms-lang(am, ti), +.win-type-xx-small:-ms-lang(am, ti), +input:-ms-lang(am, ti), +textarea:-ms-lang(am, ti), +.win-textarea:-ms-lang(am, ti), +button:-ms-lang(am, ti), +select:-ms-lang(am, ti), +option:-ms-lang(am, ti) { + font-family: + "Ebrima", + "Segoe UI", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-xx-large:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-x-large:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-large:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-medium:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-small:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-x-small:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-type-xx-small:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +input:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +textarea:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +.win-textarea:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +button:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +select:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te), +option:-ms-lang(as, bn, gu, hi, kn, kok, ml, mr, ne, or, pa, si, ta, te) { + font-family: + "Nirmala UI", + "Segoe UI", + "Ebrima", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:lang(chr-CHER-US), +.win-type-xx-large:lang(chr-CHER-US), +.win-type-x-large:lang(chr-CHER-US), +.win-type-large:lang(chr-CHER-US), +.win-type-medium:lang(chr-CHER-US), +.win-type-small:lang(chr-CHER-US), +.win-type-x-small:lang(chr-CHER-US), +.win-type-xx-small:lang(chr-CHER-US), +input:lang(chr-CHER-US), +textarea:lang(chr-CHER-US), +.win-textarea:lang(chr-CHER-US), +button:lang(chr-CHER-US), +select:lang(chr-CHER-US), +option:lang(chr-CHER-US) { + font-family: + "Gadugi", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:lang(ja), +.win-type-xx-large:lang(ja), +.win-type-x-large:lang(ja), +.win-type-large:lang(ja), +.win-type-medium:lang(ja), +.win-type-small:lang(ja), +.win-type-x-small:lang(ja), +.win-type-xx-small:lang(ja), +input:lang(ja), +textarea:lang(ja), +.win-textarea:lang(ja), +button:lang(ja), +select:lang(ja), +option:lang(ja) { + font-family: + "Meiryo UI", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:lang(km), +.win-type-xx-large:lang(km), +.win-type-x-large:lang(km), +.win-type-large:lang(km), +.win-type-medium:lang(km), +.win-type-small:lang(km), +.win-type-x-small:lang(km), +.win-type-xx-small:lang(km), +input:lang(km), +textarea:lang(km), +.win-textarea:lang(km), +button:lang(km), +select:lang(km), +option:lang(km) { + font-family: + "Khmer UI", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:lang(ko), +.win-type-xx-large:lang(ko), +.win-type-x-large:lang(ko), +.win-type-large:lang(ko), +.win-type-medium:lang(ko), +.win-type-small:lang(ko), +.win-type-x-small:lang(ko), +.win-type-xx-small:lang(ko), +input:lang(ko), +textarea:lang(ko), +.win-textarea:lang(ko), +button:lang(ko), +select:lang(ko), +option:lang(ko) { + font-family: + "Malgun Gothic", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:lang(lo), +.win-type-xx-large:lang(lo), +.win-type-x-large:lang(lo), +.win-type-large:lang(lo), +.win-type-medium:lang(lo), +.win-type-small:lang(lo), +.win-type-x-small:lang(lo), +.win-type-xx-small:lang(lo), +input:lang(lo), +textarea:lang(lo), +.win-textarea:lang(lo), +button:lang(lo), +select:lang(lo), +option:lang(lo) { + font-family: + "Lao UI", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:lang(th), +.win-type-xx-large:lang(th), +.win-type-x-large:lang(th), +.win-type-large:lang(th), +.win-type-medium:lang(th), +.win-type-small:lang(th), +.win-type-x-small:lang(th), +.win-type-xx-small:lang(th), +input:lang(th), +textarea:lang(th), +.win-textarea:lang(th), +button:lang(th), +select:lang(th), +option:lang(th) { + font-family: + "Leelawadee", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-xx-large:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-x-large:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-large:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-medium:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-small:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-x-small:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-type-xx-small:-ms-lang(zh-CN, zh-Hans, zh-SG), +input:-ms-lang(zh-CN, zh-Hans, zh-SG), +textarea:-ms-lang(zh-CN, zh-Hans, zh-SG), +.win-textarea:-ms-lang(zh-CN, zh-Hans, zh-SG), +button:-ms-lang(zh-CN, zh-Hans, zh-SG), +select:-ms-lang(zh-CN, zh-Hans, zh-SG), +option:-ms-lang(zh-CN, zh-Hans, zh-SG) { + font-family: + "Microsoft YaHei UI", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft JhengHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + +body:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-xx-large:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-x-large:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-large:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-medium:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-small:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-x-small:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-type-xx-small:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +input:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +textarea:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +.win-textarea:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +button:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +select:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO), +option:-ms-lang(zh-HK, zh-TW, zh-Hant, zh-MO) { + font-family: + "Microsoft JhengHei UI", + "Segoe UI", + "Ebrima", + "Nirmala UI", + "Gadugi", + "Segoe UI Symbol", + "Meiryo UI", + "Khmer UI", + "Tunga", + "Lao UI", + "Raavi", + "Iskoola Pota", + "Latha", + "Leelawadee", + "Microsoft YaHei UI", + "Malgun Gothic", + "Estrangelo Edessa", + "Microsoft Himalaya", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Tai Le", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "MV Boli", + "Myanmar Text", + "Cambria Math"; +} + diff --git a/shared/html/libs/winjs/1.0/js/base.js b/shared/html/libs/winjs/1.0/js/base.js new file mode 100644 index 0000000..07952e3 --- /dev/null +++ b/shared/html/libs/winjs/1.0/js/base.js @@ -0,0 +1,11946 @@ +/// +/*! + © Microsoft. All rights reserved. + + This library is supported for use in Windows Store apps only. + + Build: 1.0.9200.20602.win8_ldr.130108-1504 + + Version: Microsoft.WinJS.1.0 +*/ +/// +(function baseInit(global, undefined) { + "use strict"; + + function initializeProperties(target, members) { + var keys = Object.keys(members); + 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; + } + properties = properties || {}; + properties[key] = member; + continue; + } + } + if (!enumerable) { + properties = properties || {}; + properties[key] = { value: member, enumerable: enumerable, configurable: true, writable: true } + continue; + } + target[key] = member; + } + if (properties) { + Object.defineProperties(target, properties); + } + } + + (function (rootNamespace) { + + // Create the rootNamespace in the global namespace + if (!global[rootNamespace]) { + global[rootNamespace] = Object.create(Object.prototype); + } + + // Cache the rootNamespace we just created in a local variable + var _rootNamespace = global[rootNamespace]; + if (!_rootNamespace.Namespace) { + _rootNamespace.Namespace = Object.create(Object.prototype); + } + + 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 = parentNamespace, + namespaceFragments = name.split("."); + + 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]; + } + + if (members) { + initializeProperties(currentNamespace, members); + } + + 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); + } + + // 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 } + + }); + + })("WinJS"); + + (function (WinJS) { + + 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 () { }; + WinJS.Utilities.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); + WinJS.Utilities.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 + }); + + })(WinJS); + +})(this); + + +(function baseUtilsInit(global, WinJS) { + "use strict"; + + var hasWinRT = !!global.Windows; + + var strings = { + get notSupportedForProcessing() { return WinJS.Resources._getWinJSString("base/notSupportedForProcessing").value; } + }; + + 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); + } + + // Establish members of "WinJS.Utilities" namespace + WinJS.Namespace.define("WinJS.Utilities", { + // Used for mocking in tests + _setHasWinRT: { + value: function (value) { + hasWinRT = value; + }, + configurable: false, + writable: false, + enumerable: false + }, + + /// Determine if WinRT is accessible in this script context. + hasWinRT: { + get: function () { return hasWinRT; }, + configurable: false, + enumerable: true + }, + + _getMemberFiltered: getMemberFiltered, + + getMember: function (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); + }, + + ready: function (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 should be executed asynchronously. + /// + /// + return new WinJS.Promise(function (c, e) { + function complete() { + if (callback) { + try { + callback(); + c(); + } + catch (err) { + e(err); + } + } + else { + c(); + } + } + + var readyState = WinJS.Utilities.testReadyState; + if (!readyState) { + if (global.document) { + readyState = document.readyState; + } + else { + readyState = "complete"; + } + } + if (readyState === "complete" || (global.document && document.body !== null)) { + if (async) { + msSetImmediate(complete); + } + 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: function (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; + }, + 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 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 WinJS.ErrorFromName("WinJS.Utilities.requireSupportedForProcessing", WinJS.Resources._formatString(strings.notSupportedForProcessing, value)); + }, + configurable: false, + writable: false, + enumerable: true + }, + + }); + + WinJS.Namespace.define("WinJS", { + validation: false, + + strictProcessing: { + value: function () { + /// + /// + /// Strict processing is always enforced, this method has no effect. + /// + /// + }, + configurable: false, + writable: false, + enumerable: false + }, + }); +})(this, WinJS); + + +(function logInit() { + "use strict"; + + var spaceR = /\s+/g; + var typeR = /^(error|warn|info|log)$/; + + function format(message, tag, type) { + /// + /// + /// Adds tags and type to a logging message. + /// + /// The message to be formatted. + /// The tag(s) to be applied to the message. Multiple tags should be separated by 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 = WinJS.Utilities.formatLog(message, tag, type); + 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, "\\$&"); + } + WinJS.Namespace.define("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. Multiple tags should be separated by 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 && !WinJS.log) { + WinJS.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 = WinJS.log; + WinJS.log = result; + }, + stopLog: function () { + /// + /// + /// Removes the previously set up logger. + /// + /// + delete WinJS.log; + }, + formatLog: format + }); +})(); + +(function eventsInit(WinJS, undefined) { + "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(events) { + /// + /// + /// 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 = WinJS.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 gets 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; + } + } + } + } + }; + + WinJS.Namespace.define("WinJS.Utilities", { + _createEventProperty: createEventProperty, + createEventProperties: createEventProperties, + eventMixin: eventMixin + }); + +})(WinJS); + + +(function resourcesInit(global, WinJS, undefined) { + "use strict"; + + var resourceMap; + var mrtEventHook = false; + var contextChangedET = "contextchanged"; + + var ListenerType = WinJS.Class.mix(WinJS.Class.define(null, { /* empty */ }, { supportedForProcessing: false }), WinJS.Utilities.eventMixin); + var listeners = new ListenerType(); + + var strings = { + get malformedFormatStringInput() { return WinJS.Resources._getWinJSString("base/malformedFormatStringInput").value; }, + }; + + WinJS.Namespace.define("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 (WinJS.Utilities.hasWinRT && !mrtEventHook) { + if (type === contextChangedET) { + try { + Windows.ApplicationModel.Resources.Core.ResourceManager.current.defaultContext.qualifierValues.addEventListener("mapchanged", function (e) { + WinJS.Resources.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: function (string) { + var args = arguments; + if (args.length > 1) { + string = string.replace(/({{)|(}})|{(\d+)}|({)|(})/g, function (unused, left, right, index, illegalLeft, illegalRight) { + if (illegalLeft || illegalRight) { throw WinJS.Resources._formatString(strings.malformedFormatStringInput, illegalLeft || illegalRight); } + return (left && "{") || (right && "}") || args[(index|0) + 1]; + }); + } + return string; + }, + + _getStringWinRT: function (resourceId) { + if (!resourceMap) { + var mainResourceMap = 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 { + resCandidate = resourceMap.getValue(resourceId); + if (resCandidate) { + stringValue = resCandidate.valueAsString; + if (stringValue === undefined) { + stringValue = resCandidate.toString(); + } + } + } + catch (e) {} + + if (!stringValue) { + return { value: resourceId, empty: true }; + } + + 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 }; + } + }); + + Object.defineProperties(WinJS.Resources, WinJS.Utilities.createEventProperties(contextChangedET)); + + var getStringImpl; + + WinJS.Resources.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. + /// + /// + /// + getStringImpl = + getStringImpl || + (WinJS.Utilities.hasWinRT + ? WinJS.Resources._getStringWinRT + : WinJS.Resources._getStringJS); + + return getStringImpl(resourceId); + }; + + +})(this, WinJS); + + +(function promiseInit(global, undefined) { + "use strict"; + + global.Debug && (global.Debug.setNonUserCodeExceptions = true); + + var ListenerType = WinJS.Class.mix(WinJS.Class.define(null, { /*empty*/ }, { supportedForProcessing: false }), WinJS.Utilities.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; + 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.pop(); + 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.pop(); + 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 = WinJS.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) { + pushListener(promise, { c: onComplete, e: onError, p: onProgress }); + } + 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; + if (target) { + try { + target._setCompleteValue(onComplete ? onComplete(value) : value); + } catch (ex) { + target._setExceptionValue(ex); + } + 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; + if (target) { + try { + if (onError) { + if (!onError.handlesOnError) { + callonerror(target, value, detailsForHandledError, promise, onError); + } + target._setCompleteValue(onError(value)) + } else { + target._setChainedErrorValue(value, promise); + } + } catch (ex) { + target._setExceptionValue(ex); + } + 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); + pushListener(promise, { promise: result, c: onComplete, e: onError, p: onProgress }); + return result; + } + + // + // Internal implementation detail promise, ThenPromise is created when a promise needs + // to be returned from a then() method. + // + var ThenPromise = WinJS.Class.derive(PromiseStateMachine, + function (creator) { + + if (tagWithStack && (tagWithStack === true || (tagWithStack & tag.thenPromise))) { + this._stack = WinJS.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 = WinJS.Class.define( + function ErrorPromise_ctor(value) { + + if (tagWithStack && (tagWithStack === true || (tagWithStack & tag.errorPromise))) { + this._stack = WinJS.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 + // + setImmediate(function () { + throw 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 = WinJS.Class.derive(ErrorPromise, + function ExceptionPromise_ctor(value) { + + if (tagWithStack && (tagWithStack === true || (tagWithStack & tag.exceptionPromise))) { + this._stack = WinJS.Promise._getStack(); + } + + this._value = value; + callonerror(this, value, detailsForException); + }, { + /* empty */ + }, { + supportedForProcessing: false + } + ); + + var CompletePromise = WinJS.Class.define( + function CompletePromise_ctor(value) { + + if (tagWithStack && (tagWithStack === true || (tagWithStack & tag.completePromise))) { + this._stack = WinJS.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 + setImmediate(function () { + throw 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 WinJS.Promise( + function (c) { + if (timeoutMS) { + id = setTimeout(c, timeoutMS); + } else { + setImmediate(c); + } + }, + function () { + if (id) { + 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 = WinJS.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 = WinJS.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, progress) { + var keys = Object.keys(values); + var errors = Array.isArray(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(WinJS.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 WinJS.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(WinJS.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 (Debug.debuggerEnabled) { + try { throw new Error(); } catch (e) { return e.stack; } + } + }, + + } + ); + Object.defineProperties(Promise, WinJS.Utilities.createEventProperties(errorET)); + + var SignalPromise = WinJS.Class.derive(PromiseStateMachine, + function (cancel) { + this._oncancel = cancel; + this._setState(state_created); + this._run(); + }, { + _cancelAction: function () { this._oncancel && this._oncancel(); }, + _cleanupAction: function () { this._oncancel = null; } + }, { + supportedForProcessing: false + } + ); + + var Signal = WinJS.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, + } + ); + + // Publish WinJS.Promise + // + WinJS.Namespace.define("WinJS", { + Promise: Promise, + _Signal: Signal + }); + +}(this)); + +(function errorsInit(global, WinJS) { + "use strict"; + + + WinJS.Namespace.define("WinJS", { + // ErrorFromName establishes a simple pattern for returning error codes. + // + ErrorFromName: WinJS.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, + }) + }); + +})(this, WinJS); + + +(function xhrInit() { + "use strict"; + + + WinJS.Namespace.define("WinJS", { + xhr: function (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 WinJS.Promise( + function (c, e, p) { + /// + req = new XMLHttpRequest(); + req.onreadystatechange = function () { + if (req._canceled) { return; } + + if (req.readyState === 4) { + if (req.status >= 200 && req.status < 300) { + c(req); + } else { + e(req); + } + req.onreadystatechange = function () { }; + } else { + p(req); + } + }; + + 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); + } + + req.send(options.data); + }, + function () { + req._canceled = true; + req.abort(); + } + ); + } + }); + +})(); + + +(function safeHTMLInit(global, undefined) { + "use strict"; + + + var setInnerHTML, + setInnerHTMLUnsafe, + setOuterHTML, + setOuterHTMLUnsafe, + insertAdjacentHTML, + insertAdjacentHTMLUnsafe; + + var strings = { + get nonStaticHTML() { return WinJS.Resources._getWinJSString("base/nonStaticHTML").value; }, + }; + + 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 WinJS.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); + }; + } + + WinJS.Namespace.define("WinJS.Utilities", { + setInnerHTML: setInnerHTML, + setInnerHTMLUnsafe: setInnerHTMLUnsafe, + setOuterHTML: setOuterHTML, + setOuterHTMLUnsafe: setOuterHTMLUnsafe, + insertAdjacentHTML: insertAdjacentHTML, + insertAdjacentHTMLUnsafe: insertAdjacentHTMLUnsafe + }); + +}(this)); + +(function getWinJSStringInit() { + "use strict"; + + var appxVersion = "Microsoft.WinJS.1.0"; + var developerPrefix = "Developer."; + if (appxVersion.indexOf(developerPrefix) === 0) { + appxVersion = appxVersion.substring(developerPrefix.length); + } + + WinJS.Namespace.define("WinJS.Resources", { + _getWinJSString: function (id) { + return WinJS.Resources.getString("ms-resource://" + appxVersion + "/" + id); + } + }); +}(this)); +(function controlInit(global, WinJS, undefined) { + "use strict"; + + // not supported in WebWorker + if (!global.document) { + return; + } + + + var DOMEventMixin = { + _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 = 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); + } + }; + + 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; + } + } + } + }; + + WinJS.Namespace.define("WinJS.UI", { + DOMEventMixin: DOMEventMixin, + setOptions: setOptions, + _setOptions: _setOptions + }); + + +})(this, WinJS); + + +(function declarativeControlsInit(global, WinJS, undefined) { + "use strict"; + + // not supported in WebWorker + if (!global.document) { + return; + } + + var strings = { + get errorActivatingControl() { return WinJS.Resources._getWinJSString("base/errorActivatingControl").value; }, + }; + + var markSupportedForProcessing = WinJS.Utilities.markSupportedForProcessing; + var requireSupportedForProcessing = WinJS.Utilities.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) { + // In order to be inclusive of the scope in the lookup we do a QSA across + // the parent of the scope and see if this scope is in the list. + var scopeParent = scope.parentNode; + if (scopeParent && -1 !== Array.prototype.indexOf.call(scopeParent.querySelectorAll(selector), scope)) { + selected = scope; + } + if (selected = selected || scope.querySelector(selector)) { + break; + } + } + } + current = current.parentNode; + } + + return selected || document.querySelector(selector); + } + return markSupportedForProcessing(result); + } + + function activate(element, handler) { + return new WinJS.Promise(function activate2(complete, error) { + try { + var options; + var optionsAttribute = element.getAttribute("data-win-options"); + if (optionsAttribute) { + options = WinJS.UI.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) { + WinJS.log && WinJS.log(WinJS.Resources._formatString(strings.errorActivatingControl, err && err.message), "winjs controls", "error"); + error(err); + } + }); + }; + + function processAllImpl(rootElement) { + return new WinJS.Promise(function processAllImpl2(complete, error) { + msWriteProfilerMark("WinJS.UI:processAll,StartTM"); + rootElement = rootElement || document.body; + var pending = 0; + var selector = "[data-win-control]"; + var allElements = rootElement.querySelectorAll(selector); + var elements = []; + if (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) { complete(rootElement); return; } + + var checkAllComplete = function () { + pending = pending - 1; + if (pending < 0) { + msWriteProfilerMark("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 + // + msWriteProfilerMark("WinJS.UI:processAllActivateControls,StartTM"); + for (var i = 0, len = elements.length; i < len; i++) { + var ctl = controls[i]; + if (ctl) { + pending++; + activate(elements[i], ctl).then(checkAllComplete, error); + } + } + msWriteProfilerMark("WinJS.UI:processAllActivateControls,StopTM"); + + checkAllComplete(); + }); + }; + + function getControlHandler(element) { + if (element.getAttribute) { + var evaluator = element.getAttribute("data-win-control"); + if (evaluator) { + return WinJS.Utilities._getMemberFiltered(evaluator.trim(), global, requireSupportedForProcessing); + } + } + }; + + WinJS.Namespace.define("WinJS.UI", { + + scopedSelect: function (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); + }, + + processAll: function (rootElement) { + /// + /// + /// 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. + /// + /// + /// A promise that is fulfilled when binding has been applied to all the controls. + /// + /// + + if (!processedAllCalled) { + return WinJS.Utilities.ready().then(function () { + processedAllCalled = true; + return processAllImpl(rootElement); + }); + } + else { + return processAllImpl(rootElement); + } + }, + + process: function (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 WinJS.Promise.as(element.winControl); + } + var handler = getControlHandler(element); + if (!handler) { + return WinJS.Promise.as(); // undefined, no handler + } + else { + return activate(element, handler); + } + } + }); +})(this, WinJS); + +(function elementListUtilities(global, WinJS, undefined) { + "use strict"; + + // not supported in WebWorker + if (!global.document) { + return; + } + + var QueryCollection = WinJS.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) { + WinJS.Utilities.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 WinJS.Utilities.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) { + WinJS.Utilities.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) { + WinJS.Utilities.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 WinJS.Utilities.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) { + WinJS.UI.process(element).done(function (control) { + control && WinJS.UI.setOptions(control, options); + }) + }); + } + return this; + } + }, { + supportedForProcessing: false, + }); + + WinJS.Namespace.define("WinJS.Utilities", { + QueryCollection: QueryCollection, + 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 WinJS.Utilities.QueryCollection((element || 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 = document.getElementById(id); + return new WinJS.Utilities.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 WinJS.Utilities.QueryCollection(element.children); + } + }); +})(this, WinJS); + +(function elementUtilities(global, WinJS, undefined) { + "use strict"; + + // not supported in WebWorker + if (!global.document) { + return; + } + + 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 getDimension(element, property) { + return WinJS.Utilities.convertToPixels(element, window.getComputedStyle(element, null)[property]); + } + + WinJS.Namespace.define("WinJS.Utilities", { + _dataKey: "_msDataKey", + _pixelsRE: /^-?\d+(px)?$/i, + _numberRE: /^-?\d+/i, + + /// + /// 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 + }, + + data: function (element) { + /// + /// + /// Gets the data value associated with the specified element. + /// + /// + /// The element. + /// + /// + /// The value associated with the element. + /// + /// + if (!element[WinJS.Utilities._dataKey]) { + element[WinJS.Utilities._dataKey] = {}; + } + return element[WinJS.Utilities._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. + /// + /// + 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: function (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. + /// + /// + 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; + }, + + removeClass: function (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. + /// + /// + 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; + }, + + toggleClass: function (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. + /// + /// + 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; + }, + + 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; + }, + + 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 !== document.body && + element !== document.documentElement) { + top -= element.scrollTop; + var dir = document.defaultView.getComputedStyle(element, null).direction; + left -= dir !== "rtl" ? element.scrollLeft : -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 + }; + }, + + convertToPixels: function (element, value) { + /// + /// + /// Converts a CSS positioning string for the specified element to pixels. + /// + /// + /// The element. + /// + /// + /// The CSS positioning string. + /// + /// + /// The number of pixels. + /// + /// + if (!this._pixelsRE.test(value) && this._numberRE.test(value)) { + var previousValue = element.style.left; + + element.style.left = value; + value = element.style.pixelLeft; + + element.style.left = previousValue; + + return value; + } else { + return parseInt(value, 10) || 0; + } + }, + + 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; + } + }); +})(this, WinJS); + +(function fragmentControlInit(global) { + "use strict"; + + // not supported in WebWorker + if (!global.document) { + return; + } + + function abs(uri) { + var a = document.createElement("a"); + a.href = uri; + return a.href.toLowerCase(); + } + var viewMap = {}; + + function selfhost(uri) { + return document.location.href.toLowerCase() === uri.toLowerCase(); + } + + WinJS.Namespace.define("WinJS.UI.Pages", { + _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 WinJS.UI.Fragments.renderCopy(abs(uri)); + } + }, + 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. + /// + /// + }, + 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. + /// + /// + if (!this.selfhost) { + element.appendChild(loadResult); + } + return element; + }, + 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 + /// + /// + /// A promise that is fulfilled when the element is ready for use, if asynchronous processing is necessary. If not, returns nothing + /// + /// + }, + 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 WinJS.Promise.wrapError(err); + } + }, + define: function (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. + /// + /// + uri = abs(uri); + + var base = viewMap[uri.toLowerCase()]; + if (!base) { + base = WinJS.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.element = element = element || document.createElement("div"); + element.msSourceLocation = uri; + this.uri = uri; + this.selfhost = selfhost(uri); + element.winControl = this; + WinJS.Utilities.addClass(element, "pagecontrol"); + msWriteProfilerMark("WinJS.UI.Pages:createPage,StartTM"); + + var load = WinJS.Promise.wrap(). + then(function Pages_load() { return that.load(uri); }); + + var renderCalled = load.then(function Pages_init(loadResult) { + return WinJS.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_processAll(f) { + return WinJS.UI.processAll(f).then(function () { return f; }); + }).then(function Pages_processed(f) { + return that.processed(element, options); + }).then(function () { + return that; + }); + + var callComplete = function () { + complete && complete(that); + msWriteProfilerMark("WinJS.UI.Pages:createPage,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); + } + ); + }, + WinJS.UI.Pages._mixin + ); + base = WinJS.Class.mix(base, WinJS.UI.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 = WinJS.Class.mix(base, members); + } + + if (selfhost(uri)) { + WinJS.Utilities.ready(function () { + WinJS.UI.Pages.render(uri, document.body); + }); + } + + return base; + }, + + get: function (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. + /// + /// + uri = abs(uri); + var ctor = viewMap[uri.toLowerCase()]; + if (!ctor) { + ctor = WinJS.UI.Pages.define(uri); + } + return ctor; + }, + + _remove: function (uri) { + uri = abs(uri); + WinJS.UI.Fragments.clearCache(uri); + delete viewMap[uri] + }, + + render: function (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 = WinJS.UI.Pages.get(uri); + var control = new ctor(element, options, null, parentedPromise); + return control.renderComplete; + } + }); + + WinJS.Namespace.define("WinJS.UI", { + /// + /// Enables you to include an HTML page dynamically. + /// + /// HtmlControl + /// + /// + ///
      ]]> + /// + /// + HtmlControl: WinJS.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. + /// + /// + WinJS.UI.Pages.render(options.uri, element, options). + then(complete, function () { complete(); }); + }) + }); +})(this); + +(function fragmentLoaderInit(global, WinJS, undefined) { + "use strict"; + + var strings = { + get invalidFragmentUri() { return WinJS.Resources._getWinJSString("base/invalidFragmentUri").value; }, + }; + + // 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 = document.head || document.getElementsByTagName("head")[0]; + var scripts = {}; + var styles = {}; + var links = {}; + var initialized = false; + var cacheStore = {}; + + function addScript(scriptTag, fragmentHref, position) { + // 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; + if (!src) { + src = fragmentHref + "script[" + position + "]"; + } + src = src.toLowerCase(); + + if (!(src in scripts)) { + scripts[src] = true; + var n = document.createElement("script"); + if (scriptTag.language) { + n.setAttribute("language", "javascript"); + } + n.setAttribute("type", scriptTag.type); + if (scriptTag.id) { + n.setAttribute("id", scriptTag.id); + } + if (scriptTag.src) { + n.setAttribute("src", scriptTag.src); + } + else { + n.text = scriptTag.text; + } + head.appendChild(n); + } + }; + + 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); + n.href = styleTag.href; + head.appendChild(n); + } + }; + + function getStateRecord(href, removeFromCache) { + if (typeof href === "string") { + return loadFromCache(href, removeFromCache); + } + else { + var state = { + docfrag: WinJS.Utilities.data(href).docFragment + }; + if (!state.docfrag) { + var fragment = document.createDocumentFragment(); + while (href.childNodes.length > 0) { + fragment.appendChild(href.childNodes[0]); + }; + state.docfrag = WinJS.Utilities.data(href).docFragment = fragment; + href.setAttribute("data-win-hasfragment", ""); + } + if (removeFromCache) { + clearCache(href); + } + return WinJS.Promise.as(state); + } + } + function createEntry(state, href) { + return WinJS.UI.Fragments._populateDocument(state, href). + then(function () { + if (state.document) { + return processDocument(href, state); + } + else { + return state; + } + }). + then(WinJS.UI.Fragments._cleanupDocument). + then(function () { + 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 WinJS.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; + + forEach(cd.querySelectorAll('link[rel="stylesheet"], link[type="text/css"]'), addLink); + forEach(cd.getElementsByTagName('style'), function (e, i) { addStyle(e, href, i); }); + forEach(cd.getElementsByTagName('script'), function (e, i) { addScript(e, href, i); }); + + 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); + } + + // Create the docfrag which is just the body children + // + var fragment = document.createDocumentFragment(); + var imported = 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) { + 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; + } + + if (target) { + target.appendChild(frag); + return target; + } + else { + return frag; + } + }); + }; + + 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 WinJS.Utilities.data(href).docFragment; + href.removeAttribute("data-win-hasfragment"); + } + }; + + function forceLocal(uri) { + if (WinJS.Utilities.hasWinRT) { + // we force the URI to be cannonicalized and made absolute by IE + // + var a = 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 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 WinJS.ErrorFromName("WinJS.UI.Fragments.InvalidUri", strings.invalidFragmentUri); + } + + return absolute; + } + return uri; + } + + WinJS.Namespace.define("WinJS.UI.Fragments", { + renderCopy: renderCopy, + render: render, + cache: cache, + clearCache: clearCache, + _cacheStore: { get: function () { return cacheStore; } }, + _forceLocal: forceLocal + }); +})(this, WinJS); + +(function fragmentLoader2Init(global) { + "use strict"; + + // not supported in WebWorker + if (!global.document) { + return; + } + + function cleanupDocumentIFrame(state) { + // This is to work around a weird bug where removing the + // IFrame from the DOM triggers DOMContentLoaded a second time. + var temp = state.iframe; + if (temp) { + temp.contentDocument.removeEventListener("DOMContentLoaded", state.domContentLoaded, false); + temp.parentNode.removeChild(temp); + delete state.document; + delete state.iframe; + delete state.domContentLoaded; + } + }; + + function populateDocumentIFrame(state, href) { + return new WinJS.Promise(function (c, e, p) { + var temp = document.createElement('iframe'); + temp.src = href; + temp.style.display = 'none'; + + state.domContentLoaded = function () { + state.document = temp.contentDocument; + state.iframe = temp; + c(); + }; + + document.body.appendChild(temp); + temp.contentWindow.onerror = function (e) { + // It's OK to swallow these as they will occur in the main document + // + return true; + }; + temp.contentDocument.addEventListener("DOMContentLoaded", state.domContentLoaded, false); + }); + }; + + function cleanupDocumentXHR(state) { + if (state.document) { + delete state.document; + } + return WinJS.Promise.as(); + }; + + function populateDocumentXHR(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 = WinJS.UI.Fragments._forceLocal(href); + + var htmlDoc = document.implementation.createHTMLDocument("frag"); + var base = htmlDoc.createElement("base"); + htmlDoc.head.appendChild(base); + var anchor = htmlDoc.createElement("a"); + htmlDoc.body.appendChild(anchor); + base.href = 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 WinJS.xhr({ url: href }).then(function (req) { + WinJS.Utilities.setInnerHTMLUnsafe(htmlDoc.documentElement, req.responseText); + htmlDoc.head.appendChild(base); + }); + }; + + if (global.MSApp) { + WinJS.Namespace.define("WinJS.UI.Fragments", { + _populateDocument: populateDocumentXHR, + _cleanupDocument: cleanupDocumentXHR + }); + } + else { + WinJS.Namespace.define("WinJS.UI.Fragments", { + _populateDocument: populateDocumentIFrame, + _cleanupDocument: cleanupDocumentIFrame + }); + } +})(this); + +(function optionsLexerInit(global, undefined) { + "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 + +*/ + + 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)); + } 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 type = undefined; + var token = undefined; + 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; + + WinJS.Namespace.define("WinJS.UI", { + _optionsLexer: lexer + }); +})(this); +(function optionsParserInit(global, undefined) { + "use strict"; + + var strings = { + get invalidOptionsRecord() { return WinJS.Resources._getWinJSString("base/invalidOptionsRecord").value; }, + get unexpectedTokenExpectedToken() { return WinJS.Resources._getWinJSString("base/unexpectedTokenExpectedToken").value; }, + get unexpectedTokenExpectedTokens() { return WinJS.Resources._getWinJSString("base/unexpectedTokenExpectedTokens").value; }, + get unexpectedTokenGeneric() { return WinJS.Resources._getWinJSString("base/unexpectedTokenGeneric").value; }, + }; + + /* + 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 *** + +*/ + + var lexer = WinJS.UI._optionsLexer; + var tokenType = lexer.tokenType; + var requireSupportedForProcessing = WinJS.Utilities.requireSupportedForProcessing; + + function tokenTypeName(type) { + var keys = Object.keys(tokenType); + for (var i = 0, len = keys.length; i < len; i++) { + if (type === tokenType[keys[i]]) { + return keys[i]; + } + } + return ""; + } + + var BaseInterpreter = WinJS.Class.define(null, + { + _error: function (message) { + throw new WinJS.ErrorFromName("WinJS.UI.ParseError", message); + }, + _currentOffset: function () { + var l = this._tokens.length; + 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 tokenType.dot: + this._read(); + switch (this._current.type) { + case tokenType.identifier: + case this._current.keyword && this._current.type: + var id = this._current.value; + this._read(); + return value[id]; + + default: + this._unexpectedToken(tokenType.identifier, tokenType.reservedWord); + break; + } + return; + + case tokenType.leftBracket: + this._read(); + var index = this._evaluateValue(); + this._read(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 tokenType.dot: + case 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 tokenType.dot: + case 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 !== tokenType.eof) { + this._current = this._tokens[++this._pos]; + } + }, + _peek: function (expected) { + if (expected && this._current.type !== expected) { + return; + } + if (this._current !== tokenType.eof) { + return this._tokens[this._pos + 1]; + } + }, + _readAccessExpression: function (parts) { + switch (this._current.type) { + case tokenType.dot: + this._read(); + switch (this._current.type) { + case tokenType.identifier: + case this._current.keyword && this._current.type: + parts.push(this._current.value); + this._read(); + break; + + default: + this._unexpectedToken(tokenType.identifier, tokenType.reservedWord); + break; + } + return; + + case tokenType.leftBracket: + this._read(); + parts.push(this._evaluateValue()); + this._read(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 tokenType.dot: + case tokenType.leftBracket: + this._readAccessExpression(parts); + break; + + default: + return; + } + } + }, + _readIdentifier: function () { + var id = this._current.value; + this._read(tokenType.identifier); + return id; + }, + _readIdentifierExpression: function () { + var parts = []; + if (this._peek(tokenType.thisKeyword) && parts.length === 0) { + this._read(); + } + else { + parts.push(this._readIdentifier()); + } + + switch (this._current.type) { + case tokenType.dot: + case tokenType.leftBracket: + this._readAccessExpressions(parts); + break; + } + + return parts; + }, + _unexpectedToken: function (expected) { + var unexpected = (this._current.type === tokenType.error ? "'" + this._current.value + "'" : tokenTypeName(this._current.type)); + if (expected) { + if (arguments.length == 1) { + expected = tokenTypeName(expected); + this._error(WinJS.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(WinJS.Resources._formatString(strings.unexpectedTokenExpectedTokens, unexpected, expected, this._currentOffset())); + } + } else { + this._error(WinJS.Resources._formatString(strings.unexpectedTokenGeneric, unexpected, this._currentOffset())); + } + } + }, { + supportedForProcessing: false, + } + ); + + var OptionsInterpreter = WinJS.Class.derive(BaseInterpreter, + function (tokens, originalSource, context, functionContext) { + this._initialize(tokens, originalSource, context, functionContext); + }, + { + _error: function (message) { + throw new WinJS.ErrorFromName("WinJS.UI.ParseError", WinJS.Resources._formatString(strings.invalidOptionsRecord, this._originalSource, message)); + }, + _evaluateArrayLiteral: function () { + var a = []; + this._read(tokenType.leftBracket); + this._readArrayElements(a); + this._read(tokenType.rightBracket); + return a; + }, + _evaluateObjectLiteral: function () { + var o = {}; + this._read(tokenType.leftBrace); + this._readObjectProperties(o); + this._tryReadComma(); + this._read(tokenType.rightBrace); + return o; + }, + _evaluateOptionsLiteral: function () { + var value = this._evaluateValue(); + if (this._current.type !== tokenType.eof) { + this._unexpectedToken(tokenType.eof); + } + return value; + }, + _peekValue: function () { + switch (this._current.type) { + case tokenType.falseLiteral: + case tokenType.nullLiteral: + case tokenType.stringLiteral: + case tokenType.trueLiteral: + case tokenType.numberLiteral: + case tokenType.leftBrace: + case tokenType.leftBracket: + case tokenType.identifier: + return true; + default: + return false; + } + }, + _evaluateValue: function () { + switch (this._current.type) { + case tokenType.falseLiteral: + case tokenType.nullLiteral: + case tokenType.stringLiteral: + case tokenType.trueLiteral: + case tokenType.numberLiteral: + var value = this._current.value; + this._read(); + return value; + + case tokenType.leftBrace: + return this._evaluateObjectLiteral(); + + case tokenType.leftBracket: + return this._evaluateArrayLiteral(); + + case tokenType.identifier: + if (this._peek(tokenType.identifier).type == tokenType.leftParentheses) { + return requireSupportedForProcessing(this._evaluateObjectQueryExpression()); + } + return requireSupportedForProcessing(this._evaluateIdentifierExpression()); + + default: + this._unexpectedToken(tokenType.falseLiteral, tokenType.nullLiteral, tokenType.stringLiteral, + tokenType.trueLiteral, tokenType.numberLiteral, tokenType.leftBrace, tokenType.leftBracket, + tokenType.identifier); + break; + } + }, + _tryReadElement: function (a) { + if (this._peekValue()) { + a.push(this._evaluateValue()); + return true; + } else { + return false; + } + }, + _tryReadComma: function () { + if (this._peek(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(tokenType.rightBracket)) { + var elision = this._tryReadElision(a); + var element = this._tryReadElement(a); + var comma = this._peek(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(tokenType.falseLiteral, tokenType.nullLiteral, tokenType.stringLiteral, + tokenType.trueLiteral, tokenType.numberLiteral, tokenType.leftBrace, tokenType.leftBracket, + tokenType.identifier); + break; + } + } + }, + _readObjectProperties: function (o) { + while (!this._peek(tokenType.rightBrace)) { + var property = this._tryReadObjectProperty(o); + var comma = this._peek(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(tokenType.numberLiteral, tokenType.stringLiteral, tokenType.identifier); + break; + } + } + }, + _tryReadObjectProperty: function (o) { + switch (this._current.type) { + case tokenType.numberLiteral: + case tokenType.stringLiteral: + case tokenType.identifier: + case this._current.keyword && this._current.type: + var propertyName = this._current.value; + this._read(); + this._read(tokenType.colon); + o[propertyName] = this._evaluateValue(); + return true; + + default: + return false; + } + }, + _failReadObjectProperty: function () { + this._unexpectedToken(tokenType.numberLiteral, tokenType.stringLiteral, tokenType.identifier, tokenType.reservedWord); + }, + _evaluateObjectQueryExpression: function () { + var functionName = this._current.value; + this._read(tokenType.identifier); + this._read(tokenType.leftParentheses); + var queryExpression = this._current.value; + this._read(tokenType.stringLiteral); + this._read(tokenType.rightParentheses); + + var value = requireSupportedForProcessing(this._functionContext[functionName])(queryExpression); + switch (this._current.type) { + case tokenType.dot: + case tokenType.leftBracket: + return this._evaluateAccessExpressions(value); + + default: + return value; + } + }, + run: function () { + return this._evaluateOptionsLiteral(); + } + }, { + supportedForProcessing: false, + } + ); + + var parser = function (text, context, functionContext) { + var tokens = lexer(text); + var interpreter = new OptionsInterpreter(tokens, text, context || {}, functionContext || {}); + return interpreter.run(); + }; + parser._BaseInterpreter = BaseInterpreter; + + WinJS.Namespace.define("WinJS.UI", { + optionsParser: parser + }); + +})(this); + + +(function tabManagerInit(global, WinJS, undefined) { + "use strict"; + + // not supported in WebWorker + if (!global.document) { + return; + } + + + function fireEvent(element, name) { + var event = document.createEvent('UIEvent'); + event.initUIEvent(name, false, false, window, 1); + element.dispatchEvent(event); + } + + function onBeforeActivate(e, element, hasFocus) { + fireEvent(element, hasFocus ? "onTabExit" : "onTabEnter"); + e.cancelBubble = true; + return false; + } + + function prefixBeforeActivateHandler(e) { + return onBeforeActivate(e, e.srcElement.nextSibling, e.shiftKey); + } + + function postfixBeforeActivateHandler(e) { + return onBeforeActivate(e, e.srcElement.previousSibling, !e.shiftKey); + } + + function TabHelperObject(element, tabIndex) { + function createCatcher(beforeActivateHandler) { + var fragment = document.createElement("DIV"); + fragment.tabIndex = (tabIndex ? tabIndex : 0); + fragment.attachEvent("onbeforeactivate", beforeActivateHandler); + fragment.setAttribute("aria-hidden", true); + return fragment; + }; + + var parent = element.parentNode; + + // Insert prefix focus catcher + var catcherBegin = createCatcher(prefixBeforeActivateHandler); + parent.insertBefore(catcherBegin, element); + + // Insert postfix focus catcher + var catcherEnd = createCatcher(postfixBeforeActivateHandler); + parent.insertBefore(catcherEnd, element.nextSibling); + + 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; + } + } + + WinJS.Namespace.define("WinJS.UI.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"]; + } + } + }); + + WinJS.Namespace.define("WinJS.UI", { + TabContainer: WinJS.Class.define(function TabContainer_ctor(element, options) { + /// + /// + /// 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. + /// + /// + // TabContainer uses 2 TrackTabBehavior for its implementation: one for itself, another one for the active element. + // When onTabEnter is caught on TabContainer, it directly set focus on the active element. + // When onTabExit is caught on the active element (_tabExitHandler), it first prevents focus from being set on any element, + // effectively letting the focus skip any remaining items in the TabContainer. Then, when onTabExit is caught on + // TabContainer, it turns back on the possibility to receive focus on child elements. + this._element = element; + this._tabIndex = 0; + var that = this; + this._tabExitHandler = function () { + that._canFocus(false); + }; + + element.addEventListener("onTabEnter", function () { + if (that.childFocus) { + that.childFocus.focus(); + } else { + that._canFocus(false); + } + }); + + element.addEventListener("onTabExit", function () { + that._canFocus(true); + }); + + this._elementTabHelper = WinJS.UI.TrackTabBehavior.attach(element, this._tabIndex); + }, { + + // Public members + + /// + /// Gets or sets the child element that has focus. + /// + childFocus: { + set: function (e) { + if (e != this._focusElement) { + if (this._focusElement) { + WinJS.UI.TrackTabBehavior.detach(this._focusElement); + this._childTabHelper = null; + this._focusElement.removeEventListener("onTabExit", this._tabExitHandler); + } + + if (e && e.parentNode) { + this._focusElement = e; + this._childTabHelper = WinJS.UI.TrackTabBehavior.attach(e, this._tabIndex); + this._focusElement.addEventListener("onTabExit", this._tabExitHandler); + } else { + //#DBG _ASSERT(!!e.parentNode); + 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); + if (this._childTabHelper) { + this._childTabHelper.updateTabIndex(tabIndex); + } + }, + + get: function () { + return this._tabIndex; + } + }, + + // Private members + + _element: null, + _skipper: function (e) { + e.cancelBubble = true; + return false; + }, + _canFocus: function (canfocus) { + if (canfocus) { + this._element.detachEvent("onbeforeactivate", this._skipper); + } else { + this._element.attachEvent("onbeforeactivate", this._skipper); + } + }, + + _focusElement: null + + }, { // Static Members + supportedForProcessing: false, + }) + }); +})(this, WinJS); + +(function transitionAnimationInit(global, WinJS, undefined) { + "use strict"; + + // not supported in WebWorker + if (!global.document) { + return; + } + + function makeArray(elements) { + if (elements instanceof Array || elements instanceof NodeList || elements instanceof HTMLCollection) { + return elements; + } else if (elements) { + return [elements]; + } else { + return []; + } + } + + var keyframeCounter = 0; + function getUniqueKeyframeName() { + return "WinJSUIAnimation" + ++keyframeCounter; + } + function isUniqueKeyframeName(s) { + return "WinJSUIAnimation" === s.substring(0, 16); + } + + function resolveStyles(elem) { + window.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 += UI._libraryDelay; + } + return newObj; + }; + } + + var activeActions = [ ]; + + var reason_ended = 0; + 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 = WinJS.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) { + var desc = this.desc; + delete desc.styleCaches[this.id]; + 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) { + 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); + } + } + }); + + 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: "transition", + nameProp: "transition-property", + nameField: "property", + props: [ + ["transition-duration", "duration", "ms"], + ["transition-timing-function", "timing", ""], + ["transition-delay", "delay", "ms"] + ], + isTransition: true, + styleCaches: [] + }; + + function completePromise(c, synchronous) { + if (synchronous) { + c(); + } else { + setImmediate(c); + } + } + + var uniformizeStyle; + function executeElementTransition(elem, index, transitions, promises, animate) { + if (transitions.length > 0) { + var style = elem.style; + var id = elem.uniqueID; + if (!uniformizeStyle) { + uniformizeStyle = document.createElement("DIV").style; + } + transitions = transitions.map(copyWithEvaluation(index, elem)); + transitions.forEach(function (transition) { + if (transition.hasOwnProperty("from")) { + style[transition.property] = transition.from; + } + uniformizeStyle[transition.property] = transition.to; + transition.to = uniformizeStyle[transition.property]; + }); + + if (animate) { + var styleCache = setTemporaryStyles(elem, id, style, transitions, elementTransitionProperties); + var listener = elem.disabled ? document : elem; + + transitions.forEach(function (transition) { + var finish; + promises.push(new WinJS.Promise(function (c, e, p) { + finish = function (reason) { + if (onTransitionEnd) { + listener.removeEventListener("transitionend", onTransitionEnd, false); + unregisterAction(id, transition.property); + styleCache.removeName(style, transition.property, reason ? elem : null); + clearTimeout(timeoutId); + onTransitionEnd = null; + } + completePromise(c, reason === reason_canceled); + }; + + var onTransitionEnd = function (event) { + if (event.srcElement === elem && event.propertyName === transition.property) { + finish(); + } + }; + + registerAction(id, transition.property, finish); + listener.addEventListener("transitionend", onTransitionEnd, false); + + var padding = 0; + if (style[transition.property] !== transition.to) { + style[transition.property] = transition.to; + padding = 50; + } + var timeoutId = setTimeout(function() { + timeoutId = setTimeout(finish, transition.delay + transition.duration); + }, padding); + }, function () { finish(reason_canceled); })); + }); + } else { + transitions.forEach(function (transition) { + style[transition.property] = transition.to; + }); + } + } + } + + var elementAnimationProperties = { + shorthandProp: "animation", + nameProp: "animation-name", + nameField: "keyframe", + props: [ + ["animation-duration", "duration", "ms"], + ["animation-timing-function", "timing", ""], + ["animation-delay", "delay", "ms"], + ["animation-iteration-count", "", "1"], + ["animation-direction", "", "normal"], + ["animation-fill-mode", "", "both"] + ], + isTransition: false, + styleCaches: [] + }; + + function executeElementAnimation(elem, index, anims, promises, animate) { + if (animate && anims.length > 0) { + var style = elem.style; + var id = elem.uniqueID; + anims = anims.map(copyWithEvaluation(index, elem)); + var styleElem; + var listener = elem.disabled ? document : elem; + anims.forEach(function (anim) { + if (!anim.keyframe) { + if (!styleElem) { + styleElem = document.createElement("STYLE"); + document.documentElement.appendChild(styleElem); + } + anim.keyframe = getUniqueKeyframeName(); + var kf = "@keyframes " + anim.keyframe + " { from {" + anim.property + ":" + anim.from + ";} to {" + anim.property + ":" + anim.to + ";}}"; + styleElem.sheet.insertRule(kf, 0); + } + }); + var styleCache = setTemporaryStyles(elem, id, style, anims, elementAnimationProperties); + anims.forEach(function (anim) { + var finish; + promises.push(new WinJS.Promise(function (c, e, p) { + + finish = function (reason) { + if (onAnimationEnd) { + listener.removeEventListener("animationend", onAnimationEnd, false); + unregisterAction(id, anim.property); + styleCache.removeName(style, anim.keyframe); + clearTimeout(timeoutId); + onAnimationEnd = null; + } + completePromise(c, reason === reason_canceled); + }; + + var onAnimationEnd = function (event) { + if (event.srcElement === elem && event.animationName === anim.keyframe) { + finish(); + } + }; + + registerAction(id, anim.property, finish); + var timeoutId = setTimeout(function() { + timeoutId = setTimeout(finish, anim.delay + anim.duration); + }, 50); + listener.addEventListener("animationend", onAnimationEnd, false); + }, function() { finish(reason_canceled); })); + }); + if (styleElem) { + setTimeout(function () { + var parentElement = styleElem.parentElement; + if (parentElement) { + parentElement.removeChild(styleElem); + } + }, 50); + } + } + } + + var enableCount = 0; + var animationSettings; + function initAnimations() { + if (!animationSettings) { + if (WinJS.Utilities.hasWinRT) { + animationSettings = new Windows.UI.ViewManagement.UISettings(); + } + else { + animationSettings = { animationsEnabled: true }; + } + } + } + + function isAnimationEnabled() { + /// + /// + /// 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 createImmediatePromise() { + var complete; + return new WinJS.Promise(function (c, e, p) { + complete = c; + setImmediate(c); + }, function() { complete(); }); + } + + function applyAction(element, action, execAction) { + try { + var animate = WinJS.UI.isAnimationEnabled(); + var elems = makeArray(element); + var actions = makeArray(action); + + var promises = []; + + for (var i = 0; i < elems.length; i++) { + if (elems[i] instanceof Array) { + 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 WinJS.Promise.join(promises); + } else { + return createImmediatePromise(); + } + } catch (e) { + return WinJS.Promise.wrapError(e); + } + } + + var UI = WinJS.Namespace.define("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: isAnimationEnabled, + + _libraryDelay: 34, + + 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, 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, transition, executeElementTransition); + } + }); +})(this, WinJS); + +(function utilitiesInit(global, undefined) { + "use strict"; + + var markSupportedForProcessing = WinJS.Utilities.markSupportedForProcessing; + + WinJS.Namespace.define("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 markSupportedForProcessing(handler); + } + }); + + + WinJS.Namespace.define("WinJS.Utilities", { + + _clamp: function (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)); + } + + }); + +}(this)); + +(function applicationInit(global, WinJS, undefined) { + "use strict"; + + global.Debug && (global.Debug.setNonUserCodeExceptions = true); + + var checkpointET = "checkpoint", + unloadET = "unload", + activatedET = "activated", + loadedET = "loaded", + readyET = "ready", + errorET = "error", + settingsET = "settings"; + + var outstandingPromiseErrors; + var eventQueue = []; + var running = false; + var registered = false; + // check for WinRT & document, which means we will disabled application WinRT stuff in web worker context + // + var useWinRT = WinJS.Utilities.hasWinRT && global.document; + + var ListenerType = WinJS.Class.mix(WinJS.Class.define(null, { /* empty */ }, { supportedForProcessing: false }), WinJS.Utilities.eventMixin); + var listeners = new ListenerType(); + var pendingDeferrals = {}; + var pendingDeferralID = 0; + + function safeSerialize(obj) { + var str; + try { + str = JSON.stringify(obj); + } + 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; + } + + var pendingDrain; + + var terminateAppHandler = function (data, e) { + // 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; + MSApp.terminateApp(data); + }; + + 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) { + msWriteProfilerMark("WinJS.Application:Event_" + eventRecord.type + ",StartTM"); + + var waitForPromise = WinJS.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) { + msWriteProfilerMark("WinJS.Application:Event_" + eventRecord.type + ",StopTM"); + + if (eventRecord._deferral) { + completeDeferral(eventRecord._deferral, eventRecord._deferralID); + } + return r; + } + + return waitForPromise.then(cleanup, function(r) { + return WinJS.Promise.wrapError(cleanup(r)); + }); + } + + function drainQueue(queue) { + pendingDrain = true; + if (queue.length === 0) { + if (eventQueue.length > 0) { + return drainQueue(copyAndClearQueue()); + } + pendingDrain = false; + return WinJS.Promise.as(queue); + } + function drainNext() { + if (running) { + return drainQueue(queue.slice(1)); + } + } + function drainError(err) { + queueEvent({ type: errorET, detail: err }); + return drainNext(); + } + return dispatchEvent(queue[0]). + then(drainNext, drainError); + } + + 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. + /// + /// + msWriteProfilerMark("WinJS.Application:Event_" + eventRecord.type + " queued,Info"); + eventQueue.push(eventRecord); + if (running && !pendingDrain) { + drainQueue(copyAndClearQueue()); + } + } + + function copyAndClearQueue() { + var queue = eventQueue; + eventQueue = []; + return queue; + } + + var builtInListeners = { + activated: [ + function Application_activatedHandler() { + queueEvent({ type: readyET }); + } + ], + checkpoint: [ + function Application_checkpointHandler(e) { + // comes from state.js + WinJS.Application._oncheckpoint(e); + } + ], + error: [ + function Application_errorHandler(e, handled) { + if (handled) { + return; + } + + WinJS.log && WinJS.log(safeSerialize(e), "winjs", "error"); + + if (useWinRT && WinJS.Application._terminateApp) { + var data = e.detail; + var number = data && (data.number || (data.exception && data.exception.number) || (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.error && data.error.stack) || null), + errorNumber: number, + number: number + }; + WinJS.Application._terminateApp(terminateData, e); + } + } + ], + }; + + // loaded == DOMContentLoaded + // activated == after WinRT Activated + // ready == after all of the above + // + function activatedHandler(e) { + var def = captureDeferral(e.activatedOperation); + WinJS.Application._loadState(e).then(function () { + queueEvent({ type: activatedET, detail: e, _deferral: def.deferral, _deferralID: def.id }); + }); + } + function suspendingHandler(e) { + var def = captureDeferral(e.suspendingOperation); + WinJS.Application.queueEvent({ type: checkpointET, _deferral: def.deferral, _deferralID: def.id }); + } + function domContentLoadedHandler(e) { + queueEvent({ type: loadedET }); + if (!useWinRT) { + var activatedArgs = { + arguments: "", + kind: "Windows.Launch", + previousExecutionState: 0 //Windows.ApplicationModel.Activation.ApplicationExecutionState.NotRunning + }; + WinJS.Application._loadState(activatedArgs).then(function () { + queueEvent({ type: activatedET, detail: activatedArgs }); + }); + } + } + function beforeUnloadHandler(e) { + cleanupAllPendingDeferrals(); + queueEvent({ type: unloadET }); + } + function errorHandler(e) { + var flattenedError = {}; + for (var k in e) { + flattenedError[k] = e[k]; + } + var data; + var handled = true; + var prev = WinJS.Application._terminateApp; + try { + WinJS.Application._terminateApp = function (d, e) { + handled = false; + data = d; + if (prev !== terminateAppHandler) { + prev(d, e); + } + } + dispatchEvent({ type: errorET, detail: { error: flattenedError, errorLine: event.errorLine, errorCharacter: event.errorCharacter, errorCode: event.errorCode, errorUrl: event.errorUrl, errorMessage: event.errorMessage } }); + } finally { + WinJS.Application._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; + } + + // 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) { + setImmediate(function () { + var errors = outstandingPromiseErrors; + outstandingPromiseErrors = null; + errors.forEach(function (error) { + queueEvent({ type: errorET, detail: error }); + }); + }); + } + } + + // capture this early + // + if (global.document) { + document.addEventListener("DOMContentLoaded", domContentLoadedHandler, false); + } + + function commandsRequested(e) { + var event = { e: e, applicationcommands: undefined }; + listeners.dispatchEvent(settingsET, event); + } + + function register() { + if (!registered) { + registered = true; + global.addEventListener("beforeunload", beforeUnloadHandler, false); + + if (useWinRT) { + global.addEventListener("error", errorHandler, false); + + var wui = Windows.UI.WebUI.WebUIApplication; + wui.addEventListener("activated", activatedHandler, false); + wui.addEventListener("suspending", suspendingHandler, false); + + var settingsPane = Windows.UI.ApplicationSettings.SettingsPane.getForCurrentView(); + settingsPane.addEventListener("commandsrequested", commandsRequested); + } + + WinJS.Promise.addEventListener("error", promiseErrorHandler); + } + } + function unregister() { + if (registered) { + registered = false; + global.removeEventListener("beforeunload", beforeUnloadHandler, false); + + if (useWinRT) { + global.removeEventListener("error", errorHandler, false); + + var wui = Windows.UI.WebUI.WebUIApplication; + wui.removeEventListener("activated", activatedHandler, false); + wui.removeEventListener("suspending", suspendingHandler, false); + + var settingsPane = Windows.UI.ApplicationSettings.SettingsPane.getForCurrentView(); + settingsPane.removeEventListener("commandsrequested", commandsRequested); + } + + WinJS.Promise.removeEventListener("error", promiseErrorHandler); + } + } + + + WinJS.Namespace.define("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. + // + this.onactivated = null; + this.oncheckpoint = null; + this.onerror = null; + this.onloaded = null; + this.onready = null; + this.onsettings = null; + this.onunload = null; + listeners = new ListenerType(); + WinJS.Application.sessionState = {}; + running = false; + copyAndClearQueue(); + 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(); + var queue = copyAndClearQueue(); + running = true; + drainQueue(queue); + }, + + queueEvent: queueEvent, + + _terminateApp: terminateAppHandler, + + }); + + Object.defineProperties(WinJS.Application, WinJS.Utilities.createEventProperties(checkpointET, unloadET, activatedET, loadedET, readyET, settingsET, errorET)); +})(this, WinJS); + +(function navigationInit(WinJS, undefined) { + "use strict"; + + var navigatedEventName = "navigated"; + var navigatingEventName = "navigating"; + var beforenavigateEventName = "beforenavigate"; + var ListenerType = WinJS.Class.mix(WinJS.Class.define(null, { /* empty */ }, { supportedForProcessing: false }), WinJS.Utilities.eventMixin); + var listeners = new ListenerType(); + var history = { + backStack: [], + current: { location: "", initialPlaceholder: true }, + forwardStack: [] + }; + + var raiseBeforeNavigate = function (proposed) { + msWriteProfilerMark("WinJS.Navigation:navigation,StartTM"); + return WinJS.Promise.as(). + then(function () { + var waitForPromise = WinJS.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 WinJS.Promise.as(). + then(function () { + var waitForPromise = WinJS.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) { + msWriteProfilerMark("WinJS.Navigation:navigation,StopTM"); + var waitForPromise = WinJS.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 WinJS.Promise.wrap(false); + } + + WinJS.Namespace.define("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) { + var s = history = value; + + // ensure the require fields are present + // + s.backStack = s.backStack || []; + s.forwardStack = s.forwardStack || []; + s.current = s.current || { location: "", initialPlaceholder: true }; + s.current.location = s.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); + } + }); + + Object.defineProperties(WinJS.Navigation, WinJS.Utilities.createEventProperties(navigatedEventName, navigatingEventName, beforenavigateEventName)); +})(WinJS); + +(function stateInit(global) { + "use strict"; + + function initWithWinRT() { + var local, temp, roaming; + + var IOHelper = WinJS.Class.define( + function IOHelper_ctor(folder) { + this.folder = folder; + this._path = folder.path; + }, { + 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.folder.getFileAsync(fileName). + then( + function () { return true; }, + function () { return 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 + /// + /// + var that = this; + return that.folder.getFileAsync(fileName). + then( + function (fileItem) { + return fileItem.deleteAsync(); + }, + 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 = Windows.Storage; + var that = this; + return that.folder.createFileAsync(fileName, sto.CreationCollisionOption.openIfExists). + then(function (fileItem) { + if (sto.FileIO) { + return sto.FileIO.writeTextAsync(fileItem, str); + } + else { + return sto.StorageHelpers.writeAllTextUsingFileAsync(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 = Windows.Storage; + var that = this; + function onerror() { return def; } + return that.folder.getFileAsync(fileName). + then(function (fileItem) { + if (sto.FileIO) { + return sto.FileIO.readTextAsync(fileItem); + } + else { + return sto.StorageHelpers.readAllTextUsingFileAsync(fileItem); + } + }, onerror); + } + }, { + supportedForProcessing: false, + }); + + WinJS.Namespace.define("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(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(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(Windows.Storage.ApplicationData.current.roamingFolder); + } + return roaming; + } + } + }); + }; + + function initWithStub() { + var InMemoryHelper = WinJS.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 WinJS.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 WinJS.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 WinJS.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 WinJS.Promise.as(typeof result === "string" ? result : def); + } + }, { + supportedForProcessing: false, + } + ); + + WinJS.Namespace.define("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 (WinJS.Utilities.hasWinRT) { + initWithWinRT(); + } + else { + initWithStub(); + } + + WinJS.Namespace.define("WinJS.Application", { + sessionState: {}, + _loadState: function (e) { + var app = WinJS.Application; + + // we only restore state if we are coming back from a clear termination from PLM + // + if (e.previousExecutionState === 3 /* ApplicationExecutionState.Terminated */) { + return app.local.readText("_sessionState.json", "{}"). + then(function (str) { + var sessionState = JSON.parse(str); + if (sessionState && Object.keys(sessionState).length > 0) { + app._sessionStateLoaded = true; + } + app.sessionState = sessionState; + }). + then(null, function (err) { + app.queueEvent({ type: "error", detail: err }); + }); + } + else { + return WinJS.Promise.as(); + } + }, + _oncheckpoint: function (e) { + var app = WinJS.Application; + var sessionState = app.sessionState; + if ((sessionState && Object.keys(sessionState).length > 0) || app._sessionStateLoaded) { + e.setPromise( + app.local.writeText("_sessionState.json", JSON.stringify(sessionState)). + then(null, function (err) { + app.queueEvent({ type: "error", detail: err }); + }) + ); + } + } + }); +})(this); + +(function bindingParserInit(global, undefined) { + "use strict"; + + + var strings = { + get invalidBinding() { return WinJS.Resources._getWinJSString("base/invalidBinding").value; }, + get bindingInitializerNotFound() { return WinJS.Resources._getWinJSString("base/bindingInitializerNotFound").value; }, + }; + +/* + 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 lexer = WinJS.UI._optionsLexer; + var tokenType = lexer.tokenType; + var requireSupportedForProcessing = WinJS.Utilities.requireSupportedForProcessing; + + var BindingInterpreter = WinJS.Class.derive(WinJS.UI.optionsParser._BaseInterpreter, + function (tokens, originalSource, context) { + this._initialize(tokens, originalSource, context); + }, + { + _error: function (message) { + throw new WinJS.ErrorFromName("WinJS.Binding.ParseError", WinJS.Resources._formatString(strings.invalidBinding, this._originalSource, message)); + }, + _evaluateInitializerName: function () { + if (this._current.type === tokenType.identifier) { + var initializer = this._evaluateIdentifierExpression(); + if (WinJS.log && !initializer) { + WinJS.log(WinJS.Resources._formatString(strings.bindingInitializerNotFound, this._originalSource), "winjs binding", "error"); + } + return requireSupportedForProcessing(initializer); + } + return; + }, + _evaluateValue: function () { + switch (this._current.type) { + case tokenType.stringLiteral: + case tokenType.numberLiteral: + var value = this._current.value; + this._read(); + return value; + + default: + this._unexpectedToken(tokenType.stringLiteral, tokenType.numberLiteral); + return; + } + }, + _readBindDeclarations: function () { + var bindings = []; + while (true) { + switch (this._current.type) { + case tokenType.identifier: + case tokenType.thisKeyword: + bindings.push(this._readBindDeclaration()); + break; + + case tokenType.semicolon: + this._read(); + break; + + case tokenType.eof: + return bindings; + + default: + this._unexpectedToken(tokenType.identifier, tokenType.semicolon, tokenType.eof); + return; + } + } + }, + _readBindDeclaration: function () { + var dest = this._readDestinationPropertyName(); + this._read(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, + } + ); + + function parser(text, context) { + msWriteProfilerMark("WinJS.Binding:bindingParser,StartTM"); + var tokens = lexer(text); + var interpreter = new BindingInterpreter(tokens, text, context || {}); + var res = interpreter.run(); + msWriteProfilerMark("WinJS.Binding:bindingParser,StopTM"); + return res; + } + + WinJS.Namespace.define("WinJS.Binding", { + _bindingParser: parser + }); + +})(this); + +(function dataInit(WinJS, undefined) { + "use strict"; + + + var strings = { + get exceptionFromBindingInitializer() { return WinJS.Resources._getWinJSString("base/exceptionFromBindingInitializer").value; }, + get propertyIsUndefined() { return WinJS.Resources._getWinJSString("base/propertyIsUndefined").value; }, + get unsupportedDataTypeForBinding() { return WinJS.Resources._getWinJSString("base/unsupportedDataTypeForBinding").value; }, + }; + + 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 = WinJS.Promise.timeout(). + 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) { + WinJS.log && WinJS.log(WinJS.Resources._formatString(strings.exceptionFromBindingInitializer, e.toString()), "winjs binding", "error"); + } + } + }). + then(cleanup). + then(function () { return newValue; }); + + return cap.promise; + } + + return WinJS.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 (WinJS.log && data === undefined) { + WinJS.log(WinJS.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. + // + return this.notify(name, newValue, oldValue); + } + return WinJS.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 (!WinJS.Utilities.hasWinRT) { + return func; + } + + var id = createBindRefId(); + WinJS.Utilities._getWeakRefElement(bindStateRef)[id] = func; + return function (n, o) { + var bindState = WinJS.Utilities._getWeakRefElement(bindStateRef); + if (bindState) { + bindState[id](n, o); + } + }; + } + var bindImpl = function (observable, bindingDescriptor, bindStateRef) { + observable = WinJS.Binding.as(observable); + if (!observable) { + return { cancel: function () { }, empty: true }; + } + + var bindState; + if (!bindStateRef) { + bindStateRef = createBindRefId(); + bindState = {}; + WinJS.Utilities._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 = WinJS.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) || (data instanceof Array)) { + if (WinJS.validation) { + throw new WinJS.ErrorFromName("WinJS.Binding.UnsupportedDataType", WinJS.Resources._formatString(strings.unsupportedDataTypeForBinding)); + } + else { + return; + } + } + + return WinJS.Class.mix( + function (init) { + /// + /// + /// Creates a new observable object. + /// + /// + /// The initial values for the properties. + /// + /// + + this._initObservable(init || Object.create(data)); + }, + WinJS.Binding.dynamicObservableMixin, + WinJS.Binding.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) + && !(data instanceof Array)) { + 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; + }; + + WinJS.Namespace.define("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 + }); +})(WinJS); + +(function dataTemplateInit(WinJS, undefined) { + "use strict"; + + var markSupportedForProcessing = WinJS.Utilities.markSupportedForProcessing; + + WinJS.Namespace.define("WinJS.Binding", { + + /// + /// Provides a reusable declarative binding element. + /// + /// Template + ///
      Place content here
      ]]>
      + /// + /// + /// + /// + Template: WinJS.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. + /// + /// + + msWriteProfilerMark("WinJS.Binding:newTemplate,StartTM"); + + this._element = element = element || document.createElement("div"); + var that = this; + if (element) { + element.renderItem = function (item, recycled) { return that.renderItem(item, recycled); }; + } + if (options) { + this.href = options.href; + this.enableRecycling = !!options.enableRecycling; + if (options.processTimeout) { + this.processTimeout = options.processTimeout; + } + } + if (!this.href) { + this.element.style.display = "none"; + } + this.bindingCache = { expressions: {} }; + + msWriteProfilerMark("WinJS.Binding:newTemplate,StopTM"); + }, + { + /// + /// 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: 0, + + /// + element: { + get: function () { return this._element; }, + }, + + 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. + /// + /// + 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. + // + var renderComplete = WinJS.Promise.as(item).then(function (i) { + WinJS.Binding.processAll(recycled, i.data, true, that.bindingCache); + }); + return { element: recycled, renderComplete: renderComplete }; + } + } + + var render = this._renderImpl(item.then(function (i) { return i.data; })); + render.element = render.element.then(function (e) { e.msOriginalTemplate = that; return e; }); + return render; + }, + + render: 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. + /// + /// + + var render = this._renderImpl(dataContext, container); + return render.element.then(function () { return render.renderComplete; }); + }, + _renderImpl: function (dataContext, container) { + msWriteProfilerMark("WinJS.Binding:templateRender,StartTM"); + + var d = container || document.createElement("div"); + WinJS.Utilities.addClass(d, "win-template"); + WinJS.Utilities.addClass(d, "win-loading"); + var that = this; + function done() { + WinJS.Utilities.removeClass(d, "win-loading"); + msWriteProfilerMark("WinJS.Binding:templateRender,StopTM"); + return d; + } + var initial = d.children.length; + var element = WinJS.UI.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(d, a, b, c); }; + } + // 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 + // + else { + 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 WinJS.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(WinJS.UI.processAll). + then(function () { + return dataContext; + }). + then(function Template_renderImpl_Binding_processAll(data) { + // !initial -- skipRoot when we do process on the container + return work(WinJS.Binding.processAll, data, !initial, that.bindingCache); + }); + } + if (timeout) { + if (timeout < 0) { timeout = 0; } + return WinJS.Promise.timeout(timeout).then(complete); + } + else { + return complete(); + } + }).then(done, function (err) { done(); return WinJS.Promise.wrapError(err); }); + + return { element: element, renderComplete: renderComplete }; + } + } + ) + }); + + markSupportedForProcessing(WinJS.Binding.Template.prototype.render); + + Object.defineProperties(WinJS.Binding.Template, { + 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 WinJS.Binding.Template(null, { href: href }).render(dataContext, container); + } + } + }); + + if (WinJS.Utilities && WinJS.Utilities.QueryCollection) { + WinJS.Class.mix(WinJS.Utilities.QueryCollection, { + 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 WinJS.Utilities.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(WinJS.Promise.join(donePromises)); + + return this; + } + }); + } + +})(WinJS); + +(function declarativeInit(WinJS, global, undefined) { + "use strict"; + + var uid = (Math.random() * 1000) >> 0; + + var optimizeBindingReferences = false; + + var strings = { + get attributeBindingSingleProperty() { return WinJS.Resources._getWinJSString("base/attributeBindingSingleProperty").value; }, + get cannotBindToThis() { return WinJS.Resources._getWinJSString("base/cannotBindToThis").value; }, + get creatingNewProperty() { return WinJS.Resources._getWinJSString("base/creatingNewProperty").value; }, + get dataSourceNotFound() { return WinJS.Resources._getWinJSString("base/dataSourceNotFound").value; }, + get duplicateBindingDetected() { return WinJS.Resources._getWinJSString("base/duplicateBindingDetected").value; }, + get elementNotFound() { return WinJS.Resources._getWinJSString("base/elementNotFound").value; }, + get errorInitializingBindings() { return WinJS.Resources._getWinJSString("base/errorInitializingBindings").value; }, + get propertyDoesNotExist() { return WinJS.Resources._getWinJSString("base/propertyDoesNotExist").value; }, + get idBindingNotSupported() { return WinJS.Resources._getWinJSString("base/idBindingNotSupported").value; }, + get nestedDOMElementBindingNotSupported() { return WinJS.Resources._getWinJSString("base/nestedDOMElementBindingNotSupported").value; } + } + + var markSupportedForProcessing = WinJS.Utilities.markSupportedForProcessing; + var requireSupportedForProcessing = WinJS.Utilities.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 inContainer(baseElement, control, start) { + if (control && control.constructor.isDeclarativeControlContainer) { return true; } + if (start && start !== baseElement && start.parentNode && start.parentNode !== baseElement) { + start = start.parentNode; + return inContainer(baseElement, start.winControl, start); + } + return false; + } + + function checkBindingToken(element, bindingId) { + if (element) { + if (element.winBindingToken === bindingId) { + return element; + } + else { + WinJS.log && WinJS.log(WinJS.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(WinJS.Utilities._getWeakRefElement(ref), bindingId); + if (!found) { + WinJS.log && WinJS.log(WinJS.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 = WinJS.Binding.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 (WinJS.validation) { + throw new WinJS.ErrorFromName("WinJS.Binding.IdBindingNotSupported", WinJS.Resources._formatString(strings.idBindingNotSupported, bindingStr)); + } + WinJS.log && WinJS.log(WinJS.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(WinJS.Binding._bindingParser(bindingStr, global), bindingStr); + bindingCache.expressions[bindingStr] = declBind; + } + if (!declBind) { + declBind = declBindCache; + } + return declBind; + } + else { + return filterIdBinding(WinJS.Binding._bindingParser(bindingStr, global), bindingStr); + } + } + + function declarativeBindImpl(rootElement, dataContext, skipRoot, bindingCache, c, e, p) { + msWriteProfilerMark("WinJS.Binding:processAll,StartTM"); + + var pend = { + count: 0, + checkComplete: function checkComplete() { + this.count--; + if (this.count === 0) { + msWriteProfilerMark("WinJS.Binding:processAll,StopTM"); + c(); + } + } + }; + var baseElement = (rootElement || document.body); + var attr = "data-win-bind" + var elements = baseElement.querySelectorAll("[" + attr + "]"); + var neg; + if (!skipRoot && baseElement.getAttribute(attr)) { + neg = baseElement; + } + + pend.count++; + var source = dataContext || global; + + WinJS.Utilities._DOMWeakRefTable_fastLoadPath = true; + try { + for (var i = (neg ? -1 : 0), l = elements.length; i < l; i++) { + var element = i < 0 ? neg : elements[i]; + // If this element is inside of a declarative control container + // (e.g. WinJS.Binding.Template) then we don't process it. + // + if (inContainer(baseElement, element.winControl, element)) { + continue; + } + var original = element.getAttribute(attr); + var declBind = calcBinding(original, bindingCache); + + if (!declBind.implemented) { + for (var bindIndex = 0, bindLen = declBind.length; bindIndex < bindLen; bindIndex++) { + var bind = declBind[bindIndex]; + if (bind.initializer) { + bind.implementation = initializerOneBinding; + } + else { + bind.implementation = sourceOneBinding; + } + } + declBind.implemented = true; + } + + pend.count++; + + var ref = element.id; + var bindingId = setBindingToken(element); + + 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. + // + if (optimizeBindingReferences) { + ref = bindingId; + } else { + element.id = ref = bindingId; + } + } + WinJS.Utilities._createWeakRef(element, ref); + var elementData = WinJS.Utilities.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); + } + } + pend.count--; + } + } + finally { + WinJS.Utilities._DOMWeakRefTable_fastLoadPath = false; + } + pend.checkComplete(); + } + + function declarativeBind(rootElement, dataContext, skipRoot, bindingCache) { + /// + /// + /// 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. + /// + /// + /// A promise that completes when each item that contains binding declarations has + /// been processed and the update has started. + /// + /// + + return new WinJS.Promise(function (c, e, p) { + declarativeBindImpl(rootElement, dataContext, skipRoot, bindingCache, c, e, p); + }).then(null, function (e) { + WinJS.log && WinJS.log(WinJS.Resources._formatString(strings.errorInitializingBindings, e && e.message), "winjs binding", "error"); + return WinJS.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) { + var ref = dest.id; + var bindingId = setBindingToken(dest); + + if (!ref) { + uid++; + if (optimizeBindingReferences) { + ref = bindingId; + } else { + dest.id = ref = bindingId; + } + } + WinJS.Utilities._createWeakRef(dest, ref); + + var workerResult = bindWorker(WinJS.Binding.as(source), sourceProperties, function (v) { + var found = checkBindingToken(WinJS.Utilities._getWeakRefElement(ref), bindingId); + if (found) { + nestedSet(found, destProperties, convert(requireSupportedForProcessing(v))); + } + else if (workerResult) { + WinJS.log && WinJS.log(WinJS.Resources._formatString(strings.elementNotFound, ref), "winjs binding", "info"); + workerResult.cancel(); + } + }); + + return workerResult; + }; + 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) { + WinJS.log && WinJS.log(WinJS.Resources._formatString(strings.propertyDoesNotExist, destProperties[i], destProperties.join(".")), "winjs binding", "error"); + return; + } + else if (dest instanceof Node) { + WinJS.log && WinJS.log(WinJS.Resources._formatString(strings.nestedDOMElementBindingNotSupported, destProperties[i], destProperties.join(".")), "winjs binding", "error"); + return; + } + } + if (destProperties.length === 0) { + WinJS.log && WinJS.log(strings.cannotBindToThis, "winjs binding", "error"); + return; + } + var prop = destProperties[destProperties.length - 1]; + if (WinJS.log) { + if (dest[prop] === undefined) { + WinJS.log(WinJS.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]) { + WinJS.log && WinJS.log(strings.attributeBindingSingleProperty, "winjs binding", "error"); + return; + } + dest.setAttribute(destProperties[0], v); + } + + function setAttribute(source, sourceProperties, dest, destProperties) { + /// + /// + /// 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. + /// + /// + /// An object with a cancel method that is used to coalesce bindings. + /// + /// + + var ref = dest.id; + var bindingId = setBindingToken(dest); + + if (!ref) { + uid++; + if (optimizeBindingReferences) { + ref = bindingId; + } else { + dest.id = ref = bindingId; + } + } + WinJS.Utilities._createWeakRef(dest, ref); + + var workerResult = bindWorker(WinJS.Binding.as(source), sourceProperties, function (v) { + var found = checkBindingToken(WinJS.Utilities._getWeakRefElement(ref), bindingId); + if (found) { + attributeSet(found, destProperties, requireSupportedForProcessing(v)); + } + else if (workerResult) { + WinJS.log && WinJS.log(WinJS.Resources._formatString(strings.elementNotFound, ref), "winjs binding", "info"); + workerResult.cancel(); + } + }); + + return workerResult; + } + 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)); + } + + var defaultBindImpl = converter(function defaultBind_passthrough(v) { return v; }); + + function defaultBind(source, sourceProperties, dest, destProperties) { + /// + /// + /// 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. + /// + /// + /// An object with a cancel method that is used to coalesce bindings. + /// + /// + + return defaultBindImpl(source, sourceProperties, dest, destProperties); + } + 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 WinJS.Binding.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); + } + + WinJS.Namespace.define("WinJS.Binding", { + processAll: declarativeBind, + oneTime: initializer(oneTime), + defaultBind: initializer(defaultBind), + converter: converter, + initializer: initializer, + setAttribute: initializer(setAttribute), + setAttributeOneTime: initializer(setAttributeOneTime), + optimizeBindingReferences: { + get: function () { return optimizeBindingReferences; }, + set: function (v) { + if (!!v && WinJS.Utilities.hasWinRT && global.msSetWeakWinRTProperty && global.msGetWeakWinRTProperty) { + optimizeBindingReferences = true; + } else { + optimizeBindingReferences = false; + } + } + }, + }); + +})(WinJS, this); + +(function DOMWeakRefTableInit(global, undefined) { + "use strict"; + + var U = WinJS.Utilities; + + // Defaults + var SWEEP_PERIOD = 500; + var TIMEOUT = 1000; + var table = {}; + var cleanupToken; + + function cleanup() { + if (U._DOMWeakRefTable_sweepPeriod === 0) { // If we're using post + cleanupToken = 0; // indicate that cleanup has run + } + var keys = Object.keys(table); + var time = Date.now() - U._DOMWeakRefTable_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 ((Debug.debuggerEnabled && U._DOMWeakRefTable_noTimeoutUnderDebugger) || cleanupToken) { + return; + } + var period = U._DOMWeakRefTable_sweepPeriod; + if (period === 0) { + setImmediate(cleanup); + cleanupToken = 1; + } else { + cleanupToken = setInterval(cleanup, U._DOMWeakRefTable_sweepPeriod); + } + } + + function unscheduleCleanupIfNeeded() { + if (Debug.debuggerEnabled && U._DOMWeakRefTable_noTimeoutUnderDebugger) { + return; + } + var period = U._DOMWeakRefTable_sweepPeriod; + if (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 + setImmediate(cleanup); // schedule another call to cleanup + cleanupToken = 1; // and protect against overscheduling + } + } + } else if (cleanupToken) { + if (Object.keys(table).length === 0) { + clearInterval(cleanupToken); + cleanupToken = 0; + } + } + } + + function createWeakRef(element, id) { + table[id] = { element: element, time: Date.now() }; + scheduleCleanupIfNeeded(); + return id; + } + + function getWeakRefElement(id) { + if (WinJS.Utilities._DOMWeakRefTable_fastLoadPath) { + var entry = table[id]; + if (entry) { + return entry.element; + } + else { + return document.getElementById(id); + } + } + else { + var element = 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; + } + } + + WinJS.Namespace.define("WinJS.Utilities", { + _DOMWeakRefTable_noTimeoutUnderDebugger: true, + _DOMWeakRefTable_sweepPeriod: SWEEP_PERIOD, + _DOMWeakRefTable_timeout: TIMEOUT, + _DOMWeakRefTable_tableSize: { get: function () { return Object.keys(table).length; } }, + _DOMWeakRefTable_fastLoadPath: false, + _createWeakRef: createWeakRef, + _getWeakRefElement: getWeakRefElement + + }); + + if (WinJS.Utilities.hasWinRT && global.msSetWeakWinRTProperty && global.msGetWeakWinRTProperty) { + + var host = new Windows.Foundation.Uri("about://blank"); + + WinJS.Namespace.define("WinJS.Utilities", { + + _createWeakRef: function (element, id) { + msSetWeakWinRTProperty(host, id, element); + return id; + }, + + _getWeakRefElement: function (id) { + return msGetWeakWinRTProperty(host, id); + } + + }); + + } + +}(this)); + +// WinJS.Binding.List +// +(function listInit(global, undefined) { + "use strict"; + + var strings = { + get sparseArrayNotSupported() { return WinJS.Resources._getWinJSString("base/sparseArrayNotSupported").value; }, + get illegalListLength() { return WinJS.Resources._getWinJSString("base/illegalListLength").value; }, + }; + + 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 = WinJS.Utilities._createEventProperty; + + var ListBase = WinJS.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 (item) { + /// + /// + /// 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 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 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 SortedListProjection(this, sorter); + } + }, { + supportedForProcessing: false, + } + ); + + WinJS.Class.mix(ListBase, WinJS.Binding.observableMixin); + WinJS.Class.mix(ListBase, WinJS.Utilities.eventMixin); + + var ListBaseWithMutators = WinJS.Class.derive(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, + } + ); + + var emptyOptions = {}; + + var List = WinJS.Class.derive(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 WinJS.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 = WinJS.Binding.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 = WinJS.Binding.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 = WinJS.Binding.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 WinJS.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 = WinJS.Binding.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(); + }, + + 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 (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. + /// + this._initializeKeys(); + var length = arguments.length; + for (var i = 0; i < length; i++) { + var item = arguments[i]; + if (this._binding) { + item = WinJS.Binding.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 (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. + /// + this._initializeKeys(); + var length = arguments.length; + for (var i = length - 1; i >= 0; i--) { + var item = arguments[i]; + if (this._binding) { + item = WinJS.Binding.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 = WinJS.Binding.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, howMany) { + this._initializeKeys(); + var args = copyargs(arguments); + args[0] = this._keys.indexOf(key); + return this.splice.apply(this, args); + } + }, { + supportedForProcessing: false, + } + ); + + var ListProjection = WinJS.Class.derive(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 WinJS.Binding.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, + } + ); + + var FilteredListProjection = WinJS.Class.derive(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) { + this._listItemRemoved({ detail: { key: key, index: index, value: oldValue, item: oldItem } }); + } else if (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); + if (filteredIndex !== -1) { + if (!filter(value)) { + filteredKeys.splice(filteredIndex, 1); + this._notifyItemRemoved(key, filteredIndex, value, item); + return; + } + } else { + if (filter(value)) { + this._listItemInserted({ detail: { key: key, index: this._list.indexOfKey(key), value: value } }); + return; + } + } + this._notifyItemMutated(key, value, item); + }, + _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 WinJS.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, + } + ); + + // 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; + } + + var SortedListProjection = WinJS.Class.derive(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 WinJS.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. + // + var GroupedSortedListProjection = WinJS.Class.derive(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(); + 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 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. + // + var GroupsListProjection = WinJS.Class.derive(ListBase, + function (list, groupKeyOf, groupDataOf) { + this._list = list; + // itemchanged is handled by the GroupedSortedListProjection because if the item + // changes groups it turns into a remove/insert + 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; + }, + + _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, + } + ); + + WinJS.Namespace.define("WinJS.Binding", { + List: List + }); + +}(this)); + + +(function resInit(WinJS, undefined) { + "use strict"; + + var readyComplete = false; + var resourceMap; + var resourceLoader; + + var requireSupportedForProcessing = WinJS.Utilities.requireSupportedForProcessing + + function processAllImpl(rootElement, count) { + rootElement = rootElement || 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 = WinJS.UI.optionsParser(rootElementNode); + setMembers(rootElement, rootElement, decls, count); + } + } + } + + var elements = rootElement.querySelectorAll('[data-win-res]'); + if (elements.length === 0) { + return WinJS.Promise.as(rootElement); + } + + for (var i = 0, len = elements.length; i < len; i++) { + var e = elements[i]; + // Use optionsParser that accept string format + // {name="value", name2="value2"} + var decls = WinJS.UI.optionsParser(e.getAttribute('data-win-res')); + setMembers(e, e, decls, count); + } + + } + else if (WinJS.validation) { + throw new WinJS.ErrorFromName("WinJS.Res.NestingExceeded", WinJS.Resources._getWinJSString("base/nestingExceeded").value); + } + + return WinJS.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 = WinJS.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 (WinJS.validation) { + notFound(value); + } + } + } + + function notFound(name) { + throw new WinJS.ErrorFromName("WinJS.Res.NotFound", WinJS.Resources._formatString(WinJS.Resources._getWinJSString("base/notFound").value, 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 = WinJS.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 (WinJS.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); + } + } + } + + WinJS.Namespace.define("WinJS.Resources", { + processAll: function (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 WinJS.Utilities.ready().then(function () { + readyComplete = true; + return processAllImpl(rootElement); + }); + } + else { + try { + return processAllImpl(rootElement); + } + catch (e) { + return WinJS.Promise.wrapError(e); + } + } + } + }); +})(WinJS); + diff --git a/shared/html/libs/winjs/1.0/js/en-us/base.strings.js b/shared/html/libs/winjs/1.0/js/en-us/base.strings.js new file mode 100644 index 0000000..854ade8 --- /dev/null +++ b/shared/html/libs/winjs/1.0/js/en-us/base.strings.js @@ -0,0 +1,63 @@ +/// +/*! + © Microsoft. All rights reserved. + + This library is supported for use in Windows Store apps only. + + Build: 1.0.9200.20602.win8_ldr.130108-1504 + + Version: Microsoft.WinJS.1.0 +*/ + +(function (global) { + global.strings = global.strings || {}; + + var appxVersion = "Microsoft.WinJS.1.0"; + var developerPrefix = "Developer."; + if (appxVersion.indexOf(developerPrefix) === 0) { + appxVersion = appxVersion.substring(developerPrefix.length); + } + + function addStrings(keyPrefix, strings) { + Object.keys(strings).forEach(function (key) { + global.strings[keyPrefix + key.replace("\\", "/")] = strings[key]; + }); + } + +addStrings( +"ms-resource://"+appxVersion+"/base/", + +{ + "attributeBindingSingleProperty": "Attribute binding requires a single destination attribute name, often in the form \"this['aria-label']\" or \"width\".", + "bindingInitializerNotFound": "Initializer not found:'{0}'", + "cannotBindToThis": "Can't bind to 'this'.", + "creatingNewProperty": "Creating new property {0}. Full path:{1}", + "dataSourceNotFound": "Data source not found:{0}", + "duplicateBindingDetected": "Binding against element with id {0} failed because a duplicate id was detected.", + "elementNotFound": "Element not found:{0}", + "errorActivatingControl": "Error activating control: {0}", + "errorInitializingBindings": "Error initializing bindings: {0}", + "exceptionFromBindingInitializer": "Exception thrown from binding initializer: {0}", + "idBindingNotSupported": "Declarative binding to ID field is not supported. Initializer: {0}", + "illegalListLength": "List length must be assigned a finite positive number", + "invalidBinding": "Invalid binding:'{0}'. Expected to be ':;'. {1}", + "invalidFragmentUri": "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.", + "invalidOptionsRecord": "Invalid options record: '{0}', expected to be in the format of an object literal. {1}", + "malformedFormatStringInput": "Malformed, did you mean to escape your '{0}'?", + "nestedDOMElementBindingNotSupported": "Binding through a property {0} of type HTMLElement is not supported, Full path:{1}.", + "nestingExceeded": "NestingExceeded", + "notFound": "NotFound: {0}", + "notSupportedForProcessing": "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}'", + "nonStaticHTML": "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).", + "propertyDoesNotExist": "{0} doesn't exist. Full path:{1}", + "propertyIsUndefined": "{0} is undefined", + "sparseArrayNotSupported": "Sparse arrays are not supported with proxy: true", + "unexpectedTokenExpectedToken": "Unexpected token: {0}, expected token: {1}, at offset {2}", + "unexpectedTokenExpectedTokens": "Unexpected token: {0}, expected one of: {1}, at offset {2}", + "unexpectedTokenGeneric": "Unexpected token: {0}, at offset {1}", + "unsupportedDataTypeForBinding": "Unsupported data type" +} + +); + +}(this)); diff --git a/shared/html/libs/winjs/1.0/js/en-us/ui.strings.js b/shared/html/libs/winjs/1.0/js/en-us/ui.strings.js new file mode 100644 index 0000000..1235df7 --- /dev/null +++ b/shared/html/libs/winjs/1.0/js/en-us/ui.strings.js @@ -0,0 +1,300 @@ +/// +/*! + © Microsoft. All rights reserved. + + This library is supported for use in Windows Store apps only. + + Build: 1.0.9200.20602.win8_ldr.130108-1504 + + Version: Microsoft.WinJS.1.0 +*/ + +(function (global) { + global.strings = global.strings || {}; + + var appxVersion = "Microsoft.WinJS.1.0"; + var developerPrefix = "Developer."; + if (appxVersion.indexOf(developerPrefix) === 0) { + appxVersion = appxVersion.substring(developerPrefix.length); + } + + function addStrings(keyPrefix, strings) { + Object.keys(strings).forEach(function (key) { + global.strings[keyPrefix + key.replace("\\", "/")] = strings[key]; + }); + } + +addStrings( +"ms-resource://"+appxVersion+"/ui/", + +{ + "appBarAriaLabel": "App Bar", + "appBarCommandAriaLabel": "App Bar Item", + "automaticallyLoadPagesIsInvalid": "Invalid argument: automaticallyLoadPages must be a boolean.", + "averageRating": "Average Rating", + "backbuttonarialabel": "Back", + "badAlignment": "Invalid argument: Flyout alignment should be 'center' (default), 'left', or 'right'.", + "badAxis": "Invalid argument: orientation must be a string, either 'horizontal' or 'vertical'", + "badButtonElement": "Invalid argument: For a button, toggle, or flyout command, the element must be null or a button element", + "badClick": "Invalid argument: The onclick property for an {0} must be a function", + "badCurrentPage": "Invalid argument: currentPage must be a number greater than or equal to zero and be within the bounds of the datasource", + "badFlyout": "Invalid argument: The flyout property for an {0} must be a Flyout or String id of a Flyout", + "badHrElement": "Invalid argument: For a separator, the element must be null or an hr element", + "badItemSpacingAmount": "Invalid argument: itemSpacing must be a number greater than or equal to zero", + "badLayout": "Invalid argument: The layout property must be 'custom' or 'commands'", + "badPlacement": "Invalid argument: Flyout placement should be 'top' (default), 'bottom', 'left', 'right', or 'auto'.", + "badReference": "Invalid argument: Invalid href to settings flyout fragment", + "cannotChangeCommandsWhenVisible": "Invalid argument: You must call hide() before changing {0} commands", + "cannotChangeHiddenProperty": "Unable to set hidden property while parent {0} is visible.", + "cannotChangeLayoutWhenVisible": "Invalid argument: The layout property cannot be set when the AppBar is visible, call hide() first", + "cannotChangePlacementWhenVisible": "Invalid argument: The placement property cannot be set when the AppBar is visible, call hide() first", + "clearYourRating" : "Clear your rating", + "closeOverlay" : "Close", + "duplicateConstruction": "Invalid argument: Controls may only be instantiated one time for each DOM element", + "flipViewNavigationDuringStateChange": "Error: After changing itemDataSource or itemTemplate, any navigation in the FlipView control should be delayed until the pageselected event is fired.", + "flipViewPanningContainerAriaLabel": "Scrolling Container", + "flyoutAriaLabel": "Flyout", + "indexIsInvalid": "Invalid argument: index must be a non-negative integer.", + "invalidCountReturned": "Error: data adapter should return undefined, null, CountResult.unknown, or a non-negative integer for the count.", + "invalidIndexReturned": "Error: data adapter should return undefined, null or a non-negative integer for the index.", + "invalidItemReturned": "Error: data adapter returned item that is not an object.", + "invalidItemsManagerCallback": "Invalid argument: {0} must be a function.", + "invalidKeyReturned": "Error: data adapter returned item with undefined or null key.", + "invalidViewBoxChildren": "ViewBox expects to only have one child element", + "invalidTemplate": "Invalid template: Templates must be created before being passed to the ListView, and must contain a valid tree of elements.", + "invalidZoomFactor": "Invalid zoomFactor", + "invalidRequestedCountReturned": "Error: data adapter should return CountResult.unknown, CountResult.failure, or a non-negative integer for the count.", + "itemIsInvalid": "Invalid argument: item must be a DOM element that was returned by the Items Manager, and has not been replaced or released.", + "itemRendererIsInvalid": "Invalid argument: itemRenderer must be a function.", + "keyIsInvalid": "Invalid argument: key must be a string.", + "layoutIsInvalid": "Invalid argument: layout must be one of following values: 'verticalgrid', 'horizontalgrid' or 'list'.", + "layoutNotInitialized": "Layout is not initialized.", + "listDataAdapterIsInvalid": "Invalid argument: listDataAdapter must be an object or an array.", + "listDataSourceIsInvalid": "Invalid argument: dataSource must be an object.", + "listViewInvalidItem": "Item must provide index, key or description of corresponding item.", + "listViewViewportAriaLabel": "Scrolling Container", + "loadingBehaviorIsInvalid": "Invalid argument: loadingBehavior must be 'incremental' or 'randomAccess'.", + "menuCommandAriaLabel": "Menu Item", + "menuAriaLabel": "Menu", + "modeIsInvalid": "Invalid argument: mode must be one of following values: 'none', 'single' or 'multi'.", + "mustContainCommands": "Invalid HTML: AppBars/Menus must contain only AppBarCommands/MenuCommands", + "noAnchor": "Invalid argument: Showing flyout requires a DOM element as its parameter.", + "noitemsManagerForCount": "Invalid operation: can't get count if no dataSource has been set", + "notCompatibleWithSemanticZoom": "ListView can only be used with SemanticZoom if randomAccess loading behavior is specified.", + "off" : "Off", + "on" : "On", + "pagesToLoadIsInvalid": "Invalid argument: pagesToLoad must be a positive number.", + "pagesToLoadThresholdIsInvalid": "Invalid argument: pagesToLoadThreshold must be a positive number.", + "requiresCommands": "Invalid argument: commands must not be empty", + "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", + "tooltipStringsIsInvalid": "Invalid argument: tooltipStrings must be null or an array of strings.", + "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\\next": "\uE101", //  group:Media + "appBarIcons\\play": "\uE102", //  group:Media + "appBarIcons\\pause": "\uE103", //  group:Media + "appBarIcons\\edit": "\uE104", //  group:File + "appBarIcons\\save": "\uE105", //  group:File + "appBarIcons\\clear": "\uE106", //  group:File + "appBarIcons\\delete": "\uE107", //  group:File + "appBarIcons\\remove": "\uE108", //  group:File + "appBarIcons\\add": "\uE109", //  group:File + "appBarIcons\\cancel": "\uE10A", //  group:Editing + "appBarIcons\\accept": "\uE10B", //  group:General + "appBarIcons\\more": "\uE10C", //  group:General + "appBarIcons\\redo": "\uE10D", //  group:Editing + "appBarIcons\\undo": "\uE10E", //  group:Editing + "appBarIcons\\home": "\uE10F", //  group:General + "appBarIcons\\up": "\uE110", //  group:General + "appBarIcons\\forward": "\uE111", //  group:General + "appBarIcons\\right": "\uE111", //  group:General + "appBarIcons\\back": "\uE112", //  group:General + "appBarIcons\\left": "\uE112", //  group:General + "appBarIcons\\favorite": "\uE113", //  group:Media + "appBarIcons\\camera": "\uE114", //  group:System + "appBarIcons\\settings": "\uE115", //  group:System + "appBarIcons\\video": "\uE116", //  group:Media + "appBarIcons\\sync": "\uE117", //  group:Media + "appBarIcons\\download": "\uE118", //  group:Media + "appBarIcons\\mail": "\uE119", //  group:Mail and calendar + "appBarIcons\\find": "\uE11A", //  group:Data + "appBarIcons\\help": "\uE11B", //  group:General + "appBarIcons\\upload": "\uE11C", //  group:Media + "appBarIcons\\emoji": "\uE11D", //  group:Communications + "appBarIcons\\twopage": "\uE11E", //  group:Layout + "appBarIcons\\leavechat": "\uE11F", //  group:Communications + "appBarIcons\\mailforward": "\uE120", //  group:Mail and calendar + "appBarIcons\\clock": "\uE121", //  group:General + "appBarIcons\\send": "\uE122", //  group:Mail and calendar + "appBarIcons\\crop": "\uE123", //  group:Editing + "appBarIcons\\rotatecamera": "\uE124", //  group:System + "appBarIcons\\people": "\uE125", //  group:Communications + "appBarIcons\\closepane": "\uE126", //  group:Layout + "appBarIcons\\openpane": "\uE127", //  group:Layout + "appBarIcons\\world": "\uE128", //  group:General + "appBarIcons\\flag": "\uE129", //  group:Mail and calendar + "appBarIcons\\previewlink": "\uE12A", //  group:General + "appBarIcons\\globe": "\uE12B", //  group:Communications + "appBarIcons\\trim": "\uE12C", //  group:Editing + "appBarIcons\\attachcamera": "\uE12D", //  group:System + "appBarIcons\\zoomin": "\uE12E", //  group:Layout + "appBarIcons\\bookmarks": "\uE12F", //  group:Editing + "appBarIcons\\document": "\uE130", //  group:File + "appBarIcons\\protecteddocument": "\uE131", //  group:File + "appBarIcons\\page": "\uE132", //  group:Layout + "appBarIcons\\bullets": "\uE133", //  group:Editing + "appBarIcons\\comment": "\uE134", //  group:Communications + "appBarIcons\\mail2": "\uE135", //  group:Mail and calendar + "appBarIcons\\contactinfo": "\uE136", //  group:Communications + "appBarIcons\\hangup": "\uE137", //  group:Communications + "appBarIcons\\viewall": "\uE138", //  group:Data + "appBarIcons\\mappin": "\uE139", //  group:General + "appBarIcons\\phone": "\uE13A", //  group:Communications + "appBarIcons\\videochat": "\uE13B", //  group:Communications + "appBarIcons\\switch": "\uE13C", //  group:Communications + "appBarIcons\\contact": "\uE13D", //  group:Communications + "appBarIcons\\rename": "\uE13E", //  group:File + "appBarIcons\\pin": "\uE141", //  group:System + "appBarIcons\\musicinfo": "\uE142", //  group:Media + "appBarIcons\\go": "\uE143", //  group:General + "appBarIcons\\keyboard": "\uE144", //  group:System + "appBarIcons\\dockleft": "\uE145", //  group:Layout + "appBarIcons\\dockright": "\uE146", //  group:Layout + "appBarIcons\\dockbottom": "\uE147", //  group:Layout + "appBarIcons\\remote": "\uE148", //  group:System + "appBarIcons\\refresh": "\uE149", //  group:Data + "appBarIcons\\rotate": "\uE14A", //  group:Layout + "appBarIcons\\shuffle": "\uE14B", //  group:Media + "appBarIcons\\list": "\uE14C", //  group:Editing + "appBarIcons\\shop": "\uE14D", //  group:General + "appBarIcons\\selectall": "\uE14E", //  group:Data + "appBarIcons\\orientation": "\uE14F", //  group:Layout + "appBarIcons\\import": "\uE150", //  group:Data + "appBarIcons\\importall": "\uE151", //  group:Data + "appBarIcons\\browsephotos": "\uE155", //  group:Media + "appBarIcons\\webcam": "\uE156", //  group:System + "appBarIcons\\pictures": "\uE158", //  group:Media + "appBarIcons\\savelocal": "\uE159", //  group:File + "appBarIcons\\caption": "\uE15A", //  group:Media + "appBarIcons\\stop": "\uE15B", //  group:Media + "appBarIcons\\showresults": "\uE15C", //  group:Data + "appBarIcons\\volume": "\uE15D", //  group:Media + "appBarIcons\\repair": "\uE15E", //  group:System + "appBarIcons\\message": "\uE15F", //  group:Communications + "appBarIcons\\page2": "\uE160", //  group:Layout + "appBarIcons\\calendarday": "\uE161", //  group:Mail and calendar + "appBarIcons\\calendarweek": "\uE162", //  group:Mail and calendar + "appBarIcons\\calendar": "\uE163", //  group:Mail and calendar + "appBarIcons\\characters": "\uE164", //  group:Editing + "appBarIcons\\mailreplyall": "\uE165", //  group:Mail and calendar + "appBarIcons\\read": "\uE166", //  group:Mail and calendar + "appBarIcons\\link": "\uE167", //  group:Communications + "appBarIcons\\accounts": "\uE168", //  group:Communications + "appBarIcons\\showbcc": "\uE169", //  group:Mail and calendar + "appBarIcons\\hidebcc": "\uE16A", //  group:Mail and calendar + "appBarIcons\\cut": "\uE16B", //  group:Editing + "appBarIcons\\attach": "\uE16C", //  group:Mail and calendar + "appBarIcons\\paste": "\uE16D", //  group:Editing + "appBarIcons\\filter": "\uE16E", //  group:Data + "appBarIcons\\copy": "\uE16F", //  group:Editing + "appBarIcons\\emoji2": "\uE170", //  group:Mail and calendar + "appBarIcons\\important": "\uE171", //  group:Mail and calendar + "appBarIcons\\mailreply": "\uE172", //  group:Mail and calendar + "appBarIcons\\slideshow": "\uE173", //  group:Media + "appBarIcons\\sort": "\uE174", //  group:Data + "appBarIcons\\manage": "\uE178", //  group:System + "appBarIcons\\allapps": "\uE179", //  group:System + "appBarIcons\\disconnectdrive": "\uE17A", //  group:System + "appBarIcons\\mapdrive": "\uE17B", //  group:System + "appBarIcons\\newwindow": "\uE17C", //  group:System + "appBarIcons\\openwith": "\uE17D", //  group:System + "appBarIcons\\contactpresence": "\uE181", //  group:Communications + "appBarIcons\\priority": "\uE182", //  group:Mail and calendar + "appBarIcons\\uploadskydrive": "\uE183", //  group:File + "appBarIcons\\gototoday": "\uE184", //  group:Mail and calendar + "appBarIcons\\font": "\uE185", //  group:Editing + "appBarIcons\\fontcolor": "\uE186", //  group:Editing + "appBarIcons\\contact2": "\uE187", //  group:Communications + "appBarIcons\\folder": "\uE188", //  group:File + "appBarIcons\\audio": "\uE189", //  group:Media + "appBarIcons\\placeholder": "\uE18A", //  group:General + "appBarIcons\\view": "\uE18B", //  group:Layout + "appBarIcons\\setlockscreen": "\uE18C", //  group:System + "appBarIcons\\settile": "\uE18D", //  group:System + "appBarIcons\\cc": "\uE190", //  group:Media + "appBarIcons\\stopslideshow": "\uE191", //  group:Media + "appBarIcons\\permissions": "\uE192", //  group:System + "appBarIcons\\highlight": "\uE193", //  group:Editing + "appBarIcons\\disableupdates": "\uE194", //  group:System + "appBarIcons\\unfavorite": "\uE195", //  group:Media + "appBarIcons\\unpin": "\uE196", //  group:System + "appBarIcons\\openlocal": "\uE197", //  group:File + "appBarIcons\\mute": "\uE198", //  group:Media + "appBarIcons\\italic": "\uE199", //  group:Editing + "appBarIcons\\underline": "\uE19A", //  group:Editing + "appBarIcons\\bold": "\uE19B", //  group:Editing + "appBarIcons\\movetofolder": "\uE19C", //  group:File + "appBarIcons\\likedislike": "\uE19D", //  group:Data + "appBarIcons\\dislike": "\uE19E", //  group:Data + "appBarIcons\\like": "\uE19F", //  group:Data + "appBarIcons\\alignright": "\uE1A0", //  group:Editing + "appBarIcons\\aligncenter": "\uE1A1", //  group:Editing + "appBarIcons\\alignleft": "\uE1A2", //  group:Editing + "appBarIcons\\zoom": "\uE1A3", //  group:Layout + "appBarIcons\\zoomout": "\uE1A4", //  group:Layout + "appBarIcons\\openfile": "\uE1A5", //  group:File + "appBarIcons\\otheruser": "\uE1A6", //  group:System + "appBarIcons\\admin": "\uE1A7", //  group:System + "appBarIcons\\street": "\uE1C3", //  group:General + "appBarIcons\\map": "\uE1C4", //  group:General + "appBarIcons\\clearselection": "\uE1C5", //  group:Data + "appBarIcons\\fontdecrease": "\uE1C6", //  group:Editing + "appBarIcons\\fontincrease": "\uE1C7", //  group:Editing + "appBarIcons\\fontsize": "\uE1C8", //  group:Editing + "appBarIcons\\cellphone": "\uE1C9", //  group:Communications + "appBarIcons\\reshare": "\uE1CA", //  group:Communications + "appBarIcons\\tag": "\uE1CB", //  group:Data + "appBarIcons\\repeatone": "\uE1CC", //  group:Media + "appBarIcons\\repeatall": "\uE1CD", //  group:Media + "appBarIcons\\outlinestar": "\uE1CE", //  group:Data + "appBarIcons\\solidstar": "\uE1CF", //  group:Data + "appBarIcons\\calculator": "\uE1D0", //  group:General + "appBarIcons\\directions": "\uE1D1", //  group:General + "appBarIcons\\target": "\uE1D2", //  group:General + "appBarIcons\\library": "\uE1D3", //  group:Media + "appBarIcons\\phonebook": "\uE1D4", //  group:Communications + "appBarIcons\\memo": "\uE1D5", //  group:Communications + "appBarIcons\\microphone": "\uE1D6", //  group:System + "appBarIcons\\postupdate": "\uE1D7", //  group:Communications + "appBarIcons\\backtowindow": "\uE1D8", //  group:Layout + "appBarIcons\\fullscreen": "\uE1D9", //  group:Layout + "appBarIcons\\newfolder": "\uE1DA", //  group:File + "appBarIcons\\calendarreply": "\uE1DB", //  group:Mail and calendar + "appBarIcons\\unsyncfolder": "\uE1DD", //  group:File + "appBarIcons\\reporthacked": "\uE1DE", //  group:Communications + "appBarIcons\\syncfolder": "\uE1DF", //  group:File + "appBarIcons\\blockcontact": "\uE1E0", //  group:Communications + "appBarIcons\\switchapps": "\uE1E1", //  group:System + "appBarIcons\\addfriend": "\uE1E2", //  group:Communications + "appBarIcons\\touchpointer": "\uE1E3", //  group:System + "appBarIcons\\gotostart": "\uE1E4", //  group:System + "appBarIcons\\zerobars": "\uE1E5", //  group:System + "appBarIcons\\onebar": "\uE1E6", //  group:System + "appBarIcons\\twobars": "\uE1E7", //  group:System + "appBarIcons\\threebars": "\uE1E8", //  group:System + "appBarIcons\\fourbars": "\uE1E9" //  group:System +} +); + +}(this)); diff --git a/shared/html/libs/winjs/1.0/js/ui.js b/shared/html/libs/winjs/1.0/js/ui.js new file mode 100644 index 0000000..ee71f06 --- /dev/null +++ b/shared/html/libs/winjs/1.0/js/ui.js @@ -0,0 +1,37270 @@ +/// +/*! + © Microsoft. All rights reserved. + + This library is supported for use in Windows Store apps only. + + Build: 1.0.9200.20602.win8_ldr.130108-1504 + + Version: Microsoft.WinJS.1.0 +*/ +/// +/// + + + +(function animationsInit(WinJS) { + "use strict"; + +var thisWinUI = WinJS.UI; +var mstransform = "transform"; + +// Default to 11 pixel from the left (or right if RTL) +var defaultOffset = [{ top: "0px", left: "11px", rtlflip: true }]; + +var OffsetArray = WinJS.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 (!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 window.getComputedStyle(elem).direction === "ltr" ? keyframe : keyframeRtl; + } +} + +function makeArray(elements) +{ + if (Array.isArray(elements) || elements instanceof NodeList || elements instanceof HTMLCollection) { + return elements; + } else if (elements) { + return [elements]; + } else { + return []; + } +} + +var isSnapped = function() { + if (WinJS.Utilities.hasWinRT) { + var appView = Windows.UI.ViewManagement.ApplicationView; + var snapped = Windows.UI.ViewManagement.ApplicationViewState.snapped; + isSnapped = function() { + return appView.value === snapped; + }; + } else { + isSnapped = function() { return false; } + } + return isSnapped(); +}; + +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 = window.getComputedStyle(elemArray[i], null).transform.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.transform = "translate(" + offsetArray[i].left + "px, " + offsetArray[i].top + "px)"; + } + } + return thisWinUI.executeTransition(elemArray, transition); +} + +function translateCallback(offsetArray, prefix) { + prefix = prefix || ""; + return function (i, elem) { + var offset = offsetArray.getOffset(i); + var left = offset.left; + if (offset.rtlflip && window.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, elem) { + var offset = offsetArray[i]; + return "translate(" + offset.left + "px, " + offset.top + "px) " + suffix; + }; +} + +function keyframeCallbackAnimate(offsetArray, keyframe) +{ + return function (i, elem) { + 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); +} + +var ExpandAnimation = WinJS.Class.define(function ExpandAnimation_ctor(revealedArray, affectedArray, offsetArray) { + // Constructor + this.revealedArray = revealedArray; + this.affectedArray = affectedArray; + this.offsetArray = offsetArray; +},{ // Public Members + execute: function () { + var promise1 = thisWinUI.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: mstransform, + delay: 0, + duration: 367, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "" + }); + return WinJS.Promise.join([promise1, promise2]); + } +}, { // Static Members + supportedForProcessing: false, +}); + +var CollapseAnimation = WinJS.Class.define(function CollapseAnimation_ctor(hiddenArray, affectedArray, offsetArray) { + // Constructor + this.hiddenArray = hiddenArray; + this.affectedArray = affectedArray; + this.offsetArray = offsetArray; +},{ // Public Members + execute: function () { + var promise1 = thisWinUI.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: mstransform, + delay: this.hiddenArray.length > 0 ? 167 : 0, + duration: 367, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "" + }); + return WinJS.Promise.join([promise1, promise2]); + } +}, { // Static Members + supportedForProcessing: false, +}); + +var RepositionAnimation = WinJS.Class.define(function RepositionAnimation_ctor(target, elementArray, offsetArray) { + // Constructor + this.elementArray = elementArray; + this.offsetArray = offsetArray; +},{ // Public Members + execute: function () { + return animTranslate2DTransform( + this.elementArray, + this.offsetArray, + { + property: mstransform, + delay : staggerDelay(0, 33, 1, 250), + duration : 367, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "" + }); + } +}, { // Static Members + supportedForProcessing: false, +}); + +var AddToListAnimation = WinJS.Class.define(function AddToListAnimation_ctor(addedArray, affectedArray, offsetArray) { + // Constructor + this.addedArray = addedArray; + this.affectedArray = affectedArray; + this.offsetArray = offsetArray; +},{ // Public Members + execute: function () { + var delay = this.affectedArray.length > 0 ? 240 : 0; + var promise1 = thisWinUI.executeAnimation( + this.addedArray, + [{ + keyframe: "WinJS-scale-up", + property: mstransform, + 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: mstransform, + delay: 0, + duration: 400, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "" + }); + return WinJS.Promise.join([promise1, promise2]); + } +}, { // Static Members + supportedForProcessing: false, +}); + +var DeleteFromListAnimation = WinJS.Class.define(function DeleteFromListAnimation_ctor(deletedArray, remainingArray, offsetArray) { + // Constructor + this.deletedArray = deletedArray; + this.remainingArray = remainingArray; + this.offsetArray = offsetArray; +},{ // Public Members + execute: function () { + var promise1 = thisWinUI.executeAnimation( + this.deletedArray, + [{ + keyframe: "WinJS-scale-down", + property: mstransform, + 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: mstransform, + delay: this.deletedArray.length > 0 ? 60 : 0, + duration: 400, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "" + }); + return WinJS.Promise.join([promise1, promise2]); + } +}, { // Static Members + supportedForProcessing: false, +}); + +var _UpdateListAnimation = WinJS.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 () { + makeOffsetsRelative(this.deletedArray, this.deletedOffsetArray); + + var delay = 0; + var promise1 = thisWinUI.executeAnimation( + this.deletedArray, + [{ + keyframe: keyframeCallbackAnimate(this.deletedOffsetArray, "WinJS-scale-down"), + property: mstransform, + 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: mstransform, + 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 = thisWinUI.executeAnimation( + this.addedArray, + [{ + keyframe: "WinJS-scale-up", + property: mstransform, + 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 WinJS.Promise.join([promise1, promise2, promise3]); + } +}, { // Static Members + supportedForProcessing: false, +}); + + +var AddToSearchListAnimation = WinJS.Class.define(function AddToSearchListAnimation_ctor(addedArray, affectedArray, offsetArray) { + // Constructor + this.addedArray = addedArray; + this.affectedArray = affectedArray; + this.offsetArray = offsetArray; +},{ // Public Members + execute: function () { + var promise1 = thisWinUI.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: mstransform, + delay: 0, + duration: 400, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "" + }); + return WinJS.Promise.join([promise1, promise2]); + } +}, { // Static Members + supportedForProcessing: false, +}); + +var DeleteFromSearchListAnimation = WinJS.Class.define(function DeleteFromSearchListAnimation_ctor(deletedArray, remainingArray, offsetArray) { + // Constructor + this.deletedArray = deletedArray; + this.remainingArray = remainingArray; + this.offsetArray = offsetArray; +},{ // Public Members + execute: function () { + var promise1 = thisWinUI.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: mstransform, + delay: this.deletedArray.length > 0 ? 60 : 0, + duration: 400, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "" + }); + return WinJS.Promise.join([promise1, promise2]); + } +}, { // Static Members + supportedForProcessing: false, +}); + +var PeekAnimation = WinJS.Class.define(function PeekAnimation_ctor(target, elementArray, offsetArray) { + // Constructor + this.elementArray = elementArray; + this.offsetArray = offsetArray; +},{ // Public Members + execute: function () { + return animTranslate2DTransform( + this.elementArray, + this.offsetArray, + { + property: mstransform, + delay : 0, + duration : 2000, + timing : "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "" + }); + } +}, { // Static Members + supportedForProcessing: false, +}); + +WinJS.Namespace.define("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. + /// + /// + return thisWinUI.executeTransition( + shown, + { + property: "opacity", + delay: 0, + duration: 250, + timing: "linear", + from: 0, + to: 1 + }); + }, + + 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. + /// + /// + return thisWinUI.executeTransition( + hidden, + { + property: "opacity", + delay: 0, + duration: 167, + timing: "linear", + to: 0 + }); + }, + + 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) { + /// + /// + /// 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. + /// + /// + /// Promise object that completes when the animation is complete. + /// + /// + var offsetArray = new OffsetArray(offset, "WinJS-showEdgeUI", [{ top: "-70px", left: "0px" }]); + return thisWinUI.executeAnimation( + element, + { + keyframe: offsetArray.keyframe, + property: mstransform, + delay: 0, + duration: 367, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + from: offsetArray.keyframe || translateCallback(offsetArray), + to: "none" + }); + }, + + 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 + /// + /// + var offsetArray = new OffsetArray(offset, "WinJS-showPanel", [{ top: "0px", left: "364px", rtlflip: true }]); + return thisWinUI.executeAnimation( + element, + { + keyframe: offsetArray.keyframe, + property: mstransform, + delay: 0, + duration: 550, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + from: offsetArray.keyframe || translateCallback(offsetArray), + to: "none" + }); + }, + + + hideEdgeUI: function (element, offset) { + /// + /// + /// 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. + /// + /// + /// Promise object that completes when the animation is complete. + /// + /// + var offsetArray = new OffsetArray(offset, "WinJS-hideEdgeUI", [{ top: "-70px", left: "0px" }]); + return thisWinUI.executeAnimation( + element, + { + keyframe: offsetArray.keyframe, + property: mstransform, + delay: 0, + duration: 367, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + from: "none", + to: offsetArray.keyframe || translateCallback(offsetArray) + }); + }, + + 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. + /// + /// + var offsetArray = new OffsetArray(offset, "WinJS-hidePanel", [{ top: "0px", left: "364px", rtlflip: true }]); + return thisWinUI.executeAnimation( + element, + { + keyframe: offsetArray.keyframe, + property: mstransform, + delay: 0, + duration: 550, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + from: "none", + to: offsetArray.keyframe || translateCallback(offsetArray) + }); + }, + + 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. + /// + /// + var offsetArray = new OffsetArray(offset, "WinJS-showPopup", [{ top: "50px", left: "0px" }]); + return thisWinUI.executeAnimation( + element, + [{ + keyframe: "WinJS-opacity-in", + property: "opacity", + delay: 83, + duration: 83, + timing: "linear", + from: 0, + to: 1 + }, + { + keyframe: offsetArray.keyframe, + property: mstransform, + delay: 0, + duration: 367, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + from: offsetArray.keyframe || translateCallback(offsetArray), + to: "none" + }]); + }, + + 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. + /// + /// + return thisWinUI.executeAnimation( + element, + { + keyframe: "WinJS-opacity-out", + property: "opacity", + delay: 0, + duration: 83, + timing: "linear", + from: 1, + to: 0 + }); + }, + + 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. + /// + /// + return thisWinUI.executeTransition( + element, + { + property: mstransform, + delay: 0, + duration: 167, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "scale(0.975, 0.975)" + }); + }, + + 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. + /// + /// + return thisWinUI.executeTransition( + element, + { + property: mstransform, + delay: 0, + duration: 167, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "" + }); + }, + + 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. + /// + /// + var promise1 = thisWinUI.executeTransition( + dragSource, + [{ + property: mstransform, + 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 = thisWinUI.executeTransition( + affected, + { + property: mstransform, + delay: 0, + duration: 240, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "scale(0.95)" + }); + return WinJS.Promise.join([promise1, promise2]); + }, + + 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. + /// + /// + var offsetArray = new OffsetArray(offset, "WinJS-dragSourceEnd"); + var promise1 = thisWinUI.executeTransition( + dragSource, + [{ + property: mstransform, + 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 = thisWinUI.executeAnimation( + dragSource, + { + keyframe: offsetArray.keyframe, + property: mstransform, + 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 = thisWinUI.executeTransition( + affected, + { + property: mstransform, + delay : 0, + duration : 500, + timing : "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "" + }); + return WinJS.Promise.join([promise1, promise2, promise3]); + }, + + + enterContent: function (incoming, offset) { + /// + /// + /// 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. + /// + /// + /// Promise object that completes when the animation is complete. + /// + /// + var offsetArray; + if (isSnapped()) { + offsetArray = new OffsetArray(offset, "WinJS-enterContent-snapped", [{ top: "0px", left: "20px", rtlflip: true }]); + } else { + offsetArray = new OffsetArray(offset, "WinJS-enterContent", [{ top: "0px", left: "40px", rtlflip: true }]); + } + var promise1 = thisWinUI.executeAnimation( + incoming, + { + keyframe: offsetArray.keyframe, + property: mstransform, + delay: 0, + duration: 550, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + from: offsetArray.keyframe || translateCallback(offsetArray), + to: "none" + }); + var promise2 = thisWinUI.executeTransition( + incoming, + { + property: "opacity", + delay: 0, + duration: 170, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + from: 0, + to: 1 + }); + return WinJS.Promise.join([promise1, promise2]); + }, + + 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. + /// + /// + var offsetArray = new OffsetArray(offset, "WinJS-exit", [{ top: "0px", left: "0px" }]); + var promise1 = thisWinUI.executeAnimation( + outgoing, + offset && { + keyframe: offsetArray.keyframe, + property: mstransform, + delay: 0, + duration: 117, + timing: "linear", + from: "none", + to: offsetArray.keyframe || translateCallback(offsetArray) + }); + + var promise2 = thisWinUI.executeTransition( + outgoing, + { + property: "opacity", + delay: 0, + duration: 117, + timing: "linear", + to: 0 + }); + return WinJS.Promise.join([promise1, promise2]); + }, + + 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. + /// + /// + var offsetArray = new OffsetArray(offset, null, [{ top: "-40px", left: "0px" }, { top: "40px", left: "0px" }]); + return thisWinUI.executeTransition( + target, + { + property: mstransform, + delay: 0, + duration: 200, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: translateCallback(offsetArray, "scale(0.95) ") + }); + }, + + 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. + /// + /// + return thisWinUI.executeTransition( + target, + { + property: mstransform, + delay: 0, + duration: 200, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "scale(0.95)" + }); + }, + + 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. + /// + /// + var promise1 = thisWinUI.executeTransition( + selected, + { + property: mstransform, + delay: 0, + duration: 300, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "" + }); + + var promise2 = thisWinUI.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 WinJS.Promise.join([promise1, promise2]); + }, + + 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. + /// + /// + var promise1 = thisWinUI.executeTransition( + deselected, + { + property: mstransform, + delay: 0, + duration: 300, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: "" + }); + + var promise2 = thisWinUI.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 WinJS.Promise.join([promise1, promise2]); + }, + + 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. + /// + /// + var offsetArray = new OffsetArray(offset, null, [{ top: "25px", left: "0px" }]); + return thisWinUI.executeTransition( + target, + { + property: mstransform, + delay: 0, + duration: 300, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + to: translateCallback(offsetArray) + }); + }, + + 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. + /// + /// + var offsetArray; + if (isSnapped()) { + offsetArray = new OffsetArray(offset, "WinJS-enterPage-snapped", [{ top: "0px", left: "40px", rtlflip: true }]); + } else { + offsetArray = new OffsetArray(offset, "WinJS-enterPage", [{ top: "0px", left: "100px", rtlflip: true }]); + } + var promise1 = thisWinUI.executeAnimation( + element, + { + keyframe: offsetArray.keyframe, + property: mstransform, + 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 = thisWinUI.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 WinJS.Promise.join([promise1, promise2]); + }, + + 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. + /// + /// + var offsetArray = new OffsetArray(offset, "WinJS-exit", [{ top: "0px", left: "0px" }]); + var promise1 = thisWinUI.executeAnimation( + outgoing, + offset && { + keyframe: offsetArray.keyframe, + property: mstransform, + delay: 0, + duration: 117, + timing: "linear", + from: "none", + to: offsetArray.keyframe || translateCallback(offsetArray) + }); + + var promise2 = thisWinUI.executeTransition( + outgoing, + { + property: "opacity", + delay: 0, + duration: 117, + timing: "linear", + to: 0 + }); + return WinJS.Promise.join([promise1, promise2]); + }, + + 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. + /// + /// + var promise1 = thisWinUI.executeTransition( + incoming, + { + property: "opacity", + delay: 0, + duration: 167, + timing: "linear", + to: 1 + }); + + var promise2 = thisWinUI.executeTransition( + outgoing, + { + property: "opacity", + delay: 0, + duration: 167, + timing: "linear", + to: 0 + }); + return WinJS.Promise.join([promise1, promise2]); + }, + + 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. + /// + /// + var offsetArray = new OffsetArray(offset, "WinJS-updateBadge", [{ top: "24px", left: "0px" }]); + return thisWinUI.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: mstransform, + delay: 0, + duration: 1333, + timing: "cubic-bezier(0.1, 0.9, 0.2, 1)", + from: offsetArray.keyframe || translateCallback(offsetArray), + to: "none" + }]); + } +}); + +})(WinJS); + +/*#DBG +var _ASSERT = function (condition) { + if (!condition) { + throw "ASSERT FAILED"; + } +}; +var _TRACE = function (text) { + if (window.console && console.log) { + console.log(text); + } +}; +#DBG*/ + + +// WinJS.Binding.ListDataSource +// +(function bindingListDataSourceInit(global, undefined) { + "use strict"; + + + var errors = { + get noLongerMeaningful() { return WinJS.Promise.wrapError(new WinJS.ErrorFromName(WinJS.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 = WinJS.Promise.wrap().constructor; + + var NullWrappedItem = WinJS.Class.derive(CompletePromise, + function () { + this._value = null; + }, { + release: function () { }, + retain: function () { return this; } + }, { + /* empty */ + }, { + supportedForProcessing: false, + } + ); + + var WrappedItem = WinJS.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; + } + }, { + /* empty */ + }, { + supportedForProcessing: false, + } + ); + + var AsyncWrappedItem = WinJS.Class.derive(WinJS.Promise, + function (listBinding, item) { + var that = this; + this._item = item; + this._listBinding = listBinding; + WinJS.Promise.call(this, function (c) { + setImmediate(function () { + if (listBinding._released) { + that.cancel(); + return; + } + c(item); + }); + }); + }, { + 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; + } + }, { + /* empty */ + }, { + supportedForProcessing: false, + } + ); + + function wrap(listBinding, item) { + return item ? new WrappedItem(listBinding, item) : new NullWrappedItem(); + } + + function wrapAsync(listBinding, item) { + return item ? new AsyncWrappedItem(listBinding, item) : new NullWrappedItem(); + } + + function cloneWithIndex(list, item, index) { + return item && list._annotateWithIndex(item, index); + } + + var ListBinding = WinJS.Class.define( + function ListBinding_ctor(dataSource, list, notificationHandler) { + this._dataSource = dataSource; + this._list = list; + this._editsCount = 0; + this._notificationHandler = notificationHandler; + this._pos = -1; + this._retained = []; + this._retained.length = list.length; + this._retainedKeys = {}; + if (this._notificationHandler) { + var that = this; + this._handlers = { + itemchanged: function (event) { + var key = event.detail.key; + var index = event.detail.index; + var newItem = event.detail.newItem; + var oldItem = that._retained[index]; + if (oldItem) { + if (oldItem.index !== index) { + var oldIndex = oldItem.index; + oldItem.index = index; + if (that._notificationHandler.indexChanged) { + that._notificationHandler.indexChanged(newItem.key, index, oldIndex); + } + } + newItem = cloneWithIndex(list, newItem, index); + newItem._retainedCount = oldItem._retainedCount; + that._retained[index] = newItem; + that._retainedKeys[key] = newItem; + + that._beginEdits(list.length); + if (notificationHandler && notificationHandler.changed) { + notificationHandler.changed( + newItem, + oldItem + ); + } + that._endEdits(); + } + }, + iteminserted: function (event) { + var key = event.detail.key; + var index = event.detail.index; + that._beginEdits(list.length - 1); + if (index <= that._pos) { + that._pos = Math.min(that._pos + 1, list.length); + } + var retained = that._retained; + // create a hole for this thing and then immediately make it undefined + retained.splice(index, 0, 0); + delete retained[index]; + if (that._shouldNotify(index) || list.length === 1) { + if (notificationHandler && notificationHandler.inserted) { + notificationHandler.inserted( + wrap(that, cloneWithIndex(list, list.getItem(index), index)), + findPreviousKey(list, index), + findNextKey(list, index) + ); + } + } + that._endEdits(); + }, + itemmoved: function (event) { + var key = event.detail.key; + var oldIndex = event.detail.oldIndex; + var newIndex = event.detail.newIndex; + that._beginEdits(list.length); + if (oldIndex < that._pos || newIndex <= that._pos) { + if (newIndex > that._pos) { + that._pos = Math.max(-1, that._pos - 1); + } else if (oldIndex > that._pos) { + that._pos = Math.min(that._pos + 1, list.length); + } + } + var retained = that._retained; + var item = retained.splice(oldIndex, 1)[0]; + retained.splice(newIndex, 0, item); + if (!item) { + delete retained[newIndex]; + item = cloneWithIndex(list, list.getItem(newIndex), newIndex); + } + item._moved = true; + that._addRef(item, newIndex); + that._endEdits(); + }, + itemremoved: function (event) { + var key = event.detail.key; + var index = event.detail.index; + that._beginEdits(list.length + 1); + if (index < that._pos) { + that._pos = Math.max(-1, that._pos - 1); + } + var retained = that._retained; + var retainedKeys = that._retainedKeys; + var wasRetained = index in retained; + retained.splice(index, 1); + delete retainedKeys[key]; + if (wasRetained && notificationHandler && notificationHandler.removed) { + notificationHandler.removed(key, false); + } + that._endEdits(); + }, + reload: function () { + that._retained = []; + that._retainedKeys = {}; + if (notificationHandler && notificationHandler.reload) { + notificationHandler.reload(); + } + } + }; + subscribe(this._list, this._handlers); + } + }, { + _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) { + //#DBG _ASSERT(retained.key === item.key); + if (retained._retainedCount === 1) { + delete this._retained[index]; + delete this._retainedKeys[retained.key]; + } else { + retained._retainedCount--; + } + } + /*#DBG + // If an item isn't found in the retained map, it was either removed from retainedCount reaching zero, or removed from the map by a removed notification. + // We'll decrement the count here for debugging purposes. If retainedCount is less than zero, there's a refcounting error somewhere. + if (!retained) { + item._retainedCount--; + _ASSERT(item._retainedCount >= 0); + } + #DBG*/ + }, + _shouldNotify: function (index) { + var retained = this._retained; + return index in retained || index + 1 in retained || index - 1 in retained; + }, + + _notifyCountChanged: function () { + var oldCount = this._countAtBeginEdits; + var newCount = this._list.length; + if (oldCount !== newCount) { + if (this._notificationHandler && this._notificationHandler.countChanged) { + this._notificationHandler.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; + if (this._notificationHandler && this._notificationHandler.indexChanged) { + this._notificationHandler.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)) { + if (this._notificationHandler && this._notificationHandler.moved) { + this._notificationHandler.moved( + wrap(this, item), + findPreviousKey(this._list, i), + findNextKey(this._list, i) + ); + } + } + } + } + }, + + _beginEdits: function (length, explicit) { + this._editsCount++; + if (this._editsCount === 1 && this._notificationHandler) { + if (!explicit) { + // Batch all edits between now and the setImmediate firing. This has the effect + // of batching synchronous edits. + // + this._editsCount++; + var that = this; + setImmediate(function () { + that._endEdits(); + }); + } + if (this._notificationHandler && this._notificationHandler.beginNotifications) { + this._notificationHandler.beginNotifications(); + } + this._countAtBeginEdits = length; + } + }, + _endEdits: function () { + this._editsCount--; + if (this._editsCount === 0 && this._notificationHandler) { + this._notifyIndicesChanged(); + this._notifyMoved(); + this._notifyCountChanged(); + if (this._notificationHandler && this._notificationHandler.endNotifications) { + this._notificationHandler.endNotifications(); + } + } + }, + + jumpToItem: function (item) { + var index = this._list.indexOfKey(item.handle); + if (index === -1) { + return WinJS.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); + }, + next: function () { + this._pos = Math.min(this._pos + 1, this._list.length); + return this._fromIndex(this._pos, true); + }, + 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: function (index, async) { + 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) : wrap(this, item); + }, + }, { + supportedForProcessing: false, + } + ); + + function insertAtStart(unused, data) { + // List ignores the key because its key management is internal + this._list.unshift(data); + return this.itemFromIndex(0); + } + function insertBefore(unused, data, nextKey) { + // 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) { + // 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) { + // 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) { + var index = this._list.indexOfKey(key); + if (index === -1) { + return errors.noLongerMeaningful; + } + this._list.setAt(index, newData); + return this.itemFromIndex(index); + } + function moveToStart(key) { + 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) { + 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) { + 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) { + 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) { + var index = this._list.indexOfKey(key); + if (index === -1) { + return errors.noLongerMeaningful; + } + this._list.splice(index, 1); + return WinJS.Promise.wrap(); + } + + var bindingId = 0; + var DataSource = WinJS.Class.define( + function DataSource_ctor(list) { + 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 binding = new ListBinding(this, this._list, notificationHandler); + binding._id = (++bindingId); + this._bindings[binding._id] = binding; + return binding; + }, + + getCount: function () { + return WinJS.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 WinJS.Promise.wrap(item); + }, + itemFromIndex: function (index) { + return WinJS.Promise.wrap(cloneWithIndex(this._list, this._list.getItem(index), index)); + }, + + list: { + get: function () { return this._list; } + }, + + beginEdits: function () { + var length = this._list.length; + this._bindings.forEach(function (binding) { + binding._beginEdits(length, true); + }); + }, + endEdits: function () { + this._bindings.forEach(function (binding) { + binding._endEdits(); + }); + }, + + invalidateAll: function() { + return WinJS.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, + } + ); + + if (WinJS.Binding && WinJS.Binding.List) { + Object.defineProperty(Object.getPrototypeOf(Object.getPrototypeOf(WinJS.Binding.List.prototype)), "dataSource", { + get: function () { return (this._dataSource = this._dataSource || new DataSource(this)); } + }); + } + +}(this)); + +// Virtualized Data Source + +(function listDataSourceInit(undefined) { + "use strict"; + + +var Promise = WinJS.Promise, + Signal = WinJS._Signal, + UI = WinJS.UI; + +// Private statics + +var strings = { + get listDataAdapterIsInvalid() { return WinJS.Resources._getWinJSString("ui/listDataAdapterIsInvalid").value; }, + get indexIsInvalid() { return WinJS.Resources._getWinJSString("ui/indexIsInvalid").value; }, + get keyIsInvalid() { return WinJS.Resources._getWinJSString("ui/keyIsInvalid").value; }, + get invalidItemReturned() { return WinJS.Resources._getWinJSString("ui/undefinedItemReturned").value; }, + get invalidKeyReturned() { return WinJS.Resources._getWinJSString("ui/invalidKeyReturned").value; }, + get invalidIndexReturned() { return WinJS.Resources._getWinJSString("ui/invalidIndexReturned").value; }, + get invalidCountReturned() { return WinJS.Resources._getWinJSString("ui/invalidCountReturned").value; }, + get invalidRequestedCountReturned() { return WinJS.Resources._getWinJSString("ui/invalidRequestedCountReturned").value; }, +}; + +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 + + 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; + + /*#DBG + + var totalSlots = 0; + + function VERIFYLIST() { + _ASSERT(slotListEnd.lastInSequence); + _ASSERT(slotsEnd.firstInSequence); + checkListIntegrity(slotsStart, slotsEnd, keyMap, indexMap); + } + + function VERIFYREFRESHLIST() { + checkListIntegrity(refreshStart, refreshEnd, refreshKeyMap, refreshIndexMap); + } + + function checkListIntegrity(listStart, listEnd, keyMapForSlot, indexMapForSlot) { + if (UI.VirtualizedDataSource._internalValidation) { + var listEndReached, + slotWithoutIndexReached; + for (var slotCheck = listStart; slotCheck !== listEnd; slotCheck = slotCheck.next) { + _ASSERT(slotCheck.next); + _ASSERT(slotCheck.next.prev === slotCheck); + if (slotCheck.lastInSequence) { + _ASSERT(slotCheck.next.firstInSequence); + } + + if (slotCheck !== listStart) { + _ASSERT(slotCheck.prev); + _ASSERT(slotCheck.prev.next === slotCheck); + if (slotCheck.firstInSequence) { + _ASSERT(slotCheck.prev.lastInSequence); + } + } + + if (slotCheck.item || slotCheck.itemNew) { + _ASSERT(editsQueued || slotCheck.key); + } + + if (slotCheck.key) { + _ASSERT(keyMapForSlot[slotCheck.key] === slotCheck); + + if (slotCheck.item) { + _ASSERT(slotCheck.item.key === slotCheck.key); + } + } + + if (typeof slotCheck.index === "number") { + _ASSERT(!listEndReached); + + if (!indexUpdateDeferred) { + _ASSERT(indexMapForSlot[slotCheck.index] === slotCheck); + _ASSERT(slotCheck === listStart || slotCheck.prev.index < slotCheck.index); + _ASSERT(!slotCheck.firstInSequence || !slotCheck.prev || slotCheck.prev.index !== slotCheck.index - 1); + _ASSERT(!slotCheck.lastInSequence || !slotCheck.next || slotCheck.next.index !== slotCheck.index + 1); + + if (slotCheck.item) { + _ASSERT(listStart === refreshStart || slotCheck.item.index === slotCheck.index); + } + } + } else { + slotWithoutIndexReached = true; + } + + if (slotCheck === slotListEnd) { + listEndReached = true; + } + + if (slotCheck.lastInSequence && !listEndReached && !indexUpdateDeferred) { + _ASSERT(!slotWithoutIndexReached); + } + } + } + } + + function OUTPUTLIST() { + outputList("Main List", slotsStart); + } + + function OUTPUTREFRESHLIST() { + outputList("Refresh List", refreshStart); + } + + function outputList(header, slotFirst) { + _TRACE("-- " + header + " --"); + + for (var slot = slotFirst; slot; slot = slot.next) { + var line = (slot.firstInSequence ? "[" : " "); + + if (slot.index !== undefined && slot !== slotsStart && slot !== refreshStart) { + line += slot.index + ": "; + } + + if (slot === slotsStart || slot === refreshStart) { + line += "{"; + } else if (slot === slotListEnd || slot === refreshEnd) { + line += "}"; + } else if (slot === slotsEnd) { + line += "-"; + } else { + line += (slot.key ? '"' + slot.key + '"' : "?"); + } + + if (slot.bindingMap) { + line += " ("; + var first = true; + for (var listBindingID in slot.bindingMap) { + if (first) { + first = false; + } else { + line += ", "; + } + + line += listBindingID; + } + line += ")"; + } + + if (slot.itemNew) { + line += " itemNew"; + } + + if (slot.item) { + line += " item"; + } + + if (slot.fetchListeners) { + line += " fetching"; + } + + if (slot.directFetchListeners) { + line += " directFetching"; + } + + if (slot.indexRequested) { + line += " index"; + } + + if (slot.keyRequested) { + line += " key"; + } + + if (slot.description) { + line += " description=" + JSON.stringify(slot.description); + } + + if (slotFetchInProgress(slot)) { + line += " now"; + } + + if (typeof slot.handle === "string") { + line += " <" + slot.handle + ">"; + } + + if (slot.lastInSequence) { + line += "]"; + } + + _TRACE(line); + } + } + + #DBG*/ + + 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 WinJS.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 WinJS.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() { + /*#DBG + totalSlots++; + #DBG*/ + + return createSlot(); + } + + function insertSlot(slot, slotNext) { + //#DBG _ASSERT(slotNext); + //#DBG _ASSERT(slotNext.prev); + + slot.prev = slotNext.prev; + slot.next = slotNext; + + slot.prev.next = slot; + slotNext.prev = slot; + } + + function removeSlot(slot) { + //#DBG _ASSERT(slot.prev.next === slot); + //#DBG _ASSERT(slot.next.prev === slot); + //#DBG _ASSERT(slot !== slotsStart && slot !== slotListEnd && slot !== slotsEnd); + + 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) { + //#DBG _ASSERT(slotFirst !== slotsStart && slotLast !== slotListEnd && slotLast !== slotsEnd); + //#DBG _ASSERT(slotFirst.firstInSequence && slotLast.lastInSequence); + //#DBG _ASSERT(slotNext.firstInSequence && slotNext.prev.lastInSequence); + + 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) { + //#DBG _ASSERT(slotFirst !== slotsStart && slotLast !== slotsEnd); + //#DBG _ASSERT(slotFirst.firstInSequence && slotLast.lastInSequence); + //#DBG _ASSERT(slotPrev.lastInSequence && slotPrev.next.firstInSequence); + + 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) { + //#DBG _ASSERT(slotNext.firstInSequence); + + 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) { + //#DBG _ASSERT(!slot.key); + //#DBG _ASSERT(!keyMap[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) { + //#DBG _ASSERT(indexUpdateDeferred || !indexMapForSlot[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) { + //#DBG _ASSERT(slotNext.prev.lastInSequence); + //#DBG _ASSERT(slotNext.firstInSequence); + + 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) { + //#DBG _ASSERT(slotNext.firstInSequence); + //#DBG _ASSERT(slotNext.prev.lastInSequence); + 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) { + //#DBG _ASSERT(slotPrev !== slotListEnd); + //#DBG _ASSERT(slotPrev.lastInSequence); + //#DBG _ASSERT(slotPrev.next.firstInSequence); + 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); + //#DBG _ASSERT(!keyMap[slot.key]); + keyMap[slot.key] = slot; + var index = slot.index; + if (slot.index !== undefined) { + indexMap[slot.index] = slot; + } + } + + function removeSlotPermanently(slot) { + /*#DBG + _ASSERT(totalSlots > 0); + totalSlots--; + #DBG*/ + + 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) { + //#DBG _ASSERT(index !== undefined); + + // 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) { + //#DBG _ASSERT(slotNext); + //#DBG _ASSERT(slotNext.index !== index); + + 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) { + //#DBG _ASSERT(slot !== slotsStart && slot !== slotsEnd); + 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 WinJS.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) { + 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)) || + (!listDataAdapter.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)) { + if (UI._PerfMeasurement_leakSlots) { + return; + } + + 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; + setImmediate(function () { + reduceReleasedSlotCountPosted = false; + reduceReleasedSlotCount(); + }); + } + } + } + } + + // 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) { + //#DBG _ASSERT(bindingRecord.notificationHandler); + + 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) { + //#DBG _ASSERT(bindingRecord.notificationHandler); + + 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) { + //#DBG _ASSERT(bindingRecord.notificationHandler); + if (bindingRecord.notificationHandler.indexChanged) { + handlerToNotify(bindingRecord).indexChanged(handleForBinding(slot, listBindingID), slot.index, indexOld); + } + }); + } + + function changeSlotIndex(slot, index) { + //#DBG _ASSERT(indexUpdateDeferred || ((typeof slot.index !== "number" || indexMap[slot.index] === slot) && !indexMap[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) { + //#DBG _ASSERT(!slot.indexRequested); + 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) { + //#DBG _ASSERT(bindingRecord.notificationHandler); + 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]; + //#DBG _ASSERT(bindingRecord.notificationHandler); + 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) { + //#DBG _ASSERT((!slot.fetchListeners && !slot.directFetchListeners) || !slot.item); + completeFetchPromises(slot, true); + + forEachBindingRecordOfSlot(slot, function (bindingRecord, listBindingID) { + //#DBG _ASSERT(bindingRecord.notificationHandler); + 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; + } + + //#DBG _ASSERT(slot !== slotsStart); + + 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) { + //#DBG _ASSERT(typeof slot.indexNew !== "number"); + + 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) { + //#DBG _ASSERT(indexDelta !== 0); + + // 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) { + //#DBG _ASSERT(indexDelta !== 0); + + 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) { + //#DBG _ASSERT(index !== slotUpdate.index || +index !== index || indexMap[index] === slotUpdate); + 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 + for (var listenerID2 in fetchListeners) { + 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) { + //#DBG _ASSERT(slot.index - 1 !== slot.prev.index); + addSlotBefore(slot, indexMap); + } + if (slot.lastInSequence) { + //#DBG _ASSERT(slot.index + 1 !== slot.next.index); + 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 WinJS.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; + } + + //#DBG _ASSERT(slot.key === key); + + return requestSlot(slot); + } + + function slotFromIndex(index) { + if (typeof index !== "number" || index < 0) { + throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.IndexIsInvalid", strings.indexIsInvalid); + } + + if (slotListEnd.index <= index) { + return null; + } + + var slot = indexMap[index]; + //#DBG _ASSERT(slot !== slotListEnd); + + 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); + } + } + + //#DBG _ASSERT(slot.index === 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 === WinJS.UI.DataSourceStatus.failure) { + dispatch(); + } else if (!statusChangePosted) { + statusChangePosted = true; + + // Delay the event to filter out rapid changes + 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 + //#DBG _ASSERT(!fetchID || fetchesInProgress[fetchID]); + 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) { + if (resultsValid(slot, refreshID, fetchID)) { + if (+index === index) { + fetchResult.absoluteIndex = index; + } + addMarkers(fetchResult); + processResults(slot, fetchResult.items, fetchResult.offset, fetchResult.totalCount, fetchResult.absoluteIndex); + } + } else { + return Promise.wrapError(new WinJS.ErrorFromName(FetchError.doesNotExist)); + } + }).then(null, function (error) { + if (resultsValid(slot, refreshID, fetchID)) { + processErrorResult(slot, error); + } + }); + } + + function fetchItemsForIndex(indexRequested, slot, promiseItems) { + var refreshID = currentRefreshID; + promiseItems.then(function (fetchResult) { + if (fetchResult.items && fetchResult.items.length) { + if (resultsValid(slot, refreshID, null)) { + //#DBG _ASSERT(+indexRequested === indexRequested); + fetchResult.absoluteIndex = indexRequested; + addMarkers(fetchResult); + processResultsForIndex(indexRequested, slot, fetchResult.items, fetchResult.offset, fetchResult.totalCount, fetchResult.absoluteIndex); + } + } else { + return Promise.wrapError(new WinJS.ErrorFromName(FetchError.doesNotExist)); + } + }).then(null, function (error) { + if (resultsValid(slot, refreshID, null)) { + processErrorResultForIndex(indexRequested, slot, refreshID, name); + } + }); + } + + function fetchItemsFromStart(slot, count) { + //#DBG _ASSERT(!refreshInProgress); + + var fetchID = setFetchIDs(slot, 0, count - 1); + + if (listDataAdapter.itemsFromStart) { + fetchItems(slot, fetchID, listDataAdapter.itemsFromStart(count), 0); + } else { + fetchItems(slot, fetchID, listDataAdapter.itemsFromIndex(0, 0, count - 1), 0); + } + } + + function fetchItemsFromEnd(slot, count) { + //#DBG _ASSERT(!refreshInProgress); + + fetchItems(slot, setFetchIDs(slot, count - 1, 0), listDataAdapter.itemsFromEnd(count)); + } + + function fetchItemsFromKey(slot, countBefore, countAfter) { + //#DBG _ASSERT(!refreshInProgress); + //#DBG _ASSERT(listDataAdapter.itemsFromKey); + //#DBG _ASSERT(slot.key); + + fetchItems(slot, setFetchIDs(slot, countBefore, countAfter), listDataAdapter.itemsFromKey(slot.key, countBefore, countAfter, slot.hints)); + } + + function fetchItemsFromIndex(slot, countBefore, countAfter) { + //#DBG _ASSERT(!refreshInProgress); + //#DBG _ASSERT(slot !== slotsStart); + + var index = slot.index; + + // Don't ask for items with negative indices + if (countBefore > index) { + countBefore = index; + } + + if (listDataAdapter.itemsFromIndex) { + var fetchID = setFetchIDs(slot, countBefore, countAfter); + + fetchItems(slot, fetchID, listDataAdapter.itemsFromIndex(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) { + //#DBG _ASSERT(index > slotSearch.index); + 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) { + //#DBG _ASSERT(slotSearch.index > index); + delta = slotSearch.index - index; + if (closestDelta > delta) { + closestDelta = delta; + slotClosest = slotSearch; + } + break; + } + } + + if (slotClosest === slotsStart) { + fetchItemsForIndex(0, slot, listDataAdapter.itemsFromStart(index + 1)); + } else { + fetchItemsForIndex(slotClosest.index, slot, listDataAdapter.itemsFromKey( + slotClosest.key, + Math.max(slotClosest.index - index, 0), + Math.max(index - slotClosest.index, 0), + slot.hints + )); + } + } + } + } + + function fetchItemsFromDescription(slot, countBefore, countAfter) { + //#DBG _ASSERT(!refreshInProgress); + + fetchItems(slot, setFetchIDs(slot, countBefore, countAfter), listDataAdapter.itemsFromDescription(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) { + //#DBG _ASSERT(slot.key); + slotRequestedByKey = slot; + requestedKeyOffset = placeholderCount - 1; + } + + if (slot.description !== undefined && !slotRequestedByDescription) { + slotRequestedByDescription = slot; + requestedDescriptionOffset = placeholderCount - 1; + } + + if (slot.indexRequested && !slotRequestedByIndex) { + //#DBG _ASSERT(typeof slot.index === "number"); + 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 && listDataAdapter.itemsFromKey) { + fetchItemsFromKey(slotFirstPlaceholder.prev, 0, placeholderCount); + } else if (!slot.lastInSequence && slotNext.key && listDataAdapter.itemsFromKey) { + fetchItemsFromKey(slotNext, placeholderCount, 0); + } else if (slotFirstPlaceholder.prev === slotsStart && !slotFirstPlaceholder.firstInSequence && (listDataAdapter.itemsFromStart || listDataAdapter.itemsFromIndex)) { + fetchItemsFromStart(slotFirstPlaceholder, placeholderCount); + } else if (slotNext === slotListEnd && !slot.lastInSequence && listDataAdapter.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 + //#DBG _ASSERT(slot.lastInSequence); + 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; + setImmediate(function () { + fetchesPosted = false; + fetchItemsForAllSlots(); + + // A mirage sequence might have been removed + finishNotifications(); + }); + } + } + + // 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: + //#DBG _ASSERT(property !== "handle"); + //#DBG _ASSERT(property !== "index"); + 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) { + //#DBG _ASSERT(slot.itemNew); + + if (slot.item) { + changeSlotIfNecessary(slot); + } else { + //#DBG _ASSERT(slot.key); + completeFetchPromises(slot); + } + } + + function updateSlot(slot, item) { + //#DBG _ASSERT(item !== startMarker && item !== endMarker); + + if (!slot.key) { + setSlotKey(slot, item.key); + } + slot.itemNew = item; + + //#DBG _ASSERT(slot.key === item.key); + + 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; + //#DBG _ASSERT(bindingRecord.notificationHandler); + + 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 + //#DBG _ASSERT(!slot.item || !slot.itemNew); + + // Only one of the two slots should have a key + //#DBG _ASSERT(!slot.key || !slotToDiscard.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 + //#DBG _ASSERT(refreshInProgress || slot.index !== undefined); + + 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 = {}; + } + + //#DBG _ASSERT(!slot.bindingMap[listBindingID]); + + var slotBinding = bindingMap[listBindingID]; + + if (!slotBinding.handle) { + slotBinding.handle = slotToDiscard.handle; + } + //#DBG _ASSERT(handleMap[slotBinding.handle] === slotToDiscard); + 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; + //#DBG _ASSERT(!item || !slot.key); + + if (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) { + //#DBG _ASSERT(!item || slotToDiscard.key === item.key); + //#DBG _ASSERT(!slotToDiscard.bindingMap); + + 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 WinJS.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 WinJS.ErrorFromName("WinJS.UI.ListDataSource.InvalidKeyReturned", strings.invalidKeyReturned); + } else { + if (WinJS.validation) { + validateKey(result.key); + } + return keyMap[result.key]; + } + } + + function matchSlot(slot, result) { + //#DBG _ASSERT(result !== startMarker && result !== endMarker); + + // 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) { + //#DBG _ASSERT(typeof slot.index !== "number"); + //#DBG _ASSERT(+index === index || !indexMap[index]); + + 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); + } + + //#DBG _ASSERT(!slotWithIndex || slotWithIndex.prev.next !== 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) { + //#DBG _ASSERT(isNonNegativeInteger(countMax)); + + 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; ) { + //#DBG _ASSERT(!refreshInProgress); + + 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); + //#DBG _ASSERT(slot.index < countMax); + } else { + if (listDataAdapter.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) { + 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) { + 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(); + return; + } + + resultsProcessed = true; + + /*#DBG + VERIFYLIST(); + #DBG*/ + + (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) { + // 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; + 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; + } + } + + var 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); + } + //#DBG _ASSERT(!slotAfterSequence); + } 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) { + //#DBG _ASSERT(slotFirstInSequence === slotExisting); + 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)) { + //#DBG _ASSERT(!slot.key); + + slotBeforeSequence = (slot.firstInSequence ? null : slot.prev); + slotAfterSequence = (slot.lastInSequence ? null : slot.next); + + //#DBG _ASSERT(!slotBeforeSequence || !slotBeforeSequence.key); + //#DBG _ASSERT(!slotAfterSequence || !slotAfterSequence.key); + + 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) { + //#DBG _ASSERT(slotExisting.index === undefined); + 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(); + + /*#DBG + VERIFYLIST(); + #DBG*/ + + // Finally complete any promises for newly obtained items + callFetchCompleteCallbacks(); + } + + 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) { + //#DBG _ASSERT(isPlaceholder(slot)); + //#DBG _ASSERT(slot.index !== undefined); + + // 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 WinJS.ErrorFromName(UI.FetchError.doesNotExist)); + } else { + // We didn't get all the results we requested - pick up where they left off + if (slot.index < indexFirst) { + fetchItemsForIndex(indexFirst, slot, listDataAdapter.itemsFromKey( + results[0].key, + indexFirst - slot.index, + 0 + )); + } else { + var indexLast = indexFirst + resultsCount - 1; + //#DBG _ASSERT(slot.index > indexLast); + + fetchItemsForIndex(indexLast, slot, listDataAdapter.itemsFromKey( + 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. + //#DBG _ASSERT(refreshRequested); + } else { + // Something has changed, but this index might still exist, so request a refresh + beginRefresh(); + } + break; + + default: + processErrorResult(slot, error); + break; + } + } + + // Refresh + + function resetRefreshState() { + // 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; + + /*#DBG + refreshStart.debugInfo = "*** refreshStart ***"; + refreshEnd.debugInfo = "*** refreshEnd ***"; + #DBG*/ + + 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 + //#DBG _ASSERT(editsQueued); + applyNextEdit(); + return; + } + + if (editsQueued) { + // The refresh will be started once the edit queue empties out + return; + } + + var refreshID = ++currentRefreshID; + refreshInProgress = true; + refreshFetchesInProgress = 0; + + // Do the rest of the work asynchronously + setImmediate(function () { + if (currentRefreshID !== refreshID) { + return; + } + + //#DBG _ASSERT(refreshRequested); + 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(); + }); + } + + function requestRefresh() { + refreshSignal = refreshSignal || new Signal(); + + beginRefresh(); + + return refreshSignal.promise; + } + + function resultsValidForRefresh(refreshID, fetchID) { + // This fetch has completed, whatever it has returned + //#DBG _ASSERT(fetchesInProgress[fetchID]); + delete fetchesInProgress[fetchID]; + + if (refreshID !== currentRefreshID) { + // This information is out of date. Ignore it. + return false; + } + + //#DBG _ASSERT(refreshFetchesInProgress > 0); + refreshFetchesInProgress--; + + return true; + } + + function fetchItemsForRefresh(key, fromStart, fetchID, promiseItems, index) { + var refreshID = currentRefreshID; + + refreshFetchesInProgress++; + + promiseItems.then(function (fetchResult) { + if (fetchResult.items && fetchResult.items.length) { + if (resultsValidForRefresh(refreshID, fetchID)) { + addMarkers(fetchResult); + processRefreshResults(key, fetchResult.items, fetchResult.offset, fetchResult.totalCount, (typeof index === "number" ? index : fetchResult.absoluteIndex)); + } + } else { + return Promise.wrapError(new WinJS.ErrorFromName(FetchError.doesNotExist)); + } + }).then(null, function (error) { + if (resultsValidForRefresh(refreshID, fetchID)) { + processRefreshErrorResult(key, fromStart, error); + } + }); + } + + function refreshRange(slot, fetchID, countBefore, countAfter) { + if (listDataAdapter.itemsFromKey) { + // Keys are the preferred identifiers when the item might have moved + fetchItemsForRefresh(slot.key, false, fetchID, listDataAdapter.itemsFromKey(slot.key, countBefore, countAfter, slot.hints)); + } else { + // Request additional items to try to locate items that have moved + var searchDelta = 10, + index = slot.index; + + //#DBG _ASSERT(+index === index); + + if (refreshIndexMap[index] && refreshIndexMap[index].firstInSequence) { + // Ensure at least one element is observed before this one + fetchItemsForRefresh(slot.key, false, fetchID, listDataAdapter.itemsFromIndex(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, listDataAdapter.itemsFromIndex(index + 1, Math.min(countBefore + searchDelta, index) + 1, countAfter - 1 + searchDelta), index + 1); + } else { + fetchItemsForRefresh(slot.key, false, fetchID, listDataAdapter.itemsFromIndex(index, Math.min(countBefore + searchDelta, index), countAfter + searchDelta), index); + } + } + } + + function refreshFirstItem(fetchID) { + if (listDataAdapter.itemsFromStart) { + fetchItemsForRefresh(null, true, fetchID, listDataAdapter.itemsFromStart(1), 0); + } else if (listDataAdapter.itemsFromIndex) { + fetchItemsForRefresh(null, true, fetchID, listDataAdapter.itemsFromIndex(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 + //#DBG _ASSERT(listDataAdapter.itemsFromKey); + if (!refreshKeyMap[slot.key]) { + // Fetch at least one item before and after, just to verify item's position in list + fetchItemsForRefresh(slot.key, false, newFetchID(), listDataAdapter.itemsFromKey(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); + + /*#DBG + fetchID = undefined; + #DBG*/ + + 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()); + } + + //#DBG _ASSERT(fetchID === undefined); + } + + 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 WinJS.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 WinJS.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) { + //#DBG _ASSERT(result.key); + slotRefresh.key = result.key; + //#DBG _ASSERT(!refreshKeyMap[slotRefresh.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); + + /*#DBG + VERIFYREFRESHLIST(); + #DBG*/ + + 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) { + //#DBG _ASSERT(slotNext.firstInSequence); + + 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); + } + } + + /*#DBG + VERIFYREFRESHLIST(); + #DBG*/ + + 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 + //#DBG _ASSERT(refreshStart.next === refreshEnd); + //#DBG _ASSERT(refreshStart.lastInSequence && refreshEnd.firstInSequence); + + 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 { + //#DBG _ASSERT(!slotExisting); + return false; + } + } + + function indexForRefresh(slot) { + var indexNew; + + if (slot.indexRequested) { + //#DBG _ASSERT(!slot.key); + indexNew = slot.index; + } else { + var slotRefresh = slotRefreshFromSlot(slot); + if (slotRefresh) { + indexNew = slotRefresh.index; + } + } + + return indexNew; + } + + function concludeRefresh() { + //#DBG _ASSERT(refreshInProgress); + //#DBG _ASSERT(!indexUpdateDeferred); + + indexUpdateDeferred = true; + + keyFetchIDs = {}; + + var i, + j, + slot, + slotPrev, + slotNext, + slotBefore, + slotAfter, + slotRefresh, + slotExisting, + slotsAvailable = [], + slotFirstInSequence, + sequenceCountOld, + sequencesOld = [], + sequenceOld, + sequenceOldPrev, + sequenceOldBestMatch, + sequenceCountNew, + sequencesNew = [], + sequenceNew, + index, + offset; + + /*#DBG + VERIFYLIST(); + VERIFYREFRESHLIST(); + #DBG*/ + + // 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) { + //#DBG _ASSERT(slotRefresh); + + // Store the new item; this value will be compared with that stored in slot.item later + slot.itemNew = slotRefresh.item; + } else { + //#DBG _ASSERT(!slot.item); + + // 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; + } + + /*#DBG + VERIFYLIST(); + #DBG*/ + + // 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; + + //#DBG _ASSERT(!slot.key || refreshKeyMap[slot.key]); + + if (slot.indexRequested) { + //#DBG _ASSERT(!slot.item); + //#DBG _ASSERT(slot.index !== undefined); + + slotRefresh = refreshIndexMap[slot.index]; + if (slotRefresh) { + matchSlotForRefresh(slotFromSlotRefresh(slotRefresh), slot, slotRefresh); + } + } + + slot = slotNext; + } + + /*#DBG + VERIFYLIST(); + #DBG*/ + + // 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) { + //#DBG _ASSERT(slotRefresh.sequenceNumber !== undefined); + 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; + } + } + } + + //#DBG _ASSERT(sequenceOldEnd); + + // 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]) { + //#DBG _ASSERT(sequencesOld[0].first === slotsStart); + //#DBG _ASSERT(!slotsStart.lastInSequence); + 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]) { + //#DBG _ASSERT(sequenceOldEnd.last === slotListEnd); + //#DBG _ASSERT(!slotListEnd.firstInSequence); + 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; + + /*#DBG + VERIFYLIST(); + #DBG*/ + + // Merge additional indexed old sequences when possible + + // First do a forward pass + for (i = 0; i <= sequenceIndexEnd; i++) { + sequenceOld = sequencesOld[i]; + + //#DBG _ASSERT(sequenceOld); + if (sequenceOld.sequenceNew && (sequenceOldBestMatch = sequenceOld.sequenceNew.sequenceOld) === sequenceOldPrev && sequenceOldPrev.last !== slotListEnd) { + //#DBG _ASSERT(sequenceOldBestMatch.last.next === sequenceOld.first); + 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) { + //#DBG _ASSERT(sequenceOld.last.next === sequenceOldBestMatch.first); + 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)) { + //#DBG _ASSERT(!sequenceOld.indexRequested); + + // 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) { + //#DBG _ASSERT(!slot.indexRequested); + 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); + } + } + + /*#DBG + VERIFYLIST(); + #DBG*/ + + // 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) { + //#DBG _ASSERT(slot === sequenceOld.last); + 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; + //#DBG _ASSERT(stationaryItemCount > 0); + slotRefresh = piles[stationaryItemCount - 1]; + for (j = stationaryItemCount; j--; ) { + slotRefresh.stationary = true; + stationaryItems[j] = slotRefresh; + slotRefresh = slotRefresh.predecessor; + } + //#DBG _ASSERT(!slotRefresh); + 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); + //#DBG _ASSERT(slot); + var slotRefreshStop = stationaryItems[j + 1], + slotRefreshMergePoint = null, + slotStop = slotFromSlotRefresh(slotRefreshStop), + slotExisting; + //#DBG _ASSERT(slotStop); + + // 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; + } + } + } + } + } + } + } + + /*#DBG + VERIFYLIST(); + #DBG*/ + + // 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) { + //#DBG _ASSERT(slot !== slotsStart); + //#DBG _ASSERT(slot !== slotsEnd); + + 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; + } + } + } + } + + /*#DBG + VERIFYLIST(); + #DBG*/ + + // 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*/ + //#DBG _ASSERT(slotRefreshOld !== sequenceNew.lastInner); + } + 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; + } + } + } + } + + /*#DBG + VERIFYLIST(); + #DBG*/ + + // 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 { + //#DBG _ASSERT(slot.index !== undefined); + 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; + + /*#DBG + VERIFYLIST(); + #DBG*/ + + // Now that all the sequences have been moved, merge any colliding slots + mergeSequencePairs(sequencePairsToMerge); + + /*#DBG + VERIFYLIST(); + #DBG*/ + + // Send countChanged notification + if (refreshCount !== undefined && refreshCount !== knownCount) { + changeCount(refreshCount); + } + + /*#DBG + VERIFYLIST(); + #DBG*/ + + 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) { + //#DBG _ASSERT(slotRefresh === sequenceNew.last); + break; + } + } + + if (slot) { + fetchResults.push({ + slot: slot, + results: results, + offset: slotOffset + }); + } + } + + resetRefreshState(); + refreshInProgress = false; + + /*#DBG + VERIFYLIST(); + #DBG*/ + + // 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; + + //#DBG _ASSERT(editQueue.next !== editQueue); + 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 WinJS.ErrorFromName(UI.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) { + if (keyUpdate && keyUpdate.key !== item.key) { + //#DBG _ASSERT(edit.editType === EditType.insert); + + 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 { + var slot = keyUpdate.slot; + if (slot) { + var keyOld = slot.key; + if (keyOld) { + //#DBG _ASSERT(slot.key === keyOld); + //#DBG _ASSERT(keyMap[keyOld] === slot); + delete keyMap[keyOld]; + } + /*#DBG + // setSlotKey asserts that the slot key is absent + delete slot.key; + #DBG*/ + + setSlotKey(slot, keyNew); + slot.itemNew = item; + if (slot.item) { + changeSlot(slot); + finishNotifications(); + } else { + completeFetchPromises(slot); + } + } + } + } else if (edit.editType === EditType.change) { + //#DBG _ASSERT(slot.item); + 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() { + //#DBG _ASSERT(!editsInProgress); + + 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); + } + + 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; + /*#DBG + editQueue.debugInfo = "*** editQueueHead/Tail ***"; + #DBG*/ + + // 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; + + /*#DBG + slotsStart.debugInfo = "*** slotsStart ***"; + slotListEnd.debugInfo = "*** slotListEnd ***"; + slotsEnd.debugInfo = "*** slotsEnd ***"; + #DBG*/ + + // 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 WinJS.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(); + + /*#DBG + this._debugBuild = true; + + Object.defineProperty(this, "_totalSlots", { + get: function () { + return totalSlots; + } + }); + + Object.defineProperty(this, "_releasedSlots", { + get: function () { + return releasedSlots; + } + }); + #DBG*/ + + // 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) { + //#DBG _ASSERT(slot.cursorCount > 0); + 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]; + + //#DBG _ASSERT(slot); + 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 (listDataAdapter.itemsFromStart || listDataAdapter.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 (listDataAdapter.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 (listDataAdapter.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 (listDataAdapter.itemsFromIndex || (listDataAdapter.itemsFromStart && listDataAdapter.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 (listDataAdapter.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 (promise, dataSource) { + var signal = new WinJS._Signal(); + promise.then( + function (v) { signal.complete(v); }, + function (e) { signal.error(e); } + ); + var count = 0; + return { + get: function () { + count++; + return new Promise( + function (c, error) { + signal.promise.then(c, function (e) { + if (e.name === "WinJS.UI.VirtualizedDataSource.resetCount") { + getCountPromise = null; + return dataSource.getCount(); + } + error(e); + }); + }, + function () { + if (--count === 0) { + // when the count reaches zero cancel the incoming promise + promise.cancel(); + } + } + ); + }, + reset: function () { + signal.error(new WinJS.ErrorFromName("WinJS.UI.VirtualizedDataSource.resetCount")); + }, + cancel: function () { + // if explicitly asked to cancel the incoming promise + promise.cancel(); + } + }; + } + + 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.timeout().then(function () { + if (editsInProgress || editsQueued) { + return knownCount; + } + + var requestPromise; + + if (!getCountPromise) { + + // Make a request for the count + // + requestPromise = listDataAdapter.getCount(); + var synchronous; + requestPromise.then( + function () { + getCountPromise = null; + synchronous = true; + }, + function () { + 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) { + getCountPromise = countedCancelation(requestPromise, that); + } + } + + return getCountPromise ? getCountPromise.get() : requestPromise; + + }).then(function (count) { + if (!isNonNegativeInteger(count) && count !== undefined) { + throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.InvalidRequestedCountReturned", strings.invalidRequestedCountReturned); + } + + if (count !== knownCount) { + 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 WinJS.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 (listDataAdapter.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 (listDataAdapter.itemsFromIndex || (listDataAdapter.itemsFromStart && listDataAdapter.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 (listDataAdapter.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 + +// Public definitions + +WinJS.Namespace.define("WinJS.UI", { + + DataSourceStatus: { + ready: "ready", + waiting: "waiting", + failure: "failure" + }, + + CountResult: { + unknown: "unknown" + }, + + CountError: { + noResponse: "noResponse" + }, + + FetchError: { + noResponse: "noResponse", + doesNotExist: "doesNotExist" + }, + + EditError: { + noResponse: "noResponse", + canceled: "canceled", + notPermitted: "notPermitted", + noLongerMeaningful: "noLongerMeaningful" + }, + + VirtualizedDataSource: WinJS.Class.mix( + WinJS.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 + }, { // Static Members + supportedForProcessing: false, + }), + WinJS.Utilities.eventMixin + ) + +}); + +var DataSourceStatus = UI.DataSourceStatus, + CountResult = UI.CountResult, + FetchError = UI.FetchError, + EditError = UI.EditError; + +})(); + + +// Group Data Source + +(function groupDataSourceInit() { + "use strict"; + + +var UI = WinJS.UI; +var Promise = WinJS.Promise; + +// Private statics + +function errorDoesNotExist() { + return new WinJS.ErrorFromName(UI.FetchError.doesNotExist); +} + +var batchSizeDefault = 101; + +function groupReady(group) { + return group && group.firstReached && group.lastReached; +} + +var ListNotificationHandler = WinJS.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 = WinJS.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 (failed) { + 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 (failed) { + 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") { + //#DBG _ASSERT(this._count === this._lastGroup.index); + + 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) { + //#DBG _ASSERT(that._fetchQueue[0] === fetch); + 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)) { + //#DBG _ASSERT(typeof this._indexMap[index] !== "number"); + 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; + Promise.timeout().then(function () { + that._continueFetch(fetch); + }); + } 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, countAfter) { + //#DBG _ASSERT(countBefore + 1 + countAfter === this._batchSize); + 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 + //#DBG _ASSERT(groupHighestIndex.firstReached); + 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); + //#DBG _ASSERT(fetch.countAfter > 0 && !groupReady(groupNext)); + 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, error) { + 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) { + //#DBG _ASSERT(that._fetchQueue[0] === fetch); + 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; + + 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, +}); + +// Class definition + +WinJS.Namespace.define("WinJS.UI", { + + _GroupDataSource: WinJS.Class.derive(UI.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, + }) + +}); + +})(); + + +// Grouped Item Data Source + +(function groupedItemDataSourceInit() { + "use strict"; + + +WinJS.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 WinJS.UI._GroupDataSource(listDataSource, groupKey, groupData, options); + } + return groupDataSource; + }, + enumerable: true, + configurable: true + }); + + return groupedItemDataSource; + } + +}); + +})(); + + +// Storage Item Data Source + +(function storageDataSourceInit(global) { + "use strict"; + + + var StorageDataAdapter = WinJS.Class.define(function StorageDataAdapter_ctor(query, options) { + // Constructor + msWriteProfilerMark("WinJS.UI.StorageDataSource:constructor,StartTM"); + + var mode = Windows.Storage.FileProperties.ThumbnailMode.singleItem, + size = 256, + flags = Windows.Storage.FileProperties.ThumbnailOptions.useCurrentScale, + delayLoad = true, + library; + + if (query === "Pictures") { + mode = Windows.Storage.FileProperties.ThumbnailMode.picturesView; + library = Windows.Storage.KnownFolders.picturesLibrary; + size = 190; + } else if (query === "Music") { + mode = Windows.Storage.FileProperties.ThumbnailMode.musicView; + library = Windows.Storage.KnownFolders.musicLibrary; + size = 256; + } else if (query === "Documents") { + mode = Windows.Storage.FileProperties.ThumbnailMode.documentsView; + library = Windows.Storage.KnownFolders.documentsLibrary; + size = 40; + } else if (query === "Videos") { + mode = Windows.Storage.FileProperties.ThumbnailMode.videosView; + library = Windows.Storage.KnownFolders.videosLibrary; + size = 190; + } + + if (!library) { + this._query = query; + } else { + var queryOptions = new Windows.Storage.Search.QueryOptions; + queryOptions.folderDepth = Windows.Storage.Search.FolderDepth.deep; + queryOptions.indexerOption = 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 Windows.Storage.FileProperties.ThumbnailMode.picturesView: + case Windows.Storage.FileProperties.ThumbnailMode.videosView: + size = 190; + break; + case Windows.Storage.FileProperties.ThumbnailMode.documentsView: + case Windows.Storage.FileProperties.ThumbnailMode.listView: + size = 40; + break; + case Windows.Storage.FileProperties.ThumbnailMode.musicView: + case 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 Windows.Storage.BulkAccess.FileInformationFactory(this._query, mode, size, flags, delayLoad); + this.compareByIdentity = false; + this.firstDataRequest = true; + msWriteProfilerMark("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; + msWriteProfilerMark("WinJS.UI.StorageDataSource:itemsFromEnd,info"); + return this.getCount().then(function (totalCount) { + if (totalCount === 0) { + return WinJS.Promise.wrapError(new WinJS.ErrorFromName(WinJS.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) + ")"; + msWriteProfilerMark(perfId + ",StartTM"); + return this._loader.getItemsAsync(first, count).then(function (itemsVector) { + var vectorSize = itemsVector.size; + if (vectorSize <= countBefore) { + return WinJS.Promise.wrapError(new WinJS.ErrorFromName(WinJS.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; + } + msWriteProfilerMark(perfId + ",StopTM"); + return result; + }); + }, + + itemsFromDescription: function (description, countBefore, countAfter) { + var that = this; + msWriteProfilerMark("WinJS.UI.StorageDataSource:itemsFromDescription,info"); + return this._query.findStartIndexAsync(description).then(function (index) { + return that.itemsFromIndex(index, countBefore, countAfter); + }); + }, + + getCount: function () { + msWriteProfilerMark("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, + }); + + // Public definitions + + WinJS.Namespace.define("WinJS.UI", { + StorageDataSource: WinJS.Class.derive(WinJS.UI.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 */ + }, { + supportedForProcessing: false, + }) + }); + + WinJS.Namespace.define("WinJS.UI.StorageDataSource", { + + 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 WinJS.Promise(function (complete, error, progress) { + // 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 = 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 = WinJS.UI.Animation.fadeIn(image).then(function () { + return image; + }); + } else { + image.style.opacity = 1; + imagePromise = WinJS.Promise.wrap(image); + } + return imagePromise; + }); + }); + } + // Otherwise, replace the existing version without animation. + else { + 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 != Windows.Storage.FileProperties.ThumbnailType.icon) && !thumbnail.returnedSmallerCachedSize) { + msWriteProfilerMark("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; + } + }); + } + }); +})(); +// Items Manager + +(function itemsManagerInit(global) { + "use strict"; + +/*#DBG + function dbg_stackTraceDefault() { return "add global function dbg_stackTrace to see stack traces"; } + + global.dbg_stackTrace = global.dbg_stackTrace || dbg_stackTraceDefault; +#DBG*/ + + var markSupportedForProcessing = WinJS.Utilities.markSupportedForProcessing; + +WinJS.Namespace.define("WinJS.UI", { + _normalizeRendererReturn: function (v) { + if (v) { + if (typeof v === "object" && v.element) { + var elementPromise = WinJS.Promise.as(v.element); + return elementPromise.then(function (e) { return { element: e, renderComplete: WinJS.Promise.as(v.renderComplete) } }); + } + else { + var elementPromise = WinJS.Promise.as(v); + return elementPromise.then(function (e) { return { element: e, renderComplete: WinJS.Promise.as() } }); + } + } + else { + return { element: null, renderComplete: WinJS.Promise.as() }; + } + }, + simpleItemRenderer: function (f) { + return markSupportedForProcessing(function (itemPromise, element) { + return itemPromise.then(function (item) { + return (item ? f(item, element) : null); + }); + }); + } +}); +var Promise = WinJS.Promise; +var UI = WinJS.UI; + +// Private statics + +var strings = { + get listDataSourceIsInvalid() { return WinJS.Resources._getWinJSString("ui/listDataSourceIsInvalid").value; }, + get itemRendererIsInvalid() { return WinJS.Resources._getWinJSString("ui/itemRendererIsInvalid").value; }, + get itemIsInvalid() { return WinJS.Resources._getWinJSString("ui/itemIsInvalid").value; }, + get invalidItemsManagerCallback() { return WinJS.Resources._getWinJSString("ui/invalidItemsManagerCallback").value; } +}; + +var Signal = WinJS._Signal; + +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 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 +WinJS.Namespace.define("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) { + imageLoader = imageLoader || new WinJS.UI._ParallelWorkQueue(6); + return imageLoader.queue(function () { + return new WinJS.Promise(function (c, e, p) { + if (!image) { + image = document.createElement("img"); + } + + var seen = seenUrls[srcUrl]; + + if (!seen) { + var tempImage = document.createElement("img"); + + var cleanup = function () { + tempImage.removeEventListener("load", complete, false); + tempImage.removeEventListener("error", error, 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 complete = function () { + seenUrl(srcUrl); + cleanup(); + c(image); + } + + var error = function () { + cleanup(); + e(image); + } + + tempImage.addEventListener("load", complete, false); + tempImage.addEventListener("error", error, false); + tempImage.src = srcUrl; + } else { + seenUrl(srcUrl); + image.src = srcUrl; + c(image); + } + }); + }, data); +} + +function isImageCached(srcUrl) { + return seenUrls[srcUrl]; +} + +function defaultRenderer(item) { + return document.createElement("div"); +} + +// Type-checks a callback parameter, since a failure will be hard to diagnose when it occurs +function checkCallback(callback, name) { + if (typeof callback !== "function") { + throw new WinJS.ErrorFromName("WinJS.UI.ItemsManager.CallbackIsInvalid", WinJS.Resources._formatString(strings.invalidItemsManagerCallback, name)); + } +} + +var ListNotificationHandler = WinJS.Class.define(function ListNotificationHandler_ctor(itemsManager) { + // Constructor + + this._itemsManager = itemsManager; + /*#DBG + this._notificationsCount = 0; + #DBG*/ +}, { + // Public methods + + beginNotifications: function () { + /*#DBG + if (this._notificationsCount !== 0) { + throw new "ACK! Unbalanced beginNotifications call"; + } + this._notificationsCount++; + #DBG*/ + 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); + }, + + endNotifications: function () { + /*#DBG + if (this._notificationsCount !== 1) { + throw new "ACK! Unbalanced endNotifications call"; + } + this._notificationsCount--; + #DBG*/ + this._itemsManager._versionManager.endNotifications(); + this._itemsManager._endNotifications(); + }, + + reload: function () { + this._itemsManager._versionManager.receivedNotification(); + this._itemsManager._reload(); + } +}, { // Static Members + supportedForProcessing: false, +}); + +var ItemsManager = WinJS.Class.define(function ItemsManager_ctor(listDataSource, itemRenderer, elementNotificationHandler, options) { + // Constructor + + if (!listDataSource) { + throw new WinJS.ErrorFromName("WinJS.UI.ItemsManager.ListDataSourceIsInvalid", strings.listDataSourceIsInvalid); + } + if (!itemRenderer) { + throw new WinJS.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._versionManager = options.versionManager || new WinJS.UI._VersionManager(); + } + + this._resetItem = options && options.resetItem || function () { return null; }; + this._indexInView = options && options.indexInView; + this._itemRenderer = itemRenderer; + + // Map of (the uniqueIDs of) elements to records for items + this._elementMap = {}; + + // Map of handles to records for items + this._handleMap = {}; + + // Worker queue to process ready events for async data sources + this._workQueue = new WinJS.UI._TimeBasedQueue(); + this._workQueue.start(); + + // 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)) + }, + _itemAtIndex: function (index) { + /*#DBG + var that = this; + var startVersion = that._versionManager.version; + #DBG*/ + var itemPromise = this._itemPromiseAtIndex(index) + var result = this._itemFromItemPromise(itemPromise)/*#DBG . + then(function (v) { + var rec = that._recordFromElement(v); + var endVersion = that._versionManager.version; + if (rec.item.index !== index) { + throw "ACK! inconsistent index"; + } + if (startVersion !== endVersion) { + throw "ACK! inconsistent version"; + } + if (WinJS.Utilities.data(v).itemData && + WinJS.Utilities.data(v).itemData.itemsManagerRecord.item.index !== index) { + throw "ACK! inconsistent itemData.index"; + } + return v; + }) #DBG*/; + return result.then(null, function (e) { + itemPromise.cancel(); + return WinJS.Promise.wrapError(e); + }); + }, + _itemPromiseAtIndex: function (index) { + /*#DBG + var that = this; + var startVersion = that._versionManager.version; + if (that._versionManager.locked) { + throw "ACK! Attempt to get an item while editing"; + } + #DBG*/ + var itemPromise = this._listBinding.fromIndex(index); + /*#DBG + itemPromise.then(function (item) { + var endVersion = that._versionManager.version; + if (item.index !== index) { + throw "ACK! inconsistent index"; + } + if (startVersion !== endVersion) { + throw "ACK! inconsistent version"; + } + return item; + }); + #DBG*/ + return itemPromise; + }, + _waitForElement: function (possiblePlaceholder) { + var that = this; + return new WinJS.Promise(function (c, e, p) { + if (possiblePlaceholder) { + if (!that.isPlaceholder(possiblePlaceholder)) { + c(possiblePlaceholder); + } + else { + var placeholderID = possiblePlaceholder.uniqueID; + 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 = oldElement.uniqueID; + 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._workQueue.cancel(); + this._released = true; + }, + + releaseItem: function (element) { + var record = this._elementMap[element.uniqueID]; + if (!record) { return; } + + /*#DBG + record = this._recordFromElement(element); + if (record.released) { + throw "ACK! Double release on item"; + } + #DBG*/ + + if (record.renderPromise) { + record.renderPromise.cancel(); + } + if (record.itemPromise) { + record.itemPromise.cancel(); + } + if (record.imagePromises) { + record.imagePromises.cancel(); + } + if (record.itemReadyPromise) { + record.itemReadyPromise.cancel(); + } + if (record.renderComplete) { + record.renderComplete.cancel(); + } + + this._removeEntryFromElementMap(element); + this._removeEntryFromHandleMap(record.item ? record.item.handle : record.itemPromise.handle, record); + + if (record.item) { + this._listBinding.releaseItem(record.item); + } + + /*#DBG + record.released = true; + if (record.updater) { + throw "ACK! attempt to release item current held by updater"; + } + #DBG*/ + }, + + refresh: function () { + return this._listDataSource.invalidateAll(); + }, + + // Private members + + _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) { + var that = this; + var indexInView = that._indexInView || function () { return true; }; + var readySignal = new Signal(); + var itemForRendererPromise = itemPromise.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 = WinJS.Promise.join([record.imagePromises, loadImagePromise]); + } else { + record.imagePromises = loadImagePromise; + } + return loadImagePromise; + }; + itemForRenderer.isImageCached = isImageCached; + return itemForRenderer; + } else { + return WinJS.Promise.cancel; + } + }); + itemForRendererPromise.handle = itemPromise.handle; + record.itemPromise = itemForRendererPromise; + record.itemReadyPromise = readySignal.promise; + record.readyComplete = false; + + return WinJS.Promise.as(that._itemRenderer(itemForRendererPromise, record.element)). + then(WinJS.UI._normalizeRendererReturn). + then(function (v) { + if (that._released) { + return WinJS.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) { + readySignal.complete(item); + record.pendingReady = null; + record.readyComplete = true; + } + } + that._workQueue.push(record.pendingReady); + }); + return v; + }); + }, + + _replaceElement: function (record, elementNew) { + /*#DBG + if (!this._handleInHandleMap(record.item.handle)) { + throw "ACK! replacing element not present in handle map"; + } + #DBG*/ + this._removeEntryFromElementMap(record.element); + record.element = elementNew; + this._addEntryToElementMap(elementNew, record); + }, + + _changeElement: function (record, elementNew, elementNewIsPlaceholder) { + //#DBG _ASSERT(elementNew); + 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); + this._handlerToNotify().itemAvailable(record.element, elementOld); + } else { + this._handlerToNotify().changed(elementNew, elementOld, itemOld); + } + }, + + _elementForItem: function (itemPromise) { + 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). + 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) { + /*#DBG + if (WinJS.Utilities.data(element).itemsManagerRecord) { + throw "ACK! Extra call to _addEntryToElementMap, ref counting error"; + } + WinJS.Utilities.data(element).itemsManagerRecord = record; + #DBG*/ + this._elementMap[element.uniqueID] = record; + }, + + _removeEntryFromElementMap: function (element) { + /*#DBG + if (!WinJS.Utilities.data(element).itemsManagerRecord) { + throw "ACK! Extra call to _removeEntryFromElementMap, ref counting error"; + } + WinJS.Utilities.data(element).removeElementMapRecord = WinJS.Utilities.data(element).itemsManagerRecord; + WinJS.Utilities.data(element).removeEntryMapStack = dbg_stackTrace(); + delete WinJS.Utilities.data(element).itemsManagerRecord; + #DBG*/ + delete this._elementMap[element.uniqueID]; + }, + + _recordFromElement: function (element, ignoreFailure) { + var record = this._elementMap[element.uniqueID]; + if (!record) { + /*#DBG + var removeElementMapRecord = WinJS.Utilities.data(element).removeElementMapRecord; + var itemsManagerRecord = WinJS.Utilities.data(element).itemsManagerRecord; + #DBG*/ + throw new WinJS.ErrorFromName("WinJS.UI.ItemsManager.ItemIsInvalid", strings.itemIsInvalid); + } + + return record; + }, + + _addEntryToHandleMap: function (handle, record) { + /*#DBG + if (this._handleMap[handle]) { + throw "ACK! Extra call to _addEntryToHandleMap, ref counting error"; + } + this._handleMapLeak = this._handleMapLeak || {}; + this._handleMapLeak[handle] = { record: record, addHandleMapStack: dbg_stackTrace() }; + #DBG*/ + this._handleMap[handle] = record; + }, + + _removeEntryFromHandleMap: function (handle, record) { + /*#DBG + if (!this._handleMap[handle]) { + throw "ACK! Extra call to _removeEntryFromHandleMap, ref counting error"; + } + this._handleMapLeak[handle].removeHandleMapStack = dbg_stackTrace(); + #DBG*/ + delete this._handleMap[handle]; + }, + + _handleInHandleMap: function (handle) { + return !!this._handleMap[handle]; + }, + + _recordFromHandle: function (handle, ignoreFailure) { + var record = this._handleMap[handle]; + if (!record && !ignoreFailure) { + /*#DBG + var leak = this._handleMapLeak[handle]; + #DBG*/ + throw new WinJS.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); + + //#DBG _ASSERT(record); + if (record.renderPromise) { + record.renderPromise.cancel(); + } + if (record.itemPromise) { + record.itemPromise.cancel(); + } + if (record.imagePromises) { + record.imagePromises.cancel(); + } + if (record.itemReadyPromise) { + record.itemReadyPromise.cancel(); + } + if (record.renderComplete) { + record.renderComplete.cancel(); + } + + record.newItem = newItem; + + var that = this; + if (record.item && record.item.key) { + this._resetItem(record.item, record.element); + } + record.renderPromise = this._renderItem(WinJS.Promise.as(newItem), 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), + item = this.itemObject(element); + + //#DBG _ASSERT(element); + if (item && item.key) { + this._resetItem(item, element); + } + 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 that = this; + record.renderPromise = this._renderItem(itemToRender, 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); + } + }, + _beginNotifications: function () { + // accessing _handlerToNotify will force the call to beginNotifications on the client + // + this._externalBegin = true; + var x = 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; + setImmediate(function () { + that._endNotificationsPosted = false; + that._endNotifications(); + }); + } + }, + + _presentElement: function (record) { + var elementOld = record.element; + //#DBG _ASSERT(elementOld); + + // 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); + 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); + } + }); + } +}, { // Static Members + supportedForProcessing: false, +}); + +// Public definitions + +WinJS.Namespace.define("WinJS.UI", { + _createItemsManager: function (dataSource, itemRenderer, elementNotificationHandler, options) { + return new ItemsManager(dataSource, itemRenderer, elementNotificationHandler, options); + } +}); + +})(this); + + +(function parallelWorkQueueInit(global) { + "use strict"; + + +var ParallelWorkQueue = WinJS.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) { + setImmediate(run); + } + } + 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 WinJS.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, + } +); + +WinJS.Namespace.define("WinJS.UI", { + _ParallelWorkQueue : ParallelWorkQueue +}); + +})(this); + + +(function versionManagerInit(global) { + "use strict"; + + + WinJS.Namespace.define("WinJS.UI", { + _VersionManager: WinJS.Class.define( + function _VersionManager_ctor() { + this._unlocked = new WinJS._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(); + /*#DBG + if (this._updateCount !== 0) { + throw "ACK! incorrect begin/endUpdating pair on version manager"; + } + #DBG*/ + this._updateCount++; + }, + endUpdating: function() { + this._updateCount--; + /*#DBG + if (this._updateCount < 0) { + throw "ACK! incorrect begin/endUpdating pair on version manager"; + } + #DBG*/ + this._checkUnlocked(); + }, + beginNotifications: function () { + this._checkLocked(); + this._notificationCount++; + }, + endNotifications: function () { + this._notificationCount--; + /*#DBG + if (this._notificationCount < 0) { + throw "ACK! incorrect begin/endNotifications pair on version manager"; + } + #DBG*/ + this._checkUnlocked(); + }, + _checkLocked: function () { + if (!this.locked) { + this._dispose(); + this._unlocked = new WinJS._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, + } + ) + }); +})(); + + +(function flipperInit(WinJS) { + "use strict"; + + var thisWinUI = WinJS.UI; + var utilities = WinJS.Utilities; + var animation = WinJS.UI.Animation; + + // 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, + leftArrowGlyph = "", + rightArrowGlyph = "", + topArrowGlyph = "", + bottomArrowGlyph = "", + animationMoveDelta = 40; + + function flipViewPropertyChanged(e) { + var that = e.srcElement.winControl; + if (that && that instanceof WinJS.UI.FlipView) { + if (e.propertyName === "dir" || e.propertyName === "style.direction") { + that._rtl = window.getComputedStyle(that._flipviewDiv, null).direction === "rtl"; + that._setupOrientation(); + } + } + } + + function flipviewResized(e) { + var that = e.srcElement && e.srcElement.winControl; + if (that && that instanceof WinJS.UI.FlipView) { + msWriteProfilerMark("WinJS.UI.FlipView:resize,StartTM"); + that._resize(); + } + } + + WinJS.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: WinJS.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. + /// + /// + msWriteProfilerMark("WinJS.UI.FlipView:constructor,StartTM"); + + element = element || document.createElement("div"); + + var horizontal = true, + dataSource = null, + itemRenderer = WinJS.UI._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": + horizontal = true; + break; + + case "vertical": + horizontal = 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 WinJS.Binding.List(); + dataSource = list.dataSource; + } + utilities.empty(element); + + this._initializeFlipView(element, horizontal, dataSource, itemRenderer, initialIndex, itemSpacing); + + element.winControl = this; + + // Call _setOption with eventsOnly flag on + WinJS.UI._setOptions(this, options, true); + + msWriteProfilerMark("WinJS.UI.FlipView:constructor,StopTM"); + }, { + + // Public methods + + next: function () { + /// + /// + /// 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. + /// + /// + msWriteProfilerMark("WinJS.UI.FlipView:next,info"); + var cancelAnimationCallback = this._nextAnimation ? null : this._cancelDefaultAnimation; + return this._navigate(true, cancelAnimationCallback); + }, + + previous: function () { + /// + /// + /// 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. + /// + /// + msWriteProfilerMark("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 () { + if (this._animating) { + this._cancelAnimation(); + } + return this._getCurrentIndex(); + }, + set: function (index) { + msWriteProfilerMark("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; + 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(); + msWriteProfilerMark("WinJS.UI.FlipView:set_currentPage.animationComplete,info"); + } + }).done(); + } + }); + } + } + }, + + /// + /// Gets or sets the layout orientation of the FlipView, horizontal or vertical. + /// + orientation: { + get: function () { + return this._axisAsString(); + }, + set: function (orientation) { + msWriteProfilerMark("WinJS.UI.FlipView:set_orientation,info"); + var horizontal = orientation === "horizontal"; + if (horizontal !== this._horizontal) { + this._horizontal = horizontal; + this._setupOrientation(); + this._pageManager.setOrientation(this._horizontal); + } + } + }, + + /// + /// 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) { + msWriteProfilerMark("WinJS.UI.FlipView:set_itemDataSource,info"); + this._dataSourceAfterRefresh = dataSource || new WinJS.Binding.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) { + msWriteProfilerMark("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) { + msWriteProfilerMark("WinJS.UI.FlipView:set_itemSpacing,info"); + spacing = spacing >> 0; + spacing = spacing < 0 ? 0 : spacing; + this._pageManager.setItemSpacing(spacing); + } + }, + + count: function () { + /// + /// + /// 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. + /// + /// + + msWriteProfilerMark("WinJS.UI.FlipView:count,info"); + var that = this; + return new WinJS.Promise(function (complete, error) { + if (that._itemsManager) { + if (that._pageManager._cachedSize === WinJS.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(thisWinUI.FlipView.noitemsManagerForCount); + } + }); + }, + + setCustomAnimations: function (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. + /// + /// + msWriteProfilerMark("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 () { + /// + /// + /// 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". + /// + /// + msWriteProfilerMark("WinJS.UI.FlipView:forceLayout,info"); + + this._pageManager.resized(); + }, + + // Private members + + _initializeFlipView: function (element, horizontal, dataSource, itemRenderer, initialIndex, itemSpacing) { + this._flipviewDiv = element; + utilities.addClass(this._flipviewDiv, flipViewClass); + this._contentDiv = document.createElement("div"); + this._panningDivContainer = document.createElement("div"); + this._panningDivContainer.className = "win-surface"; + this._panningDiv = document.createElement("div"); + this._prevButton = document.createElement("button"); + this._nextButton = document.createElement("button"); + this._horizontal = horizontal; + this._dataSource = dataSource; + this._itemRenderer = itemRenderer; + this._itemsManager = null; + this._pageManager = null; + + 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); + + var that = this; + + this._itemsManagerCallback = { + // Callbacks for itemsManager + inserted: function (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 (newCount, oldCount) { + that._pageManager._cachedSize = newCount; + + // Don't fire the datasourcecountchanged event when there is a state transition + if (oldCount !== WinJS.UI.CountResult.unknown) { + that._fireDatasourceCountChangedEvent(); + } + }, + + changed: function (newElement, oldElement) { + that._pageManager.changed(newElement, oldElement); + }, + + moved: function (element, prev, next, itemPromise) { + var elementReady = function (element) { + //#DBG _ASSERT(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 (element, mirage) { + if (element) { + that._pageManager.removed(element, mirage, true); + } + }, + + knownUpdatesComplete: function () { + }, + + beginNotifications: function () { + that._pageManager.notificationsStarted(); + }, + + endNotifications: function () { + that._pageManager.notificationsEnded(); + }, + + itemAvailable: function (real, placeholder) { + that._pageManager.itemRetrieved(real, placeholder); + }, + + reload: function () { + that._pageManager.reload(); + } + }; + + if (this._dataSource) { + this._itemsManager = thisWinUI._createItemsManager(this._dataSource, this._itemRenderer, this._itemsManagerCallback, { + ownerElement: this._flipviewDiv + }); + } + + this._pageManager = new thisWinUI._FlipPageManager(this._flipviewDiv, this._panningDiv, this._panningDivContainer, this._itemsManager, itemSpacing, + { + 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._horizontal); + + 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); + + this._flipviewDiv.attachEvent("onpropertychange", flipViewPropertyChanged, false); + + this._flipviewDiv.attachEvent("onresize", flipviewResized); + + this._contentDiv.addEventListener("mouseleave", function () { + that._mouseInViewport = false; + }, false); + + var PT_TOUCH = 2; + 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(); + } + } + this._contentDiv.addEventListener("MSPointerHover", handleShowButtons, false); + this._contentDiv.addEventListener("MSPointerMove", handleShowButtons, false); + + this._contentDiv.addEventListener("MSPointerDown", function (e) { + if (e.pointerType === PT_TOUCH) { + that._mouseInViewport = false; + that._touchInteraction = true; + that._fadeOutButtons(true); + } else { + that._touchInteraction = false; + if (!that._isInteractive(e.srcElement)) { + // Disable the default behavior of the mouse wheel button to avoid auto-scroll + if ((e.buttons & 4) !== 0) { + e.stopPropagation(); + e.preventDefault(); + } + } + } + }, false); + + this._contentDiv.addEventListener("MSPointerUp", function (e) { + if (e.pointerType !== PT_TOUCH) { + that._touchInteraction = false; + } + }, false); + + this._panningDivContainer.addEventListener("wheel", function (e) { + if (!that._isInteractive(e.srcElement)) { + e.stopPropagation(); + e.preventDefault(); + } + }, false); + + this._panningDivContainer.addEventListener("scroll", function () { + that._scrollPosChanged(); + }, false); + + this._panningDiv.addEventListener("deactivate", function (event) { + if (!that._touchInteraction) { + that._fadeOutButtons(); + } + }, true); + + // When an element is removed and inserted, its scroll position gets reset to 0 (and no onscroll event is generated). This is a major problem + // for the flipview thanks to the fact that we 1) Do a lot of inserts/removes of child elements, and 2) Depend on our scroll location being right to + // display the right stuff. The page manager preserves scroll location. When a flipview element is reinserted, it'll fire DOMNodeInserted and we can reset + // its scroll location there. + // This event handler won't be hit in IE8. + this._flipviewDiv.addEventListener("DOMNodeInserted", function (event) { + if (event.target === that._flipviewDiv) { + that._pageManager.resized(); + } + }, false); + + this._flipviewDiv.addEventListener("keydown", function (event) { + function 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; + } + var cancelBubbleIfHandled = true; + if (!that._isInteractive(event.srcElement)) { + var Key = utilities.Key, + handled = false; + if (that._horizontal) { + 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(); + event.cancelBubble = cancelBubbleIfHandled; + return true; + } + } + }, false); + }, + + _isInteractive: function (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 () { + 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 () { + if (!this._refreshTimer) { + var that = this; + this._refreshTimer = true; + setImmediate(function () { + if (that._refreshTimer) { + that._refreshHandler(); + } + }); + } + }, + + _getItemRenderer: function (itemTemplate) { + var itemRenderer = null; + if (typeof itemTemplate === "function") { + var itemPromise = new WinJS.Promise(function (c, e, p) {}); + 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 = document.createElement("div"); + elementRoot.className = "win-template"; + 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 = document.createElement("div"); + elementRoot.className = "win-template"; + // 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 (item) { + return itemTemplate(itemPromise).then(function (element) { + elementRoot.appendChild(element); + }); + }) + }; + } + }; + } else if (typeof itemTemplate === "object") { + itemRenderer = itemTemplate.renderItem; + } + return itemRenderer; + }, + + _navigate: function (goForward, cancelAnimationCallback) { + if (WinJS.validation && this._refreshTimer) { + throw new WinJS.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 (outgoingElement, incomingElement) { + // Cancel the fadeOut animation + outgoingElement.style.opacity = 0; + + // Cancel the enterContent animation + incomingElement.style.animationName = ""; + incomingElement.style.opacity = 1; + }, + + _cancelAnimation: function () { + 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 (goForward) { + 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 () { + 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 () { + this._pageManager.resized(); + }, + + _setCurrentIndex: function (index) { + return this._pageManager.jumpToIndex(index); + }, + + _getCurrentIndex: function () { + return this._pageManager.currentIndex(); + }, + + _setDatasource: function (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 = thisWinUI._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 () { + var that = this; + setImmediate(function () { + var event = document.createEvent("Event"); + event.initEvent(thisWinUI.FlipView.datasourceCountChangedEvent, true, true); + msWriteProfilerMark("WinJS.UI.FlipView:datasourceCountChangedEvent,info"); + that._flipviewDiv.dispatchEvent(event); + }); + }, + + _scrollPosChanged: function () { + this._pageManager.scrollPosChanged(); + }, + + _axisAsString: function () { + return (this._horizontal ? "horizontal" : "vertical"); + }, + + _setupOrientation: function () { + if (this._horizontal) { + this._panningDivContainer.style["overflow-x"] = "scroll"; + this._panningDivContainer.style["overflow-y"] = "hidden"; + var rtl = window.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["overflow-y"] = "scroll"; + this._panningDivContainer.style["overflow-x"] = "hidden"; + this._prevButton.className = navButtonClass + " " + navButtonTopClass; + this._nextButton.className = navButtonClass + " " + navButtonBottomClass; + this._prevButton.innerHTML = topArrowGlyph; + this._nextButton.innerHTML = bottomArrowGlyph; + } + this._panningDivContainer.style["-ms-overflow-style"] = "none"; + }, + + _fadeInButton: function (button, forceShow) { + if (this._mouseInViewport || forceShow) { + 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 (button) { + var that = this; + if (button === "next") { + if (this._nextButtonAnimation) { + this._nextButtonAnimation.cancel(); + this._nextButtonAnimation = null; + } + + this._nextButtonAnimation = animation.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 = animation.fadeOut(this._prevButton). + then(function () { + that._prevButton.style.visibility = "hidden"; + }); + return this._prevButtonAnimation; + } + }, + + _fadeOutButtons: function (immediately) { + if (this._buttonFadePromise) { + this._buttonFadePromise.cancel(); + this._buttonFadePromise = null; + } + + var that = this; + this._buttonFadePromise = WinJS.Promise.timeout(immediately ? 0 : buttonFadeDelay).then(function () { + that._fadeOutButton("prev"); + that._fadeOutButton("next"); + that._buttonFadePromise = null; + }); + }, + + _animationsStarted: function () { + this._animating = true; + }, + + _animationsFinished: function () { + this._animating = false; + }, + + _defaultAnimation: function (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._horizontal ? (this._rtl ? -pageDirection : pageDirection) : 0) + "px"; + incomingPageMove.top = (this._horizontal ? 0 : pageDirection) + "px"; + var fadeOutPromise = animation.fadeOut(curr), + enterContentPromise = animation.enterContent(next, [incomingPageMove]); + return WinJS.Promise.join([fadeOutPromise, enterContentPromise]); + }, + + _fadeInFromCurrentValue: function (shown) { + // Intentionally not using the PVL fadeIn animation because we don't want + // to start always from 0 in some cases + return thisWinUI.executeTransition( + shown, + { + property: "opacity", + delay: 0, + duration: 167, + timing: "linear", + to: 1 + }); + } + }) + }); + + // Statics + + // Events + thisWinUI.FlipView.datasourceCountChangedEvent = "datasourcecountchanged"; + thisWinUI.FlipView.pageVisibilityChangedEvent = "pagevisibilitychanged"; + thisWinUI.FlipView.pageSelectedEvent = "pageselected"; + thisWinUI.FlipView.pageCompletedEvent = "pagecompleted"; + + WinJS.Class.mix(thisWinUI.FlipView, WinJS.Utilities.createEventProperties( + thisWinUI.FlipView.datasourceCountChangedEvent, + thisWinUI.FlipView.pageVisibilityChangedEvent, + thisWinUI.FlipView.pageSelectedEvent, + thisWinUI.FlipView.pageCompletedEvent)); + WinJS.Class.mix(thisWinUI.FlipView, WinJS.UI.DOMEventMixin); + + var strings = { + get badAxis() { return WinJS.Resources._getWinJSString("ui/badAxis").value; }, + get badCurrentPage() { return WinJS.Resources._getWinJSString("ui/badCurrentPage").value; }, + get noitemsManagerForCount() { return WinJS.Resources._getWinJSString("ui/noitemsManagerForCount").value; }, + get badItemSpacingAmount() { return WinJS.Resources._getWinJSString("ui/badItemSpacingAmount").value; }, + get navigationDuringStateChange() { return WinJS.Resources._getWinJSString("ui/flipViewNavigationDuringStateChange").value; }, + get panningContainerAriaLabel() { return WinJS.Resources._getWinJSString("ui/flipViewPanningContainerAriaLabel").value; } + }; + +})(WinJS); + + +(function flipperPageManagerInit(WinJS) { + "use strict"; + + var thisWinUI = WinJS.UI; + + // Utilities are private and global pointer will be deleted so we need to cache it locally + var utilities = WinJS.Utilities; + + var animations = WinJS.UI.Animation; + + var leftBufferAmount = 500, + itemSelectedEventDelay = 250; + + var strings = { + get badCurrentPage() { return WinJS.Resources._getWinJSString("ui/badCurrentPage").value; } + }; + + function isFlipper(element) { + var control = element.winControl; + if (control && control instanceof WinJS.UI.FlipView) { + return true; + } + + return false; + } + + function flipperPropertyChanged(e) { + var element = e.srcElement; + if (element.winControl && element.tabIndex >= 0) { + element.winControl._pageManager._updateTabIndex(element.tabIndex); + element.tabIndex = -1; + } + + if (e.propertyName === "dir" || e.propertyName === "style.direction") { + var that = element.winControl; + if (that && that instanceof WinJS.UI.FlipView) { + that._pageManager._rtl = window.getComputedStyle(that._pageManager._flipperDiv, null).direction === "rtl"; + that._pageManager.resized(); + } + } + } + + WinJS.Namespace.define("WinJS.UI", { + + // Definition of our private utility + _FlipPageManager: WinJS.Class.define( + function _FlipPageManager_ctor(flipperDiv, panningDiv, panningDivContainer, itemsManager, itemSpacing, buttonVisibilityHandler) { + // Construction + this._visibleElements = []; + this._flipperDiv = flipperDiv; + this._panningDiv = panningDiv; + this._panningDivContainer = panningDivContainer; + this._buttonVisibilityHandler = buttonVisibilityHandler; + this._currentPage = null; + this._rtl = window.getComputedStyle(this._flipperDiv, null).direction === "rtl"; + this._itemsManager = itemsManager; + this._itemSpacing = itemSpacing; + this._tabIndex = (flipperDiv.tabIndex !== undefined && flipperDiv.tabIndex >= 0 ? flipperDiv.tabIndex : 0); + flipperDiv.tabIndex = -1; + this._tabManager = new WinJS.UI.TabContainer(this._panningDivContainer); + this._tabManager.tabIndex = this._tabIndex; + this._lastSelectedPage = null; + this._lastSelectedElement = null; + this._bufferSize = thisWinUI._FlipPageManager.flipPageBufferCount; + this._cachedSize = -1; + + var that = this; + this._panningDiv.addEventListener("keydown", function (event) { + if (that._blockTabs && event.keyCode === utilities.Key.tab) { + event.stopImmediatePropagation(); + event.preventDefault(); + } + }, true); + this._flipperDiv.addEventListener("focus", function (event) { + if (event.srcElement === that._flipperDiv) { + if (that._currentPage.element) { + try { + that._currentPage.element.setActive(); + } catch (e) { } + } + } + }, false); + this._flipperDiv.attachEvent("onpropertychange", flipperPropertyChanged, false); + this._panningDiv.addEventListener("activate", function (event) { + that._hasFocus = true; + }, true); + this._panningDiv.addEventListener("deactivate", function (event) { + that._hasFocus = false; + }, true); + this._panningDivContainer.addEventListener("MSManipulationStateChanged", function (event) { + that._manipulationState = event.currentState; + if (event.currentState === 0) { + that._itemSettledOn(); + } + }, true); + }, + { + // Public Methods + + initialize: function (initialIndex, horizontal) { + 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._horizontal = horizontal; + if (!this._currentPage) { + this._bufferAriaStartMarker = document.createElement("div"); + this._bufferAriaStartMarker.id = this._bufferAriaStartMarker.uniqueID; + 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 = document.createElement("div"); + this._bufferAriaEndMarker.id = this._bufferAriaEndMarker.uniqueID; + this._panningDiv.appendChild(this._bufferAriaEndMarker); + } + + this._prevMarker = this._currentPage.prev.prev; + + if (this._itemsManager) { + this.setNewItemsManager(this._itemsManager, initialIndex); + } + }, + + setOrientation: function (horizontal) { + if (this._notificationsEndedSignal) { + var that = this; + this._notificationsEndedSignal.promise.done(function() { + that._notificationsEndedSignal = null; + that.setOrientation(horizontal); + }); + return; + } + + if (horizontal !== this._horizontal) { + this._isOrientationChanging = true; + this._horizontal = horizontal; + this._forEachPage(function (curr) { + var currStyle = curr.pageRoot.style; + currStyle.left = "0px"; + currStyle.top = "0px"; + }); + this._panningDivContainer.scrollLeft = 0; + this._panningDivContainer.scrollTop = 0; + var containerStyle = this._panningDivContainer.style; + containerStyle["overflow-x"] = "hidden"; + containerStyle["overflow-y"] = "hidden"; + + var that = this; + requestAnimationFrame(function () { + that._isOrientationChanging = false; + containerStyle["overflow-x"] = that._horizontal ? "scroll" : "hidden"; + containerStyle["overflow-y"] = that._horizontal ? "hidden" : "scroll"; + that._ensureCentered(); + }); + } + }, + + resetState: function (initialIndex) { + msWriteProfilerMark("WinJS.UI.FlipView:resetState,info"); + if (initialIndex !== 0) { + var indexValid = this.jumpToIndex(initialIndex, true); + if (!indexValid && WinJS.validation) { + throw new WinJS.ErrorFromName("WinJS.UI.FlipView.BadCurrentPage", strings.badCurrentPage); + } + return indexValid; + } else { + this._resetBuffer(null, true); + var that = this; + var work = WinJS.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) { + var item = this._itemsManager.itemObject(element); + if (item) { + index = item.index; + } + } + return index; + }, + + resetScrollPos: function () { + this._ensureCentered(); + }, + + scrollPosChanged: function () { + if (!this._itemsManager || !this._currentPage.element || this._isOrientationChanging) { + return; + } + + var newPos = this._viewportStart(), + bufferEnd = (this._lastScrollPos > newPos ? this._getTailOfBuffer() : this._getHeadOfBuffer()); + + if (newPos === this._lastScrollPos) { + return; + } + + while (this._currentPage.element && this._itemStart(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; + this._tabManager.childFocus = this._currentPage.pageRoot; + 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) { + 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(); + msWriteProfilerMark("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 WinJS.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 i, + currIndex = this._itemsManager.itemObject(this._currentPage.element).index, + distance = Math.abs(index - currIndex); + + if (distance === 0) { + return WinJS.Promise.wrap(false); + } + } + + var tail = WinJS.Promise.wrap(true); + var that = this; + + tail = tail.then(function () { + var itemPromise = that._itemsManager._itemPromiseAtIndex(index); + return WinJS.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) { + if (this._currentPage.element) { + var outgoingPage = this._currentPage, + incomingPage = (goForward ? this._currentPage.next : this._currentPage.prev); + + if (incomingPage.element) { + if (this._hasFocus) { + try { + // 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. + this._panningDiv.setActive(); + } catch (e) { } + } + 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 = outgoingElement.uniqueID; + incomingPage.setElement(null, true); + incomingPage.elementUniqueID = incomingElement.uniqueID; + + var outgoingFlipPage = this._createDiscardablePage(outgoingElement), + incomingFlipPage = this._createDiscardablePage(incomingElement); + + outgoingFlipPage.pageRoot.itemIndex = this._itemsManager.itemObject(outgoingElement).index; + 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._itemStart(outgoingFlipPage, 0, 0); + this._itemStart(incomingFlipPage, 0, 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) { + 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._viewportStart(this._itemStart(goForward ? this._currentPage.next : this._currentPage.prev)); + } + this._navigationAnimationRecord = null; + this._itemSettledOn(); + } + }, + + startAnimatedJump: function (index, cancelAnimationCallback, completionCallback) { + if (this._currentPage.element) { + var oldElement = this._currentPage.element; + var oldIndex = this._itemsManager.itemObject(oldElement).index; + 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 = newElement.uniqueID; + + 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._itemStart(oldFlipPage, 0, 0); + that._itemStart(newFlipPage, that._itemSize(that._currentPage), 0); + that._visibleElements.push(newElement); + that._announceElementVisible(newElement); + that._navigationAnimationRecord.elementContainers = [oldFlipPage, newFlipPage]; + that._blockTabs = true; + return { + oldPage: oldFlipPage, + newPage: newFlipPage + }; + }); + } + + return WinJS.Promise.wrap(null); + }, + + endAnimatedJump: function (oldCurr, newCurr) { + if (this._navigationAnimationRecord.oldCurrentPage) { + this._navigationAnimationRecord.oldCurrentPage.setElement(oldCurr.element, true); + } else { + 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) { + var curr = this._prevMarker, + passedCurrent = false, + elementSuccessfullyPlaced = false; + + if (animateInsertion) { + this._createAnimationRecord(element.uniqueID, null); + this._getAnimationRecord(element).inserted = true; + } + + if (!prev) { + if (!next) { + this._currentPage.setElement(element); + } else { + while (curr.next !== this._prevMarker && curr.elementUniqueID !== next.uniqueID) { + if (curr === this._currentPage) { + passedCurrent = true; + } + curr = curr.next; + } + + // We never should go past current if prev is null/undefined. + //#DBG _ASSERT(!passedCurrent); + + if (curr.elementUniqueID === next.uniqueID && curr !== this._prevMarker) { + curr.prev.setElement(element); + elementSuccessfullyPlaced = true; + } else { + this._itemsManager.releaseItem(element); + } + } + } else { + do { + if (curr === this._currentPage) { + passedCurrent = true; + } + if (curr.elementUniqueID === prev.uniqueID) { + elementSuccessfullyPlaced = true; + var pageShifted = curr, + lastElementMoved = element, + lastElementMovedUniqueID = element.uniqueID, + 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 (lastElementMoved.uniqueID === curr.elementUniqueID) { + reused = true; + return true; + } + }); + if (!reused) { + this._itemsManager.releaseItem(lastElementMoved); + } + } + break; + } + curr = curr.next; + } while (curr !== this._prevMarker); + } + + this._getAnimationRecord(element).successfullyMoved = elementSuccessfullyPlaced; + this._setButtonStates(); + }, + + changed: function (newVal, element) { + var curr = this._prevMarker; + var that = this; + this._forEachPage(function (curr) { + if (curr.elementUniqueID === element.uniqueID) { + 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 = newVal.uniqueID; + that._animationRecords[newVal.uniqueID] = 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 === element.uniqueID) { + page.element = newVal; + page.elementUniqueID = newVal.uniqueID; + } + } + + var newElement = this._navigationAnimationRecord.newCurrentElement; + if (newElement && newElement.uniqueID === element.uniqueID) { + this._navigationAnimationRecord.newCurrentElement = newVal; + } + } + }, + + moved: function (element, prev, next) { + var record = this._getAnimationRecord(element); + + if (!record) { + /*#DBG + // When a moved notification is received, and it doesn't have a record, it shouldn't be in the buffer + this._forEachPage(function (curr) { + _ASSERT(curr.element !== element); + }); + #DBG*/ + record = this._createAnimationRecord(element.uniqueID); + } + + 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) { + var that = this; + var prevMarker = this._prevMarker; + var work = WinJS.Promise.wrap(); + + if (mirage) { + var clearNext = false; + this._forEachPage(function (curr) { + if (curr.elementUniqueID === element.uniqueID || 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 === element.uniqueID) { + 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 === element.uniqueID) { + 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 === element.uniqueID) { + 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 === element.uniqueID) { + this._shiftRight(curr); + handled = true; + } + + curr = curr.prev; + } + + curr = this._currentPage.next; + while (curr !== prevMarker && !handled) { + if (curr.elementUniqueID === element.uniqueID) { + this._shiftLeft(curr); + handled = true; + } + + curr = curr.next; + } + } + + return work.then(function () { + that._setButtonStates(); + }); + }, + + reload: function () { + this.resetState(0); + }, + + getItemSpacing: function () { + return this._itemSpacing; + }, + + setItemSpacing: function (space) { + this._itemSpacing = space; + this._ensureCentered(); + }, + + notificationsStarted: function () { + msWriteProfilerMark("WinJS.UI.FlipView:changeNotifications,StartTM"); + this._notificationsStarted = this._notificationsStarted || 0; + this._notificationsStarted++; + this._notificationsEndedSignal = new WinJS._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; + var animationPromises = []; + this._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._itemStart(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) { + var removedPage = that._createDiscardablePage(item); + that._itemStart(removedPage, record.originalLocation); + animationPromises.push(that._deleteFlipPage(removedPage)); + } + + function animateOldViewportItemMoved(record, item) { + 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._itemsManager.itemObject(item).index; + var newCurrentIndex = (that._currentPage.element ? that._itemsManager.itemObject(that._currentPage.element).index : 0); + newLocation += (newCurrentIndex > indexMovedTo ? -100 * that._bufferSize : 100 * that._bufferSize); + } else { + movedPage = flipPageFromElement(item); + newLocation = record.newLocation; + } + that._itemStart(movedPage, record.originalLocation); + animationPromises.push(that._moveFlipPage(movedPage, function () { + that._itemStart(movedPage, newLocation); + })); + } + + var oldCurrent = this._animationRecords.currentPage, + oldCurrentRecord = this._getAnimationRecord(oldCurrent), + oldNext = this._animationRecords.nextPage, + oldNextRecord = this._getAnimationRecord(oldNext); + if (oldCurrentRecord && oldCurrentRecord.changed) { + oldCurrent = oldCurrentRecord.newElement; + } + if (oldNextRecord && oldNextRecord.changed) { + oldNext = oldNextRecord.newElement; + } + + if (oldCurrent !== this._currentPage.element || oldNext !== this._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(WinJS.Promise.wrap()); + } + + return WinJS.Promise.join(animationPromises); + } + this._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._itemStart(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(); + } + msWriteProfilerMark("WinJS.UI.FlipView:changeNotifications,StopTM"); + }); + }); + }); + }, + + // Private methods + + _timeoutPageSelection: function () { + var that = this; + if (this._lastTimeoutRequest) { + this._lastTimeoutRequest.cancel(); + } + this._lastTimeoutRequest = WinJS.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; + }, + + _getAnimationRecord: function (element) { + return (element ? this._animationRecords[element.uniqueID] : 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; + } + }, + + _resetBuffer: function (elementToSave, skipReleases) { + 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) { + var newPage = this._createFlipPage(prevElement, this); + this._panningDiv.appendChild(newPage.pageRoot); + return newPage; + }, + + _fetchNextItems: function () { + var tail = WinJS.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) { + 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) { + var that = this; + + var tail = WinJS.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) { + 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 WinJS.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._itemStart(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._itemStart(this._currentPage); + this._viewportStart(this._lastScrollPos); + this._checkElementVisibility(true); + this._setupSnapPoints(); + if (!boundariesSet) { + this._setListEnds(); + } + }, + + _shiftLeft: function (startingPoint) { + 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); + }); + } + }, + + _shiftRight: function (startingPoint) { + 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 = document.createEvent("CustomEvent"); + msWriteProfilerMark("WinJS.UI.FlipView:pageVisibilityChangedEvent(visible:true),info"); + event.initCustomEvent(thisWinUI.FlipView.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 = document.createEvent("CustomEvent"); + msWriteProfilerMark("WinJS.UI.FlipView:pageVisibilityChangedEvent(visible:false),info"); + event.initCustomEvent(thisWinUI.FlipView.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: content.uniqueID, + discard: function () { + if (page.pageRoot.parentNode) { + page.pageRoot.parentNode.removeChild(page.pageRoot); + } + if (page.element.parentNode) { + page.elementRoot.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 = document.createElement("div"), + pageStyle = parentDiv.style, + flexBox = 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["-ms-overflow-style"] = "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); + } + } + page.element = element; + page.elementUniqueID = (element ? element.uniqueID : null); + utilities.empty(page.elementRoot); + + if (page.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 = page.element.uniqueID; + } + + var setAriaFlowAttributeIfIDChanged = function (element, target, attributeName) { + var record = manager._itemsManager._recordFromElement(element, true); + record && record.renderComplete.then(function () { + var completeElement = record.element; + if (target && completeElement && target.getAttribute(attributeName) !== completeElement.id) { + target.setAttribute(attributeName, completeElement.id); + } + }); + }; + + var setFlowAttribute = function (source, target, attributeName, isStaticID) { + source.setAttribute(attributeName, target.id); + + if (!isStaticID) { + // Update aria flow attribute if the element id changed in renderComplete + setAriaFlowAttributeIfIDChanged(target, source, attributeName); + } + } + + var isEnd = !page.next.element || page === manager._prevMarker.prev; + if (isEnd) { + setFlowAttribute(page.element, manager._bufferAriaEndMarker, "aria-flowto", true); + 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", true); + // 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._viewportStart() && this._itemStart(flipPage) < this._viewportEnd(); + }, + + _viewportStart: function (newValue) { + if (this._horizontal) { + if (newValue === undefined) { + return this._panningDivContainer.scrollLeft; + } + this._panningDivContainer.scrollLeft = newValue; + } else { + if (newValue === undefined) { + return this._panningDivContainer.scrollTop; + } + + this._panningDivContainer.scrollTop = newValue; + } + }, + + _viewportEnd: function () { + var element = this._panningDivContainer; + if (this._horizontal) { + if (this._rtl) { + return this._viewportStart() + this._panningDivContainerOffsetWidth; + } else { + return element.scrollLeft + this._panningDivContainerOffsetWidth; + } + } else { + return element.scrollTop + this._panningDivContainerOffsetHeight; + } + }, + + _viewportSize: function () { + return this._horizontal ? this._panningDivContainerOffsetWidth : this._panningDivContainerOffsetHeight; + }, + + _itemStart: function (flipPage, newValue) { + if (newValue === undefined) { + return flipPage.location; + } + + if (this._horizontal) { + flipPage.pageRoot.style.left = (this._rtl ? -newValue : newValue) + "px"; + } else { + flipPage.pageRoot.style.top = newValue + "px"; + } + + flipPage.location = newValue; + }, + + _itemEnd: function (flipPage) { + return (this._horizontal ? flipPage.location + this._panningDivContainerOffsetWidth : flipPage.location + this._panningDivContainerOffsetHeight) + this._itemSpacing; + }, + + _itemSize: function (flipPage) { + return this._horizontal ? this._panningDivContainerOffsetWidth : this._panningDivContainerOffsetHeight; + }, + + _movePageAhead: function (referencePage, pageToPlace) { + var delta = this._itemSize(referencePage) + this._itemSpacing; + this._itemStart(pageToPlace, this._itemStart(referencePage) + delta); + }, + + _movePageBehind: function (referencePage, pageToPlace) { + var delta = this._itemSize(referencePage) + this._itemSpacing; + this._itemStart(pageToPlace, this._itemStart(referencePage) - delta); + }, + + _setupSnapPoints: function () { + var containerStyle = this._panningDivContainer.style; + containerStyle["-ms-scroll-snap-type"] = "mandatory"; + var viewportSize = this._viewportSize(); + var snapInterval = viewportSize + this._itemSpacing; + var propertyName = "-ms-scroll-snap-points"; + var startSnap = 0; + var currPos = this._itemStart(this._currentPage); + startSnap = currPos % (viewportSize + this._itemSpacing); + containerStyle[(this._horizontal ? propertyName + "-x" : propertyName + "-y")] = "snapInterval(" + startSnap + "px, " + snapInterval + "px)"; + }, + + _setListEnds: function () { + if (this._currentPage.element) { + var containerStyle = this._panningDivContainer.style, + startScroll = 0, + endScroll = 0, + startNonEmptyPage = this._getTailOfBuffer(), + endNonEmptyPage = this._getHeadOfBuffer(), + startBoundaryStyle = "-ms-scroll-limit-" + (this._horizontal ? "x-min" : "y-min"), + endBoundaryStyle = "-ms-scroll-limit-" + (this._horizontal ? "x-max" : "y-max"); + + 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._itemStart(endNonEmptyPage); + startScroll = this._itemStart(startNonEmptyPage); + containerStyle[startBoundaryStyle] = startScroll + "px"; + containerStyle[endBoundaryStyle] = endScroll + "px"; + } + }, + + _viewportOnItemStart: function () { + return this._itemStart(this._currentPage) === this._viewportStart(); + }, + + _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 === discardablePage.element.uniqueID && !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; + // setImmediate needed to be able to register for the pageselected event after instantiating the control and still get the event + setImmediate(function () { + if (that._viewportOnItemStart()) { + that._blockTabs = false; + if (that._currentPage.element) { + if (that._hasFocus) { + try { + that._currentPage.element.setActive(); + that._tabManager.childFocus = that._currentPage.element; + } catch (e) { } + } + 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); + } + + // setImmediate needed in case a navigation is triggered inside the pageselected listener + setImmediate(function () { + if (that._currentPage.element) { + var event = document.createEvent("CustomEvent"); + event.initCustomEvent(thisWinUI.FlipView.pageSelectedEvent, true, false, { source: that._flipperDiv }); + msWriteProfilerMark("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 = document.createEvent("CustomEvent"); + event.initCustomEvent(thisWinUI.FlipView.pageCompletedEvent, true, false, { source: that._flipperDiv }); + msWriteProfilerMark("WinJS.UI.FlipView:pageCompletedEvent,info"); + that._currentPage.element.dispatchEvent(event); + } + }); + } + } + } + }); + } + } + } + }); + }, + + _forEachPage: function (callback) { + var go = true; + var curr = this._prevMarker; + while (go) { + if (callback(curr)) { + break; + } + curr = curr.next; + go = (curr !== this._prevMarker); + } + }, + + _changeFlipPage: function (page, oldElement, newElement) { + 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 WinJS.Promise.timeout().then(function () { + return animations.fadeOut(oldElement).then(function () { + oldElement.parentNode.removeChild(oldElement); + }); + }); + }, + + _deleteFlipPage: function (page) { + page.elementRoot.style.opacity = 0; + var animation = animations.createDeleteFromListAnimation([page.elementRoot]); + + + return WinJS.Promise.timeout().then(function () { + return animation.execute().then(function () { + if (page.discardable) { + page.discard(); + } + }); + }); + }, + + _insertFlipPage: function (page) { + page.elementRoot.style.opacity = 1.0; + var animation = animations.createAddToListAnimation([page.elementRoot]); + + return WinJS.Promise.timeout().then(function () { + return animation.execute().then(function () { + if (page.discardable) { + page.discard(); + } + }); + }); + }, + + _moveFlipPage: function (page, move) { + var animation = animations.createRepositionAnimation(page.pageRoot); + + return WinJS.Promise.timeout().then(function () { + move(); + return animation.execute().then(function () { + if (page.discardable) { + page.discard(); + } + }); + }); + } + }, { + supportedForProcessing: false, + } + ) + }); + + thisWinUI._FlipPageManager.flipPageBufferCount = 2; // The number of items that should surround the current item as a buffer at any time +})(WinJS); + + +(function animationHelperInit(global, WinJS, undefined) { + "use strict"; + + +var utilities = WinJS.Utilities, + Promise = WinJS.Promise, + Signal = WinJS._Signal, + Animation = WinJS.UI.Animation; + +var AnimationStage = { + waiting: 0, + remove: 1, + move: 2, + add: 3, + done: 4 +}; + +var gridReflowOutgoingDuration = 80; +var gridReflowIncomingDuration = 366; +var gridReflowTimeoutBuffer = 50; +function gridReflowOutgoingTransition() { + return "transform " + gridReflowOutgoingDuration + "ms linear " + WinJS.UI._libraryDelay + "ms"; +} + +function gridReflowIncomingTransition() { + return "transform " + gridReflowIncomingDuration + "ms cubic-bezier(0.1, 0.9, 0.2, 1) " + WinJS.UI._libraryDelay + "ms"; +} + +function _ListviewAnimationStage() { + this._affectedItems = {}; + this._running = false; +} + +_ListviewAnimationStage.prototype = { + stageCompleted: function () { + if (!this._completed) { + var itemKeys = Object.keys(this._affectedItems); + for (var i = 0, len = itemKeys.length; i < len; i++) { + var element = this._affectedItems[itemKeys[i]].element; + if (element._currentAnimationStage === this) { + // An item can be moved between stages, so the currentAnimationStage should only be cleared if this stage is the right stage + element._animating = false; + delete element._currentAnimationStage; + } + } + } + this._completed = true; + this._running = false; + }, + + /*#DBG + // _ListviewAnimationStage is an abstract class; these functions need to be implemented by the subclasses. + // mergeStage is called by the animation tracker. The stages being merged should be instances of the same class. + mergeStage: function () { + _ASSERT(false); + }, + + animateStage: function () { + _ASSERT(false); + }, + + cancel: function() { + }, + + replaceItemInStage: function (oldItem, newItem) { + _ASSERT(false); + }, + #DBG*/ + + clearAnimationProperties: function (item) { + item.style.transition = ""; + item.style.animationName = ""; + item.style.transform = ""; + item.style.opacity = 1.0; + }, + + removeItemFromStage: function (item) { + if (!item.parentNode) { + this.clearAnimationProperties(item); + } + item._animating = false; + delete item._currentAnimationStage; + delete this._affectedItems[item.uniqueID]; + }, + + running: function () { + return this._running; + } +}; + +function _ListviewAnimationRemoveStage(itemsRemoved, canvas, rtl) { + var itemIDs = Object.keys(itemsRemoved); + this._affectedItems = {}; + this._targetSurface = canvas; + this._positionProperty = (rtl ? "right" : "left"); + + for (var i = 0, len = itemIDs.length; i < len; i++) { + var itemID = itemIDs[i], + itemData = itemsRemoved[itemID], + itemAnimationStage = itemData.element._currentAnimationStage, + skipItemAnimation = false; + + if (itemAnimationStage && !itemAnimationStage.running()) { + // An item can already be attached to a different animation stage. + // Remove animations take precedence over the other two animation stages. + // If an item is in an add stage and is now being removed and that add animation hasn't played yet, + // then it's okay to just skip the item's animation entirely. + itemAnimationStage.removeItemFromStage(itemsRemoved[itemID].element); + if (itemAnimationStage instanceof _ListviewAnimationAddStage) { + skipItemAnimation = true; + if (itemData.element.parentNode) { + itemData.element.parentNode.removeChild(itemData.element); + } + } + } + + itemData.element._animating = true; + if (!skipItemAnimation) { + itemData.element._currentAnimationStage = this; + this._affectedItems[itemID] = itemsRemoved[itemID]; + } + } +} + +_ListviewAnimationRemoveStage.prototype = new _ListviewAnimationStage(); +_ListviewAnimationRemoveStage.prototype.mergeStage = function (stage) { + var newItemIDs = Object.keys(stage._affectedItems); + for (var i = 0, len = newItemIDs.length; i < len; i++) { + var itemID = newItemIDs[i], + itemData = stage._affectedItems[itemID]; + this._affectedItems[itemID] = itemData; + itemData.element._currentAnimationStage = this; + } +}; + +_ListviewAnimationRemoveStage.prototype.animateStage = function () { + this._running = true; + var itemIDs = Object.keys(this._affectedItems), + items = []; + if (itemIDs.length === 0) { + return Promise.wrap(); + } + + msWriteProfilerMark("WinJS.UI.ListView.Animation:remove,StartTM"); + var that = this; + function done() { + if (!that._canceled) { + for (var i = 0, len = itemIDs.length; i < len; i++) { + var item = that._affectedItems[itemIDs[i]].element; + if (item.parentNode) { + item.parentNode.removeChild(item); + item.style.opacity = 1.0; + } + } + } + that.stageCompleted(); + } + + for (var j = 0, lenJ = itemIDs.length; j < lenJ; j++) { + // It's necessary to set the opacity of every item being removed to 0 here. + // The deleteFromList animation will reset the item's opacity to 1.0 for the sake of animation, + // but once that animation is finished it will clean up every state it put on the item and reset the + // item back to its original style. If the opacity isn't set, the item will briefly flicker back + // on screen at full size+opacity until the cleanup code in done() runs. + var item = that._affectedItems[itemIDs[j]].element; + item.style.opacity = 0.0; + if (item.parentNode !== this._targetSurface) { + this._targetSurface.appendChild(item); + } + items.push(item); + } + + // One way or another, these promises are finishing. If something goes wrong with the animations, things will still be okay. + var animationPromise = Animation.createDeleteFromListAnimation(items).execute().then(done, done); + + msWriteProfilerMark("WinJS.UI.ListView.Animation:remove(removed:" + items.length + "),info"); + msWriteProfilerMark("WinJS.UI.ListView.Animation:remove,StopTM"); + return animationPromise; +}; + +_ListviewAnimationRemoveStage.prototype.replaceItemInStage = function (oldItem, newItem) { + if (!this._running) { + _prepareReplacement(this, oldItem, newItem); + this._affectedItems[newItem.uniqueID] = { element: newItem }; + } + + oldItem._animating = false; + delete oldItem._currentAnimationStage; + this.removeItemFromStage(oldItem); +}; + +_ListviewAnimationRemoveStage.prototype.cancel = function () { + var itemIDs = Object.keys(this._affectedItems); + for (var i = 0, len = itemIDs.length; i < len; i++) { + var item = this._affectedItems[itemIDs[i]].element; + this.clearAnimationProperties(item); + if (item.parentNode) { + item.parentNode.removeChild(item); + } + } + this._affectedItems = []; + this._canceled = true; +}; + +// The listview has two types of move animations: A Reflow animation, and a fade transition between the two views. +// A fade transition will play if there are variably sized items in or near the viewport. Reflows will play at all other times. +// The problem is that one move may be triggered using reflow, and another using fade (it's a very rare scenario, but possible). +// When this happens, the fade transition should take precedence. If the old move animation used fades, then the new one will use +// it too. +function _ListviewAnimationMoveStage(itemsMoved, gridLayout, useFadeAnimation, canvas, rtl, canvasHeight, totalItemHeight) { + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveConstructor,StartTM"); + + // The itemsMoved parameter passed to the move stage must map items to their old+new locations. + this._affectedItems = {}; + this._targetSurface = canvas; + this._positionProperty = (rtl ? "right" : "left"); + this._useFadeAnimation = useFadeAnimation; + this._gridLayout = gridLayout; + this._rtl = rtl; + // Canvas height and totalItemHeight are only used in a grid reflow animation, and as such will only be defined when that animation is being created + this._canvasHeight = canvasHeight; + this._totalItemHeight = totalItemHeight; + + var itemIDs = Object.keys(itemsMoved); + for (var i = 0, len = itemIDs.length; i < len; i++) { + var itemID = itemIDs[i], + itemData = itemsMoved[itemID], + itemAnimationStage = itemData.element._currentAnimationStage, + skipItemAnimation = false, + itemCameFromRunningMoveAnimation = false; + + if (itemAnimationStage) { + // An item can already be attached to a different animation stage. + // Moved items should never have a removed stage animation attached to them. + // If the moved item has another move stage attached to it, there can be two possibilities: + // 1 - The animation is already running. If this happens, the animation tracker will automatically handle + // the chaining of the old move animation and this new one. The item's animation stage just needs to be + // updated to point to this new stage instead. + // 2 - The animation hasn't yet run. If this is the case, the item is still in its original position before any + // move animations were queued up. In this case, this new stage will take the old stage's record of the item's + // old location, remove the item from the old stage, and animate the item going from oldRecordLoc to newRecordLoc. + // If the moved item has an add animation attached to it, there are two cases: + // 1 - Animation is not yet running. If that's the case, this stage won't animate the item being moved, but will + // move that item instantly to its final location and leave the item's stage alone. + // 2 - Animation is already running. In this case, the move animation should play once this item is done + // animating in. This stage will remove the item from the old add stage and prepare to animate it. + // The tracker will handle firing the move animation at the appropriate time. + //#DBG _ASSERT(!(itemAnimationStage instanceof _ListviewAnimationRemoveStage)); + if (!itemAnimationStage.running()) { + if (itemAnimationStage instanceof _ListviewAnimationMoveStage) { + var oldMoveData = itemAnimationStage._affectedItems[itemID], + newMoveData = itemsMoved[itemID]; + newMoveData.oldRow = oldMoveData.oldRow; + newMoveData.oldColumn = oldMoveData.oldColumn; + newMoveData.oldLeft = oldMoveData.oldLeft; + newMoveData.oldTop = oldMoveData.oldTop; + itemAnimationStage.removeItemFromStage(itemsMoved[itemID].element); + this._useFadeAnimation = this._useFadeAnimation || itemAnimationStage._useFadeAnimation; + } else if (itemAnimationStage instanceof _ListviewAnimationAddStage) { + skipItemAnimation = true; + itemAnimationStage.updateItemLocation (itemsMoved[itemID]); + } + } else if (itemAnimationStage instanceof _ListviewAnimationMoveStage) { + itemCameFromRunningMoveAnimation = true; + } + } + + itemData.element._animating = true; + if (!skipItemAnimation) { + if (!itemCameFromRunningMoveAnimation) { + // If an item came from a running move animation, we don't want to change its top/left properties mid animation. + var elementStyle = itemData.element.style; + elementStyle[this._positionProperty] = itemData.oldLeft + "px"; + elementStyle.top = itemData.oldTop + "px"; + } + this._affectedItems[itemID] = itemsMoved[itemID]; + itemData.element._currentAnimationStage = this; + } + } + + // This cancelAnimation function is the default cancel for move stages. If a move stage hasn't begun yet, it's clear to just reposition elements immediately. + // When the stage begins with whatever move animation should actually play, this default cancelAnimation function will be overwritten by a cancel function + // specific to that move animation. + this.cancelAnimation = function () { + for (var i = 0, len = itemIDs.length; i < len; i++) { + var itemData = this._affectedItems[itemIDs[i]]; + if (itemData) { + var element = itemData.element; + element.style.top = itemData.top + "px"; + element.style[this._positionProperty] = itemData.left + "px"; + } + } + this.stageCompleted(); + } + + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveConstructor,StopTM"); +} + +_ListviewAnimationMoveStage.prototype = new _ListviewAnimationStage(); +_ListviewAnimationMoveStage.prototype.mergeStage = function (stage) { + this._useFadeAnimation = this._useFadeAnimation || stage._useFadeAnimation; + var newItemIDs = Object.keys(stage._affectedItems); + for (var i = 0, len = newItemIDs.length; i < len; i++) { + // There shouldn't be any duplicate items in this merge. Items with two move stages would have been handled + // in the second stage's constructor. + var itemID = newItemIDs[i]; + if (!this._affectedItems[itemID]) { + this._affectedItems[itemID] = stage._affectedItems[itemID]; + this._affectedItems[itemID].element._currentAnimationStage = this; + } + } +}; + +_ListviewAnimationMoveStage.prototype.animateStage = function () { + this._running = true; + var itemIDs = Object.keys(this._affectedItems); + if (itemIDs.length === 0) { + return Promise.wrap(); + } + + var that = this; + function done() { + that.stageCompleted(); + } + + var animation = (this._gridLayout ? (this._useFadeAnimation ? this.createFadeReflowAnimation (itemIDs) : this.createGridReflowAnimation (itemIDs)) : this.createListReflowAnimation (itemIDs)); + return animation.then(done, done); +}; + +_ListviewAnimationMoveStage.prototype.createGridReflowAnimation = function (itemIDs) { + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveGridReflow,StartTM"); + var reflowingMoves = []; + var reflowingMoveData = []; + var remainingMoves = []; + var remainingMoveData = []; + var incomingColumnInfo = []; + var i = 0; + var len = 0; + + function updateColumnInfo(goingDown, row, column) { + if (!incomingColumnInfo[column]) { + incomingColumnInfo[column] = {}; + } + + var columnInfo = incomingColumnInfo[column]; + if (goingDown && (columnInfo.maxRow === undefined || columnInfo.maxRow < row)) { + columnInfo.maxRow = row; + } else if (!goingDown && (columnInfo.minRow === undefined || columnInfo.minRow > row)) { + columnInfo.minRow = row; + } + } + // The move animations work using transitions on position transforms. + // Once all items' move deltas have been calculated, they're moved to their final location. + // That means that all offsets to old positions need to be relative to their final location, not their original. + for (var i = 0, len = itemIDs.length; i < len; i++) { + var itemData = this._affectedItems[itemIDs[i]]; + var leftOffset = itemData.oldLeft - itemData.left; + var topOffset = itemData.oldTop - itemData.top; + + // This final offset check is necessary in the event of move stages being merged repeatedly. An item may have moved, but then been moved back + // before the first move stage could animate. + if (leftOffset !== 0 || topOffset !== 0) { + leftOffset = (this._rtl ? -leftOffset : leftOffset); + if (itemData.oldColumn !== undefined && itemData.oldColumn !== itemData.column) { + var outgoingEndOffset; + if (itemData.oldColumn > itemData.column) { + outgoingEndOffset = -this._canvasHeight + itemData.oldTop - itemData.top; + updateColumnInfo(false, itemData.row, itemData.column); + } else { + outgoingEndOffset = this._canvasHeight + itemData.oldTop - itemData.top; + updateColumnInfo(true, itemData.row, itemData.column); + } + reflowingMoves.push(itemData.element); + reflowingMoveData.push({ + outgoingLeftOffset: leftOffset, + outgoingStartOffset: itemData.oldTop - itemData.top, + outgoingEndOffset: outgoingEndOffset, + column: itemData.column, + goingDown: (itemData.oldColumn < itemData.column) + }); + } else { + remainingMoves.push(itemData.element); + remainingMoveData.push({ oldLeftOffset: leftOffset, oldTopOffset: itemData.oldTop - itemData.top }); + } + } + var element = itemData.element; + element.style.top = itemData.top + "px"; + element.style[this._positionProperty] = itemData.left + "px"; + } + var signal = new WinJS._Signal(), + moveData; + + for (i = 0, len = reflowingMoves.length; i < len; i++) { + moveData = reflowingMoveData[i]; + reflowingMoves[i].style.transform = "translate(" + moveData.outgoingLeftOffset + "px, " + moveData.outgoingStartOffset + "px)"; + var columnInfo = incomingColumnInfo[moveData.column]; + if (moveData.goingDown) { + moveData.incomingStartOffset = -this._totalItemHeight * (columnInfo.maxRow + 1); + } else { + moveData.incomingStartOffset = this._canvasHeight - (columnInfo.minRow * this._totalItemHeight); + } + } + for (i = 0, len = remainingMoves.length; i < len; i++) { + moveData = remainingMoveData[i]; + remainingMoves[i].style.transform = "translate(" + moveData.oldLeftOffset + "px, " + moveData.oldTopOffset + "px)"; + } + + var that = this; + var finishedOutgoing = false; + var finishedIncoming = false; + var elementsRemovedFromStage = {}; + var secondStageEventHandler = null; + + function forceRecalculateLayout() { + // Force trident to resolve the styles. This allows us to change the styles (transform/opacity/etc) + // of the element and then transition from those styles to a new set of styles. Without this the + // intermediate styles are ignored and it uses the original styles as the starting point. + for (i = 0, len = reflowingMoves.length; i < len; i++) { + window.getComputedStyle(reflowingMoves[i], null).transform; + } + for (i = 0, len = remainingMoves.length; i < len; i++) { + window.getComputedStyle(remainingMoves[i], null).transform; + } + } + + function onOutgoingEnd(eventObject) { + if (eventObject && eventObject.srcElement !== reflowingMoves[0]) { + return; + } + if (!finishedOutgoing) { + finishedOutgoing = true; + reflowingMoves[0].removeEventListener("transitionend", onOutgoingEnd, false); + playRemainingMoves(); + } + } + + function animationsComplete(eventObject) { + if (!finishedIncoming) { + if (eventObject && eventObject.srcElement !== secondStageEventHandler) { + // If this callback is triggered via an animation playing inside of an animating element, then we want to ignore the + // event and keep waiting. + return; + } + finishedIncoming = true; + for (i = 0, len = reflowingMoves.length; i < len; i++) { + if (!elementsRemovedFromStage[reflowingMoves[i].uniqueID]) { + reflowingMoves[i].style.transition = ""; + reflowingMoves[i].style.transform = ""; + } + } + if (reflowingMoves.length > 0) { + reflowingMoves[0].removeEventListener("transitionend", onOutgoingEnd, false); + } + for (i = 0, len = remainingMoves.length; i < len; i++) { + if (!elementsRemovedFromStage[remainingMoves[i].uniqueID]) { + remainingMoves[i].style.transition = ""; + remainingMoves[i].style.transform = ""; + } + } + if (secondStageEventHandler) { + secondStageEventHandler.removeEventListener("transitionend", animationsComplete, false); + } + signal.complete(); + } + } + + +/* +These transitions use watchdog timeouts to catch abandonment scenarios. +The watchdog timeout is set to the expected end of the action, plus a little extra time to ensure that when the action completes normally, +the completion event fires ahead of the watchdog. + +Since setTimeout doesn't wait for the next animation frame to start, we schedule the watchdog timer as a series of timeouts. +The first timeout is for a fixed small amount of time. This guarantees that the first timeout will not expire +until the first render pass is complete and the animation has started. +At that point, we can schedule the real timeout, and this one will not fire prematurely. +*/ + function setTimeoutAfterTTFF(callback, delay) { + setTimeout(function() { + setTimeout(callback, delay); + }, gridReflowTimeoutBuffer); + } + + function playRemainingMoves() { + if (that._canceled) { + return; + } + if (remainingMoves.length === 0 && reflowingMoves.length === 0) { + animationsComplete(); + } else { + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveGridReflowRemaining,StartTM"); + // Since the move animation is broken down into two parts, we need to check each element to see if it's been removed from this animation via + // removeItemFromStage. removeItemFromStage is called when an element is discarded through virtualization logic. + for (i = 0, len = reflowingMoves.length; i < len; i++) { + if (!elementsRemovedFromStage[reflowingMoves[i].uniqueID]) { + reflowingMoves[i].style.transition = ""; + reflowingMoves[i].style.transform = "translate(0px, " + reflowingMoveData[i].incomingStartOffset + "px)"; + } + } + + forceRecalculateLayout(); + var eventHandlingElement; + var incomingTransition = gridReflowIncomingTransition(); + for (i = 0, len = reflowingMoves.length; i < len; i++) { + if (!elementsRemovedFromStage[reflowingMoves[i].uniqueID]) { + eventHandlingElement = reflowingMoves[i]; + reflowingMoves[i].style.transition = incomingTransition; + reflowingMoves[i].style.transform = "translate(0px, 0px)"; + } + } + for (i = 0, len = remainingMoves.length; i < len; i++) { + if (!elementsRemovedFromStage[remainingMoves[i].uniqueID]) { + eventHandlingElement = remainingMoves[i]; + remainingMoves[i].style.transition = incomingTransition; + remainingMoves[i].style.transform = "translate(0px, 0px)"; + } + } + if (eventHandlingElement) { + secondStageEventHandler = eventHandlingElement; + eventHandlingElement.addEventListener("transitionend", animationsComplete, false); + setTimeoutAfterTTFF(animationsComplete, gridReflowIncomingDuration); + } else { + animationsComplete(); + } + + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveGridReflowRemaining,StopTM"); + } + } + + if (reflowingMoves.length > 0) { + forceRecalculateLayout(); + var outgoingTransition = gridReflowOutgoingTransition(); + for (i = 0, len = reflowingMoves.length; i < len; i++) { + reflowingMoves[i].style.transition = outgoingTransition; + moveData = reflowingMoveData[i]; + reflowingMoves[i].style.transform = "translate(" + moveData.outgoingLeftOffset + "px, " + moveData.outgoingEndOffset + "px)"; + } + reflowingMoves[0].addEventListener("transitionend", onOutgoingEnd, false); + setTimeoutAfterTTFF(onOutgoingEnd, gridReflowOutgoingDuration); + } else { + playRemainingMoves(); + } + + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveGridReflow:(reflowing:" + reflowingMoves.length + ", moving: " + remainingMoves.length + "),info"); + + + this.cancelAnimation = function() { + animationsComplete(); + }; + + this.removeItemFromStage = function(item) { + // If this version of removeItemFromStage is called, then it means an item has been recycled (other animation stages will only call + // removeItemFromStage when a new stage is created using that item AND the item's old animation stage hadn't yet been started). When this happens + // we need to mark the item as removed so it doesn't get animated in any later move stages. removeItemFromStage will clear any animation properties that might've been set on that element. + elementsRemovedFromStage[item.uniqueID] = true; + _ListviewAnimationMoveStage.prototype.removeItemFromStage.call(this, item); + }; + + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveGridReflow,StopTM"); + return signal.promise; +}; + +_ListviewAnimationMoveStage.prototype.createListReflowAnimation = function (itemIDs) { + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveListReflow,StartTM"); + + var that = this; + function done() { + that.stageCompleted(); + } + + var items = [], + i, len; + for (i = 0, len = itemIDs.length; i < len; i++) { + items.push(this._affectedItems[itemIDs[i]].element); + } + + var animation = Animation.createDeleteFromListAnimation ([], items); + + for (i = 0, len = itemIDs.length; i < len; i++) { + var itemData = this._affectedItems[itemIDs[i]]; + itemData.element.style[this._positionProperty] = itemData.left + "px"; + itemData.element.style.top = itemData.top + "px"; + } + + var that = this; + this.cancelAnimation = function() { + for (i = 0, len = items.length; i < len; i++) { + that.clearAnimationProperties(items[i]); + } + done(); + }; + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveListReflow:(moving:" + items.length + "),info"); + + var animationPromise = animation.execute().then(done, done); + + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveListReflow,StopTM"); + return animationPromise; +}; + +_ListviewAnimationMoveStage.prototype.createFadeReflowAnimation = function (itemIDs) { + var movedItems = [], + moveData = [], + i, len; + for (i = 0, len = itemIDs.length; i < len; i++) { + // This check is only necessary for the fade animation. In horizontal grid layout with + // variably sized items, the items' left and top properties will stay the same, + // but their row/column will change, so endLayout will treat the item as an affected item. + // This check will filter out the items that never moved. + var itemData = this._affectedItems[itemIDs[i]]; + if (itemData.oldLeft !== itemData.left || itemData.oldTop !== itemData.top) { + movedItems.push(itemData.element); + moveData.push(itemData); + } + } + + if (movedItems.length === 0) { + return Promise.wrap(); + } + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow,StartTM"); + + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow(moving: " + movedItems.length + "),info"); + + var signal = new WinJS._Signal(); + var that = this; + function done() { + signal.complete(); + } + function moveItems() { + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow:move,StartTM"); + for (i = 0, len = movedItems.length; i < len; i++) { + var itemStyle = movedItems[i].style, + itemData = moveData[i]; + itemStyle[that._positionProperty] = itemData.left + "px"; + itemStyle.top = itemData.top + "px"; + } + WinJS.UI.Animation.fadeIn(movedItems).then(done, done); + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow:move,StopTM"); + } + + WinJS.UI.Animation.fadeOut(movedItems).then(moveItems, moveItems); + + this.cancelAnimation = function () { + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow:move,StartTM"); + for (i = 0, len = movedItems.length; i < len; i++) { + var itemStyle = movedItems[i].style, + itemData = moveData[i]; + that.clearAnimationProperties(movedItems[i]); + itemStyle[that._positionProperty] = itemData.left + "px"; + itemStyle.top = itemData.top + "px"; + } + + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow:move,StopTM"); + done(); + }; + + msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow,StopTM"); + return signal.promise; +}; + +function _prepareReplacement(newStage, oldItem, newItem) { + newItem._currentAnimationStage = newStage; + newItem._animating = true; + newItem.style.position = oldItem.style.position; + newItem.style.left = oldItem.offsetLeft + "px"; + newItem.style.top = oldItem.offsetTop + "px"; +} + +_ListviewAnimationMoveStage.prototype.replaceItemInStage = function (oldItem, newItem) { + if (!this._running) { + _prepareReplacement(this, oldItem, newItem); + this._affectedItems[newItem.uniqueID] = this._affectedItems[oldItem.uniqueID]; + this._affectedItems[newItem.uniqueID].element = newItem; + } + + oldItem._animating = false; + delete oldItem._currentAnimationStage; + this.removeItemFromStage(oldItem); +}; + +_ListviewAnimationMoveStage.prototype.cancel = function () { + this.cancelAnimation(); + this._affectedItems = []; + this._canceled = true; +}; + +function _ListviewAnimationAddStage(itemsAdded, canvas, rtl) { + // The itemsMoved parameter passed to the move stage must map items to their old+new locations. + var itemIDs = Object.keys(itemsAdded); + this._affectedItems = {}; + this._targetSurface = canvas; + this._positionProperty = (rtl ? "right" : "left"); + + for (var i = 0, len = itemIDs.length; i < len; i++) { + var itemID = itemIDs[i], + itemData = itemsAdded[itemID], + itemAnimationStage = itemData.element._currentAnimationStage, + skipItemAnimation = false; + + if (itemAnimationStage) { + // An item can already be attached to a different animation stage. + // If an item is already attached to a remove stage, we'll follow this logic: + // - If remove animation is running, queue up add animation + // - If remove animation isn't running, cancel remove animation for that item and don't play add. + // Added items should never be attached to a move stage + // Added items should never be attached to another add stage + //#DBG _ASSERT(!(itemAnimationStage instanceof _ListviewAnimationMoveStage)); + //#DBG _ASSERT(!(itemAnimationStage instanceof _ListviewAnimationAddStage)); + if (itemAnimationStage instanceof _ListviewAnimationRemoveStage) { + if (!itemAnimationStage.running()) { + itemAnimationStage.removeItemFromStage(itemsAdded[itemID].element); + skipItemAnimation = true; + } + } + } + + itemData.element._animating = true; + if (!skipItemAnimation) { + this._affectedItems[itemID] = itemsAdded[itemID]; + itemData.element._currentAnimationStage = this; + } + } +} + +_ListviewAnimationAddStage.prototype = new _ListviewAnimationStage(); +_ListviewAnimationAddStage.prototype.mergeStage = function (stage) { + var newItemIDs = Object.keys(stage._affectedItems); + for (var i = 0, len = newItemIDs.length; i < len; i++) { + // There shouldn't be any duplicate items in this merge. + var itemID = newItemIDs[i]; + if (!this._affectedItems[itemID]) { + this._affectedItems[itemID] = stage._affectedItems[itemID]; + this._affectedItems[itemID].element._currentAnimationStage = this; + } + } +}; + +_ListviewAnimationAddStage.prototype.animateStage = function () { + this._running = true; + var itemIDs = Object.keys(this._affectedItems), + items = []; + + if (itemIDs.length === 0 || this._canceled) { + return Promise.wrap(); + } + msWriteProfilerMark("WinJS.UI.ListView.Animation:add,StartTM"); + + var that = this; + + function done() { + that.stageCompleted(); + } + + for (var i = 0, len = itemIDs.length; i < len; i++) { + var item = this._affectedItems[itemIDs[i]].element; + item.style.opacity = 1.0; + if (!item.parentNode) { + this._targetSurface.appendChild(item); + } + items.push(item); + } + + msWriteProfilerMark("WinJS.UI.ListView.Animation:add(added:" + items.length + "),info"); + + var animationPromise = Animation.createAddToListAnimation(items).execute().then(done, done); + msWriteProfilerMark("WinJS.UI.ListView.Animation:add,StopTM"); + return animationPromise; +}; + +// updateItemLocation will be called by the move animation stage. +// It will only be called if an item waiting to be added gets moved before its animation plays. +_ListviewAnimationAddStage.prototype.updateItemLocation = function (itemData) { + itemData.element.style[this._positionProperty] = itemData.left + "px"; + itemData.element.style.top = itemData.top + "px"; +}; + +_ListviewAnimationAddStage.prototype.replaceItemInStage = function (oldItem, newItem) { + if (!this._running) { + newItem._currentAnimationStage = this; + newItem._animating = true; + newItem._insertedItemAwaitingLayout = true; + newItem.style.opacity = 0.0; + this._affectedItems[newItem.uniqueID] = { element: newItem }; + } + + oldItem._animating = false; + delete oldItem._currentAnimationStage; + this.removeItemFromStage(oldItem); +}; + +_ListviewAnimationAddStage.prototype.cancel = function () { + var affectedItems = this._affectedItems, + itemIDs = Object.keys(affectedItems); + for (var i = 0, len = itemIDs.length; i < len; i++) { + this.clearAnimationProperties(affectedItems[itemIDs[i]].element); + } + this.stageCompleted(); + this._canceled = true; +}; + +function _ListViewAnimationTracker(removeStage, moveStage, addStage, oldTracker) { + var startRemoveStage = true; + + this._removeStage = removeStage; + this._moveStage = moveStage; + this._addStage = addStage; + this._started = false; + this._stopped = false; + this._currentStage = AnimationStage.waiting; + this._animationsSignal = new Signal(); + + if (oldTracker && !oldTracker.done()) { + if (oldTracker.waiting()) { + removeStage.mergeStage(oldTracker._removeStage); + moveStage.mergeStage(oldTracker._moveStage); + addStage.mergeStage(oldTracker._addStage); + this._waitingPromise = oldTracker._waitingPromise; + this._waitingPromise.then(waitComplete, waitComplete); + startRemoveStage = false; + } else { + var oldTrackerStage = oldTracker.getCurrentStage(); + switch (oldTrackerStage) { + case AnimationStage.remove: + moveStage.mergeStage(oldTracker._moveStage); + // Fallthrough is intentional here. If the old tracker was on the remove stage, then the new one + // needs to merge the move+add stages of the old tracker + case AnimationStage.move: + addStage.mergeStage(oldTracker._addStage); + break; + } + + // If the old tracker was in its remove stage, the tracker can play its remove animations while the older animation is still running. + if (oldTrackerStage !== AnimationStage.remove) { + startRemoveStage = false; + } + } + oldTracker.stopAnimations(); + } + + this._oldTracker = oldTracker; + if (startRemoveStage) { + this.startAnimations(oldTracker && oldTracker._waitingPromise); + } else { + var waitComplete = this.waitingComplete.bind(this); + this._waitingPromise = oldTracker._waitingPromise; + this._waitingPromise.then(waitComplete, waitComplete); + } +} + +_ListViewAnimationTracker.prototype = { + getCompletionPromise: function () { + if (this._done) { + return Promise.wrap(); + } + + return this._animationsSignal.promise; + }, + + waitingComplete: function () { + if (!this._stopped) { + this.startAnimations(); + } + }, + + nextStage: function () { + this._waitingPromise = null; + this._currentStage++; + + if (this._stopped) { + return; + } + + var targetStage = (this._currentStage === AnimationStage.move ? this._moveStage : (this._currentStage === AnimationStage.add ? this._addStage : null)); + + if (targetStage) { + this._waitingPromise = targetStage.animateStage(); + var that = this; + var moveToNext = function () { + that.nextStage(); + } + this._waitingPromise.then(moveToNext, moveToNext); + } else { + this._animationsSignal.complete(); + } + }, + + startAnimations: function (previousAnimationPromise) { + if (this._started) { + return; + } + this._started = true; + this._currentStage = AnimationStage.remove; + this._waitingPromise = Promise.join([previousAnimationPromise, this._removeStage.animateStage()]); + var moveToNext = this.nextStage.bind(this); + this._waitingPromise.then(moveToNext, moveToNext); + }, + + stopAnimations: function () { + this._stopped = true; + this._animationsSignal.complete(); + }, + + cancelAnimations: function () { + this._stopped = true; + if (this._currentStage === AnimationStage.waiting) { + this._oldTracker.cancelAnimations(); + } + if (this._currentStage < AnimationStage.move) { + this._removeStage.cancel(); + } + if (this._currentStage < AnimationStage.add) { + this._moveStage.cancel(); + } + if (this._currentStage < AnimationStage.done) { + this._addStage.cancel(); + } + this._animationsSignal.complete(); + }, + + getCurrentStage: function () { + return this._currentStage; + }, + + done: function () { + return this._currentStage === AnimationStage.done || this._stopped; + }, + + waiting: function () { + return this._currentStage === AnimationStage.waiting; + } +}; + +WinJS.Namespace.define("WinJS.UI", { + _ListViewAnimationHelper: { + fadeInElement: function (element) { + return Animation.fadeIn(element); + }, + + fadeOutElement: function (element) { + return Animation.fadeOut(element); + }, + + animateListFadeBetween: function (oldAnimationTracker, canvas, rtl, affectedItems, inserted, removed) { + return new _ListViewAnimationTracker(new _ListviewAnimationRemoveStage(removed, canvas, rtl), + new _ListviewAnimationMoveStage(affectedItems, true, true, canvas, rtl), + new _ListviewAnimationAddStage(inserted, canvas, rtl), oldAnimationTracker); + }, + + animateEntrance: function (canvas, firstEntrance) { + return Animation.enterContent(canvas, [{left: firstEntrance ? "100px" : "40px", top: "0px"}]); + }, + + animateReflow: function (oldAnimationTracker, canvas, rtl, affectedItems, inserted, removed, canvasHeight, totalItemHeight) { + return new _ListViewAnimationTracker(new _ListviewAnimationRemoveStage(removed, canvas, rtl), + new _ListviewAnimationMoveStage(affectedItems, true, false, canvas, rtl, canvasHeight, totalItemHeight), + new _ListviewAnimationAddStage(inserted, canvas, rtl), oldAnimationTracker); + }, + + animateListReflow: function (oldAnimationTracker, canvas, rtl, affectedItems, inserted, removed) { + return new _ListViewAnimationTracker(new _ListviewAnimationRemoveStage(removed, canvas, rtl), + new _ListviewAnimationMoveStage(affectedItems, false, false, canvas, rtl), + new _ListviewAnimationAddStage(inserted, canvas, rtl), oldAnimationTracker); + } + } +}); + +})(this, WinJS); + +(function browseModeInit(global, WinJS, undefined) { + "use strict"; + + var utilities = WinJS.Utilities, + Promise = WinJS.Promise, + Animation = WinJS.UI.Animation, + AnimationHelper = WinJS.UI._ListViewAnimationHelper; + + var SwipeBehaviorState = { + started: 0, + dragging: 1, + selecting: 2, + selectSpeedBumping: 3, + speedBumping: 4, + rearranging: 5, + completed: 6, + + selected: function (state) { + return state === this.selecting || state === this.selectSpeedBumping; + } + }; + + var RELEASE_TIMEOUT = 500; + var PT_TOUCH = 2; + + function getElementWithClass(parent, className) { + return parent.querySelector("." + className); + } + + function releaseWhen(winRTObject, predicate) { + if (!winRTObject) { return; } + + if (predicate(winRTObject)) { + try { + msReleaseWinRTObject(winRTObject); + } + catch (e) { + // double-release throws, and in this scenario we are OK with just + // ignoring that + } + } + else { + setTimeout(function () { releaseWhen(winRTObject, predicate); }, RELEASE_TIMEOUT); + } + } + + // This component is responsible for handling input in Browse Mode. + // When the user clicks on an item in this mode itemInvoked event is fired. + WinJS.Namespace.define("WinJS.UI", { + _getCursorPos: function (eventObject) { + var docElement = document.documentElement; + + return { + left: eventObject.clientX + (document.body.dir === "rtl" ? -docElement.scrollLeft : docElement.scrollLeft), + top: eventObject.clientY + docElement.scrollTop + }; + }, + + _getElementsByClasses: function (parent, classes) { + var retVal = [] + + for (var i = 0, len = classes.length; i < len; i++) { + var element = getElementWithClass(parent, classes[i]); + if (element) { + retVal.push(element); + } + } + return retVal; + }, + + _SelectionMode: function (modeSite) { + this.initialize(modeSite); + } + }); + + WinJS.UI._SelectionMode.prototype = { + _dispose: function () { + releaseWhen(this.cachedRecognizer, function (reco) { return !reco.isActive; }); + this.cachedRecognizer = null; + }, + initialize: function (modeSite) { + this.site = modeSite; + this.pressedItem = null; + this.pressedIndex = WinJS.UI._INVALID_INDEX; + this.pressedPosition = null; + + this._work = []; + this.animations = {}; + this.keyboardNavigationHandlers = {}; + this.keyboardAcceleratorHandlers = {}; + + function createArrowHandler(direction, clampToBounds) { + var handler = function (oldFocus) { + var items = modeSite._view.items; + return modeSite._layout.getKeyboardNavigatedItem(oldFocus, items.wrapperAt(oldFocus), direction); + }; + handler.clampToBounds = clampToBounds; + return handler; + } + + var Key = utilities.Key, + that = this; + 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 () { + return Promise.wrap(0); + }; + this.keyboardNavigationHandlers[Key.end] = function () { + // The two views need to treat their ends a bit differently. Scroll view is virtualized and will allow one to jump + // to the end of the list, but incremental view requires that the item be loaded before it can be jumped to. + // Due to that limitation, we need to ask the view what its final item is and jump to that. The incremental view + // will give the final loaded item, while the scroll view will give count - 1. + return that.site._view.finalItem(); + }; + + this.keyboardAcceleratorHandlers[Key.a] = function () { + if (that.site._multiSelection()) { + that.site._selection.selectAll(); + } + }; + + setTimeout(function () { + if (!that.cachedRecognizer && !that.site._isZombie()) { + that.cachedRecognizer = that.createGestureRecognizer(); + } + }, 500); + }, + + staticMode: function SelectionMode_staticMode() { + return this.site._tap === WinJS.UI.TapBehavior.none && this.site._selectionMode === WinJS.UI.SelectionMode.none; + }, + + togglePressed: function SelectionMode_togglePressed(add) { + if (!this.staticMode()) { + // If we are adding the pressed effect do it immediately because we + // already delayed it but if we are removing it add the delay here. + if (add) { + msWriteProfilerMark("WinJS.UI.ListView:applyPressedUI,info"); + utilities.addClass(this.pressedItem, WinJS.UI._pressedClass); + + // 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._layout.getItemPosition(this.pressedIndex).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 + }); + + this.pressedItem.style.transform = "scale(" + scale + "," + scale + ")"; + } else { + var element = this.pressedItem; + setImmediate(function () { + if (utilities.hasClass(element, WinJS.UI._pressedClass)) { + msWriteProfilerMark("WinJS.UI.ListView:removePressedUI,info"); + utilities.removeClass(element, WinJS.UI._pressedClass); + Animation.pointerUp(element); + } + }); + } + } + }, + + // In single selection mode, in addition to itemIndex's selection state being toggled, + // all other items will become deselected + _toggleItemSelection: function SelectionMode_toggleItemSelection(itemIndex) { + var site = this.site, + selection = this.site._selection, + selected = selection._isIncluded(itemIndex); + + if (site._selectionMode === WinJS.UI.SelectionMode.single) { + if (!selected) { + selection.set(itemIndex); + } else { + selection.clear(); + } + } else { + if (!selected) { + selection.add(itemIndex); + } else { + selection.remove(itemIndex); + } + } + }, + + handleTap: function SelectionMode_handleTap(itemIndex) { + var site = this.site, + selection = site._selection; + + if (site._selectionAllowed() && site._selectOnTap()) { + if (site._tap === WinJS.UI.TapBehavior.toggleSelect) { + this._toggleItemSelection(itemIndex); + } else { + // site._tap === WinJS.UI.TapBehavior.directSelect so ensure only itemIndex is selected + if (site._selectionMode === WinJS.UI.SelectionMode.multi || !selection._isIncluded(itemIndex)) { + selection.set(itemIndex); + } + } + } + }, + + handleSwipeBehavior: function SelectionMode_handleSwipeBehavior(itemIndex) { + var site = this.site; + + if (site._selectionAllowed()) { + this._toggleItemSelection(itemIndex); + } + }, + + itemUnrealized: function SelectionMode_itemUnrealized(index) { + if (this.pressedIndex === index) { + this.resetPointerDownState(); + } + }, + + fireInvokeEvent: function SelectionMode_fireInvokeEvent(itemIndex, itemElement) { + if (itemElement && + this.site._tap !== WinJS.UI.TapBehavior.none && + itemIndex !== WinJS.UI._INVALID_INDEX) { + + var listBinding = this.site.itemDataSource.createListBinding(), + itemPromise = listBinding.fromIndex(itemIndex); + + itemPromise.then(function (item) { + listBinding.release(); + }); + + var eventObject = document.createEvent("CustomEvent"); + eventObject.initCustomEvent("iteminvoked", true, true, { + itemPromise: itemPromise, + itemIndex: itemIndex + }); + + // If preventDefault was not called, call the default action on the site + if (itemElement.dispatchEvent(eventObject)) { + this.site._defaultInvoke(itemIndex); + } + } + }, + + selectionAllowed: function SelectionMode_selectionAllowed(itemIndex) { + var site = this.site; + if (site._selectionAllowed() && (site._selectOnTap() || site._swipeBehavior === WinJS.UI.SwipeBehavior.select)) { + 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 = 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 + }; + } + }, + + prepareItem: function SelectionMode_prepareItem(pressedIndex, pressedElement, selected) { + var that = this, + site = this.site; + + if (!selected) { + (this.animations[pressedIndex] || Promise.wrap()).then(function () { + if (!that.site._isZombie()) { + var items = site._view.items, + itemData = items.itemDataAt(pressedIndex), + pressedElement = itemData.element, + wrapper = itemData.wrapper; + + utilities.addClass(wrapper, WinJS.UI._swipeClass); + + if (!WinJS.UI._isSelectionRenderer(wrapper)) { + site._renderSelection(wrapper, pressedElement, true); + + utilities.removeClass(wrapper, WinJS.UI._selectedClass); + + var nodes = wrapper.querySelectorAll(WinJS.UI._selectionPartsSelector); + for (var i = 0, len = nodes.length; i < len; i++) { + nodes[i].style.opacity = 0; + } + } + } + }); + } else { + var wrapper = site._view.items.itemDataAt(pressedIndex).wrapper; + utilities.addClass(wrapper, WinJS.UI._swipeClass); + } + }, + + clearItem: function SelectionMode_clearItem(pressedIndex, selected) { + var site = this.site, + itemData = site._view.items.itemDataAt(pressedIndex); + + if (itemData) { + var wrapper = itemData.wrapper; + + utilities.removeClass(wrapper, WinJS.UI._swipeClass); + + site._renderSelection(wrapper, itemData.element, selected, true); + } + }, + + isInteractive: function SelectionMode_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; + }, + + resetPointerDownState: function SelectionMode_resetPointerDownState() { + if (this.gestureRecognizer) { + this.endSelfRevealGesture(); + this.endSwipeBehavior(true); + } + + if (this.pressedItem) { + this.togglePressed(false); + this.pressedItem = null; + } + + this.removeSelectionHint(); + + this.pressedIndex = WinJS.UI._INVALID_INDEX; + this.pointerId = null; + this.waitingForGot = false; + }, + + onMSPointerDown: function SelectionMode_onMSPointerDown(eventObject) { + msWriteProfilerMark("WinJS.UI.ListView:MSPointerDown,StartTM"); + var site = this.site, + that = this, + items = site._view.items, + touchInput = (eventObject.pointerType === PT_TOUCH), + leftButton, + rightButton; + + if (WinJS.Utilities.hasWinRT) { + // 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 === WinJS.UI._LEFT_MSPOINTER_BUTTON); + rightButton = (eventObject.button === WinJS.UI._RIGHT_MSPOINTER_BUTTON); + } + + this.swipeBehaviorState = null; + var swipeEnabled = site._swipeBehavior === WinJS.UI.SwipeBehavior.select, + swipeBehavior = touchInput && swipeEnabled, + isInteractive = this.isInteractive(eventObject.srcElement), + currentPressedIndex = items.index(eventObject.srcElement), + mustSetCapture = !isInteractive && currentPressedIndex !== WinJS.UI._INVALID_INDEX; + + if ((touchInput || leftButton || (site._selectionAllowed() && swipeEnabled && rightButton)) && this.pressedIndex === WinJS.UI._INVALID_INDEX && !isInteractive) { + this.pressedIndex = currentPressedIndex; + + if (this.pressedIndex !== WinJS.UI._INVALID_INDEX) { + this.pressedPosition = WinJS.UI._getCursorPos(eventObject); + + var allowed = this.selectionAllowed(this.pressedIndex); + this.canSelect = allowed.canSelect; + this.canTapSelect = allowed.canTapSelect; + + this.swipeBehaviorSelectionChanged = false; + this.selectionHint = null; + + this.pressedItem = items.wrapperAt(this.pressedIndex); + + this.togglePressed(true); + + if (swipeBehavior && this.canSelect) { + // Record the swipe start location on down because the first move could be far from the down + // location. Store the position off the pointerpoint since GestureRecognizer will not give us + // the delta they only give us the new location. + this.swipeBehaviorStart = this.getCurrentPoint(eventObject).position[this.site._layout.horizontal ? "y" : "x"]; + this.startSwipeBehavior(); + } + + if (this.canSelect) { + this.addSelectionHint(); + } + + // Even though we are calling msSetPointerCapture we may not recieve Pointer Capture. This can happen + // when the Semantic Zoom control calls msSetPointerCapture after us. When this happens we need to + // reset the pointer down state. We detect this scenario by recieving an MSPointerOut before an + // MSGotPointerCapture event. + this.waitingForGot = true; + + this.pointerId = eventObject.pointerId; + this.pointerRightButton = rightButton; + this.pointerTriggeredSRG = false; + + if (this.gestureRecognizer) { + this.gestureRecognizer.processDownEvent(this.getCurrentPoint(eventObject)); + } + + mustSetCapture = false; + if (touchInput) { + try { + site._canvasProxy.msSetPointerCapture(eventObject.pointerId); + } catch (e) { + this.resetPointerDownState(); + msWriteProfilerMark("WinJS.UI.ListView:MSPointerDown,StopTM"); + return; + } + } + + // Stop MSPointerDown from moving focus which is quite expensive sometimes + eventObject.preventDefault(); + } + } + + if (mustSetCapture) { + if (touchInput) { + try { + // Move pointer capture to avoid hover visual on second finger + site._canvasProxy.msSetPointerCapture(eventObject.pointerId); + } catch (e) { + msWriteProfilerMark("WinJS.UI.ListView: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 ((leftButton || rightButton) && !touchInput && // Left or right mouse/pen click + site._selectionAllowed() && site._multiSelection() && // Multi selection enabled + this.pressedIndex !== WinJS.UI._INVALID_INDEX && // A valid item was clicked + site._selection._getFocused() !== WinJS.UI._INVALID_INDEX && site._selection._pivot === WinJS.UI._INVALID_INDEX) { + site._selection._pivot = site._selection._getFocused(); + } + + msWriteProfilerMark("WinJS.UI.ListView:MSPointerDown,StopTM"); + }, + + // Play the self-reveal gesture (SRG) animation which jiggles the item to reveal the selection hint behind it + startSelfRevealGesture: function SelectionMode_startSelfRevealGesture(eventObject) { + if (eventObject.holdingState === Windows.UI.Input.HoldingState.started && this.canSelect && + this.site._swipeBehavior === WinJS.UI.SwipeBehavior.select) { + msWriteProfilerMark("WinJS.UI.ListView:playSelfRevealGesture,info"); + + var that = this; + var site = this.site, + Animation = WinJS.UI.Animation, + index = this.pressedIndex, + element = site._view.items.wrapperAt(index), + selected = site._selection._isIncluded(index); + + var swipeReveal = function () { + var top, + left; + + if (site._layout.horizontal) { + top = WinJS.UI._VERTICAL_SWIPE_SELF_REVEAL_GESTURE + "px"; + left = "0px"; + } else { + top = "0px"; + left = (site._rtl() ? "" : "-") + WinJS.UI._HORIZONTAL_SWIPE_SELF_REVEAL_GESTURE + "px"; + } + + return Animation.swipeReveal(element, { top: top, left: left }); + } + + var swipeHide = function () { + return Animation.swipeReveal(element, { top: "0px", left: "0px" }); + } + + var cleanUp = function (selectionHint) { + if (!that.site._isZombie()) { + if (selectionHint) { + that.removeSelectionHint(selectionHint); + } + that.clearItem(index, site._selection._isIncluded(index)); + } + } + + // Cancels the SRG animation by stopping the item at its current location. Leaves the item in the "prepared" + // state. + var freezeAnimation = function () { + // Replace the SRG animation with a no-op animation. The no-op animation works by moving the item to its current location. + var transformationMatrix = window.getComputedStyle(element).transform.slice(4, -1).split(", "), + left = transformationMatrix[4] + "px", + top = transformationMatrix[5] + "px"; + + that.selfRevealGesture._promise.cancel(); + Animation.swipeReveal(element, { top: top, left: left }); + } + + // 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(); + var selectionHint = that.selectionHint; + that.selectionHint = null; + return swipeHide().then(function () { + cleanUp(selectionHint); + }); + } + + this.prepareItem(index, element, selected); + this.showSelectionHintCheckmark(); + + this.pointerTriggeredSRG = true; + this.selfRevealGesture = { + freezeAnimation: freezeAnimation, + finishAnimation: finishAnimation, + _promise: swipeReveal(). + then(swipeHide). + then(function () { + that.hideSelectionHintCheckmark(); + cleanUp(); + that.selfRevealGesture = null; + }) + }; + } + }, + + endSelfRevealGesture: function SelectionMode_endSelfRevealGesture() { + if (this.selfRevealGesture) { + this.selfRevealGesture.finishAnimation(); + this.selfRevealGesture = null; + } + }, + + onMSPointerMove: function SelectionMode_onMSPointerMove(eventObject) { + if (this.pointerId === eventObject.pointerId) { + if (this.gestureRecognizer) { + this.gestureRecognizer.processMoveEvents(this.getIntermediatePoints(eventObject)); + } + } + }, + + onclick: function SelectionMode_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 index = this.site._view.items.index(eventObject.srcElement); + + if (utilities.hasClass(eventObject.srcElement, WinJS.UI._itemClass) && index !== WinJS.UI._INVALID_INDEX) { + var allowed = this.selectionAllowed(index); + if (allowed.canTapSelect) { + this.handleTap(index); + } + this.fireInvokeEvent(index, eventObject.srcElement); + } + } + }, + + _releasedElement: function SelectionMode_releasedElement(eventObject) { + return document.elementFromPoint(eventObject.clientX, eventObject.clientY); + }, + + onMSPointerUp: function SelectionMode_onMSPointerUp(eventObject) { + msWriteProfilerMark("WinJS.UI.ListView:MSPointerUp,StartTM"); + + this.skipClick = true; + var that = this; + var swipeEnabled = this.site._swipeBehavior === WinJS.UI.SwipeBehavior.select; + setImmediate(function () { + that.skipClick = false; + }); + + if (this.gestureRecognizer) { + this.gestureRecognizer.processUpEvent(this.getCurrentPoint(eventObject)); + } + + try { + // Release the pointer capture to allow in air touch pointers to be reused for multiple interactions + this.site._canvasProxy.msReleasePointerCapture(eventObject.pointerId); + } catch (e) { + // This can throw if SeZo had capture or if the pointer was not already captured + } + + var site = this.site, + currentFocus = site._selection._getFocused(), + touchInput = (eventObject.pointerType === PT_TOUCH), + items = site._view.items, + releasedElement = this._releasedElement(eventObject), + releasedIndex = items.index(releasedElement); + + if (this.pointerId === eventObject.pointerId) { + if (this.pressedItem) { + this.togglePressed(false); + } + + if (this.pressedItem && !touchInput && this.pressedIndex === releasedIndex) { + if (!eventObject.shiftKey) { + // Reset the shift selection pivot when the user clicks w/o pressing shift + site._selection._pivot = WinJS.UI._INVALID_INDEX; + } + + if (eventObject.shiftKey) { + // Shift selection should work when shift or shift+ctrl are depressed for both left- and right-click + if (site._selectionAllowed() && site._multiSelection() && site._selection._pivot !== WinJS.UI._INVALID_INDEX) { + site._selection.set({ + firstIndex: Math.min(this.pressedIndex, site._selection._pivot), + lastIndex: Math.max(this.pressedIndex, site._selection._pivot) + }); + } + } else if (eventObject.ctrlKey || (site._selectionAllowed() && swipeEnabled && this.pointerRightButton)) { + // Swipe emulation + this.handleSwipeBehavior(this.pressedIndex); + } + } + + if (this.pressedItem && this.swipeBehaviorState !== SwipeBehaviorState.completed) { + var upPosition = WinJS.UI._getCursorPos(eventObject); + var isTap = Math.abs(upPosition.left - this.pressedPosition.left) <= WinJS.UI._TAP_END_THRESHOLD && + Math.abs(upPosition.top - this.pressedPosition.top) <= WinJS.UI._TAP_END_THRESHOLD; + + this.endSelfRevealGesture(); + this.clearItem(this.pressedIndex, this.isSelected(this.pressedIndex)); + + // 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.pressedIndex === releasedIndex))) { + this.pressedItem = items.wrapperAt(this.pressedIndex); + if (this.canTapSelect) { + this.handleTap(this.pressedIndex); + } + this.fireInvokeEvent(this.pressedIndex, this.pressedItem); + } + } + + if (this.pressedIndex !== WinJS.UI._INVALID_INDEX) { + site._changeFocus(this.pressedIndex, true, false, true); + } + + this.resetPointerDownState(); + } + + msWriteProfilerMark("WinJS.UI.ListView:MSPointerUp,StopTM"); + }, + + onMSPointerOut: function SelectionMode_onMSPointerOut(eventObject) { + if (this.waitingForGot && eventObject.pointerType === PT_TOUCH && this.pointerId === eventObject.pointerId) { + this.resetPointerDownState(); + } + }, + + onMSPointerCancel: function SelectionMode_onMSPointerCancel(eventObject) { + if (this.pointerId === eventObject.pointerId) { + msWriteProfilerMark("WinJS.UI.ListView:MSPointerCancel,info"); + this.resetPointerDownState(); + } + }, + + onMSGotPointerCapture: function SelectionMode_onMSGotPointerCapture(eventObject) { + if (this.pointerId === eventObject.pointerId) { + this.waitingForGot = false; + msWriteProfilerMark("WinJS.UI.ListView:MSGotPointerCapture,info"); + } + }, + + onMSLostPointerCapture: function SelectionMode_onMSLostPointerCapture(eventObject) { + if (this.pointerId === eventObject.pointerId && eventObject.target === this.site._canvasProxy) { + msWriteProfilerMark("WinJS.UI.ListView:MSLostPointerCapture,info"); + this.resetPointerDownState(); + } + }, + + // In order for ListView 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 SelectionMode_onContextMenu(eventObject) { + var itemWrapperElement = this.site._view.items.wrapperFrom(eventObject.srcElement); + + if (this.site._selectionAllowed() && itemWrapperElement && !this.isInteractive(eventObject.srcElement)) { + eventObject.preventDefault(); + } + }, + + onMSHoldVisual: function SelectionMode_onMSHoldVisual(eventObject) { + if (!this.isInteractive(eventObject.srcElement)) { + eventObject.preventDefault(); + } + }, + + onDataChanged: function SelectionMode_onDataChanged() { + this.resetPointerDownState(); + }, + + createGestureRecognizer: function SelectionMode_createGestureRecognizer() { + if (WinJS.Utilities.hasWinRT) { + var recognizer = new Windows.UI.Input.GestureRecognizer(); + var settings = Windows.UI.Input.GestureSettings; + recognizer.gestureSettings = settings.hold | settings.crossSlide | settings.manipulationTranslateX | settings.manipulationTranslateY; + recognizer.showGestureFeedback = false; + + var that = this; + recognizer.addEventListener("crosssliding", function (eventObject) { + that.dispatchSwipeBehavior(eventObject); + }); + recognizer.addEventListener("manipulationstarted", function (eventObject) { + that.manipulationStarted(eventObject); + }); + recognizer.addEventListener("holding", function (eventObject) { + that.startSelfRevealGesture(eventObject); + }); + + return recognizer; + } else { + return null; + } + }, + + getCurrentPoint: function SelectionMode_getCurrentPoint(eventObject) { + return Windows.UI.Input.PointerPoint.getCurrentPoint(eventObject.pointerId); + }, + + getIntermediatePoints: function SelectionMode_getIntermediatePoints(eventObject) { + return Windows.UI.Input.PointerPoint.getIntermediatePoints(eventObject.pointerId); + }, + + startSwipeBehavior: function SelectionMode_startSwipeBehavior() { + if (WinJS.UI._PerfMeasurement_setCachedRecognizerStatus && this.cachedRecognizer) { + WinJS.UI._PerfMeasurement_setCachedRecognizerStatus(this.cachedRecognizer.isActive); + } + + if (!this.cachedRecognizer || this.cachedRecognizer.isActive) { + releaseWhen(this.cachedRecognizer, function (reco) { return !reco.isActive; }); + this.cachedRecognizer = this.createGestureRecognizer(); + } + + this.gestureRecognizer = this.cachedRecognizer; + if (this.gestureRecognizer) { + var thresholds = { + rearrangeStart: null + }; + + if (this.site._layout.horizontal) { + thresholds.selectionStart = WinJS.UI._VERTICAL_SWIPE_SELECTION_THRESHOLD; + thresholds.speedBumpStart = WinJS.UI._VERTICAL_SWIPE_SPEED_BUMP_START; + thresholds.speedBumpEnd = WinJS.UI._VERTICAL_SWIPE_SPEED_BUMP_END; + } else { + thresholds.selectionStart = WinJS.UI._HORIZONTAL_SWIPE_SELECTION_THRESHOLD; + thresholds.speedBumpStart = WinJS.UI._HORIZONTAL_SWIPE_SPEED_BUMP_START; + thresholds.speedBumpEnd = WinJS.UI._HORIZONTAL_SWIPE_SPEED_BUMP_END; + } + + thresholds.speedBumpStart += thresholds.selectionStart; + thresholds.speedBumpEnd += thresholds.speedBumpStart; + this.gestureRecognizer.crossSlideThresholds = thresholds; + + this.gestureRecognizer.crossSlideHorizontally = !this.site._layout.horizontal; + } + }, + + manipulationStarted: function SelectionMode_manipulationStarted(eventObject) { + this.resetPointerDownState(); + }, + + animateSelectionChange: function SelectionMode_animateSelectionChange(select) { + var that = this, + pressedItem = this.pressedItem; + + function toggleClasses() { + var classOperation = select ? "addClass" : "removeClass"; + utilities[classOperation](pressedItem, WinJS.UI._selectedClass); + if (that.selectionHint) { + var hintCheckMark = getElementWithClass(that.selectionHint, WinJS.UI._selectionHintClass); + if (hintCheckMark) { + utilities[classOperation](hintCheckMark, WinJS.UI._revealedClass); + } + } + } + + this.swipeBehaviorSelectionChanged = true; + this.swipeBehaviorSelected = select; + + var elementsToShowHide = WinJS.UI._getElementsByClasses(this.pressedItem, [WinJS.UI._selectionBorderContainerClass, WinJS.UI._selectionBackgroundClass]); + + if (!select) { + elementsToShowHide = elementsToShowHide.concat(WinJS.UI._getElementsByClasses(this.pressedItem, [WinJS.UI._selectionCheckmarkBackgroundClass, WinJS.UI._selectionCheckmarkClass])); + } + + msWriteProfilerMark("WinJS.UI.ListView:" + (select ? "hitSelectThreshold" : "hitUnselectThreshold") + ",info"); + + this.applyUIInBatches(function () { + msWriteProfilerMark("WinJS.UI.ListView:" + (select ? "apply" : "remove") + "SelectionVisual,info"); + var opacity = (select ? 1 : 0); + for (var i = 0; i < elementsToShowHide.length; i++) { + elementsToShowHide[i].style.opacity = opacity; + } + + toggleClasses(); + }); + }, + + isSelected: function SelectionMode_isSelected(index) { + return (!this.swipeBehaviorSelectionChanged && this.site._selection._isIncluded(index)) || (this.swipeBehaviorSelectionChanged && this.swipeBehaviorSelected); + }, + + endSwipeBehavior: function SelectionMode_endSwipeBehavior(animateBack) { + var that = this; + + this.flushUIBatches(); + + var selectionHint = this.selectionHint; + this.selectionHint = null; + + if (this.gestureRecognizer) { + this.gestureRecognizer.completeGesture(); + this.gestureRecognizer = null; + } + + return new Promise(function (complete) { + var pressedIndex = that.pressedIndex, + selected = that.isSelected(pressedIndex); + + function cleanUp() { + if (!that.site._isZombie()) { + that.clearItem(pressedIndex, that.site._selection._isIncluded(pressedIndex)); + if (selectionHint) { + that.removeSelectionHint(selectionHint); + } + delete that.animations[pressedIndex]; + + complete(); + } + } + + if (!that.pressedItem) { + complete(); + } else if (animateBack) { + if (selected) { + var elementsToShowHide = WinJS.UI._getElementsByClasses(that.pressedItem, [WinJS.UI._selectionCheckmarkClass, WinJS.UI._selectionCheckmarkBackgroundClass]); + for (var i = 0; i < elementsToShowHide.length; i++) { + elementsToShowHide[i].style.opacity = 1; + } + } + that.animations[pressedIndex] = Animation.swipeSelect(that.pressedItem, []); + that.animations[pressedIndex].then(cleanUp, cleanUp); + } else { + cleanUp(); + } + }); + }, + + dispatchSwipeBehavior: function SelectionMode_dispatchSwipeBehavior(eventObject) { + if (this.pressedItem) { + if (this.swipeBehaviorState !== eventObject.crossSlidingState) { + if (eventObject.crossSlidingState === SwipeBehaviorState.started) { + msWriteProfilerMark("WinJS.UI.ListView:crossSlidingStarted,info"); + var site = this.site, + items = site._view.items, + pressedElement = items.itemAt(this.pressedIndex), + selected = site._selection._isIncluded(this.pressedIndex); + + if (this.selfRevealGesture) { + this.selfRevealGesture.freezeAnimation(); + this.selfRevealGesture = null; + } else if (this.canSelect) { + this.prepareItem(this.pressedIndex, pressedElement, selected); + } + + if (utilities.hasClass(pressedElement, WinJS.UI._pressedClass)) { + utilities.removeClass(pressedElement, WinJS.UI._pressedClass); + } + + this.swipeBehaviorTransform = ""; + this.showSelectionHintCheckmark(); + } else if (eventObject.crossSlidingState === SwipeBehaviorState.completed) { + msWriteProfilerMark("WinJS.UI.ListView:crossSlidingCompleted,info"); + var that = this, + site = this.site, + selection = site._selection, + pressedIndex = this.pressedIndex, + swipeBehaviorSelectionChanged = this.swipeBehaviorSelectionChanged, + swipeBehaviorSelected = this.swipeBehaviorSelected; + + + // snap back and remove addional elements + this.endSwipeBehavior(true); + if (swipeBehaviorSelectionChanged) { + if (site._selectionAllowed() && site._swipeBehavior === WinJS.UI.SwipeBehavior.select) { + if (site._selectionMode === WinJS.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); + } + } + } + } + } else if (SwipeBehaviorState.selected(eventObject.crossSlidingState) && !SwipeBehaviorState.selected(this.swipeBehaviorState) && this.canSelect) { + this.animateSelectionChange(!this.site._selection._isIncluded(this.pressedIndex)); + } else if (!SwipeBehaviorState.selected(eventObject.crossSlidingState) && SwipeBehaviorState.selected(this.swipeBehaviorState) && this.canSelect) { + this.animateSelectionChange(this.site._selection._isIncluded(this.pressedIndex)); + } + this.swipeBehaviorState = eventObject.crossSlidingState; + } + + if (eventObject.crossSlidingState !== SwipeBehaviorState.completed) { + // When swiping we get many pointer move events to update the UI. To save the CPU and layout work + // we only do one transform per animation frame. + this.crossSlideTransformOffset = Math.floor(eventObject.position[this.site._layout.horizontal ? "y" : "x"]) - this.swipeBehaviorStart; + this.applyUIInBatches(this._processTransform.bind(this)); + } + } + }, + + _processTransform: function SelectionMode_processTransform() { + if (this.pressedItem && +this.crossSlideTransformOffset === this.crossSlideTransformOffset) { + msWriteProfilerMark("WinJS.UI.ListView:applyCrossSlideTransform,info"); + var transform = "translate" + (this.site._layout.horizontal ? "Y" : "X") + "(" + this.crossSlideTransformOffset + "px)"; + this.pressedItem.style.transform = this.swipeBehaviorTransform + " " + transform; + this.crossSlideTransformOffset = null; + } + }, + + applyUIInBatches: function SelectionMode_applyUIInBatches(work) { + var that = this; + this._work.push(work); + + if (!this._paintedThisFrame) { + applyUI(); + } + + function applyUI() { + if (that._work.length > 0) { + that.flushUIBatches(); + that._paintedThisFrame = requestAnimationFrame(applyUI.bind(that)); + } else { + that._paintedThisFrame = null; + } + } + }, + + flushUIBatches: function SelectionMode_flushUIBatches() { + if (this._work.length > 0) { + var workItems = this._work; + this._work = []; + + for (var i = 0; i < workItems.length; i++) { + workItems[i](); + } + } + }, + + showSelectionHintCheckmark: function SelectionMode_showSelectionHintCheckmark() { + if (this.selectionHint) { + var hintCheckMark = getElementWithClass(this.selectionHint, WinJS.UI._selectionHintClass); + if (hintCheckMark) { + hintCheckMark.style.display = 'block'; + } + } + }, + + hideSelectionHintCheckmark: function SelectionMode_hideSelectionHintCheckmark() { + if (this.selectionHint) { + var hintCheckMark = getElementWithClass(this.selectionHint, WinJS.UI._selectionHintClass); + if (hintCheckMark) { + hintCheckMark.style.display = 'none'; + } + } + }, + + addSelectionHint: function SelectionMode_addSelectionHint() { + var selectionHint = this.selectionHint = document.createElement("div"); + selectionHint.className = WinJS.UI._wrapperClass + " " + WinJS.UI._footprintClass; + + if (!this.site._selection._isIncluded(this.pressedIndex)) { + var element = document.createElement("div"); + element.className = WinJS.UI._selectionHintClass; + element.innerText = WinJS.UI._SELECTION_CHECKMARK; + element.style.display = 'none'; + this.selectionHint.appendChild(element); + } + + var that = this; + this.site._layout.getItemPosition(this.pressedIndex).then(function (pos) { + if (!that.site._isZombie() && that.selectionHint && that.selectionHint === selectionHint) { + var style = selectionHint.style; + var cssText = ";position:absolute;" + + (that.site._rtl() ? "right:" : "left:") + pos.left + "px;top:" + + pos.top + "px;width:" + pos.contentWidth + "px;height:" + pos.contentHeight + "px"; + style.cssText += cssText; + that.site._itemCanvas.insertBefore(that.selectionHint, that.pressedItem); + } + }, function() { + // Swallow errors in case data source changes + }); + }, + + removeSelectionHint: function SelectionMode_removeSelectionHint(selectionHint) { + if (!selectionHint) { + selectionHint = this.selectionHint; + this.selectionHint = null; + } + if (selectionHint && selectionHint.parentNode) { + selectionHint.parentNode.removeChild(selectionHint); + } + }, + + onDragStart: function SelectionMode_onDragStart(eventObject) { + eventObject.preventDefault(); + }, + + onKeyDown: function SelectionMode_onKeyDown(eventObject) { + if (eventObject.altKey) { + return; + } + + var that = this, + site = this.site, + swipeEnabled = site._swipeBehavior === WinJS.UI.SwipeBehavior.select, + view = site._view, + oldFocus = site._selection._getFocused(), + handled = true, + handlerName, + ctrlKeyDown = eventObject.ctrlKey; + + function setNewFocus(newFocus, skipSelection, clampToBounds) { + // We need to get the final item in the view so that we don't try setting focus out of bounds. + return view.finalItem().then(function (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) { + newFocus = Math.max(0, Math.min(maxIndex, newFocus)); + } else if (newFocus < 0 || newFocus > maxIndex) { + invalidIndex = true; + } + if (!invalidIndex && oldFocus !== newFocus) { + var navigationEvent = document.createEvent("CustomEvent"); + navigationEvent.initCustomEvent("keyboardnavigating", true, true, { + oldFocus: oldFocus, + newFocus: newFocus + }); + var changeFocus = that.site._element.dispatchEvent(navigationEvent); + if (changeFocus) { + site._changeFocus(newFocus, skipSelection, ctrlKeyDown, false, true); + moveView = false; + } + } + + // 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(oldFocus, true); + site.ensureVisible(oldFocus); + } + if (invalidIndex) { + return WinJS.UI._INVALID_INDEX; + } else { + return newFocus; + } + }); + } + + var Key = utilities.Key, + keyCode = eventObject.keyCode; + + if (!this.isInteractive(eventObject.srcElement)) { + if (eventObject.ctrlKey && !eventObject.altKey && !eventObject.shiftKey && this.keyboardAcceleratorHandlers[keyCode]) { + this.keyboardAcceleratorHandlers[keyCode](); + } + + if (this.keyboardNavigationHandlers[keyCode]) { + this.keyboardNavigationHandlers[keyCode](oldFocus).then(function (index) { + var clampToBounds = that.keyboardNavigationHandlers[keyCode].clampToBounds; + if (eventObject.shiftKey && site._selectionAllowed() && site._multiSelection()) { + // Shift selection should work when shift or shift+ctrl are depressed + if (site._selection._pivot === WinJS.UI._INVALID_INDEX) { + site._selection._pivot = oldFocus; + } + setNewFocus(index, true, clampToBounds).then(function (newFocus) { + if (newFocus !== WinJS.UI._INVALID_INDEX) { + site._selection.set({ + firstIndex: Math.min(newFocus, site._selection._pivot), + lastIndex: Math.max(newFocus, site._selection._pivot) + }); + } + }); + } else { + site._selection._pivot = WinJS.UI._INVALID_INDEX; + setNewFocus(index, false, clampToBounds); + } + }); + } else if (!eventObject.ctrlKey && keyCode === Key.enter) { + var item = site._view.items.itemAt(oldFocus); + if (item) { + this.pressedIndex = oldFocus; + this.pressedItem = item; + + var allowed = this.selectionAllowed(oldFocus); + if (allowed.canTapSelect) { + this.handleTap(oldFocus); + } + this.fireInvokeEvent(oldFocus, this.pressedItem); + + // Check if fireInvokeEvent changed the data source, which caused pressedItem to become null + if (this.pressedItem) { + this.pressedItem = null; + this.pressedIndex = WinJS.UI._INVALID_INDEX; + this.site._changeFocus(oldFocus, true, ctrlKeyDown, false, true); + } + } + } else if (eventObject.ctrlKey && keyCode === Key.enter || + (swipeEnabled && eventObject.shiftKey && keyCode === Key.F10) || + (swipeEnabled && keyCode === Key.menu) || + keyCode === Key.space) { + // Swipe emulation + this.handleSwipeBehavior(oldFocus); + this.site._changeFocus(oldFocus, true, ctrlKeyDown, false, true); + } else if (keyCode === Key.escape && this.site._selection.count() > 0) { + site._selection._pivot = WinJS.UI._INVALID_INDEX; + site._selection.clear(); + } else { + handled = false; + } + + if (handled) { + eventObject.stopPropagation(); + eventObject.preventDefault(); + } + } + + if (keyCode === Key.tab) { + this.site._keyboardFocusInbound = true; + } + } + }; + + WinJS.Namespace.define("WinJS.UI", { + _DataTransfer: function () { + this.formatsMap = {}; + this.dropEffect = "move"; + } + }); + + WinJS.UI._DataTransfer.prototype = { + setData: function DataTransfer_setData(format, data) { + this.formatsMap[format] = data; + }, + + getData: function DataTransfer_getData(format) { + return this.formatsMap[format]; + }, + + count: function DataTransfer_count() { + return Object.keys(this.formatsMap).length; + } + }; +})(this, WinJS); + +(function constantsInit(global, WinJS, undefined) { + "use strict"; + + var thisWinUI = WinJS.UI; + thisWinUI._listViewClass = "win-listview"; + thisWinUI._viewportClass = "win-viewport"; + thisWinUI._rtlListViewClass = "win-rtl"; + thisWinUI._horizontalClass = "win-horizontal"; + thisWinUI._verticalClass = "win-vertical"; + thisWinUI._scrollableClass = "win-surface"; + thisWinUI._proxyClass = "_win-proxy"; + thisWinUI._backdropClass = "win-backdrop"; + thisWinUI._itemClass = "win-item"; + thisWinUI._wrapperClass = "win-container"; + thisWinUI._footprintClass = "win-footprint"; + thisWinUI._groupsClass = "win-groups"; + thisWinUI._selectedClass = "win-selected"; + thisWinUI._swipeableClass = "win-swipeable"; + thisWinUI._swipeClass = "win-swipe"; + thisWinUI._selectionBorderContainerClass = "win-selectionbordercontainer"; + thisWinUI._selectionBorderClass = "win-selectionborder"; + thisWinUI._selectionBorderTopClass = "win-selectionbordertop"; + thisWinUI._selectionBorderRightClass = "win-selectionborderright"; + thisWinUI._selectionBorderBottomClass = "win-selectionborderbottom"; + thisWinUI._selectionBorderLeftClass = "win-selectionborderleft"; + thisWinUI._selectionBackgroundClass = "win-selectionbackground"; + thisWinUI._selectionCheckmarkClass = "win-selectioncheckmark"; + thisWinUI._selectionCheckmarkBackgroundClass = "win-selectioncheckmarkbackground"; + thisWinUI._selectionPartsSelector = ".win-selectionbordercontainer, .win-selectionbackground, .win-selectioncheckmark, .win-selectioncheckmarkbackground"; + thisWinUI._pressedClass = "win-pressed"; + thisWinUI._headerClass = "win-groupheader"; + thisWinUI._progressClass = "win-progress"; + thisWinUI._selectionHintClass = "win-selectionhint"; + thisWinUI._revealedClass = "win-revealed"; + thisWinUI._itemFocusClass = "win-focused"; + thisWinUI._itemFocusOutlineClass = "win-focusedoutline"; + thisWinUI._zoomingXClass = "win-zooming-x"; + thisWinUI._zoomingYClass = "win-zooming-y"; + + thisWinUI._INVALID_INDEX = -1; + thisWinUI._UNINITIALIZED = -1; + + thisWinUI._LEFT_MSPOINTER_BUTTON = 0; + thisWinUI._RIGHT_MSPOINTER_BUTTON = 2; + + thisWinUI._TAP_END_THRESHOLD = 10; + + thisWinUI._DEFAULT_PAGES_TO_LOAD = 5; + thisWinUI._DEFAULT_PAGE_LOAD_THRESHOLD = 2; + thisWinUI._INCREMENTAL_CANVAS_PADDING = 100; + + thisWinUI._DEFERRED_ACTION = 500; + + // For horizontal layouts + thisWinUI._VERTICAL_SWIPE_SELECTION_THRESHOLD = 39; + thisWinUI._VERTICAL_SWIPE_SPEED_BUMP_START = 0; + thisWinUI._VERTICAL_SWIPE_SPEED_BUMP_END = 127; + thisWinUI._VERTICAL_SWIPE_SELF_REVEAL_GESTURE = 15; + + // For vertical layouts + thisWinUI._HORIZONTAL_SWIPE_SELECTION_THRESHOLD = 27; + thisWinUI._HORIZONTAL_SWIPE_SPEED_BUMP_START = 0; + thisWinUI._HORIZONTAL_SWIPE_SPEED_BUMP_END = 150; + thisWinUI._HORIZONTAL_SWIPE_SELF_REVEAL_GESTURE = 23; + + thisWinUI._SELECTION_CHECKMARK = "\uE081"; + + thisWinUI._LISTVIEW_PROGRESS_DELAY = 2000; +})(this, WinJS); + +(function errorMessagesInit(global, WinJS, undefined) { + "use strict"; + + WinJS.Namespace.define("WinJS.UI._strings", { + + layoutIsInvalid: { + get: function () { return WinJS.Resources._getWinJSString("ui/layoutIsInvalid").value; } + }, + + modeIsInvalid: { + get: function () { return WinJS.Resources._getWinJSString("ui/modeIsInvalid").value; } + }, + + loadingBehaviorIsInvalid: { + get: function () { return WinJS.Resources._getWinJSString("ui/loadingBehaviorIsInvalid").value; } + }, + + pagesToLoadIsInvalid: { + get: function () { return WinJS.Resources._getWinJSString("ui/pagesToLoadIsInvalid").value; } + }, + + pagesToLoadThresholdIsInvalid: { + get: function () { return WinJS.Resources._getWinJSString("ui/pagesToLoadThresholdIsInvalid").value; } + }, + + automaticallyLoadPagesIsInvalid: { + get: function () { return WinJS.Resources._getWinJSString("ui/automaticallyLoadPagesIsInvalid").value; } + }, + + layoutNotInitialized: { + get: function () { return WinJS.Resources._getWinJSString("ui/layoutNotInitialized").value; } + }, + + invalidTemplate: { + get: function () { return WinJS.Resources._getWinJSString("ui/invalidTemplate").value; } + } + + }); + +})(this, WinJS); + +(function groupsContainerInit(global, WinJS, undefined) { + "use strict"; + + var utilities = WinJS.Utilities, + Promise = WinJS.Promise; + + // This component is responsible for dividing the items into groups and storing the information about these groups. + WinJS.Namespace.define("WinJS.UI", { + _GroupsContainer: function (listView, groupDataSource) { + this._listView = listView; + this.groupDataSource = groupDataSource; + this.groups = []; + this.groupsStage = []; + this.pendingChanges = []; + this.dirty = true; + + var that = this, + notificationHandler = { + beginNotifications: function GroupsContainer_beginNotifications() { + that._listView._versionManager.beginNotifications(); + }, + + endNotifications: function GroupsContainer_endNotifications() { + //#DBG _ASSERT(that.assertValid()); + that._listView._versionManager.endNotifications(); + + if (that._listView._ifZombieDispose()) { return; } + + if (!that.ignoreChanges && that._listView._groupsChanged) { + that._listView._scheduleUpdate(); + } + }, + + indexChanged: function GroupsContainer_indexChanged(item, newIndex, oldIndex) { + that._listView._versionManager.receivedNotification(); + + if (that._listView._ifZombieDispose()) { return; } + + this.scheduleUpdate(); + }, + + itemAvailable: function GroupsContainer_itemAvailable(item, placeholder) { + }, + + countChanged: function GroupsContainer_countChanged(newCount, oldCount) { + that._listView._versionManager.receivedNotification(); + + if (that._listView._ifZombieDispose()) { return; } + + this.scheduleUpdate(); + }, + + changed: function GroupsContainer_changed(newItem, oldItem) { + that._listView._versionManager.receivedNotification(); + + if (that._listView._ifZombieDispose()) { return; } + + //#DBG _ASSERT(newItem.key == oldItem.key); + var groupEntry = that.stageObject.fromKey(newItem.key); + if (groupEntry) { + groupEntry.group.userData = newItem; + groupEntry.group.startIndex = newItem.firstItemIndexHint; + //#DBG _ASSERT(that.assertValid()); + this.markToRemove(groupEntry.group); + } + + this.scheduleUpdate(); + }, + + removed: function GroupsContainer_removed(itemHandle, mirage) { + that._listView._versionManager.receivedNotification(); + + if (that._listView._ifZombieDispose()) { return; } + + var groupEntry = that.stageObject.fromHandle(itemHandle); + if (groupEntry) { + that.groupsStage.splice(groupEntry.index, 1); + var index = that.groups.indexOf(groupEntry.group, groupEntry.index); + + if (index > -1) { + that.groups.splice(index, 1); + } + + //#DBG _ASSERT(that.assertValid()); + this.markToRemove(groupEntry.group); + } + + this.scheduleUpdate(); + }, + + inserted: function GroupsContainer_inserted(itemPromise, previousHandle, nextHandle) { + that._listView._versionManager.receivedNotification(); + + if (that._listView._ifZombieDispose()) { return; } + + var notificationHandler = this; + itemPromise.retain().then(function (item) { + //#DBG _ASSERT(!that.stageObject.fromKey(item.key)) + + var index = notificationHandler.findIndex(previousHandle, nextHandle); + if (index !== -1) { + var newGroup = { + key: item.key, + startIndex: item.firstItemIndexHint, + userData: item, + handle: itemPromise.handle + }; + newGroup.startFound = Promise.wrap(newGroup); + + that.groupsStage.splice(index, 0, newGroup); + that._updateGroupsView(); + } + notificationHandler.scheduleUpdate(); + }); + that.pendingChanges.push(itemPromise); + }, + + moved: function GroupsContainer_moved(itemPromise, previousHandle, nextHandle) { + that._listView._versionManager.receivedNotification(); + + if (that._listView._ifZombieDispose()) { return; } + + var notificationHandler = this; + itemPromise.then(function (item) { + var newIndex = notificationHandler.findIndex(previousHandle, nextHandle), + groupEntry = that.stageObject.fromKey(item.key); + + if (groupEntry) { + that.groupsStage.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.groupsStage.splice(newIndex, 0, groupEntry.group); + } + + that._updateGroupsView(); + } else if (newIndex !== -1) { + var newGroup = { + key: item.key, + startIndex: item.firstItemIndexHint, + userData: item, + handle: itemPromise.handle + }; + newGroup.startFound = Promise.wrap(newGroup); + that.groupsStage.splice(newIndex, 0, newGroup); + that._updateGroupsView(); + itemPromise.retain(); + } + + //#DBG _ASSERT(that.assertValid()); + notificationHandler.scheduleUpdate(); + }); + that.pendingChanges.push(itemPromise); + }, + + reload: function GroupsContainer_reload() { + that._listView._versionManager.receivedNotification(); + + if (that._listView._ifZombieDispose()) { + return; + } + + for (var i = 0, len = that.groups.length; i < len; i++) { + this.removeElements(that.groups[i]); + } + + // Set the lengths to zero to clear the arrays, rather than setting = [], which re-instantiates + that.groups.length = 0; + that.groupsStage.length = 0; + delete that.pinnedItem; + delete that.pinnedOffset; + this.scheduleUpdate(); + }, + + markToRemove: function GroupsContainer_markToRemove(group) { + if (group.elements) { + var elements = group.elements; + group.elements = null; + group.left = -1; + group.width = -1; + group.decorator = null; + + that._listView._groupsToRemove[elements.header.uniqueID] = elements.header; + that._listView._groupsToRemove[elements.group.uniqueID] = elements.group; + } + }, + + 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.stageObject.fromHandle(previousHandle); + if (groupEntry) { + index = groupEntry.index + 1; + } + } + + if (index === -1 && nextHandle) { + groupEntry = that.stageObject.fromHandle(nextHandle); + if (groupEntry) { + index = groupEntry.index; + } + } + + return index; + }, + + removeElements: function GroupsContainer_removeElements(group) { + if (group.elements) { + var canvas = that._listView._canvas; + canvas.removeChild(group.elements.header); + canvas.removeChild(group.elements.group); + group.elements = null; + group.left = -1; + group.width = -1; + } + } + }; + + this.listBinding = this.groupDataSource.createListBinding(notificationHandler); + + // This object is used to call a few functions below in a context that redirects + // references to the "groups" array to the "groupsStage" array + // This saves us from having to duplicate these functions. + // Note that we don't redirect the "dirty" property because we don't want that bit + // flipped when we modify groupsStage. + this.stageObject = { + groups: that.groupsStage, + groupFrom: function () { + return that.groupFrom.apply(that.stageObject, arguments); + }, + groupFromImpl: function () { + return that.groupFromImpl.apply(that.stageObject, arguments); + }, + fromKey: function () { + return that.fromKey.apply(that.stageObject, arguments); + }, + fromHandle: function () { + return that.fromHandle.apply(that.stageObject, arguments); + } + }; + } + }); + + WinJS.UI._GroupsContainer.prototype = { + _updateGroupsView: function GroupsContainer_updateGroupsView() { + var that = this; + this.groups = []; + + this.groupsStage.forEach(function (group) { + if (group.userData !== undefined) { + that.groups.push(group); + } + }); + }, + + addItem: function GroupsContainer_addItem(itemIndex, itemPromise) { + var that = this; + return this.ensureFirstGroup().then(function () { + return itemPromise; + }).then(function (item) { + if (!item) { + return WinJS.Promise.cancel; + } + + var currentIndex = that.groupFromItem.call(that.stageObject, itemIndex); + + var currentGroup = null, + currentGroupKey = null, + nextGroup = null, + previousGroup = null; + + if (currentIndex !== null) { + currentGroup = that.groupsStage[currentIndex]; + currentGroupKey = currentGroup.key; + + if (currentIndex + 1 < that.groupsStage.length) { + nextGroup = that.groupsStage[currentIndex + 1]; + } + if (currentIndex > 0) { + previousGroup = that.groupsStage[currentIndex - 1]; + } + } + + var newGroupKey = item.groupKey; + //#DBG _ASSERT(newGroupKey); + if (currentGroupKey && newGroupKey === currentGroupKey) { + if (itemIndex < currentGroup.startIndex) { + currentGroup.startIndex = itemIndex; + that.dirty = true; + } + //#DBG _ASSERT(that.assertValid()); + // The item belongs to the current group + return currentGroup.startFound; + // Maybe the item belongs to the next group. This can happen when the beginning of the next group is still not known (nextGroup.startFound !== undefined). + } else if (nextGroup && nextGroup.key === newGroupKey) { + return nextGroup.startFound; + } else if (previousGroup && previousGroup.key === newGroupKey) { + return previousGroup.startFound; + } else { + // The item belongs to a new group + var newGroup = { + key: newGroupKey, + startIndex: itemIndex + }; + that.addGroup.call(that.stageObject, currentGroup, currentIndex, newGroup); + //#DBG _ASSERT(that.assertValid()); + newGroup.startFound = that.listBinding.fromKey(newGroupKey, { groupMemberKey: item.key, groupMemberIndex: itemIndex }); + newGroup.handle = newGroup.startFound.handle; + newGroup.startFound.retain().then(function (newGroupData) { + if (newGroupData) { + newGroup.userData = newGroupData; + newGroup.startIndex = newGroupData.firstItemIndexHint; + that._updateGroupsView(); + + that.dirty = true; + //#DBG _ASSERT(that.assertValid()); + return newGroup; + } else { + return WinJS.Promise.cancel; + } + }).then(null, function (err) { + for (var i = 0, len = that.groupsStage.length; i < len; i++) { + if (that.groupsStage[i] === newGroup) { + that.groupsStage.splice(i, 1); + break; + } + } + return WinJS.Promise.wrapError(err); + }); + return newGroup.startFound; + } + }); + }, + + ensureFirstGroup: function GroupsContainer_ensureFirstGroup() { + var that = this; + + function firstGroupLoaded() { + return that.groups.length > 0 && that.groups[0].startIndex === 0; + } + + if (firstGroupLoaded()) { + return Promise.wrap(); + } + + var itemPromise = this.listBinding.first().retain(); + return itemPromise.then(function (firstGroupData) { + if (firstGroupData) { + if (firstGroupLoaded()) { + // Somebody loaded the first group before us so we don't need to retain it + itemPromise.release(); + } else { + var newGroup = { + key: firstGroupData.key, + startIndex: firstGroupData.firstItemIndexHint, + userData: firstGroupData, + handle: firstGroupData.handle + }; + newGroup.startFound = Promise.wrap(newGroup); + that.groups.unshift(newGroup); + that.groupsStage.unshift(newGroup); + that.dirty = true; + + //#DBG _ASSERT(that.assertValid()); + + return newGroup; + } + } else { + return WinJS.Promise.cancel; + } + }); + }, + + addGroup: function GroupsContainer_addGroup(currentGroup, currentIndex, toInsert) { + if (currentGroup) { + this.groups.splice((currentGroup.startIndex < toInsert.startIndex ? currentIndex + 1 : currentIndex), 0, toInsert); + } else { + this.groups.unshift(toInsert); + } + + this.dirty = true; + }, + + groupFromImpl: function GroupsContainer_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 GroupsContainer_groupFrom(comp) { + //#DBG _ASSERT(this.assertValid()); + 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 GroupsContainer_groupFromItem(itemIndex) { + return this.groupFrom(function (group) { + return itemIndex < group.startIndex; + }); + }, + + groupFromOffset: function GroupsContainer_groupFromOffset(offset) { + //#DBG _ASSERT(this.assertValid()); + return this.groupFrom(function (group, groupIndex) { + //#DBG _ASSERT(group.offset !== undefined); + return offset < group.offset; + }); + }, + + group: function GroupsContainer_getGroup(index) { + return this.groups[index]; + }, + + length: function GroupsContainer_length() { + return this.groups.length; + }, + + renderGroup: function GroupsContainer_renderGroup(index) { + var group = this.groups[index], + renderedHeaderPromise, + renderedGroupPromise; + + if (this._listView.groupHeaderTemplate) { + renderedHeaderPromise = this._listView._headersPool.renderItemAsync(Promise.wrap(group.userData)); + renderedGroupPromise = this._listView._groupsPool.renderItemAsync(Promise.wrap(null)); + return Promise.join({ header: renderedHeaderPromise, group: renderedGroupPromise }); + } else { + return Promise.wrap(null); + } + }, + + setDomElements: function GroupsContainer_setDomElements(index, headerElement, groupElement) { + var group = this.groups[index]; + //#DBG _ASSERT(!group.elements); + group.elements = { + header: headerElement, + group: groupElement + }; + }, + + removeElements: function GroupsContainer_removeElements() { + var canvas = this._listView._canvas, + elements = this._listView._groupsToRemove || {}, + keys = Object.keys(elements); + + for (var i = 0, len = keys.length; i < len; i++) { + var element = elements[keys[i]]; + canvas.removeChild(element); + } + + this._listView._groupsToRemove = {}; + }, + + resetGroups: function GroupsContainer_resetGroups() { + var groupsStage = this.groupsStage.slice(0); + + for (var i = 0, len = groupsStage.length; i < len; i++) { + var group = groupsStage[i]; + + group.startFound.cancel(); + 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.groupsStage.length = 0; + delete this.pinnedItem; + delete this.pinnedOffset; + this.dirty = true; + }, + + pinItem: function GroupsContainer_pinItem(item, offset) { + this.pinnedItem = item; + this.pinnedOffset = offset ? offset.offset : null; + this.dirty = true; + }, + + cleanUp: function GroupsContainer_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 GroupsContainer_dispose() { + this.cleanUp(); + }, + + /*#DBG + assertValid: function () { + if (WinJS.validation) { + if (this.groups.length) { + var prevIndex = this.groups[0].startIndex, + prevKey = this.groups[0].key, + keys = {}; + + //#DBG _ASSERT(prevIndex === 0); + keys[prevKey] = true; + + for (var i = 1, len = this.groups.length; i < len; i++) { + var group = this.groups[i]; + //#DBG _ASSERT(group.startIndex > prevIndex); + prevIndex = group.startIndex; + + //#DBG _ASSERT(!keys[group.key]); + keys[group.key] = true; + + prevKey = group.key; + } + } + } + return true; + }, + #DBG*/ + + groupOf: function GroupsContainer_groupOf(item) { + return item ? this.addItem(item.index, item) : Promise.wrap(); + }, + + synchronizeGroups: function GroupsContainer_synchronizeGroups() { + var that = this; + + function done() { + that.ignoreChanges = false; + } + + this.pendingChanges = []; + this.ignoreChanges = true; + return this.groupDataSource.invalidateAll().then(function () { + return Promise.join(that.pendingChanges); + }).then(function () { + if (that._listView._ifZombieDispose()) { + return WinJS.Promise.cancel; + } + }).then(done, done); + }, + + fromKey: function GroupsContainer_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 GroupsContainer_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; + }, + + purgeDecorator: function GroupsContainer_purgeDecorator(index) { + var group = this.groups[this.groupFromItem(index)]; + if (group) { + group.decorator = null; + } + }, + + purgeDecorators: function GroupsContainer_purgeDecorators() { + for (var i = 0, len = this.groups.length; i < len; i++) { + this.groups[i].decorator = null; + } + } + }; + + WinJS.UI._NoGroups = function (listView) { + this._listView = listView; + this.groups = [{ startIndex: 0}]; + this.dirty = true; + + this.synchronizeGroups = function () { + return WinJS.Promise.wrap(); + }; + + this.addItem = function (itemIndex, itemPromise) { + return WinJS.Promise.wrap(this.groups[0]); + }; + + this.resetGroups = function () { + this.groups = [{ startIndex: 0}]; + delete this.pinnedItem; + delete this.pinnedOffset; + this.dirty = true; + }; + + this.renderGroup = function () { + return WinJS.Promise.wrap(null); + }; + + this.purgeDecorator = function () { + this.groups[0].decorator = null; + }; + + this.purgeDecorators = function () { + this.groups[0].decorator = null; + }; + }; + + WinJS.UI._NoGroups.prototype = WinJS.UI._GroupsContainer.prototype; + +})(this, WinJS); + +(function horizontalGropuedGridLayoutInit(global, WinJS, undefined) { + "use strict"; + + +var utilities = WinJS.Utilities, + Key = utilities.Key, + Promise = WinJS.Promise, + Signal = WinJS._Signal, + Animation = WinJS.UI.Animation, + AnimationHelper = WinJS.UI._ListViewAnimationHelper; + +function getDimension(element, property) { + return WinJS.Utilities.convertToPixels(element, window.getComputedStyle(element, null)[property]); +} + + +// This component is responsible for calculating items' positions in horizontal grid mode. +WinJS.Namespace.define("WinJS.UI", { + Layout: WinJS.Class.define(function Layout_ctor(options) { + /// + /// + /// Creates a new Layout object. + /// + /// + /// The set of options to be applied initially to the new Layout object. + /// + /// + /// The new Layout object. + /// + /// + }), + + _getMargins: function Layout_getMargins(element) { + return { + left: getDimension(element, "marginLeft"), + right: getDimension(element, "marginRight"), + top: getDimension(element, "marginTop"), + bottom: getDimension(element, "marginBottom") + }; + }, + + _LayoutCommon: WinJS.Class.derive(WinJS.UI.Layout, function (options) { + }, { + init: function _LayoutCommon_init() { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + this._disableBackdrop = false; + this._trackedAnimation = null; + this._items = {}; + this.reset(); + }, + + setSite: function _LayoutCommon_setSite(layoutSite) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + this._site = layoutSite; + }, + + /// + /// 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: { + get: function () { + return this._disableBackdrop; + }, + set: function (value) { + if (value) { + // Hide the existing backdrop elements + if (this._backdropBefore && this._backdropBefore.parentNode) { + this._backdropBefore.parentNode.removeChild(this._backdropBefore); + } + if (this._backdropAfter && this._backdropAfter.parentNode) { + this._backdropAfter.parentNode.removeChild(this._backdropAfter); + } + } + return this._disableBackdrop = value; + } + }, + + /// + /// 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: { + get: function _LayoutCommon_backdropColor_get() { + return this._backdropColor || "rgba(155,155,155,0.23)"; + }, + set: function _LayoutCommon_backdropColor_set(color) { + this._backdropColor = color; + + // Reset the canvas to null so a new background-image is created via canvas + this._backdropCanvas = null; + + // Hide the existing backdrop so a quick pan after this changes doesn't show the previous backdrop + if (this._backdropBefore && this._backdropBefore.parentNode) { + this._backdropBefore.parentNode.removeChild(this._backdropBefore); + } + if (this._backdropAfter && this._backdropAfter.parentNode) { + this._backdropAfter.parentNode.removeChild(this._backdropAfter); + } + } + }, + + reset: function _LayoutCommon_reset() { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + if (this._trackedAnimation) { + this._trackedAnimation.cancelAnimations(); + } + + this._measuring = null; + this._totalItemWidth = WinJS.UI._UNINITIALIZED; + this._totalItemHeight = WinJS.UI._UNINITIALIZED; + this._itemWidth = WinJS.UI._UNINITIALIZED; + this._itemHeight = WinJS.UI._UNINITIALIZED; + this._totalHeaderWidth = WinJS.UI._UNINITIALIZED; + this._totalHeaderHeight = WinJS.UI._UNINITIALIZED; + this._itemsPerColumn = WinJS.UI._UNINITIALIZED; + this._itemMargins = { + left: 0, + right: 0, + top: 0, + bottom: 0, + horizontal: 0, + vertical: 0 + }; + if (this._item0PositionPromise) { + this._item0PositionPromise.cancel(); + this._item0PositionPromise = null; + } + this._backdropCanvas = null; + this._headerPaddingAndBorder = 0; + this._headerMargins = { + left: 0, + right: 0, + top: 0 + }; + this._canvasScrollBounds = { + left: 0, + right: 0, + }; + + this._cachedInserted = []; + if (this._cachedRemoved) { + this._cachedRemoved.forEach(function (element) { + if (element.parentNode) { + element.parentNode.removeChild(element); + } + }); + } + this._cachedRemoved = []; + this._cachedItemRecords = {}; + this._dataModifiedPromise = null; + + if (this._site) { + if (this._site._groups) { + this._site._groups.purgeDecorators(); + } + + this._site.itemSurface.style.clip = "auto"; + } + }, + + /// + /// Determines the size of the item and whether + /// the item should be placed in a new column. + /// + itemInfo: { + enumerable: true, + get: function () { + return this._itemSize; + }, + set: function (itemSize) { + this._itemSize = itemSize; + this._invalidateLayout(); + } + }, + + _invalidateLayout: function _LayoutCommon_invalidateLayout() { + if (this._site) { + this._site.invalidateLayout(); + } + }, + + _getSize: function _LayoutCommon_getSize(group) { + if (this._totalItemWidth === WinJS.UI._UNINITIALIZED || this._totalItemHeight === WinJS.UI._UNINITIALIZED) { + var size; + if (this._itemSize) { + size = (typeof this._itemSize === "function" ? this._itemSize() : this._itemSize); + } else { + // Normalize the getGroupInfo result because it already includes the margins. + size = { + width: this._getGroupInfo(group).cellWidth - this._itemMargins.horizontal, + height: this._getGroupInfo(group).cellHeight - this._itemMargins.vertical + }; + } + this._itemWidth = size.width; + this._itemHeight = size.height; + this._totalItemWidth = this._itemWidth + this._itemMargins.horizontal; + this._totalItemHeight = this._itemHeight + this._itemMargins.vertical; + } + }, + + _getItemWidth: function _LayoutCommon_getItemWidth(group) { + this._getSize(group); + return this._itemWidth; + }, + + _getItemHeight: function _LayoutCommon_getItemHeight(group) { + this._getSize(group); + return this._itemHeight; + }, + + _getTotalItemWidth: function _LayoutCommon_getTotalItemWidth(group) { + this._getSize(group); + return this._totalItemWidth; + }, + + _getTotalItemHeight: function _LayoutCommon_getTotalItemHeight(group) { + this._getSize(group); + return this._totalItemHeight; + }, + + _getItemInfo: function _LayoutCommon_getItemInfo(index) { + var cached = this._items[index]; + if (!cached) { + this._items[index] = cached = {}; + } + return cached; + }, + + _purgeItemCache: function _LayoutCommon_purgeItemCache(begin, end) { + var keys = Object.keys(this._items) + for (var i = 0, len = keys.length; i < len; i++) { + var index = parseInt(keys[i], 10); + if (index < begin || index >= end) { + delete this._items[index]; + } + } + }, + + _addElements: function _LayoutCommon_addElements(scratch, element) { + var wrapper = document.createElement("div"); + utilities.addClass(wrapper, WinJS.UI._wrapperClass); + + // This function clones element returned by itemAtIndex and adds them to viewport. + // Element is cloned in order to avoid changes to element stored in ItemsManager cache. + var retVal = [wrapper, element.cloneNode(true)]; + for (var i = 0, len = retVal.length; i < len; i++) { + scratch.appendChild(retVal[i]); + } + return retVal; + }, + + _addHeader: function _LayoutCommon_addHeader(scratch, header) { + utilities.addClass(header, WinJS.UI._headerClass); + + scratch.appendChild(header); + + return header; + }, + + _measureHeaders: function _LayoutCommon_measureHeaders(header) { + var width = utilities.getContentWidth(header); + this._totalHeaderWidth = utilities.getTotalWidth(header); + this._totalHeaderHeight = utilities.getTotalHeight(header); + this._headerMargins = WinJS.UI._getMargins(header); + this._headerPaddingAndBorder = this._totalHeaderWidth - width - this._headerMargins.left - this._headerMargins.right; + }, + + _noHeaders: function _LayoutCommon_noHeaders(headers) { + this._totalHeaderWidth = this._totalHeaderHeight = this._headerPaddingAndBorder = 0; + this._headerMargins = { + left: 0, + right: 0, + top: 0 + }; + }, + + _isEmpty: function _LayoutCommon_isEmpty(count) { + if (+count === count) { + return Promise.wrap(count === 0); + } else { + return this._site._itemsManager.dataSource.getCount().then(function (count) { + return count === 0; + }); + } + }, + + _initialized: function _LayoutCommon_initialized() { + return this._measuring; + }, + + _initializeBase: function _LayoutCommon_initializeBase(count) { + var that = this; + if (!this._measuring) { + return this._isEmpty(count).then(function (isEmpty) { + if (!isEmpty) { + return that._site._groups.ensureFirstGroup().then(function () { + return that._measureItems(); + }); + } else { + return false; + } + }); + } else { + return this._measuring; + } + }, + + _measureMargin: function _LayoutCommon_measureMargin(elements) { + var wrapper = elements[0]; + + this._itemMargins = WinJS.UI._getMargins(wrapper); + this._itemMargins.horizontal = this._itemMargins.left + this._itemMargins.right; + this._itemMargins.vertical = this._itemMargins.top + this._itemMargins.bottom; + }, + + _measureItem: function _LayoutCommon_measureItem(elements) { + var element = elements[1]; + + if (element.offsetWidth !== 0 && element.offsetHeight !== 0) { + var totalItemWidth = utilities.getTotalWidth(element), + totalItemHeight = utilities.getTotalHeight(element); + + this._itemWidth = totalItemWidth; + this._itemHeight = totalItemHeight; + this._totalItemWidth = totalItemWidth + this._itemMargins.horizontal; + this._totalItemHeight = totalItemHeight + this._itemMargins.vertical; + + return true; + } else { + return false; + } + }, + + _measureItems: function _LayoutCommon_measureItems() { + if (!this._measuring) { + var that = this, + viewport = that._site.viewport; + + this._measuring = new Promise(function (complete) { + var scratch = document.createElement("div"); + + function error() { + complete(false); + } + + function computeSizeOfRendered(element, itemResult) { + if (element) { + var elements = [], + header; + + var measure = function (newGroup) { + if (that._site._isZombie()) { + error(); + return; + } + + viewport.appendChild(scratch); + that._measureMargin(elements); + + if (!that._multiSize(newGroup) && !that._measureItem(elements)) { + viewport.removeChild(scratch); + error(); + return; + } + + if (header) { + that._measureHeaders(header); + } else { + that._noHeaders(); + } + + viewport.removeChild(scratch); + complete(true); + } + + that._site._groupOf(itemResult).then(function (newGroup) { + if (newGroup.userData && that._site._groupHeaderTemplate) { + var rendered = WinJS.UI._normalizeRendererReturn(that._site._groupHeaderTemplate(Promise.wrap(newGroup.userData))); + rendered.then(function (headerRecord) { + elements = that._addElements(scratch, element); + header = that._addHeader(scratch, headerRecord.element); + measure(newGroup); + }, error); + } else { + elements = that._addElements(scratch, element); + measure(newGroup); + } + }, error); + } else { + error(); + } + } + + Promise.join({ + element: that._site._itemsManager._itemAtIndex(0), + item: that._site._itemsManager._itemPromiseAtIndex(0) + }).then(function (v) { + computeSizeOfRendered(v.element, v.item); + }, error); + }); + } + return this._measuring; + }, + + _adjustDirection: function _LayoutCommon_adjustDirection(keyPressed) { + if (this._site.rtl) { + if (keyPressed === Key.leftArrow) { + keyPressed = Key.rightArrow; + } else if (keyPressed === Key.rightArrow) { + keyPressed = Key.leftArrow; + } + } + return keyPressed; + }, + + _setZIndex: function _LayoutCommon_setZIndex(items, zIndex) { + for (var i = 0, len = items.length; i < len; i++) { + items[i].style.zIndex = zIndex; + } + }, + + getKeyboardNavigatedItem: function _LayoutCommon_getKeyboardNavigatedItem(itemIndex, element, keyPressed) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + var scrollbarPos = this._site.scrollbarPos, + viewportLength = this._site.viewportSize[this.horizontal ? "width" : "height"], + offsetProp = this.horizontal ? "offsetWidth" : "offsetHeight", + currentItemLength = (element ? element[offsetProp] : 0), + that = this; + + if (keyPressed === Key.pageUp) { + return new Promise(function (complete) { + that.calculateFirstVisible(scrollbarPos, true).then(function (firstElementOnPage) { + if (itemIndex !== firstElementOnPage) { + complete(firstElementOnPage); + } else { + that.calculateFirstVisible(Math.max(0, scrollbarPos - viewportLength + currentItemLength), false).then(function (newFocus) { + // This check is necessary for items that are larger than the viewport + complete(newFocus < itemIndex ? newFocus : itemIndex - 1); + }); + } + }); + }); + } else { + //#DBG _ASSERT(keyPressed === Key.pageDown); + return new Promise(function (complete) { + that.calculateLastVisible(scrollbarPos + viewportLength - 1, true).then(function (lastElementOnPage) { + if (itemIndex !== lastElementOnPage) { + complete(lastElementOnPage); + } else { + that.calculateLastVisible(scrollbarPos + 2 * viewportLength - currentItemLength - 1, false).then(function (newFocus) { + // This check is necessary for items that are larger than the viewport + complete(newFocus > itemIndex ? newFocus : itemIndex + 1); + }); + } + }); + }); + } + }, + + /// + /// Indicates whether a group has variable-sized items. + /// + groupInfo: { + enumerable: true, + get: function () { + return this._groupInfo; + }, + set: function (groupInfo) { + this._groupInfo = groupInfo; + this._invalidateLayout(); + } + }, + + _multiSize: function _LayoutCommon_multiSize(group) { + return this._groupInfo && this._getGroupInfo(group).enableCellSpanning; + }, + + _getGroupInfo: function _LayoutCommon_getGroupInfo(group) { + var groupInfo = (typeof this._groupInfo === "function" ? this._groupInfo(group.userData) : this._groupInfo), + margins = this._itemMargins, + adjustedInfo = null; + + if (groupInfo) { + adjustedInfo = { + enableCellSpanning: groupInfo.enableCellSpanning, + cellWidth: groupInfo.cellWidth + margins.horizontal, + cellHeight: groupInfo.cellHeight + margins.vertical + }; + } + return adjustedInfo; + }, + + updateBackdrop: function _LayoutCommon_updateBackdrop(count) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + + if (this._disableBackdrop) { + return; + } + + // Create the backdrops and reinsert them into the surface if necessary + if (!this._backdropBefore) { + this._backdropBefore = document.createElement('div'); + this._backdropBefore.setAttribute("aria-hidden", "true"); + this._backdropBefore.className = WinJS.UI._backdropClass; + } + if (!this._backdropBefore.parentNode) { + this._site.surface.insertBefore(this._backdropBefore, this._site.surface.firstElementChild); + } + if (!this._backdropAfter) { + this._backdropAfter = document.createElement('div'); + this._backdropAfter.setAttribute("aria-hidden", "true"); + this._backdropAfter.className = WinJS.UI._backdropClass; + } + if (!this._backdropAfter.parentNode) { + this._site.surface.insertBefore(this._backdropAfter, this._site.surface.firstElementChild); + } + + var backdropBefore = this._backdropBefore; + var backdropAfter = this._backdropAfter; + + if (count === 0) { + // Empty list so hide the backdrops + backdropBefore.style.height = "0px"; + backdropAfter.style.height = "0px"; + backdropBefore.style.width = "0px"; + backdropAfter.style.width = "0px"; + } else { + // If the viewport size has changed (snap/resize) the item may have changed size. For example items + // are 100% width in the vertical list view. In this case we need to redraw the background image. + var viewPortSizeKey = this._site.viewportSize.height + "x" + this._site.viewportSize.width; + if (this._viewPortSizeKey !== viewPortSizeKey) { + this._viewPortSizeKey = viewPortSizeKey; + if (this._item0PositionPromise) { + this._item0PositionPromise.cancel(); + this._item0PositionPromise = null; + } + this._backdropCanvas = null; + } + + if (!this._item0PositionPromise) { + this._item0PositionPromise = this.getItemPosition(0); + } + + var that = this; + this._item0PositionPromise.then(function (pos) { + var itemMargins = that._itemMargins; + + var left = that._site.rtl ? "right" : "left"; + var right = that._site.rtl ? "left" : "right"; + + // For multisize instead of using the first item size use the slot size of the first group. + var groupInfo; + if (typeof that._groupInfo === "function") { + var group0 = that._getGroup(0); + if (group0) { + groupInfo = that.groupInfo(group0.userData); + } + } else { + groupInfo = that._groupInfo; + } + + if (groupInfo && groupInfo.cellHeight && groupInfo.cellWidth) { + pos.contentWidth = groupInfo.cellWidth; + pos.contentHeight = groupInfo.cellHeight; + pos.totalWidth = groupInfo.cellWidth + itemMargins.right + itemMargins.left; + pos.totalHeight = groupInfo.cellHeight + itemMargins.top + itemMargins.bottom; + } + + if (!that._backdropCanvas) { + that._backdropCanvas = document.createElement("canvas"); + that._backdropCanvas.width = pos.totalWidth; + that._backdropCanvas.height = pos.totalHeight; + var ctx = that._backdropCanvas.getContext("2d"); + ctx.fillStyle = that.backdropColor; + if (that.horizontal) { + ctx.fillRect(itemMargins[left], itemMargins.top, pos.contentWidth, pos.contentHeight); + } else { + ctx.fillRect(itemMargins.left, itemMargins.top, pos.contentWidth, pos.contentHeight); + } + + var backgroundImage = "url(" + that._backdropCanvas.toDataURL("image/png") + ")"; + backdropBefore.style.backgroundImage = backgroundImage; + backdropAfter.style.backgroundImage = backgroundImage; + } + + // Find the start of the first item realized and the end of the last item realized. Scroll view + // does not leave holes between realized ranges so 2 backdrops are sufficent. + var itemsStart; + var itemsEnd; + + Object.keys(that._items).forEach(function (index) { + var itemData = that._items[index]; + if (itemData && itemData.element && itemData.element.parentNode) { + if (that.horizontal) { + itemsStart = (+itemsStart === itemsStart) ? Math.min(itemData.left, itemsStart) : itemData.left; + itemsEnd = (+itemsEnd === itemsEnd) ? Math.max(itemData.left + itemData.width, itemsEnd) : (itemData.left + itemData.width); + } + else { + itemsStart = (+itemsStart === itemsStart) ? Math.min(itemData.top, itemsStart) : itemData.top; + itemsEnd = (+itemsEnd === itemsEnd) ? Math.max(itemData.top + itemData.height, itemsEnd) : (itemData.top + itemData.height); + } + } + }); + + if (that.horizontal) { + var usableSurfaceHeight = that._site.viewportSize.height - that._site._surfaceMargins.top - that._site._surfaceMargins.bottom; + // These orgins represent the origin for a single backdrop that spans the whole list + var backdropOriginY = pos.top; + // Unable to use pos.left because it is a dynamic value that changes with group/item pinning + // and we cache the value in that._item0PositionPromise. Instead use header margin, header + // width or 0 which is used in item positioning. + var item0Offset = that._groupHeaderPosition === "top" ? that._headerMargin : that._totalHeaderWidth; + var backdropOriginX = Math.max(0, item0Offset); + var placeholderRows = Math.floor((usableSurfaceHeight - backdropOriginY) / pos.totalHeight); + if (that.maxRows) { + placeholderRows = Math.min(that.maxRows, placeholderRows); + } + placeholderRows = Math.max(1, placeholderRows); + // This width/height is if there was a single backdrop across the whole list + var width = that._site._surfaceLength - itemMargins[left]; + var height = placeholderRows * pos.totalHeight; + + itemsStart = (+itemsStart === itemsStart) ? Math.max(0, itemsStart) : 0; + itemsEnd = (+itemsEnd === itemsEnd) ? Math.max(0, itemsEnd + itemMargins[left] + itemMargins[right]) : 0; + + backdropOriginX = Math.max(backdropOriginX, that._site._surfaceScrollLimitMin); + itemsEnd = Math.max(itemsEnd, that._site._surfaceScrollLimitMin); + + // We place one backdrop before the realized items (itemsStart) and one after the realized items (itemsEnd) + backdropBefore.style.cssText += ";" + left + ":" + backdropOriginX + "px;top:" + backdropOriginY + "px;width:" + + Math.max(0, (itemsStart - backdropOriginX)) + "px;height:" + height + "px"; + backdropAfter.style.cssText += ";" + left + ":" + itemsEnd + "px;top:" + backdropOriginY + "px;width:" + + Math.max(0, (width - itemsEnd)) + "px;height:" + height + "px"; + } else { + var usableSurfaceWidth = that._site.viewportSize.width - that._site._surfaceMargins[left] - that._site._surfaceMargins[right]; + // These orgins represent the origin for a single backdrop that spans the whole list + var backdropOriginX = pos.left; + var backdropOriginY = Math.max(0, pos.top); + var placeholderColumns = Math.floor((usableSurfaceWidth - backdropOriginX) / pos.totalWidth); + placeholderColumns = Math.max(1, placeholderColumns); + // This width/height is if there was a single backdrop across the whole list + var height = that._site._surfaceLength - itemMargins.top; + var width = placeholderColumns * pos.totalWidth; + + itemsStart = (+itemsStart === itemsStart) ? Math.max(0, itemsStart) : 0; + itemsEnd = (+itemsEnd === itemsEnd) ? Math.max(0, itemsEnd + itemMargins.top + itemMargins.bottom) : 0; + + backdropOriginY = Math.max(backdropOriginY, that._site._surfaceScrollLimitMin); + itemsEnd = Math.max(itemsEnd, that._site._surfaceScrollLimitMin); + + // We place one backdrop before the realized items (itemsStart) and one after the realized items (itemsEnd) + backdropBefore.style.cssText += ";top:" + backdropOriginY + "px;" + left + ":" + backdropOriginX + "px;height:" + + Math.max(0, (itemsStart - backdropOriginY)) + "px;width:" + width + "px"; + backdropAfter.style.cssText += ";top:" + itemsEnd + "px;" + left + ":" + backdropOriginX + "px;height:" + + Math.max(0, (height - itemsEnd)) + "px;width:" + width + "px"; + } + }, function () { + // Error occurred because the promise was canceled or data source was reset so hide the backdrop + // since we do not know the correct location of the items. + backdropBefore.style.height = "0px"; + backdropAfter.style.height = "0px"; + backdropBefore.style.width = "0px"; + backdropAfter.style.width = "0px"; + }); + } + } + }) +}); + +function FixedSizeDecorator() { +} + +FixedSizeDecorator.prototype = { + getGroupSize: function (layout, group, groupIndex, itemsCount) { + return Math.ceil(itemsCount / layout._itemsPerColumn) * layout._getTotalItemWidth(group) + layout._headerSlot.cx; + }, + + calcItemPosition: function (layout, group, groupIndex, index) { + var coordinates = layout._indexToCoordinate(index - group.startIndex), + pos = { + left: group.offset + layout._headerSlot.cx + coordinates.column * layout._getTotalItemWidth(group), + top: layout._headerSlot.cy + coordinates.row * layout._getTotalItemHeight(group), + contentWidth: layout._getItemWidth(group), + contentHeight: layout._getItemHeight(group), + totalWidth: layout._getTotalItemWidth(group), + totalHeight: layout._getTotalItemHeight(group), + row: coordinates.row, + column: coordinates.column + }; + + pos.offset = pos.left; + + return Promise.wrap(pos); + }, + + itemOffset: function (layout, group, index) { + var coordinates = layout._indexToCoordinate(index - group.startIndex); + return coordinates.column * layout._getTotalItemWidth(group); + }, + + itemFromOffset: function (layout, group, groupIndex, offset, wholeItem, last) { + if (wholeItem) { + offset += (last ? -1 : 1) * (layout._getTotalItemWidth(group) - 1); + } + return (Math.max(0, Math.floor((offset - group.offset - layout._headerSlot.cx) / layout._getTotalItemWidth(group))) + last) * layout._itemsPerColumn - last; + }, + + getKeyboardNavigatedItem: function (layout, group, groupSize, index, element, keyPressed) { + return new Promise(function (complete) { + var currentColumn = Math.floor((index - group.startIndex) / layout._itemsPerColumn), + currentRow = (index - group.startIndex) % layout._itemsPerColumn; + + switch (keyPressed) { + case Key.upArrow: + complete({ index: (currentRow === 0 ? index : index - 1) }); + break; + case Key.downArrow: + var isLastIndexOfGroup = index === group.startIndex + groupSize - 1, + inLastRow = currentRow === layout._itemsPerColumn - 1; + complete({ index: (isLastIndexOfGroup || inLastRow ? index : index + 1) }); + break; + case Key.leftArrow: + complete(currentColumn > 0 ? { index: index - layout._itemsPerColumn } : { group: -1 }); + break; + case Key.rightArrow: + var newIndex = index + layout._itemsPerColumn, + lastIndexOfGroup = group.startIndex + groupSize - 1, + lastColumn = Math.floor((lastIndexOfGroup - group.startIndex) / layout._itemsPerColumn); + if (lastColumn === currentColumn) { + complete({ group: 1 }); + } else { + complete(newIndex <= lastIndexOfGroup ? { index: newIndex } : { index: lastIndexOfGroup }); + } + break; + default: + WinJS.UI._LayoutCommon.prototype.getKeyboardNavigatedItem.call(layout, index, element, keyPressed).then(function (newIndex) { + complete({ index: newIndex }); + }); + break; + } + }); + }, + + getLogicalIndex: function (layout, group, groupIndex, wholeItem, x, y, last) { + var groupSize = layout._getItemsCount(group, groupIndex), + startIndex = group.startIndex, + index = this.itemFromOffset(layout, group, groupIndex, x, false, last), + row = Math.min(layout._itemsPerColumn - 1, Math.floor((y - layout._headerSlot.cy) / layout._getTotalItemHeight(group))); + + return Math.min(startIndex + index + row, startIndex + groupSize - 1); + } +}; + +/*#DBG +function dumpMap(occupancyMap, itemsPerColumn, inMapIndex, groupIndex) { + var lastColumn = Math.floor((occupancyMap.length - 1) / itemsPerColumn); + _TRACE("Group "+groupIndex+". Map length = " + occupancyMap.length + " lastColumn=" + lastColumn); + + var text = " "; + for (var c = 0; c <= Math.min(999, lastColumn); c++) { + var column = c.toString(); + text += " " + ("000".substr(0, 3 - column.length)) + column + "."; + } + _TRACE(text); + for (var r = 0; r < itemsPerColumn; r++) { + text = r + " [ "; + for (c = 0; c <= lastColumn; c++) { + var i = c * itemsPerColumn + r; + text += (i === inMapIndex ? "*" : " "); + var entry = occupancyMap[i]; + if (entry) { + var index = entry.index.toString(); + text += ("000".substr(0, 3 - index.length)) + index + "|"; + } else { + text += "___|"; + } + } + text += " ]"; + _TRACE(text); + } +} +#DBG*/ + +function VariableSizeDecorator() { + this.occupancyMap = []; + this.lastAdded = 0; + this.adding = []; + this.slotsPerColumn = 0; +} + +VariableSizeDecorator.prototype = { + + getGroupSize: function (layout, group, groupIndex, itemsCount) { + if (this.occupancyMap.length === 0 && group.cachedSize) { + return group.cachedSize; + } + + var measuredItems = 0, + groupInfo = layout._getGroupInfo(group); + + if (this.occupancyMap.length > 0) { + measuredItems = Math.ceil(this.occupancyMap.length / this.slotsPerColumn) * groupInfo.cellWidth; + itemsCount -= this.getOccupancyMapItemCount(); + } else { + measuredItems = 0; + } + + var otherItems = Math.ceil(itemsCount / this.slotsPerColumn) * groupInfo.cellWidth + layout._headerSlot.cx; + group.cachedSize = measuredItems + otherItems; + return group.cachedSize; + }, + + getOccupancyMapItemCount: function () { + 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 (layout, c, r) { + return c * this.slotsPerColumn + r; + }, + + markSlotAsFull: function (layout, index, itemEntry) { + var coordinates = layout._indexToCoordinate(index, this.slotsPerColumn); + for (var r = coordinates.row, toRow = coordinates.row + itemEntry.rows; r < toRow; r++) { + for (var c = coordinates.column, toColumn = coordinates.column + itemEntry.columns; c < toColumn; c++) { + this.occupancyMap[this.coordinateToIndex(layout, c, r)] = itemEntry; + } + } + }, + + isSlotEmpty: function (layout, 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(layout, c, r)] !== undefined)) { + return false; + } + } + } + return true; + }, + + findEmptySlot: function (layout, startIndex, itemSize, newColumn) { + var coordinates = 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(layout, itemSize, 0, c)) { + return this.coordinateToIndex(layout, c, 0); + } + } + } else { + for (var c = coordinates.column; c <= lastColumn; c++) { + for (var r = startRow; r < this.slotsPerColumn; r++) { + if (this.isSlotEmpty(layout, itemSize, r, c)) { + return this.coordinateToIndex(layout, c, r); + } + } + startRow = 0; + } + } + + return (lastColumn + 1) * this.slotsPerColumn; + }, + + findItem: function (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 (layout, group, element, index) { + var added; + + var wrapper = element.parentNode; + + //#DBG _ASSERT(utilities.hasClass(wrapper, WinJS.UI._wrapperClass)); + //#DBG _ASSERT(utilities.hasClass(element, WinJS.UI._itemClass)); + + if (!wrapper.parentNode) { + added = true; + layout._site.surface.appendChild(wrapper); + } + + var itemWidth = utilities.getTotalWidth(element), + itemHeight = utilities.getTotalHeight(element), + totalItemWidth = itemWidth + layout._itemMargins.horizontal, + totalItemHeight = itemHeight + layout._itemMargins.vertical; + + if (added) { + layout._site.surface.removeChild(wrapper); + } + + var groupInfo = layout._getGroupInfo(group); + return { + index: index, + contentWidth: itemWidth, + contentHeight: itemHeight, + columns: Math.max(1, Math.ceil(totalItemWidth / groupInfo.cellWidth)), + rows: Math.min(this.slotsPerColumn, Math.max(1, Math.ceil(totalItemHeight / groupInfo.cellHeight))) + }; + }, + + + addItemToMap: function (layout, group, index) { + var that = this; + + function add(mapEntry, newColumn) { + var inMapIndex = that.findEmptySlot(layout, that.lastAdded, mapEntry, newColumn); + that.lastAdded = inMapIndex; + that.markSlotAsFull(layout, inMapIndex, mapEntry); + layout._site._groups.dirty = true; + return inMapIndex; + } + + return new Promise(function (complete) { + var mapEntry, newColumn; + + if (layout._itemSize && typeof layout._itemSize === "function") { + var size = layout._itemSize(group.startIndex + index); + if (size.width && size.height) { + var groupInfo = layout._getGroupInfo(group); + mapEntry = { + index: index, + contentWidth: size.width, + contentHeight: size.height, + columns: Math.max(1, Math.ceil(size.width / groupInfo.cellWidth)), + rows: Math.min(that.slotsPerColumn, Math.max(1, Math.ceil(size.height / groupInfo.cellHeight))) + }; + } + + newColumn = size.newColumn; + } + + if (mapEntry) { + var inMapIndex = add(mapEntry, newColumn); + complete(inMapIndex); + } else { + var processElement = function (element) { + if (!layout._site._isZombie()) { + inMapIndex = add(that.getItemSize(layout, group, element, index), newColumn); + complete(inMapIndex); + } + } + + layout._site._itemsManager._itemAtIndex(group.startIndex + index).then(processElement); + } + }); + }, + + ensureInMap: function (layout, group, index) { + var that = this; + if (index >= this.adding.length) { + for (var i = this.adding.length; i < index; i++) { + this.ensureInMap(layout, group, i); + } + var previous = index > 0 ? this.adding[index - 1] : Promise.wrap(); + this.adding[index] = previous.then(function () { + return that.addItemToMap(layout, group, index); + }); + } + return this.adding[index]; + }, + + calcItemPosition: function (layout, group, groupIndex, index) { + var that = this; + return new Promise(function (complete) { + that.ensureInMap(layout, group, index - group.startIndex).then( function (inMapIndex) { + layout._updateOffsets(); + var groupInfo = layout._getGroupInfo(group), + itemEntry = that.occupancyMap[inMapIndex], + coordinates = layout._indexToCoordinate(inMapIndex, that.slotsPerColumn), + pos = { + left: group.offset + layout._headerSlot.cx + coordinates.column * groupInfo.cellWidth, + top: layout._headerSlot.cy + coordinates.row * groupInfo.cellHeight, + contentWidth: itemEntry.contentWidth, + contentHeight: itemEntry.contentHeight, + totalWidth: itemEntry.columns * groupInfo.cellWidth, + totalHeight: itemEntry.rows * groupInfo.cellHeight, + row: coordinates.row, + column: coordinates.column + }; + + pos.offset = pos.left; + + complete(pos); + }); + }); + }, + + itemOffset: function (layout, group, index) { + var inMapIndex = this.findItem(index - group.startIndex), + coordinates = layout._indexToCoordinate(inMapIndex, this.slotsPerColumn), + groupInfo = layout._getGroupInfo(group); + return coordinates.column * groupInfo.cellWidth; + }, + + itemFromOffset: function (layout, group, groupIndex, offset, wholeItem, last) { + offset -= group.offset + layout._headerSlot.cx + ((last ? 1 : -1) * layout._itemMargins[last ? "left" : "right"]); + + return this.indexFromOffset(layout, group, groupIndex, offset, wholeItem, last).item; + }, + + indexFromOffset: function (layout, group, groupIndex, adjustedOffset, wholeItem, last) { + var measuredWidth = 0, + lastItem = 0, + groupInfo = layout._getGroupInfo(group), + 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 + }; + }, + + getKeyboardNavigatedItem: function (layout, group, groupSize, index, element, keyPressed) { + if (keyPressed === Key.upArrow || + keyPressed === Key.downArrow || + keyPressed === Key.leftArrow || + keyPressed === Key.rightArrow) { + var that = this, + originalIndex = index; + + index -= group.startIndex; + + 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), + entry, + c; + + switch (keyPressed) { + case Key.upArrow: + if (row > 0) { + inMapIndex--; + } else { + return Promise.wrap({ index: originalIndex }); + } + break; + case Key.downArrow: + if (row + 1 < this.slotsPerColumn) { + inMapIndex++; + } else { + return Promise.wrap({ 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 = that.occupancyMap[inMapIndex] ? that.occupancyMap[inMapIndex].index : undefined; + } + + } while (inMap && (index === newIndex || newIndex === undefined)); + + this.lastAdjacent = newIndex; + this.lastInMapIndex = inMapIndex; + + return Promise.wrap(inMap ? { index: group.startIndex + newIndex } : { group: inMapIndex < 0 ? -1 : 1 }); + } else { + return new Promise(function (complete) { + WinJS.UI._LayoutCommon.prototype.getKeyboardNavigatedItem.call(layout, index, element, keyPressed).then(function (newIndex) { + complete({ index: newIndex }); + }); + }); + } + }, + + getLogicalIndex: function (layout, group, groupIndex, wholeItem, x, y, last) { + var groupSize = layout._getItemsCount(group, groupIndex), + startIndex = group.startIndex, + itemIndex = 0; + + if (this.occupancyMap.length > 0) { + var adjustedX = (x -= group.offset + layout._headerSlot.cx); + var result = this.indexFromOffset(layout, group, groupIndex, adjustedX, wholeItem, last); + + var counter = Math.min(this.slotsPerColumn - 1, Math.floor((y - layout._headerSlot.cy) / layout._getTotalItemHeight(group))), + curr = result.index; + while (this.occupancyMap[curr] && counter-- > 0) { + curr++; + } + if (!this.occupancyMap[curr]) { + curr--; + } + + itemIndex = this.occupancyMap[curr].index; + } + + return Math.min(startIndex + itemIndex, startIndex + groupSize - 1); + } +}; + + +WinJS.Namespace.define("WinJS.UI", { + HeaderPosition: { + left: "left", + top: "top" + }, + + GridLayout: WinJS.Class.derive(WinJS.UI._LayoutCommon, function (options) { + /// + /// + /// Creates a new GridLayout. + /// + /// + /// The set of properties and values to apply to the new GridLayout. + /// + /// + /// The new GridLayout. + /// + /// + this.init(); + this._groupHeaderPosition = WinJS.UI.HeaderPosition.top; + this._itemsPerColumn = WinJS.UI._UNINITIALIZED; + WinJS.UI.setOptions(this, options); + }, { + /// + horizontal: { + enumerable: true, + get: function () { + return true; + } + }, + + /// + /// 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(); + } + }, + + + /// + /// The maximum number of rows that the ListView displays. + /// + maxRows: { + enumerable: true, + get: function () { + return this._maxRows; + }, + set: function (maxRows) { + this._maxRows = maxRows; + this._invalidateLayout(); + + // If we have itemsPerColumn, recompute it with the new maxRows setting + if (this._itemsPerColumn !== WinJS.UI._UNINITIALIZED) { + this._itemsPerColumn = this._computeItemsPerColumn(); + } + } + }, + + _initialize: function GridLayout_initialize(count) { + var that = this + return this._initializeBase(count).then(function (initialized) { + if (initialized) { + that._rtl = that._site.rtl; + var positionProperty = that._rtl ? "right" : "left"; + if (positionProperty != that._positionProperty) { + that._items = {}; + } + that._positionProperty = positionProperty; + + var surfaceMargin = that._site._surfaceMargins; + that._surfaceMargin = surfaceMargin[positionProperty]; + that._headerMargin = that._headerMargins[positionProperty]; + if (that._groupHeaderPosition === "top") { + that._headerSlot = { + cx: that._headerMargin, + cy: that._totalHeaderHeight + }; + } else { + //#DBG _ASSERT(that._groupHeaderPosition === "left"); + that._headerSlot = { + cx: that._totalHeaderWidth, + cy: 0 + }; + } + } + return initialized; + }); + }, + + startLayout: function GridLayout_startLayout(beginScrollPosition, endScrollPosition, count) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + var that = this; + + if (count) { + this._count = count; + return this._initialize(count).then(function (initialized) { + if (initialized) { + var dataModifiedPromise = that._dataModifiedPromise ? that._dataModifiedPromise : Promise.wrap(); + return dataModifiedPromise.then(function () { + return that._site._groups.ensureFirstGroup().then(function () { + that._itemsPerColumn = WinJS.UI._UNINITIALIZED; + // GridLayout needs to cancel animations when it's resized while animations are playing. To do that, we cache the last calculated surface height + // and recalculate it every time startLayout is called. If there's a difference and animations are playing, cancel them. + var surfaceMargins = that._site._surfaceMargins, + surfaceHeight = that._site.viewportSize.height - surfaceMargins.top - surfaceMargins.bottom; + if (that._oldSurfaceHeight !== surfaceHeight && that._trackedAnimation) { + that._trackedAnimation.cancelAnimations(); + that._site.itemSurface.style.clip = "auto"; + } + that._oldSurfaceHeight = surfaceHeight; + var currentScrollLocation = that._site.scrollbarPos, + animationRangePromise = Promise.wrap(), + animationRangeBegin = Math.max(beginScrollPosition, currentScrollLocation - that._site.viewportSize.width), + animationRangeEnd = Math.min(endScrollPosition, currentScrollLocation + 2 * that._site.viewportSize.width); + + if (that._site.loadingBehavior !== "incremental") { + animationRangePromise = Promise.join([that.calculateFirstVisible(animationRangeBegin, false), that.calculateLastVisible(animationRangeEnd, false)]).then(function(indices) { + that._viewAnimationRange = { + start: indices[0], + end: indices[1] + }; + }); + } + return animationRangePromise.then(function() { + // In incremental mode, startLayout needs to give a range of whole items, and not round up if there isn't enough space in the scroll range. + var incremental = (that._site.loadingBehavior === "incremental"); + return that.calculateFirstVisible(beginScrollPosition, incremental).then(function (begin) { + return that.calculateLastVisible(endScrollPosition, incremental).then(function (last) { + var end = last + 1; + that._purgeItemCache(begin, end); + + return { + beginIndex: begin, + endIndex: end + }; + }); + }); + }); + }); + }); + } else { + return null; + } + }); + } else { + return Promise.wrap(null); + } + }, + + _getCanvasWidth: function GridLayout_getCanvasWidth(count) { + var groupIndex = this._site._groups.length() - 1, + lastGroup = this._getGroup(groupIndex); + + return lastGroup.offset + this._decorateGroup(lastGroup).getGroupSize(this, lastGroup, groupIndex, count - lastGroup.startIndex); + }, + + _getGroupType: function GridLayout_getGroupType(group) { + return group.decorator instanceof VariableSizeDecorator ? "variable" : "fixed"; + }, + + getScrollbarRange: function GridLayout_getScrollbarRange(count) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + var that = this; + return this._initialize(count).then(function (initialized) { + if (initialized) { + that._updateOffsets(); + var firstGroup = that._getGroup(0), + canvasWidth = that._getCanvasWidth(count); + that._canvasScrollBounds.left = firstGroup.offset; + that._canvasScrollBounds.right = canvasWidth; + return { + beginScrollPosition: firstGroup.offset, + endScrollPosition: canvasWidth + }; + } else { + that._canvasScrollBounds.left = 0; + that._canvasScrollBounds.right = 0; + return { + beginScrollPosition: 0, + endScrollPosition: 0 + }; + } + }); + }, + + getKeyboardNavigatedItem: function GridLayout_getKeyboardNavigatedItem(itemIndex, element, keyPressed) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + if (!this._initialized()) { + return Promise.wrap(-1); + } + + var that = this; + return new Promise(function (complete) { + + var groupIndex = that._site._groups.groupFromItem(itemIndex), + group = that._site._groups.group(groupIndex); + + // In the case that we ask the data source for information about the groups + // and it throws up it's hands (basically we don't have any items and are + // asking about stale data), we can safely just report -1, indicating to + // select nothing. + // + if (groupIndex === null) { + complete(-1); + return; + } + + that._measureItems().then(function() { + that._decorateGroup(group).getKeyboardNavigatedItem(that, group, that._getItemsCount(group, groupIndex), itemIndex, element, that._adjustDirection(keyPressed)).then(function (newPosition) { + if (newPosition.group) { + var newGroupIndex = groupIndex + newPosition.group, + newGroup = that._site._groups.group(newGroupIndex); + + if (newGroupIndex < 0) { + complete(-1); + } else if (newGroupIndex >= that._site._groups.length()) { + complete(that._count); + } else { + var oldGroupType = that._getGroupType(group), + newGroupType = that._getGroupType(newGroup), + oldItemsPerColumn = (oldGroupType === "variable" ? group.decorator.slotsPerColumn : that._itemsPerColumn), + oldCoordinates = that._indexToCoordinate(itemIndex - group.startIndex, oldItemsPerColumn), + newGroupCount = that._getItemsCount(newGroup, newGroupIndex), + lastItemIndex = newGroup.startIndex + newGroupCount - 1, + coords = null; + + if (oldGroupType === "fixed" && newGroupType === "fixed") { + // We're moving between two fixed-size groups, so select the parallel item + if (newPosition.group < 0) { + var lastItemCoordinates = that._indexToCoordinate(lastItemIndex - newGroup.startIndex); + coords = (lastItemCoordinates.row >= oldCoordinates.row ? (lastItemIndex - (lastItemCoordinates.row - oldCoordinates.row)) : lastItemIndex); + } else { + coords = (newGroupCount > oldCoordinates.row ? (newGroup.startIndex + oldCoordinates.row) : lastItemIndex); + } + } else { + // We're moving to or from a variable-size group, so select the first or last item + coords = newPosition.group < 0 ? lastItemIndex : newGroup.startIndex; + } + + complete(coords); + } + } else { + complete(newPosition.index); + } + }); + }); + }); + }, + + _indexToCoordinate: /*@varargs*/ function GridLayout_indexToCoordinate(index, itemsPerColumn) { + itemsPerColumn = itemsPerColumn || this._itemsPerColumn; + var column = Math.floor(index / itemsPerColumn); + return { + column: column, + row: index - column * itemsPerColumn + }; + }, + + _calcItemPosition: function GridLayout_calcItemPosition(index, groupIndex) { + this._updateOffsets(); + + var that = this, + group = this._getGroup(groupIndex); + + return this._decorateGroup(group).calcItemPosition(this, group, groupIndex, index); + }, + + prepareItem: function GridLayout_prepareItem(element) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + + // Do nothing because position absolute is already set in CSS. + }, + + prepareHeader: function GridLayout_prepareHeader(element) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + + // Do nothing because position absolute is already set in CSS. + }, + + releaseItem: function GridLayout_releaseItem(item, newItem) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + if (item._currentAnimationStage) { + if (newItem) { + item._currentAnimationStage.replaceItemInStage(item, newItem); + } else { + item._animating = false; + item._currentAnimationStage.removeItemFromStage(item); + } + } + }, + + layoutItem: function GridLayout_layoutItem(itemIndex, element) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + var that = this, + groupIndex = this._site._groups.groupFromItem(itemIndex); + + this._calcItemPosition(itemIndex, groupIndex).then(function (itemPos) { + var itemData = that._getItemInfo(itemIndex); + + if (itemData.left !== itemPos.left || itemData.top !== itemPos.top || itemData.width != itemPos.contentWidth || itemData.height != itemPos.contentHeight || element !== itemData.element) { + var oldTop = (itemData.element === element ? itemData.top : undefined), + oldLeft = (itemData.element === element ? itemData.left : undefined); + itemData.element = element; + itemData.left = itemPos.left; + itemData.top = itemPos.top; + itemData.width = itemPos.contentWidth; + itemData.height = itemPos.contentHeight; + itemData.row = itemPos.row; + itemData.column = itemPos.column; + + var cachedRecord = that._cachedItemRecords[element.uniqueID]; + if (cachedRecord) { + cachedRecord.left = itemData.left; + cachedRecord.top = itemData.top; + cachedRecord.itemIndex = itemIndex; + if (!cachedRecord.appearedInView) { + // The row+column properties of a record are only used for determining whether the animation played on the element + // should be a reflow across columns, or just a move. Since elements just appearing in view shouldn't move between columns, + // we only update the row/col properties of records that didn't appear, that way the animation helper won't detect + // a column change on an appeared item and animate it. + cachedRecord.row = itemData.row; + cachedRecord.column = itemData.column; + } else { + // When an item is laid out, has an animation record, and appearedInView is true, it hasn't had a chance to + // animate in from the bottom of the view yet. We need to update its left/top and its record's oldLeft/Top + // so that it gets animated properly when coming into view. + cachedRecord.oldLeft = itemData.left; + cachedRecord.oldTop = itemData.top + (that._itemsPerColumn * that._totalItemHeight); + element.style.cssText += ";" + that._positionProperty + ":" + cachedRecord.left + "px;top:" + that._cachedItemRecords[element.uniqueID].oldTop; + } + } + + if (!element._animating && !cachedRecord) { + // When an element isn't marked as animating, and no animation record exists for it, it's still not necessarily safe to animate. + // If a lot of elements are being deleted, we can run into cases where new items that we've never seen before need to be realized and put + // in the view. If an item falls into the animation range and has no record, we'll set up a special record for it and animate it coming in from the bottom + // of the view. + if (that._dataModifiedPromise && itemIndex >= that._viewAnimationRange.start && itemIndex <= that._viewAnimationRange.end) { + that._cachedItemRecords[element.uniqueID] = { + oldTop: itemData.top + (that._itemsPerColumn * that._totalItemHeight), + oldLeft: itemData.left, + row: itemData.row, + column: itemData.column, + top: itemData.top, + left: itemData.left, + element: element, + itemIndex: itemIndex, + appearedInView: true + }; + // Since this item just appeared in the view and we're going to animate it coming in from the bottom, we set its opacity to zero because it'll be moved to the bottom of the listview + // but can still be visible since the canvas region's clip style isn't set until endLayout + element.style.cssText += ";" + that._positionProperty + ":" + itemPos.left + "px;top:" + that._cachedItemRecords[element.uniqueID].oldTop + "px;width:" + itemPos.contentWidth + "px;height:" + itemPos.contentHeight + "px; opacity: 0"; + } else { + // Setting the left, top, width, and height via cssText is approximately 50% faster than setting each one individually. + // Start with semicolon since cssText does not end in one even if you provide one at the end. + element.style.cssText += ";" + that._positionProperty + ":" + itemPos.left + "px;top:" + itemPos.top + "px;width:" + itemPos.contentWidth + "px;height:" + itemPos.contentHeight + "px"; + } + } + + // When an element is laid out again while animating but doesn't have a cached record, that means that enough changes have occurred to + // make that element outside of the range of usual animations (that range is found in the dataModified handler). We don't want to move the + // element instantly, since while it may be outside of the range of visible indices, it may still be visible because it's in the midst + // of a move animation. We'll instead make a new record for this element and let endLayout animate it again. + if (element._animating && !cachedRecord) { + that._cachedItemRecords[element.uniqueID] = { + oldTop: oldTop, + oldLeft: oldLeft, + row: itemData.row, + column: itemData.column, + top: itemData.top, + left: itemData.left, + element: element, + itemIndex: itemIndex + }; + } + } + }); + }, + + layoutHeader: function GridLayout_layoutHeader(groupIndex, element) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + this._updateOffsets(); + + var groups = this._site._groups, + group = groups.group(groupIndex), + nextGroup = (groupIndex + 1 < groups.length() ? groups.group(groupIndex + 1) : null), + left = group.offset, + width; + + //#DBG _ASSERT(group.offset !== undefined); + + if (this._groupHeaderPosition === "top") { + if (nextGroup) { + width = nextGroup.offset; + } else { + // This is the last group, so derive the header width from the canvas width + width = this._canvasScrollBounds.right; + } + + width -= group.offset + this._headerMargin + this._headerPaddingAndBorder; + } + + if (group.left !== left || group.width !== width) { + var style = element.style; + + style[this._positionProperty] = left + "px"; + style.width = (width ? width + "px" : ""); + + group.left = left; + group.width = width; + } + }, + + endLayout: function GridLayout_endLayout() { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + if (!this._animateEndLayout) { + return; + } + var dataModifiedPromise = this._dataModifiedPromise ? this._dataModifiedPromise : Promise.wrap(); + this._dataModifiedPromise = null; + this._animateEndLayout = false; + var that = this; + + var endLayoutPromise = dataModifiedPromise.then(function () { + var i, len, element; + if (that._site.animationsDisabled) { + for (i = 0, len = that._cachedInserted.length; i < len; i++) { + that._cachedInserted[i].style.opacity = 1.0; + } + for (i = 0, len = that._cachedRemoved.length; i < len; i++) { + element = that._cachedRemoved[i]; + if (element.parentNode) { + element.parentNode.removeChild(element); + } + } + var cachedRecordKeys = Object.keys(that._cachedItemRecords); + for (i = 0, len = cachedRecordKeys.length; i < len; i++) { + var itemRecord = that._cachedItemRecords[cachedRecordKeys[i]]; + if (itemRecord.element) { + if (itemRecord.oldLeft !== itemRecord.left || itemRecord.oldTop !== itemRecord.top) { + if (itemRecord.left !== undefined) { + itemRecord.element.style[that._positionProperty] = itemRecord.left + "px"; + } + if (itemRecord.top !== undefined) { + itemRecord.element.style.top = itemRecord.top + "px"; + } + } + + if (itemRecord.appearedInView) { + itemRecord.element.style.opacity = 1; + } + } + } + that._cachedInserted = []; + that._cachedRemoved = []; + that._cachedItemRecords = {}; + return; + } + + var affectedItems = {}, + groups = that._site._groups, + animationSignal = new Signal(), + variablySizedItemsFound = false, + cachedRecordKeys = Object.keys(that._cachedItemRecords); + + var insertedMap = {}, + removedMap = {}; + for (i = 0, len = that._cachedInserted.length; i < len; i++) { + element = that._cachedInserted[i]; + var itemRecord = that._cachedItemRecords[element.uniqueID]; + if (itemRecord) { + element.style.cssText += ";" + that._positionProperty + ":" + itemRecord.left + "px;top:" + itemRecord.top + "px;"; + } + insertedMap[element.uniqueID] = { element: element }; + } + for (i = 0, len = that._cachedRemoved.length; i < len; i++) { + element = that._cachedRemoved[i]; + removedMap[element.uniqueID] = { element: element }; + } + var appearingItems = []; + for (i = 0, len = cachedRecordKeys.length; i < len; i++) { + var itemRecord = that._cachedItemRecords[cachedRecordKeys[i]]; + if (itemRecord.element) { + var itemID = itemRecord.element.uniqueID; + if (!insertedMap[itemID] && !removedMap[itemID]) { + variablySizedItemsFound = variablySizedItemsFound || that._multiSize(groups.group(groups.groupFromItem(itemRecord.itemIndex))); + if ((itemRecord.oldRow !== itemRecord.row || itemRecord.oldColumn !== itemRecord.column || itemRecord.oldLeft !== itemRecord.left || itemRecord.oldTop !== itemRecord.top)) { + // The itemData object can be reused by the ListView, but item records are recreated every time a change happens. The stages + // need unchanging records to function properly, so we give it the itemRecord. + affectedItems[itemID] = itemRecord; + itemRecord.element._animating = true; + } + + if (itemRecord.appearedInView && !insertedMap[itemID]) { + appearingItems.push(itemRecord.element); + } + } + } + } + + // If items are appearing from out of no where in uniform/grouped grids, we'll set their opacity to 1 since they'll be hidden by the clipping set up a little later. + // Multisized items that appear will have no where to fade out from, so we leave their opacity as 0 so they just fade in when animated + if (!variablySizedItemsFound) { + for (i = 0, len = appearingItems.length; i < len; i++) { + appearingItems[i].style.opacity = 1; + } + } + + if (variablySizedItemsFound) { + that._trackedAnimation = AnimationHelper.animateListFadeBetween(that._trackedAnimation, that._site.surface, that._site.rtl, affectedItems, insertedMap, removedMap); + } else { + var itemSurface = that._site.itemSurface, + itemStart = that._headerSlot.cy, + itemEnd = that._itemsPerColumn * that._totalItemHeight + itemStart; + + // Some columns may have been deleted, and we don't want them to be clipped. Here we calculate what the max clipping should be + if (that._itemsPerColumn === 1) { + that._trackedAnimation = AnimationHelper.animateListReflow(that._trackedAnimation, itemSurface, that._site.rtl, affectedItems, insertedMap, removedMap); + } else { + that._trackedAnimation = AnimationHelper.animateReflow(that._trackedAnimation, itemSurface, that._site.rtl, affectedItems, insertedMap, removedMap, itemEnd - itemStart, that._totalItemHeight); + } + itemSurface.style.clip = "rect(" + itemStart + "px,auto," + itemEnd + "px,auto)"; + } + + function trackerDone() { + if (!that._site._isZombie()) { + // It's important to reset the surface clip once animations are done. We don't want to clip an item that's at the bottom row and being selected via cross slide when the ListView is static + that._site.itemSurface.style.clip = "auto"; + that._trackedAnimation = null; + animationSignal.complete(); + } + } + that._trackedAnimation.getCompletionPromise().then(trackerDone, trackerDone); + + that._cachedInserted = []; + that._cachedRemoved = []; + that._cachedItemRecords = {}; + + return animationSignal.promise; + }); + + return { + animationPromise: endLayoutPromise + }; + }, + + _groupFromOffset: function GridLayout_groupFromOffset(offset) { + this._updateOffsets(); + return offset < this._getGroup(0).offset ? 0 : this._site._groups.groupFromOffset(offset); + }, + + _getGroup: function GridLayout_getGroup(groupIndex) { + return this._site._groups.group(groupIndex); + }, + + _getItemsCount: function GridLayout_getItemsCount(group, groupIndex) { + if (groupIndex + 1 < this._site._groups.length()) { + return this._site._groups.group(groupIndex + 1).startIndex - group.startIndex; + } else { + return this._count - group.startIndex; + } + }, + + calculateFirstVisible: function GridLayout_calculateFirstVisible(beginScrollPosition, wholeItem) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + var that = this; + return this._initialize().then(function (initialized) { + if (initialized) { + beginScrollPosition -= that._surfaceMargin; + var groupIndex = that._groupFromOffset(beginScrollPosition), + group = that._site._groups.group(groupIndex), + groupSize = that._getItemsCount(group, groupIndex), + startIndex = group.startIndex; + + if (!wholeItem) { + beginScrollPosition += that._itemMargins[that._site.rtl ? "left" : "right"]; + } + var index = that._decorateGroup(group).itemFromOffset(that, group, groupIndex, beginScrollPosition, wholeItem, 0); + return Math.min(startIndex + index, startIndex + groupSize - 1); + } else { + return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized)); + } + }); + }, + + calculateLastVisible: function GridLayout_calculateLastVisible(endScrollPosition, wholeItem) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + var that = this; + return this._initialize().then(function (initialized) { + if (initialized) { + var offset = endScrollPosition - that._surfaceMargin, + groupIndex = that._groupFromOffset(offset), + group = that._site._groups.group(groupIndex), + groupSize = that._getItemsCount(group, groupIndex), + startIndex = group.startIndex, + groupOffset; + if (offset - group.offset >= that._headerSlot.cx) { + if (!wholeItem) { + offset -= that._itemMargins[that._site.rtl ? "right" : "left"]; + } + var index = that._decorateGroup(group).itemFromOffset(that, group, groupIndex, offset, wholeItem, 1); + return Math.min(startIndex + index, startIndex + groupSize - 1); + } else { + return Math.max(0, startIndex - 1); + } + } else { + return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized)); + } + }); + }, + + hitTest: function GridLayout_hitTest(x, y) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + if (this._count) { + x -= this._surfaceMargin; + if (this._rtl) { + x = this._getCanvasWidth(this._count) - x; + } + var groups = this._site._groups; + for (var i = 0, len = groups.length() ; i < len; i++) { + if (x < groups.group(i).offset) { + break; + } + } + var groupIndex = i - 1, + group = groups.group(groupIndex); + if (group) { + var decorator = this._decorateGroup(group); + return decorator.getLogicalIndex(this, group, groupIndex, false, x, y, 0); + } else { + if (groupIndex < 0) { + return 0; + } else { + var lastGroupIndex = groups.length() - 1, + lastGroup = groups.group(lastGroupIndex), + lastGroupSize = this._getItemsCount(lastGroup, lastGroupIndex); + return lastGroup.startIndex + lastGroupSize - 1; + } + } + } else { + return -1; + } + }, + + getItemPosition: function GridLayout_getItemPosition(itemIndex) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + var that = this; + return this._initialize().then(function (initialized) { + if (initialized) { + var itemPromise = that._site._itemsManager._itemPromiseAtIndex(itemIndex); + return that._site._groupOf(itemPromise).then( + function () { + itemPromise.cancel(); + var groupIndex = that._site._groups.groupFromItem(itemIndex); + return that._calcItemPosition(itemIndex, groupIndex); + }, + function (e) { + itemPromise.cancel(); + return WinJS.Promise.wrapError(e); + } + ); + } else { + return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized)); + } + }); + }, + + _computeItemsPerColumn: function GridLayout_computeItemsPerColumn(group) { + var surfaceMargin = this._site._surfaceMargins, + surfaceHeight = this._site.viewportSize.height - surfaceMargin.top - surfaceMargin.bottom, + groupInfo = this._getGroupInfo(group), + itemsPerColumn = Math.floor((surfaceHeight - this._headerSlot.cy) / (groupInfo && groupInfo.enableCellSpanning ? groupInfo.cellHeight : this._getTotalItemHeight(group))); + + if (this._maxRows) { + itemsPerColumn = Math.min(itemsPerColumn, this._maxRows); + } + + itemsPerColumn = Math.max(itemsPerColumn, 1); + return itemsPerColumn; + }, + + _decorateGroup: function GridLayout_decorateGroup(group) { + if (!group.decorator) { + if (this._multiSize(group)) { + group.decorator = new VariableSizeDecorator(); + group.decorator.slotsPerColumn = this._computeItemsPerColumn(group); + } else { + group.decorator = new FixedSizeDecorator(); + } + } + + if (this._itemsPerColumn === WinJS.UI._UNINITIALIZED && group.decorator instanceof FixedSizeDecorator) { + this._itemsPerColumn = this._computeItemsPerColumn(group); + } + + return group.decorator; + }, + + itemsAdded: function GridLayout_itemsAdded(elements) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + this._dataModified(elements, []); + }, + + itemsRemoved: function GridLayout_itemsRemoved(elements) { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + this._dataModified([], elements); + }, + + itemsMoved: function GridLayout_itemsMoved() { + /// + /// + /// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + this._dataModified([], []); + }, + + _dataModified: function GridLayout_dataModified(inserted, removed) { + var i, len; + for (i = 0, len = inserted.length; i < len; i++) { + this._cachedInserted.push(inserted[i]); + } + for (i = 0, len = removed.length; i < len; i++) { + this._cachedRemoved.push(removed[i]); + } + + // If the layout's already been cached (or being cached) in this cycle, skip this datamodified run + if (this._dataModifiedPromise) { + return; + } + + var currentPromise = new Signal(); + this._dataModifiedPromise = currentPromise.promise; + this._animateEndLayout = true; + function completePromise() { + currentPromise.complete(); + } + if (this._initialized()) { + var that = this; + this._isEmpty().then(function (isEmpty) { + if (!isEmpty) { + var animationRangePromise = Promise.wrap(); + if (that._site.loadingBehavior === "incremental") { + var viewportSize = that._site.viewportSize.width, + firstPromise = that.calculateFirstVisible(Math.max(0, that._site.scrollbarPos - viewportSize), false), + lastPromise = that.calculateLastVisible(that._site.scrollbarPos + 2 * viewportSize - 1, false); + + animationRangePromise = Promise.join([firstPromise, lastPromise]).then(function (indices) { + that._viewAnimationRange = { + start: indices[0], + end: indices[1] + }; + }); + } + animationRangePromise.then(function () { + if (that._viewAnimationRange) { + var firstNearbyItem = that._viewAnimationRange.start - 1, + lastNearbyItem = that._viewAnimationRange.end + that._cachedRemoved.length + 1; + for (i = firstNearbyItem; i < lastNearbyItem; i++) { + var itemData = that._getItemInfo(i); + if (itemData && itemData.element) { + if (!that._cachedItemRecords[itemData.element.uniqueID]) { + that._cachedItemRecords[itemData.element.uniqueID] = { + oldRow: itemData.row, + row: itemData.row, + oldColumn: itemData.column, + column: itemData.column, + oldLeft: itemData.left, + oldTop: itemData.top, + left: itemData.left, + top: itemData.top, + element: itemData.element, + itemIndex: i + }; + } + } + } + } + completePromise(); + }, completePromise); + } else { + completePromise(); + } + }, completePromise); + } else { + completePromise(); + } + }, + + _updateOffsets: function GridLayout_updateOffsets() { + if (this._site._groups.dirty) { + var count = this._site._groups.length(); + + if (count) { + var previousGroup; + + for (var i = 0; i < count; i++) { + var group = this._site._groups.group(i); + + if (previousGroup) { + var itemsCount = group.startIndex - previousGroup.startIndex; + group.offset = previousGroup.offset + this._decorateGroup(previousGroup).getGroupSize(this, previousGroup, i - 1, itemsCount); + group.absoluteOffset = group.offset; + } else { + group.offset = 0; + } + + previousGroup = group; + } + + if (this._site._groups.pinnedItem !== undefined) { + var pinnedGroupIndex = this._site._groups.groupFromItem(this._site._groups.pinnedItem), + pinnedGroup = this._site._groups.group(pinnedGroupIndex), + pinnedCoordinates = this._indexToCoordinate(this._site._groups.pinnedItem - pinnedGroup.startIndex), + pinnedGroupOffset = this._site._groups.pinnedOffset - this._headerSlot.cx - this._decorateGroup(pinnedGroup).itemOffset(this, pinnedGroup, this._site._groups.pinnedItem), + correction = pinnedGroupOffset - pinnedGroup.offset; + + for (i = 0; i < count; i++) { + this._site._groups.group(i).offset += correction; + } + } + } + + this._site._groups.dirty = false; + } + } + }) +}); +})(this, WinJS); + +(function incrementalViewInit(global, WinJS, undefined) { + "use strict"; + + +var utilities = WinJS.Utilities, + Promise = WinJS.Promise; + +// Incremental View doesn't use virtualization. It creates all the items immediately but it creates +// only a small set of items - a chunk. By default there are 50 items in a chunk. When the user +// scrolls to the last item the next chunk of items is created. +WinJS.Namespace.define("WinJS.UI", { + _IncrementalView: function (viewSite) { + this.site = viewSite; + this.items = new WinJS.UI._ItemsContainer(viewSite); + this.lastPageBegin = 0; + this.lastItem = -1; + this.loadingInProgress = false; + this.realizePass = 1; + this.pagesToLoad = WinJS.UI._DEFAULT_PAGES_TO_LOAD; + this.pagesToLoadThreshold = WinJS.UI._DEFAULT_PAGE_LOAD_THRESHOLD; + this.automaticallyLoadPages = true; + this.resetView(); + this.firstLayoutPass = true; + this.firstIndexDisplayed = -1; + this.lastIndexDisplayed = -1; + this.runningAnimations = null; + this.animating = false; + this.lastAnimatedPass = -1; + this.loadingItems = false; + this.renderCompletePromise = Promise.wrap(); + this.insertedElements = []; + } +}); +WinJS.UI._IncrementalView.prototype = { + addItem: function IncrementalView_addItem(fragment, itemIndex, currentPass, finishCallback) { + var that = this; + + var itemAvailable = function (element) { + if (that.site._isZombie()) { + return; + } + + var record = that.site._itemsManager._recordFromElement(element); + var renderComplete = record.renderComplete; + + var wrapper = element.parentNode; + if (wrapper.parentNode !== fragment) { + fragment.appendChild(wrapper); + } + + function configure() { + if (that.site._isSelected(itemIndex)) { + that.site._renderSelection(wrapper, element, true, true); + } + + if (that.site._isInsertedItem(record.itemPromise)) { + wrapper.style.opacity = 0; + that.insertedElements.push(wrapper); + } + + that.items.setItemAt(itemIndex, { + element: element, + itemsManagerRecord: record, + wrapper: wrapper + }); + + that.site._layout.layoutItem(itemIndex, wrapper); + + finishCallback(renderComplete); + } + + function nextItemSibling(element) { + var curr = element.nextSibling; + while (curr && !utilities.hasClass(curr, WinJS.UI._wrapperClass)) { + curr = curr.nextSibling; + } + return curr; + } + + function insert(wrapper, insertBefore) { + if (that.realizePass === currentPass) { + if (insertBefore) { + fragment.insertBefore(wrapper, insertBefore); + } else { + fragment.appendChild(wrapper); + } + + configure(); + } else { + finishCallback(renderComplete); + } + } + + if (wrapper.parentNode !== fragment) { + if (itemIndex === 0) { + insert(wrapper); + } else { + that.items.requestItem(itemIndex - 1).then(function (previousItem) { + if (that.site._isZombie()) { + return; + } + + insert(wrapper, nextItemSibling(that.items.wrapperAt(itemIndex - 1))); + }); + } + } else if (that.realizePass === currentPass) { + configure(); + } else { + finishCallback(renderComplete); + } + }; + + this.site._itemsManager._itemAtIndex(itemIndex). + then(itemAvailable, function (err) { finishCallback(); return WinJS.Promise.wrapError(err); }); + }, + + checkRenderStatus: function () { + if (!this.loadingItems && !this.animating) { + this.site._raiseViewComplete(); + } + }, + + realizeItems: function IncrementalView_realizeItem(fragment, begin, end, count, currentPass, finishCallback) { + var that = this, + counter = end - begin, + renderCompletePromises = []; + + this.hideProgressBar(); + this.loadingItems = true; + function callCallback(renderCompletePromise) { + renderCompletePromises.push(renderCompletePromise); + if (--counter === 0) { + if (that.realizePass === currentPass) { + that.renderCompletePromise = Promise.join(renderCompletePromises); + if (that.insertedElements.length) { + that.site._layout.itemsAdded(that.insertedElements); + } + var endLayoutResult = that.site._layout.endLayout(), + wasFirstLayout = that.firstLayoutPass, + finished = false, + animationPromise; + + that.site._clearInsertedItems(); + that.insertedElements = []; + + if (that.firstLayoutPass) { + that.runningAnimations = Promise.join([that.runningAnimations, that.site._animateListEntrance(!that.firstEntranceAnimated)]); + that.firstLayoutPass = false; + that.firstEntranceAnimated = true; + } + if (endLayoutResult) { + if (endLayoutResult.newEndIndex) { + that.realizeItems(fragment, end, Math.min(count, endLayoutResult.newEndIndex), count, currentPass, finishCallback); + } + + animationPromise = endLayoutResult.animationPromise; + } + finished = !endLayoutResult || !endLayoutResult.newEndIndex; + if (finished) { + that.site._raiseViewLoaded(); + } + + if (animationPromise || wasFirstLayout) { + that.animating = true; + that.lastAnimatedPass = currentPass; + } + that.renderCompletePromise.then(function() { + if (that.realizePass === currentPass) { + that.loadingItems = false; + that.updateAriaOnItems(count); + } + }); + + that.runningAnimations = Promise.join([that.runningAnimations, animationPromise]).then(function() { + return Promise.timeout(); + }); + that.runningAnimations.then(function () { + if (that.lastAnimatedPass === currentPass) { + that.animating = false; + that.runningAnimations = null; + } + }); + + Promise.join([that.renderCompletePromise, that.runningAnimations]).then(function () { + if (that.realizePass === currentPass || that.lastAnimatedPass === currentPass) { + that.checkRenderStatus(); + } + }); + + if (finished) { + finishCallback(end - 1); + } + } + } + } + + if (counter !== 0) { + for (var itemIndex = begin; itemIndex < end; itemIndex++) { + var itemData = this.items.itemDataAt(itemIndex); + if (!itemData || itemData.removed) { + this.addItem(fragment, itemIndex, currentPass, callCallback); + } else { + // Item already exists. Only position needs to be updated + this.site._layout.layoutItem(itemIndex, itemData.wrapper); + callCallback(that.site._itemsManager._recordFromElement(itemData.element).renderComplete); + } + } + } else { + finishCallback(end - 1); + } + }, + + loadNextChunk: function IncrementalView_loadNextChunk(callback) { + if (!this.loadingInProgress) { + var that = this, + currentPass = ++this.realizePass; + + this.loadingInProgress = true; + + var done = function (realizeItemsCalled) { + that.hideProgressBar(); + that.loadingInProgress = false; + callback(realizeItemsCalled); + }; + + var noRange = function (count) { + that.site._layout.endLayout(); + that.site._clearInsertedItems(); + done(false); + }; + + this.site._itemsCount().then(function (count) { + if (!that.destroyed && that.realizePass === currentPass) { + if (count > that.lastItem + 1) { + var viewportLength = that.site._getViewportLength(); + + that.site._layout.startLayout(0, that.pagesToLoad * viewportLength - 1, count).then(function (initialRange) { + that.calculateDisplayedItems(count).then(function () { + if (initialRange) { + var itemsPerChunk = initialRange.endIndex - initialRange.beginIndex, + chunksToLoad = Math.max(0, Math.ceil(that.lastItem / itemsPerChunk)) + 1, + totalItems = Math.min(count, itemsPerChunk * chunksToLoad); + that.site._layout.startLayout(0, chunksToLoad * that.pagesToLoad * viewportLength - 1, totalItems).then(function (range) { + if (range) { + var begin = that.lastItem + 1; + + that.lastPageBegin = begin; + that.lastItem = range.endIndex - 1; + + that.realizeItems(that.site._itemCanvas, begin, range.endIndex, totalItems, currentPass, function (finalItem) { + if (that.realizePass === currentPass) { + that.lastItem = finalItem; + that.updateScrollbar(count); + } + done(true); + }); + } else { + noRange(count); + } + }); + } else { + noRange(count); + } + }); + }); + } else { + that.calculateDisplayedItems(count).then(function () { + done(false); + }); + } + } else { + that.calculateDisplayedItems(count).then(function () { + done(false); + }); + } + }); + } + }, + + updateItems: function IncrementalView_updateItems(callback, refreshView) { + var that = this, + currentPass = this.realizePass, + scrollbarPos = this.site.scrollPosition, + viewportLength = this.site._getViewportLength(); + + function noRange(count) { + that.hideProgressBar(); + + if (!count) { + var endLayoutResult = that.site._layout.endLayout(); + that.site._clearInsertedItems(); + if (endLayoutResult && endLayoutResult.animationPromise) { + that.animating = true; + that.lastAnimatedPass = currentPass; + that.runningAnimations = that.runningAnimations ? Promise.join([that.runningAnimations, endLayoutResult.animationPromise]) : endLayoutResult.animationPromise; + that.runningAnimations.then(function () { + if (that.lastAnimatedPass === currentPass) { + that.animating = false; + that.runningAnimations = null; + that.checkRenderStatus(); + } + }); + } else { + that.checkRenderStatus(); + } + that.loadingInProgress = false; + } + + callback(true); + } + + this.site._itemsCount().then(function (count) { + if (!that.destroyed && that.realizePass === currentPass) { + currentPass = ++that.realizePass; + that.site._layout.startLayout(0, that.pagesToLoad * viewportLength - 1, count).then(function (initialRange) { + that.calculateDisplayedItems(Math.min(count, that.lastItem + 1)).then(function () { + if (initialRange) { + var itemsPerChunk = initialRange.endIndex - initialRange.beginIndex, + chunksLoaded = Math.max(1, Math.ceil(that.lastItem / itemsPerChunk)), + totalItems = Math.min(count, itemsPerChunk * chunksLoaded); + that.site._layout.startLayout(0, chunksLoaded * that.pagesToLoad * viewportLength - 1, totalItems).then(function (range) { + if (that.site._isZombie()) { + return; + } + + if (range && range.beginIndex < range.endIndex) { + var end = Math.min(range.endIndex, totalItems); + if (refreshView) { + var canvas = that.site._itemCanvas, + items = that.items; + + items.eachIndex(function (index) { + if ((index < 0) || (index > (end - 1))) { + var wrapper = items.wrapperAt(index); + canvas.removeChild(wrapper); + items.removeItem(index); + } + }); + } + that.loadingInProgress = true; + that.realizeItems(that.site._itemCanvas, 0, end, totalItems, currentPass, function (finalItem) { + if (that.realizePass === currentPass) { + that.loadingInProgress = false; + that.lastItem = finalItem; + that.updateScrollbar(count); + } + callback(true); + }); + } else { + noRange(count); + } + }); + } else { + noRange(count); + } + }); + }); + } + }); + }, + + download: function IncrementalView_download(action, callback) { + var that = this; + + if (this.site._cachedCount === WinJS.UI._UNINITIALIZED || this.lastItem === WinJS.UI._UNINITIALIZED) { + this.showProgressBar(); + } + + this.site._raiseViewLoading(); + action(function (realizeItemsCalled) { + if (!realizeItemsCalled) { + that.site._raiseViewComplete(); + } + }); + }, + + loadNextPages: function IncrementalView_loadNextPages() { + this.download(this.loadNextChunk.bind(this)); + this.checkProgressBarVisibility(); + }, + + checkProgressBarVisibility: function IncrementalView_checkProgressBarVisibility() { + if (this.site._isZombie()) { + return; + } + + var viewportLength = this.site._getViewportLength(), + scrollBarPos = this.site.scrollPosition; + + if (this.site._cachedCount > this.lastItem + 1 && this.loadingInProgress && + scrollBarPos >= this.site._canvas[this.site._layout.horizontal ? "offsetWidth" : "offsetHeight"] - viewportLength) { + this.showProgressBar(); + } + }, + + showProgressBar: function IncrementalView_showProgressBar() { + var barX = "50%", + barY = "50%", + parent = this.site._element; + if (this.lastItem !== WinJS.UI._UNINITIALIZED) { + parent = this.site._canvas; + var padding = WinJS.UI._INCREMENTAL_CANVAS_PADDING / 2; + if (this.site._layout.horizontal) { + barX = "calc(100% - " + padding + "px)"; + } else { + barY = "calc(100% - " + padding + "px)"; + } + } + + this.site._showProgressBar(parent, barX, barY); + }, + + calculateDisplayedItems: function (count) { + var that = this; + + function setUpdateMarkersPromise(promise) { + if (that.updateMarkersPromise) { + that.updateMarkersPromise.cancel(); + } + that.updateMarkersPromise = promise; + } + + if (count !== 0) { + var scrollbarPos = this.site.scrollPosition, + firstPromise = this.site.layout.calculateFirstVisible(scrollbarPos, false), + lastPromise = this.site.layout.calculateLastVisible(scrollbarPos + this.site._getViewportLength() - 1, false); + + firstPromise.then(function (first) { + that.firstIndexDisplayed = first; + }); + + lastPromise.then(function (last) { + that.lastIndexDisplayed = last; + }); + + return WinJS.Promise.join([firstPromise, lastPromise]).then(function(v) { + setUpdateMarkersPromise(that.updateAriaMarkers(count, that.firstIndexDisplayed, that.lastIndexDisplayed)); + return WinJS.Promise.wrap(v); + }, function (error) { + var name = (error.length > 0 ? error[0].name : ""); + if (name !== "WinJS.UI.LayoutNotInitialized") { + return WinJS.Promise.wrapError(error); + } else { + // If the ListView is invisible (LayoutNotInitialized), eat the exception + return WinJS.Promise.wrap(); + } + }); + } else { + this.firstIndexDisplayed = -1; + this.lastIndexDisplayed = -1; + setUpdateMarkersPromise(this.updateAriaMarkers(count, this.firstIndexDisplayed, this.lastIndexDisplayed)); + return WinJS.Promise.wrap(); + } + }, + + hideProgressBar: function IncrementalView_hideProgressBar() { + this.site._hideProgressBar(); + }, + + scrollbarAtEnd: function IncrementalView_scrollbarAtEnd(scrollbarPos, scrollLength, viewportSize) { + var viewportLength = this.site._getViewportLength(), + last = this.items.wrapperAt(this.lastItem), + lastOffset = 0; + + if (last) { + lastOffset = last[ this.site._layout.horizontal ? "offsetLeft" : "offsetTop"]; + } + + return (scrollbarPos + viewportLength) > (lastOffset - viewportLength * this.pagesToLoadThreshold); + }, + + finalItem: function IncrementalView_finalItem() { + return Promise.wrap(this.lastItem); + }, + + onScroll: function IncrementalView_onScroll(scrollbarPos, scrollLength, viewportSize) { + this.checkProgressBarVisibility(); + + if (this.scrollbarAtEnd(scrollbarPos, scrollLength, viewportSize) && this.automaticallyLoadPages) { + this.download(this.loadNextChunk.bind(this)); + } else { + var that = this; + this.site._itemsCount().then(function (count) { + return that.calculateDisplayedItems(count); + }).then(function() { + if (that.animating || that.loadingItems) { + that.site._raiseViewLoaded(); + } else { + that.site._raiseViewComplete(); + } + }); + } + }, + + onResize: function IncrementalView_onResize(scrollbarPos, viewportSize) { + this.download(this.updateItems.bind(this)); + }, + + reset: function IncrementalView_reset() { + var site = this.site; + this.firstIndexDisplayed = -1; + this.lastIndexDisplayed = -1; + this.loadingInProgress = false; + this.firstLayoutPass = true; + this.runningAnimations = null; + this.animating = false; + this.loadingItems = false; + this.lastItem = -1; + site._unsetFocusOnItem(); + if (site._currentMode().onDataChanged) { + site._currentMode().onDataChanged(); + } + + this.items.each(function (index, item) { + site._itemsManager.releaseItem(item); + }); + + this.items.removeItems(); + site._resetCanvas(); + + this.insertedElements = []; + site._clearInsertedItems(); + }, + + reload: function IncrementalView_reload() { + this.download(this.loadNextChunk.bind(this)); + }, + + resetView: function IncrementalView_resetView() { + this.site.scrollPosition = 0; + }, + + refresh: function IncrementalView_refresh(scrollbarPos, scrollLength, viewportSize, newCount) { + var that = this, + end = Math.min(this.lastItem + 1, newCount); + + this.lastItem = end - 1; + that.updateScrollbar(newCount); + + var canvas = this.site._itemCanvas, + items = this.items; + + this.download(function (callback) { + that.updateItems(function () { + if (that.scrollbarAtEnd(scrollbarPos, scrollLength, viewportSize) && (newCount > end) && that.automaticallyLoadPages) { + that.loadNextChunk(callback); + } else { + that.calculateDisplayedItems(newCount).then(function () { + callback(newCount); + }); + } + }, true); + }); + }, + + updateScrollbar: function IncrementalView_updateScrollbar(count) { + var that = this, + length = Math.min(count, (this.lastItem !== -1 ? this.lastItem + 1 : count)); + + if (length) { + return this.site._layout.getScrollbarRange(length).then( + function (range) { + that.site._setCanvasLength(range.beginScrollPosition, (length < count ? WinJS.UI._INCREMENTAL_CANVAS_PADDING : 0) + range.endScrollPosition); + }, + function () { + that.site._setCanvasLength(0, 0); + } + ); + } else { + this.site._setCanvasLength(0, 0); + return Promise.wrap(); + } + }, + + // Sets the ARIA attributes on each item + updateAriaOnItems: function IncrementalView_updateAriaOnItems(count) { + if (this.site._isZombie()) { return; } + + if (count > 0) { + this.site._createAriaMarkers(); + var that = this; + var startMarker = this.site._ariaStartMarker, + endMarker = this.site._ariaEndMarker, + firstItem = this.items.itemAt(0); + + WinJS.UI._ensureId(firstItem); + WinJS.UI._setAttribute(firstItem, "x-ms-aria-flowfrom", startMarker.id); + + this.items.each(function (index, item, itemData) { + var nextItem = that.items.itemAt(index + 1); + + if (nextItem) { + // We aren't at the last item so flow to the next one + WinJS.UI._ensureId(nextItem); + WinJS.UI._setFlow(item, nextItem); + } else { + // We're at the last item so flow to the end marker + WinJS.UI._setAttribute(item, "aria-flowto", endMarker.id); + } + + WinJS.UI._setAttribute(item, "role", that.site._itemRole); + WinJS.UI._setAttribute(item, "aria-setsize", count); + WinJS.UI._setAttribute(item, "aria-posinset", index + 1); + WinJS.UI._setAttribute(item, "tabIndex", that.site._tabIndex); + }); + } + }, + + // Sets aria-flowto on _ariaStartMarker and x-ms-aria-flowfrom on _ariaEndMarker. The former + // points to the first visible item and the latter points to the last visible item. + updateAriaMarkers: function IncrementalView_updateAriaMarkers(count, firstIndexDisplayed, lastIndexDisplayed) { + if (this.site._isZombie()) { return Promise.wrap(); } + + this.site._createAriaMarkers(); + var that = this; + var startMarker = this.site._ariaStartMarker, + endMarker = this.site._ariaEndMarker; + + if (count === 0) { + WinJS.UI._setFlow(startMarker, endMarker); + return Promise.wrap(); + } else if (firstIndexDisplayed !== -1 && lastIndexDisplayed !== -1) { + return Promise.join([this.items.requestItem(firstIndexDisplayed), this.items.requestItem(lastIndexDisplayed)]).done(function (v) { + if (that.site._isZombie()) { return; } + var firstVisibleItem = v[0], + lastVisibleItem = v[1]; + WinJS.UI._ensureId(firstVisibleItem); + WinJS.UI._ensureId(lastVisibleItem); + + WinJS.UI._setAttribute(startMarker, "aria-flowto", firstVisibleItem.id); + WinJS.UI._setAttribute(endMarker, "x-ms-aria-flowfrom", lastVisibleItem.id); + }); + } else { + return Promise.wrap(); + } + }, + + // Update the ARIA attributes on item that are needed so that Narrator can announce it. + // item must be in the items container. + updateAriaForAnnouncement: function IncrementalView_updateAriaForAnnouncement(item, count) { + var index = this.items.index(item); + //#DBG _ASSERT(index !== WinJS.UI._INVALID_INDEX); + + WinJS.UI._setAttribute(item, "role", this.site._itemRole); + WinJS.UI._setAttribute(item, "aria-setsize", count); + WinJS.UI._setAttribute(item, "aria-posinset", index + 1); + }, + + cleanUp: function IncrementalView_cleanUp() { + var itemsManager = this.site._itemsManager; + this.items.each(function (index, item) { + itemsManager.releaseItem(item); + }); + this.lastItem = -1; + this.site._unsetFocusOnItem(); + this.items.removeItems(); + this.site._resetCanvas(); + this.destroyed = true; + } +}; + +})(this, WinJS); + +(function itemsContainerInit(global, WinJS, undefined) { + "use strict"; + +var utilities = WinJS.Utilities, + Promise = WinJS.Promise; + +WinJS.Namespace.define("WinJS.UI", { + _ItemsContainer: function (site) { + this.site = site; + this._itemData = {}; + this.dataIndexToLayoutIndex = {}; + this.waitingItemRequests = {}; + this.placeholders = {}; + } +}); + +WinJS.UI._ItemsContainer.prototype = { + requestItem: function ItemsContainer_requestItem(itemIndex) { + if (!this.waitingItemRequests[itemIndex]) { + this.waitingItemRequests[itemIndex] = []; + } + + var that = this; + var promise = new Promise(function (complete, error) { + 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) { + /*#DBG + delete WinJS.Utilities.data(this._itemData[index].element).itemData; + delete WinJS.Utilities.data(this._itemData[index].element).itemsContainer; + #DBG*/ + delete this._itemData[index]; + }, + + removeItems: function ItemsContainer_removeItems() { + /*#DBG + var that = this; + Object.keys(this._itemData).forEach(function (k) { + delete WinJS.Utilities.data(that._itemData[k].element).itemData; + delete WinJS.Utilities.data(that._itemData[k].element).itemsContainer; + }); + #DBG*/ + this._itemData = {}; + this.placeholders = {}; + this.waitingItemRequests = {}; + }, + + setPlaceholderAt: function ItemsContainer_setPlaceholderAt(itemIndex, placeholder) { + this.placeholders[itemIndex] = placeholder; + }, + + setItemAt: function ItemsContainer_setItemAt(itemIndex, itemData) { + /*#DBG + if (itemData.itemsManagerRecord.released) { + throw "ACK! Attempt to use a released itemsManagerRecord"; + } + var oldItemData = WinJS.Utilities.data(itemData.element).itemData; + if (oldItemData || WinJS.Utilities.data(itemData.element).itemsContainer) { + if (oldItemData.itemsManagerRecord.item.index !== itemIndex) { + throw "ACK! Attempted use of already in-use element"; + } + } + WinJS.Utilities.data(itemData.element).itemData = itemData; + WinJS.Utilities.data(itemData.element).itemsContainer = this; + #DBG*/ + //#DBG _ASSERT(itemData.element && (itemData.element instanceof HTMLElement)); + //#DBG _ASSERT(!this._itemData[itemIndex]); + 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] = []; + } + if (this.placeholders[itemIndex]) { + delete this.placeholders[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]; + }, + + wrapperAt: function ItemsContainer_wrapperAt(itemIndex) { + var itemData = this._itemData[itemIndex]; + return itemData ? itemData.wrapper : null; + }, + + wrapperFrom: function ItemsContainer_wrapperFrom(element) { + var canvas = this.site._itemCanvas, + viewport = this.site._viewport; + + while (element && element.parentNode && element.parentNode !== canvas && element.parentNode !== viewport) { + element = element.parentNode; + } + + return (element && utilities.hasClass(element, WinJS.UI._wrapperClass) ? element : null); + }, + + index: function ItemsContainer_index(element) { + var item = this.wrapperFrom(element); + if (item) { + for (var index in this._itemData) { + if (this._itemData[index].wrapper === item) { + return parseInt(index, 10); + } + } + } + + return WinJS.UI._INVALID_INDEX; + }, + + each: function ItemsContainer_each(callback) { + for (var index in this._itemData) { + if (this._itemData.hasOwnProperty(index)) { + var itemData = this._itemData[index]; + //#DBG _ASSERT(itemData); + callback(parseInt(index, 10), itemData.element, itemData); + } + } + }, + + eachIndex: function ItemsContainer_each(callback) { + for (var index in this._itemData) { + callback(parseInt(index, 10)) + } + }, + + setLayoutIndices: function ItemsContainer_setLayoutIndices(indices) { + this.dataIndexToLayoutIndex = indices; + }, + + getLayoutIndex: function ItemsContainer_getLayoutIndex(dataIndex) { + var layoutIndex = this.dataIndexToLayoutIndex[dataIndex]; + return layoutIndex === undefined ? dataIndex : layoutIndex; + } +}; +})(this, WinJS); + +(function itemsPoolInit(global, WinJS, undefined) { + "use strict"; + +var utilities = WinJS.Utilities, + Promise = WinJS.Promise; + +function getListView(element) { + while (element && !utilities.hasClass(element, WinJS.UI._listViewClass)) { + element = element.parentNode; + } + return element ? element.winControl : null; +} + +// This is onpropertychange handler. attachEvent handlers leak stuff referenced by closures so this function is global and doesn't use a closure to access ListView or an item. +function itemPropertyChange() { + if (event.propertyName === "aria-selected") { + var wrapper = event.srcElement.parentNode, + selected = event.srcElement.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 (wrapper && (selected !== WinJS.UI._isSelectionRenderer(wrapper))) { + var listView = getListView(wrapper.parentNode); + if (listView) { + var index = listView._view.items.index(wrapper), + selection = listView.selection; + + if (listView._selectionAllowed()) { + if (selected) { + selection[listView._selectionMode === WinJS.UI.SelectionMode.single ? "set" : "add"](index); + } else { + selection.remove(index); + } + } + + if (selection._isIncluded(index) !== selected) { + // If a selectionchanging event handler rejected the selection change, revert aria-selected + event.srcElement.setAttribute("aria-selected", !selected); + } + } + } + } +} + +WinJS.Namespace.define("WinJS.UI", { + _itemPropertyChange: itemPropertyChange +}); + +// Default renderer for Listview +var trivialHtmlRenderer = WinJS.UI.simpleItemRenderer(function (item) { + if (utilities._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 = document.createElement("span"); + element.innerText = data.toString(); + return element; +}); + +WinJS.Namespace.define("WinJS.UI", { + _trivialHtmlRenderer: trivialHtmlRenderer, + _ElementsPool: function (site, itemClass, prepareMethodName) { + this.init(site, itemClass, prepareMethodName); + } +}); + +var canceledPromise = WinJS.Promise.wrapError(new WinJS.ErrorFromName("Canceled")); + +WinJS.UI._ElementsPool.prototype = { + init: function (site, itemClass, prepareMethodName) { + this.site = site; + this.itemClass = itemClass; + this.prepareMethodName = prepareMethodName; + this.entries = []; + this.uidToEntry = {}; + this.renderer = WinJS.UI._trivialHtmlRenderer; + this.release = null; + }, + + setRenderer: function (newRenderer) { + if (!newRenderer) { + if (WinJS.validation) { + throw new WinJS.ErrorFromName("WinJS.UI.ListView.invalidTemplate", WinJS.UI._strings.invalidTemplate); + } + this.renderer = trivialHtmlRenderer; + } else if (typeof newRenderer === "function") { + this.renderer = newRenderer; + } else if (typeof newRenderer === "object") { + if (WinJS.validation && !newRenderer.renderItem) { + throw new WinJS.ErrorFromName("WinJS.UI.ListView.invalidTemplate", WinJS.UI._strings.invalidTemplate); + } + this.renderer = newRenderer.renderItem; + } + }, + + renderItemAsync: function (itemPromise, oldElement) { + var entry, + recycledElement; + + if (oldElement) { + recycledElement = oldElement; + } else { + entry = this.getEntry(); + recycledElement = entry ? entry.element : null; + } + + var itemForRendererPromise = itemPromise.then(function (item) { + return item || canceledPromise; + }); + + var rendered = WinJS.Promise.as(this.renderer(itemForRendererPromise, recycledElement)). + then(WinJS.UI._normalizeRendererReturn); + + var that = this; + return rendered.then(function (v) { + that.itemRendered(v.element, recycledElement, entry); + return v; + }); + }, + + renderItemSync: function (itemPromise) { + var entry = this.getEntry(); + var recycledElement = entry ? entry.element : null; + var rendered = this.renderer(itemPromise, recycledElement); + this.itemRendered(rendered.element, recycledElement, entry); + return rendered; + }, + + itemRendered: function (element, recycledElement, entry) { + if (element) { + if (element !== recycledElement) { + this.setUpItem(element); + } else { + this.resetItem(recycledElement, entry); + if (entry) { + this.entries.splice(entry.index, 1); + delete this.uidToEntry[recycledElement.uniqueID]; + } + } + } + }, + + setUpItem: function (element) { + if (this.itemClass) { + utilities.addClass(element, this.itemClass); + } + if (this.prepareMethodName) { + this.site._layout[this.prepareMethodName](element); + } + }, + + prepareForReuse: function (recycledElement) { + }, + + resetItem: function (recycledElement, entry) { + if (entry && entry.display) { + recycledElement.style.display = entry.display; + } + }, + + getEntry: function () { + for(var i = this.entries.length - 1; i >= 0; i--) { + var object = this.entries[i]; + if (!object.removed) { + object.removed = true; + this.prepareForReuse(object.element); + object.element.id = ""; + return { + element: object.element, + data: object.data, + display: object.display, + index: i + }; + } + } + return null; + }, + + remove: function (element) { + var entry = this.uidToEntry[element.uniqueID]; + if (entry) { + entry.removed = true; + } + }, + + add: function (data, element, display) { + if (this.release) { + this.release(data, element); + } + + /*#DBG + if (WinJS.Utilities.data(element).itemData + || WinJS.Utilities.data(element).itemsContainer + || WinJS.Utilities.data(element).itemsManagerRecord) { + throw "ACK! Attempt to put something into the pool that wasn't properly released"; + } + #DBG*/ + + var entry = { + element: element, + display: display, + removed: false + }; + this.entries.push(entry); + this.uidToEntry[element.uniqueID] = entry; + }, + + clear: function () { + for (var i = 0, len = this.entries.length; i < len; i++) { + var entry = this.entries[i], + element = entry.element, + parentNode = entry.element.parentNode; + + if (!entry.removed && parentNode) { + parentNode.removeChild(element); + } + } + this.entries = []; + this.uidToEntry = {}; + } +}; + +WinJS.Namespace.define("WinJS.UI", { + _ItemsPool: WinJS.Class.derive(WinJS.UI._ElementsPool, function (site) { + this.init(site); + },{ + setUpItem: function (element) { + var wrapper = this.site._wrappersPool.renderItemSync().element; + + this.site._renderSelection(wrapper, element, false, true); + + wrapper.insertBefore(element, wrapper.firstElementChild); + utilities.addClass(element, WinJS.UI._itemClass); + + this.site._layout.prepareItem(element); + this.site._layout.prepareItem(wrapper); + + element.attachEvent("onpropertychange", itemPropertyChange); + }, + prepareForReuse: function (recycledElement) { + var wrapper = recycledElement.parentNode; + if (wrapper) { + this.site._wrappersPool.remove(wrapper); + } + }, + resetItem: function (recycledElement, entry) { + var wrapper = recycledElement.parentNode; + if (wrapper) { + this.site._renderSelection(wrapper, recycledElement, false, true); + } else { + this.setUpItem(recycledElement); + } + } + }, { + supportedForProcessing: false, + }) +}); + +})(this, WinJS); + +(function listLayoutInit(global, WinJS, undefined) { + "use strict"; + + +var utilities = WinJS.Utilities, + Promise = WinJS.Promise, + Signal = WinJS._Signal, + AnimationHelper = WinJS.UI._ListViewAnimationHelper; + +// This component is responsible for calculating items' positions in list mode. +WinJS.Namespace.define("WinJS.UI", { + ListLayout: WinJS.Class.derive(WinJS.UI._LayoutCommon, function (options) { + /// + /// + /// Creates a new ListLayout object. + /// + /// + /// The set of properties and values to apply to the new ListLayout. + /// + /// + /// The new ListLayout object. + /// + /// + this.init(); + this._cachedItemRecords = {}; + }, { + /// + horizontal: { + enumerable: true, + get: function () { + return false; + } + }, + + getKeyboardNavigatedItem: function (itemIndex, element, keyPressed) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + if (!this._initialized()) { + return Promise.wrap(-1); + } + + keyPressed = this._adjustDirection(keyPressed); + + var newIndex; + switch (keyPressed) { + case WinJS.Utilities.Key.upArrow: + case WinJS.Utilities.Key.leftArrow: + newIndex = itemIndex - 1; + break; + case WinJS.Utilities.Key.downArrow: + case WinJS.Utilities.Key.rightArrow: + newIndex = itemIndex + 1; + break; + default: + return WinJS.UI._LayoutCommon.prototype.getKeyboardNavigatedItem.call(this, itemIndex, element, keyPressed); + } + + return Promise.wrap(newIndex); + }, + + setSite: function (layoutSite) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + this._site = layoutSite; + }, + + getScrollbarRange: function (count) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + var that = this; + return this._initialize(count).then(function (initialized) { + if (initialized) { + return { + beginScrollPosition: 0, + endScrollPosition: count * that._totalItemHeight + }; + } else { + return { + beginScrollPosition: 0, + endScrollPosition: 0 + }; + } + }); + }, + + getItemPosition: function (itemIndex) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + var that = this; + return this._initialize().then(function (initialized) { + if (initialized) { + var pos = that._calcItemPosition(itemIndex); + + pos.contentWidth = that._width; + pos.contentHeight = that._itemHeight; + pos.totalWidth = that._site.viewportSize.width; + pos.totalHeight = that._totalItemHeight; + + return pos; + } else { + return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized)); + } + }); + }, + + _initialize: function (count) { + var that = this + return this._initializeBase(count).then(function (initialized) { + if (initialized) { + //#DBG _ASSERT(that._totalItemWidth > 0 && that._itemWidth > 0); + var overhead = that._totalItemWidth - that._itemWidth; + that._width = that._site.viewportSize.width - overhead; + + var surfaceMargin = that._site._surfaceMargins; + that._surfaceMargin = surfaceMargin.top; + } + return initialized; + }); + }, + + startLayout: function (beginScrollPosition, endScrollPosition, count) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + var that = this; + if (count) { + this._count = count; + return this._initialize(count).then(function (initialized) { + if (initialized) { + var dataModifiedPromise = that._dataModifiedPromise ? that._dataModifiedPromise : Promise.wrap(); + return dataModifiedPromise.then(function () { + // In incremental mode, startLayout needs to give a range of whole items, and not round up if there isn't enough space in the scroll range. + var incremental = (that._site.loadingBehavior === "incremental"); + return that.calculateFirstVisible(beginScrollPosition, incremental).then(function (begin) { + return that.calculateLastVisible(endScrollPosition, incremental).then(function (last) { + var end = last + 1; + that._purgeItemCache(begin, end); + return { + beginIndex: begin, + endIndex: end + }; + }); + }); + }); + } else { + return null; + } + }); + } else { + return Promise.wrap(null); + } + }, + + prepareItem: function (element) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + + // Do nothing because position absolute is already set in CSS. + }, + + prepareHeader: function (element) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + + // Do nothing because position absolute is already set in CSS. + }, + + releaseItem: function (item, newItem) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + item._animating = false; + }, + + layoutItem: function (itemIndex, element) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + var itemPos = this._calcItemPosition(itemIndex), + itemData = this._getItemInfo(itemIndex); + + if (itemData.top !== itemPos.top || itemData.width !== this._width || itemData.height != this._itemHeight || element !== itemData.element) { + var oldTop = (itemData.element === element ? itemData.top : undefined); + itemData.element = element; + itemData.top = itemPos.top; + itemData.width = this._width; + itemData.height = this._itemHeight; + + var cachedRecord = this._cachedItemRecords[element.uniqueID]; + if (cachedRecord) { + cachedRecord.left = itemData.left; + cachedRecord.top = itemData.top; + } + + var style = element.style; + if (!element._animating && !cachedRecord) { + // When an element isn't marked as animating, and no animation record exists for it, it's still not necessarily safe to animate. + // If a lot of elements are being deleted, we can run into cases where new items that we've never seen before need to be realized and put + // in the view. If an item falls into the animation range and has no record, we'll set up a special record for it and animate it coming in from the bottom + // of the view. + if (this._dataModifiedPromise && itemIndex >= this._viewAnimationRange.start && itemIndex <= this._viewAnimationRange.end) { + this._cachedItemRecords[element.uniqueID] = { + oldTop: itemData.top + this._site.viewportSize.height * 2, + oldLeft: itemData.left, + row: itemData.row, + column: itemData.column, + top: itemData.top, + left: itemData.left, + element: element, + itemIndex: itemIndex, + appearedInView: true + }; + // Since this item just appeared in the view and we're going to animate it coming in from the bottom, we set its opacity to zero because it'll be moved to the bottom of the listview + // but can still be visible since the canvas region's clip style isn't set until endLayout + style.cssText += ";left:0px;top:" + this._cachedItemRecords[element.uniqueID].oldTop + "px;width:" + this._width + "px;height:" + this._itemHeight + "px; opacity: 0"; + } else { + // Setting the left, top, width, and height via cssText is approximately 50% faster than setting each one individually. + // Start with semicolon since cssText does not end in one even if you provide one at the end. + style.cssText += ";left:0px;top:" + itemPos.top + "px;width:" + this._width + "px;height:" + this._itemHeight + "px"; + } + } + + // When an element is laid out again while animating but doesn't have a cached record, that means that enough changes have occurred to + // make that element outside of the range of usual animations (that range is found in the dataModified handler). We don't want to move the + // element instantly, since while it may be outside of the range of visible indices, it may still be visible because it's in the midst + // of a move animation. We'll instead make a new record for this element and let endLayout animate it again. + if (element._animating && !cachedRecord) { + this._cachedItemRecords[element.uniqueID] = { + oldTop: oldTop, + top: itemData.top, + element: element + }; + } + } + }, + + layoutHeader: function (groupIndex, element) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + element.style.display = "none"; + }, + + itemsAdded: function (elements) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + this._dataModified(elements, []); + }, + + itemsRemoved: function (elements) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + this._dataModified([], elements); + }, + + itemsMoved: function GridLayout_itemsMoved() { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + this._dataModified([], []); + }, + + _dataModified: function (inserted, removed) { + var i, len; + for (i = 0, len = inserted.length; i < len; i++) { + this._cachedInserted.push(inserted[i]); + } + for (i = 0, len = removed.length; i < len; i++) { + this._cachedRemoved.push(removed[i]); + } + + // If the layout's already been cached in this cycle, skip this datamodified run + if (this._dataModifiedPromise) { + return; + } + + var currentPromise = new Signal(); + this._dataModifiedPromise = currentPromise.promise; + this._animateEndLayout = true; + if (this._initialized()) { + var that = this; + this._isEmpty().then(function (isEmpty) { + if (!isEmpty) { + var viewportSize = that._site.viewportSize.height, + firstPromise = that.calculateFirstVisible(Math.max(0, that._site.scrollbarPos - viewportSize), false), + lastPromise = that.calculateLastVisible(that._site.scrollbarPos + 2 * viewportSize - 1, false); + + WinJS.Promise.join([firstPromise, lastPromise]).done(function (indices) { + var firstNearbyItem = indices[0], + lastNearbyItem = indices[1] + that._cachedRemoved.length; + + that._viewAnimationRange = { + start: firstNearbyItem, + end: lastNearbyItem + }; + for (var i = firstNearbyItem - 1; i <= lastNearbyItem; i++) { + var itemData = that._getItemInfo(i); + if (itemData && itemData.element) { + if (!that._cachedItemRecords[itemData.element.uniqueID]) { + that._cachedItemRecords[itemData.element.uniqueID] = { + oldLeft: itemData.left, + oldTop: itemData.top, + left: itemData.left, + top: itemData.top, + element: itemData.element + }; + } + } + } + currentPromise.complete(); + }, + function (error) { + currentPromise.complete(); + var layoutNotInitialized; + if (Array.isArray(error)) { + layoutNotInitialized = error.some(function (e) { return e && e.name === "WinJS.UI.LayoutNotInitialized"; }); + } else { + layoutNotInitialized = error.name === "WinJS.UI.LayoutNotInitialized"; + } + if (!layoutNotInitialized) { + return WinJS.Promise.wrapError(error); + } + } + ); + } else { + currentPromise.complete(); + } + }); + } else { + currentPromise.complete(); + } + }, + + endLayout: function () { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + if (!this._animateEndLayout) { + return; + } + var dataModifiedPromise = this._dataModifiedPromise ? this._dataModifiedPromise : Promise.wrap(); + this._dataModifiedPromise = null; + this._animateEndLayout = false; + var that = this; + + var endLayoutPromise = dataModifiedPromise.then(function () { + var i, len, element; + if (that._site.animationsDisabled) { + for (i = 0, len = that._cachedInserted.length; i < len; i++) { + that._cachedInserted[i].style.opacity = 1.0; + } + for (i = 0, len = that._cachedRemoved.length; i < len; i++) { + element = that._cachedRemoved[i]; + if (element.parentNode) { + element.parentNode.removeChild(element); + } + } + var cachedRecordKeys = Object.keys(that._cachedItemRecords); + for (i = 0, len = cachedRecordKeys.length; i < len; i++) { + var itemRecord = that._cachedItemRecords[cachedRecordKeys[i]]; + if (itemRecord.element) { + if (itemRecord.oldLeft !== itemRecord.left || itemRecord.oldTop !== itemRecord.top) { + if (itemRecord.left !== undefined) { + itemRecord.element.style.left = itemRecord.left + "px"; + } + if (itemRecord.top !== undefined) { + itemRecord.element.style.top = itemRecord.top + "px"; + } + } + + if (itemRecord.appearedInView) { + itemRecord.element.style.opacity = 1; + } + } + } + that._cachedInserted = []; + that._cachedRemoved = []; + that._cachedItemRecords = {}; + + return; + } + + var affectedItems = {}, + animationSignal = new Signal(), + cachedRecordKeys = Object.keys(that._cachedItemRecords), + insertedElements = [], + removedElements = []; + + for (i = 0, len = that._cachedRemoved.length; i < len; i++) { + element = that._cachedRemoved[i]; + element.style.opacity = 0.0; + element._removed = true; + removedElements.push(element); + } + + for (i = 0, len = that._cachedInserted.length; i < len; i++) { + element = that._cachedInserted[i]; + if (!element._removed) { + element.style.opacity = 1; + var itemRecord = that._cachedItemRecords[element.uniqueID]; + if (itemRecord) { + element.style.cssText += ";top:" + itemRecord.top + "px;"; + } + element._inserted = true; + insertedElements.push(element); + } + } + + var movingElements = [], + moveData = []; + for (i = 0, len = cachedRecordKeys.length; i < len; i++) { + var itemRecord = that._cachedItemRecords[cachedRecordKeys[i]]; + if (itemRecord.element) { + var element = itemRecord.element; + if ((itemRecord.oldLeft !== itemRecord.left || itemRecord.oldTop !== itemRecord.top) && !element._inserted && !element._removed) { + // The itemData object is reused by the ListView, but item records are recreated every time a change happens. The stages + // need unchanging records to function properly. + movingElements.push(element); + moveData.push({left: itemRecord.left, top: itemRecord.top}); + itemRecord.element._animating = true; + } + + if (itemRecord.appearedInView && !element._inserted && !element._removed) { + element.style.opacity = 1; + } + } + } + + var animation = WinJS.UI.Animation._createUpdateListAnimation(insertedElements, removedElements, movingElements); + for (i = 0, len = insertedElements.length; i < len; i++) { + insertedElements[i]._currentAnimation = animation; + insertedElements[i]._inserted = null; + } + for (i = 0, len = removedElements.length; i < len; i++) { + removedElements[i]._currentAnimation = animation; + removedElements[i]._removed = null; + } + for (i = 0, len = movingElements.length; i < len; i++) { + movingElements[i].style.left = moveData[i].left + "px"; + movingElements[i].style.top = moveData[i].top + "px"; + movingElements[i]._currentAnimation = animation; + } + function done() { + for (i = 0, len = insertedElements.length; i < len; i++) { + if (insertedElements[i]._currentAnimation === animation) { + insertedElements[i]._currentAnimation = null; + insertedElements[i]._animating = false; + } + } + for (i = 0, len = removedElements.length; i < len; i++) { + if (removedElements[i]._currentAnimation === animation) { + removedElements[i]._currentAnimation = null; + removedElements[i]._animating = false; + if (removedElements[i].parentNode) { + removedElements[i].parentNode.removeChild(removedElements[i]); + } + } + } + for (i = 0, len = movingElements.length; i < len; i++) { + if (movingElements[i]._currentAnimation === animation) { + movingElements[i]._currentAnimation = null; + movingElements[i]._animating = false; + } + } + + if (!that._site._isZombie()) { + animationSignal.complete(); + } + } + animation.execute().then(done, done); + that._cachedInserted = []; + that._cachedRemoved = []; + that._cachedItemRecords = {}; + return animationSignal.promise; + }); + + return { + animationPromise: endLayoutPromise + }; + }, + + _calcItemPosition: function (index) { + return { + top: index * this._totalItemHeight, + left: 0, + offset: 0 + }; + }, + + calculateFirstVisible: function (beginScrollPosition, wholeItem) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + var that = this; + return this._initialize().then(function (initialized) { + if (initialized) { + if (!wholeItem) { + beginScrollPosition += that._itemMargins.bottom; + } + return Math.max(-1, Math.min(that._count - 1, Math[wholeItem ? "ceil" : "floor"](beginScrollPosition / that._totalItemHeight))); + } else { + return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized)); + } + }); + }, + + calculateLastVisible: function (endScrollPosition, wholeItem) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + var that = this; + return this._initialize().then(function (initialized) { + if (initialized) { + if (!wholeItem) { + endScrollPosition -= that._itemMargins.top; + } + var lastPossibleIndex = Math[wholeItem ? "floor" : "ceil"]((endScrollPosition + 1) / that._totalItemHeight) - 1; + return Math.max(-1, Math.min(lastPossibleIndex, that._count - 1)); + } else { + return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized)); + } + }); + }, + + hitTest: function (x, y) { + /// + /// + /// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code. + /// + /// + /// + /// + /// + /// + /// + /// + return Math.floor(y / this._totalItemHeight); + } + }) +}); + +})(this, WinJS); + +(function listViewImplInit(global, WinJS, undefined) { + "use strict"; + +var DISPOSE_TIMEOUT = 1000; +var controlsToDispose = []; +var disposeControlTimeout; +function disposeControls() { + controlsToDispose = controlsToDispose.filter(function (c) { + if (c._isZombie()) { + c._dispose(); + return false; + } else { + return true; + } + }); +} +function scheduleForDispose(lv) { + controlsToDispose.push(lv); + disposeControlTimeout && disposeControlTimeout.cancel(); + disposeControlTimeout = WinJS.Promise.timeout(DISPOSE_TIMEOUT).then(disposeControls); +} + +function ensureId(element) { + if (!element.id) { + element.id = element.uniqueID; + } +} + +function setFlow(from, to) { + WinJS.UI._setAttribute(from, "aria-flowto", to.id); + WinJS.UI._setAttribute(to, "x-ms-aria-flowfrom", from.id); +} + +// Only set the attribute if its value has changed +function setAttribute(element, attribute, value) { + if (element.getAttribute(attribute) !== "" + value) { + element.setAttribute(attribute, value); + } +} + +WinJS.Namespace.define("WinJS.UI", { + _disposeControls: disposeControls, + _ensureId: ensureId, + _setFlow: setFlow, + _setAttribute: setAttribute +}); + +var thisWinUI = WinJS.UI, + utilities = WinJS.Utilities, + Promise = WinJS.Promise, + AnimationHelper = WinJS.UI._ListViewAnimationHelper; + + var strings = { + get notCompatibleWithSemanticZoom() { return WinJS.Resources._getWinJSString("ui/notCompatibleWithSemanticZoom").value; }, + get listViewInvalidItem() { return WinJS.Resources._getWinJSString("ui/listViewInvalidItem").value; }, + get listViewViewportAriaLabel() { return WinJS.Resources._getWinJSString("ui/listViewViewportAriaLabel").value; } + }; + + var requireSupportedForProcessing = WinJS.Utilities.requireSupportedForProcessing; + +// ListView implementation + +function elementListViewHandler(eventName, caseSensitive, capture) { + return { + name: (caseSensitive ? eventName : eventName.toLowerCase()), + handler: function (eventObject) { + var srcElement = eventObject.srcElement; + if (srcElement) { + var that = srcElement.winControl; + if (that && that instanceof WinJS.UI.ListView) { + that["_on" + eventName](eventObject); + } + } + }, + capture: capture + }; +} + +var ZoomableView = WinJS.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 () { + this._listView._beginZoom(); + }, + + positionItem: function (item, position) { + return this._listView._positionItem(item, position); + }, + + endZoom: function (isCurrentView) { + this._listView._endZoom(isCurrentView); + } +}); + +WinJS.Namespace.define("WinJS.UI", { + /// + /// 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 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 whether the ListView animation is an entrance animation or a transition animation. + /// + ListViewAnimationType: { + /// + /// The animation plays when the ListView is first displayed. + /// + entrance: "entrance", + /// + /// The animation plays when the ListView is changing its content. + /// + contentTransition: "contentTransition" + }, + + /// + /// 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 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. + /// 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: WinJS.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. + /// + /// + msWriteProfilerMark("WinJS.UI.ListView:constructor,StartTM"); + element = element || document.createElement("div"); + options = options || {}; + + // Attaching JS control to DOM element + element.winControl = this; + this._versionManager = null; + this._canvasLength = 0; + this._scrollLimitMin = 0; + 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._itemCanvas = null; + this._cachedCount = WinJS.UI._UNINITIALIZED; + this._loadingState = this._LoadingState.complete; + this._firstTimeDisplayed = true; + this._currentScrollPosition = 0; + this._lastScrollPosition = 0; + this._notificationHandlers = []; + this._viewportWidth = WinJS.UI._UNINITIALIZED; + this._viewportHeight = WinJS.UI._UNINITIALIZED; + this._manipulationState = MSManipulationEvent.MS_MANIPULATION_STATE_STOPPED; + this._groupsToRemove = {}; + this._setupInternalTree(); + this._isCurrentZoomView = true; + // 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 WinJS.UI._ScrollView(this); + this._selection = new WinJS.UI._SelectionManager(this); + this._createTemplates(); + this._wrappersPool = new WinJS.UI._ElementsPool(this); + var that = this; + this._wrappersPool.setRenderer(function (itemPromise, recycledElement) { + if (recycledElement) { + recycledElement.innerText = ""; + WinJS.Utilities.removeClass(recycledElement, WinJS.UI._selectedClass); + return { element: recycledElement }; + } + else { + return { element: that._wrapperTemplate.cloneNode(true) }; + } + }); + this._itemsPool = new WinJS.UI._ItemsPool(this); + this._headersPool = new WinJS.UI._ElementsPool(this, WinJS.UI._headerClass, "prepareHeader"); + this._groupsPool = new WinJS.UI._ElementsPool(this); + this._groupsPool.setRenderer(function (groupItemPromise, recycledElement) { + return WinJS.Promise.wrap(recycledElement || document.createElement("div")); + }); + if (!options.itemDataSource) { + var list = new WinJS.Binding.List(); + this._dataSource = list.dataSource; + } else { + this._dataSource = options.itemDataSource; + } + this._selectionMode = WinJS.UI.SelectionMode.multi; + this._tap = WinJS.UI.TapBehavior.invokeOnly; + this._swipeBehavior = WinJS.UI.SwipeBehavior.select; + this._mode = new WinJS.UI._SelectionMode(this); + + // Call after swipeBehavior and modes are set + this._setSwipeClass(); + + this._updateAriaRoles(); + this._tabIndex = (this._element.tabIndex !== undefined && this._element.tabIndex >= 0) ? this._element.tabIndex : 0; + 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._groups = new WinJS.UI._NoGroups(this); + this._scrollToPriority = thisWinUI.ListView._ScrollToPriority.medium; + this._scrollToPromise = Promise.wrap(0); + this._updateItemsManager(); + this._updateLayout(new WinJS.UI.GridLayout()); + this._attachEvents(); + + this._runningInit = true; + this._incrementalViewOptions = {}; + WinJS.UI.setOptions(this, options); + this._runningInit = false; + + this._refresh(0); + msWriteProfilerMark("WinJS.UI.ListView: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._layout; + }, + set: function (layoutObject) { + this._updateLayout(layoutObject); + + if (!this._runningInit) { + this._view.reset(); + this._updateItemsManager(); + this._refresh(0); + } + } + }, + + /// + /// 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: { + get: function () { + return this._view.pagesToLoad; + }, + set: function (newValue) { + if ((typeof newValue === "number") && (newValue > 0)) { + this._incrementalViewOptions.pagesToLoad = newValue; + if (this._view instanceof WinJS.UI._IncrementalView) { + this._view.pagesToLoad = newValue; + this._refresh(0); + } + return; + } + throw new WinJS.ErrorFromName("WinJS.UI.ListView.PagesToLoadIsInvalid", WinJS.UI._strings.pagesToLoadIsInvalid); + } + }, + + /// + /// 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: { + get: function () { + return this._view.pagesToLoadThreshold; + }, + set: function (newValue) { + if ((typeof newValue === "number") && (newValue > 0)) { + this._incrementalViewOptions.pagesToLoadThreshold = newValue; + if (this._view instanceof WinJS.UI._IncrementalView) { + this._view.pagesToLoadThreshold = newValue; + this._refresh(0); + } + return; + } + throw new WinJS.ErrorFromName("WinJS.UI.ListView.PagesToLoadThresholdIsInvalid", WinJS.UI._strings.pagesToLoadThresholdIsInvalid); + } + }, + + /// + /// 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) { + msWriteProfilerMark("WinJS.UI.ListView:set_groupDataSource,info"); + + var that = this; + + function groupStatusChanged(eventObject) { + if (eventObject.detail === thisWinUI.DataSourceStatus.failure) { + that.itemDataSource = null; + that.groupDataSource = null; + } + } + + if (this._groupDataSource && this._groupDataSource.removeEventListener) { + this._groupDataSource.removeEventListener("statuschanged", groupStatusChanged, false); + } + + this._groupDataSource = newValue; + + if (this._groupDataSource && this._groupDataSource.addEventListener) { + this._groupDataSource.addEventListener("statuschanged", groupStatusChanged, false); + } + + if (this._groups) { + this._groups.cleanUp(); + } + if (newValue) { + this._groups = new WinJS.UI._GroupsContainer(this, newValue); + utilities.addClass(this._element, WinJS.UI._groupsClass); + } else { + this._groups = new WinJS.UI._NoGroups(this); + utilities.removeClass(this._element, WinJS.UI._groupsClass); + } + + if (!this._runningInit) { + this._cancelRealize(); + this._layout.reset(); + this._refresh(0); + } + } + }, + + /// + /// 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: { + get: function () { + return this._view.automaticallyLoadPages; + }, + set: function (newValue) { + if (typeof newValue === "boolean") { + this._incrementalViewOptions.automaticallyLoadPages = newValue; + if (this._view instanceof WinJS.UI._IncrementalView) { + this._view.automaticallyLoadPages = newValue; + } + return; + } + throw new WinJS.ErrorFromName("WinJS.UI.ListView.AutomaticallyLoadPagesIsInvalid", WinJS.UI._strings.automaticallyLoadPagesIsInvalid); + } + }, + + /// + /// Gets or sets a value that determines how many data items are loaded into the DOM. + /// + loadingBehavior: { + get: function () { + return (this._view instanceof WinJS.UI._IncrementalView) ? "incremental" : "randomAccess"; + }, + set: function (newValue) { + if (typeof newValue === "string") { + if (newValue.match(/^(incremental|randomaccess)$/i)) { + if (this._view) { + this._view.cleanUp(); + } + + var that = this; + this._versionManager.unlocked.then(function () { + if (newValue === "incremental") { + that._view = new WinJS.UI._IncrementalView(that); + WinJS.UI.setOptions(that._view, that._incrementalViewOptions); + } else { + that._view = new WinJS.UI._ScrollView(that); + } + + that._setLayoutSite(); + + that._refresh(0); + }); + + return; + } + } + throw new WinJS.ErrorFromName("WinJS.UI.ListView.LoadingBehaviorIsInvalid", WinJS.UI._strings.loadingBehaviorIsInvalid); + } + }, + + /// + /// 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)$/)) { + this._selectionMode = newMode; + this._element.setAttribute("aria-multiselectable", this._multiSelection()); + this._updateAriaRoles(); + this._setSwipeClass(); + return; + } + } + throw new WinJS.ErrorFromName("WinJS.UI.ListView.ModeIsInvalid", WinJS.UI._strings.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) { + this._tap = tap; + this._updateAriaRoles(); + } + }, + + /// + /// 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) { + msWriteProfilerMark("WinJS.UI.ListView:set_itemDataSource,info"); + this._dataSource = newData || new WinJS.Binding.List().dataSource; + + if (!this._runningInit) { + this._selection._reset(); + this._cancelRealize(); + this._layout.reset(); + this._updateItemsManager(); + this._refresh(0); + } + } + }, + + /// + /// 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._itemsPool.renderer; + }, + set: function (newRenderer) { + this._itemsPool.setRenderer(newRenderer); + + if (!this._runningInit) { + this._cancelRealize(); + this._layout.reset(); + this._updateItemsManager(); + this._refresh(0); + } + } + }, + + /// + /// Gets or sets the function that is called when the ListView recycles the + /// element representation of an item. + /// + resetItem: { + get: function () { + return this._itemsPool.release; + }, + set: function (release) { + this._itemsPool.release = 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._headersPool.renderer; + }, + set: function (newRenderer) { + this._headersPool.setRenderer(newRenderer); + + if (!this._runningInit) { + this._cancelRealize(); + this._layout.reset(); + this._refresh(0); + } + } + }, + + /// + /// Gets or sets the function that is called when the ListView recycles the DOM element representation + /// of a group header. + /// + resetGroupHeader: { + get: function () { + return this._headersPool.release; + }, + set: function (release) { + this._headersPool.release = 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._raiseViewLoading(true); + + var that = this, + count; + + this._scrollToPriority = thisWinUI.ListView._ScrollToPriority.high; + this._scrollToPromise = Promise.timeout().then(function () { + if (that._isZombie()) { + return WinJS.Promise.cancel; + } + + return that._itemsCount(); + }).then(function (itemsCount) { + count = itemsCount; + if (count === 0) { + return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized)); + } + return that._getItemOffset(itemIndex); + }).then(function (range) { + return that._view.updateScrollbar(count, true).then(function () { return range; }); + }).then(function (range) { + return that._correctRangeInFirstColumn(range); + }).then(function (range) { + range = that._convertFromCanvasCoordinates(range); + + var begin = Math.max(0, range.begin); + that.scrollPosition = begin; + that._lastScrollPosition = begin; + that._view.refresh( + begin, + that._viewport[that._scrollLength], + that._getViewportSize(), + that._cachedCount + ); + + return range.begin; + }).then(null, function(error) { + // If the ListView is invisible (LayoutNotInitialized), eat the exception + if (error.name !== "WinJS.UI.LayoutNotInitialized") { + return WinJS.Promise.wrapError(error); + } + }); + } + }, + + /// + /// 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 item = this._view.items.itemAt(this._selection._getFocused()), + key = null, + focusVisible = false; + if (item) { + key = this._itemsManager._recordFromElement(item).item.key; + focusVisible = !!item.parentNode.querySelector("." + thisWinUI._itemFocusOutlineClass); + } + return { + index: this._selection._getFocused(), + key: key, + hasFocus: !!this._hasKeyboardFocus, + showFocus: focusVisible + }; + }, + + set: function (data) { + this._hasKeyboardFocus = data.hasFocus || this._hasKeyboardFocus; + var that = this; + function setItemFocused(item, isInTree, itemIndex) { + var drawKeyboardFocus = !!data.showFocus && that._hasKeyboardFocus; + that._unsetFocusOnItem(isInTree); + that._selection._setFocused(itemIndex, drawKeyboardFocus); + if (that._hasKeyboardFocus) { + that._keyboardFocusInbound = drawKeyboardFocus; + that._setFocusOnItem(itemIndex); + } else { + that._tabManager.childFocus = (isInTree ? item : null); + } + } + + if (data.key && this._dataSource.itemFromKey) { + if (this.oldCurrentItemKeyFetch) { + this.oldCurrentItemKeyFetch.cancel(); + } + + this.oldCurrentItemKeyFetch = this._dataSource.itemFromKey(data.key).then(function (item) { + that.oldCurrentItemKeyFetch = null; + if (item) { + var element = that._view.items.itemAt(item.index); + setItemFocused(element, !!element, item.index); + } + }); + } else { + if (data.index !== undefined) { + var focusedIndex = data.index, + focusedItem = this._view.items.itemAt(focusedIndex); + setItemFocused(focusedItem, !!focusedItem, focusedIndex); + } + } + } + }, + + /// + /// 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; + } + }, + + // Public methods + 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(itemIndex) { + /// + /// + /// Makes the specified item visible. The ListView scrolls to the item if needed. + /// + /// + /// The index of the item to bring into view. + /// + /// + if (itemIndex < 0) { + return Promise.wrap(); + } + + this._raiseViewLoading(true); + + var that = this, + count; + + this._itemsCount().then(function (itemsCount) { + count = itemsCount; + if (count === 0) { + return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized)); + } + return that._getItemOffset(itemIndex); + }).then(function (range) { + if (range.begin < 0) { + that._groups.pinItem(); + return that._getItemOffset(itemIndex); + } else { + return range; + } + }).then(function (range) { + return that._view.updateScrollbar(count, true).then(function () { return range; }); + }).then(function (range) { + return that._correctRangeInFirstColumn(range); + }).then(function (range) { + var left = that._viewportScrollPosition, + right = left + that._getViewportLength(), + newPosition = that._viewportScrollPosition; + + range = that._convertFromCanvasCoordinates(range); + + if (range.begin < left) { + newPosition = range.begin; + } else if (range.end > right) { + newPosition = range.end - that._getViewportLength(); + } + + newPosition = Math.max(0, newPosition); + if (newPosition !== left) { + that.scrollPosition = newPosition; + that._lastScrollPosition = newPosition; + } + + that._view.refresh( + newPosition, + that._viewport[that._scrollLength], + that._getViewportSize(), + that._cachedCount + ); + }).then(null, function (error) { + // If the ListView is invisible (LayoutNotInitialized), eat the exception + if (error.name !== "WinJS.UI.LayoutNotInitialized") { + return WinJS.Promise.wrapError(error); + } + }); + }, + + loadMorePages: function ListView_loadMorePages() { + /// + /// + /// Loads the next set of pages if the ListView object's loadingBehavior is set to incremental. + /// + /// + if (this._view instanceof WinJS.UI._IncrementalView) { + this._view.loadNextPages(); + } + }, + + recalculateItemPosition: function ListView_recalculateItemPosition() { + /// + /// + /// Repositions all the visible items in the ListView to adjust for items whose sizes have changed. Most apps won't ever need to call this method. + /// + /// + var that = this; + this._versionManager.unlocked.then(function () { + that._itemMargins = null; + that._canvasMargins = null; + that._cachedRTL = null; + that._cancelRealize(); + that._layout.reset(); + that._resizeViewport(); + that._groups.pinItem(); + that._groups.purgeDecorators(); + that._view.reload(that.scrollPosition); + }); + }, + + forceLayout: function ListView_forceLayout() { + /// + /// + /// Forces the ListView to update its layout. Use this function when making the ListView visible again + /// after you set its style.display property to "none". + /// + /// + msWriteProfilerMark("WinJS.UI.ListView:forceLayoutRequested,info"); + this._clearInsertedItems(); + if (this._updater) { + this._updater.removedElements = []; + } + var that = this; + this._versionManager.unlocked.then(function () { + msWriteProfilerMark("WinJS.UI.ListView:forceLayout,info"); + that._itemMargins = null; + that._canvasMargins = null; + that._cachedRTL = null; + that._layout.reset(); + that._view.reset(); + that._resizeViewport(); + var currentScrollPosition = that._viewportScrollPosition; + + if (currentScrollPosition !== that._lastScrollPosition) { + that.scrollPosition = that._lastScrollPosition; + } + that._forceReload = true; + that._scheduleUpdate(); + }); + }, + + _viewportScrollPosition: { + get: function () { + this._currentScrollPosition = this._viewport[this._scrollProperty]; + return this._currentScrollPosition; + }, + set: function (value) { + this._viewport[this._scrollProperty] = value; + this._currentScrollPosition = value; + } + }, + + _canvasStart: { + get: function () { + if (this._rtl() && this._horizontal()) { + return this._canvas.style["right"]; + } else { + return this._canvas.style[this._startProperty]; + } + }, + set: function (value) { + if (this._rtl() && this._horizontal()) { + this._canvas.style["right"] = value; + } else { + this._canvas.style[this._startProperty] = value; + } + } + }, + + /// + /// Gets or sets the position of the ListView's scrollbar. + /// + scrollPosition: { + get: function () { + //#DBG _ASSERT(this._lastScrollPosition >= 0); + return this._currentScrollPosition; + }, + set: function (newPosition) { + this._viewportScrollPosition = newPosition; + } + }, + + _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 = {}; + }, + + // Private methods + _cancelRealize: function () { + this._view && this._view._cancelRealize && this._view._cancelRealize(); + }, + + _refreshHandler: function () { + var that = this; + function onRefresh() { + if (that._isZombie()) { return; } + + that._refreshTimer = false; + that._itemMargins = null; + that._canvasMargins = null; + that._cachedRTL = null; + // Retrieve the values before DOM modifications occur + that._getCanvasMargins(); + that._rtl(); + + if (!that._firstTimeDisplayed) { + that._view.reset(); + that._cancelRealize(); + } + + //#DBG _ASSERT(that._scrollToPriority !== thisWinUI.ListView._ScrollToPriority.uninitialized); + var promise = that._scrollToPromise; + that._scrollToPriority = thisWinUI.ListView._ScrollToPriority.uninitialized; + that._scrollToPromise = null; + + promise.then(function (position) { + position = Math.max(0, position); + if (that._lastScrollPosition !== position) { + that._lastScrollPosition = position; + that.scrollPosition = position; + } + that._view.reload(position); + that._setFocusOnItem(that._selection._getFocused()); + }); + } + + if (this._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 (this._firstTimeDisplayed || Object.keys(this._view.items._itemData).length === 0) { + onRefresh(); + this._firstTimeDisplayed = false; + } else { + this._fadeOutViewport(onRefresh); + } + }, + + _refresh: function (position) { + // Give a higher priority to requests that reset the scroll position + var priority = (position === 0 ? thisWinUI.ListView._ScrollToPriority.medium : thisWinUI.ListView._ScrollToPriority.low); + if (!this._scrollToPromise || priority > this._scrollToPriority) { + if (this._scrollToPromise) { + this._scrollToPromise.cancel(); + } + this._scrollToPriority = priority; + this._scrollToPromise = Promise.wrap(position); + } + + if (!this._refreshTimer) { + this._refreshTimer = true; + + this._raiseViewLoading(); + + var that = this; + setImmediate(function () { + that._refreshHandler(); + }); + } + }, + + _resetItemCanvas: function () { + // The item canvas MUST have style.position = absolute, otherwise clipping will not be done. + this._itemCanvas = document.createElement("div"); + this._canvas.appendChild(this._itemCanvas); + var positionProp = this._rtl() ? "right" : "left"; + this._itemCanvas.style.cssText = "position: absolute; " + positionProp + ": 0px; top: 0px; width: 100%; height: 100%;"; + this._tabManager = new WinJS.UI.TabContainer(this._itemCanvas); + function tabManagerHandler(eventName) { + return { + name: eventName, + handler: function (eventObject) { + that["_" + eventName](eventObject); + }, + capture: false + }; + } + + var itemCanvasEvents = [ + tabManagerHandler("onTabEnter"), + tabManagerHandler("onTabExit") + ]; + var that = this; + itemCanvasEvents.forEach(function (itemCanvasEvent) { + that._itemCanvas.addEventListener(itemCanvasEvent.name, itemCanvasEvent.handler, false); + }); + this._tabManager.tabIndex = this._tabIndex; + }, + + _resetCanvas: function () { + utilities.empty(this._canvas); + 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); + this._resetItemCanvas(); + }, + + _setupInternalTree: function ListView_setupInternalTree() { + + utilities.addClass(this._element, WinJS.UI._listViewClass); + utilities[this._rtl() ? "addClass" : "removeClass"](this._element, WinJS.UI._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._viewport.setAttribute("aria-label", strings.listViewViewportAriaLabel); + this._canvas = this._viewport.firstElementChild; + this._canvasProxy = this._canvas.firstElementChild; + this._keyboardEventsHelper = this._viewport.nextElementSibling.firstElementChild; + this._tabIndex = this._element.tabIndex !== undefined ? this._element.tabIndex : 0; + this._tabEventsHelper = new WinJS.UI.TabContainer(this._keyboardEventsHelper.parentNode); + this._tabEventsHelper.tabIndex = this._tabIndex; + this._resetItemCanvas(); + + this._progressBar = document.createElement("progress"); + utilities.addClass(this._progressBar, WinJS.UI._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; + this._tabEventsHelper.childFocus = this._keyboardEventsHelper; + // 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 (document.activeElement !== this._viewport && this._hasKeyboardFocus) { + this._keyboardEventsHelper._shouldHaveFocus = true; + this._keyboardEventsHelper.focus(); + } + } + this._itemFocused = false; + }, + + _setFocusOnItem: function ListView_setFocusOnItem(index) { + if (this._focusRequest) { + this._focusRequest.cancel(); + } + if (this._isZombie()) { + return; + } + var that = this; + this._focusRequest = this._view.items.requestItem(index).then(function(item) { + if (that._isZombie()) { + return; + } + that._tabEventsHelper.childFocus = null; + + if (that._tabManager.childFocus !== item) { + that._tabManager.childFocus = item; + } + that._focusRequest = null; + if (that._hasKeyboardFocus && !that._itemFocused) { + if (that._selection._keyboardFocused()) { + that._drawFocusRectangle(item); + } + //#DBG _ASSERT(that._cachedCount !== WinJS.UI._UNINITIALIZED); + // The requestItem promise just completed so _cachedCount will + // be initialized. + that._view.updateAriaForAnnouncement(item, 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 item.setActive(), 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. + try { + that._itemFocused = true; + item.setActive(); + } catch (error) { } + } + }); + }, + + _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 (currentMode[name]) { + currentMode[name](eventObject); + } + } + }; + } + + var elementEventsAttached = [ + elementListViewHandler("Resize"), + elementListViewHandler("PropertyChange") + ]; + + // Event handlers that must be added using attachEvent + elementEventsAttached.forEach(function (elementEvent) { + that._element.attachEvent("on" + elementEvent.name, elementEvent.handler); + }); + + // KeyDown handler needs to be added explicitly via addEventListener instead of using the above 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("MSPointerDown", true), + modeHandler("MSPointerMove", true), + modeHandler("click", true), + modeHandler("MSPointerUp", true), + modeHandler("MSGotPointerCapture", true), + modeHandler("MSLostPointerCapture", true), + modeHandler("MSHoldVisual", true), + modeHandler("MSPointerOut", true), + modeHandler("MSPointerCancel", true), + modeHandler("DragStart"), + modeHandler("ContextMenu") + ]; + events.forEach(function (eventHandler) { + that._viewport.addEventListener(eventHandler.name, eventHandler.handler, false); + }); + + // Focus and Blur events need to be handled during the capturing phase, they do not bubble. + var elementEvents = [ + listViewHandler("Focus", false, true), + listViewHandler("Blur", false, true), + modeHandler("KeyDown") + ]; + elementEvents.forEach(function (eventHandler) { + that._element.addEventListener(eventHandler.name, eventHandler.handler, !!eventHandler.capture); + }); + + var viewportEvents = [ + listViewHandler("MSManipulationStateChanged", true), + listViewHandler("Scroll") + ]; + viewportEvents.forEach(function (viewportEvent) { + that._viewport.addEventListener(viewportEvent.name, viewportEvent.handler, false); + }); + + this._keyboardEventsHelper.parentNode.addEventListener("onTabEnter", this._onTabEnter.bind(this), false); + this._keyboardEventsHelper.parentNode.addEventListener("onTabExit", this._onTabExit.bind(this), false); + }, + + _updateItemsManager: function ListView_updateItemsManager() { + var that = this, + notificationHandler = { + // Following methods are used by ItemsManager + beginNotifications: function ListView_beginNotifications() { + }, + + changed: function ListView_changed(newItem, oldItem, oldItemObject) { + if (that._ifZombieDispose()) { return; } + + that._createUpdater(); + + //#DBG _ASSERT(utilities._isDOMElement(newItem)); + + var elementInfo = that._updater.elements[oldItem.uniqueID]; + if (elementInfo) { + var selected = that.selection._isIncluded(elementInfo.index); + if (oldItem !== newItem) { + if (that._tabManager.childFocus === oldItem || that._updater.newFocusedItem === oldItem) { + that._updater.newFocusedItem = newItem; + that._tabManager.childFocus = null; + } + + var next = oldItem.nextElementSibling; + elementInfo.wrapper.removeChild(oldItem); + that._setAriaSelected(newItem, selected); + elementInfo.wrapper.insertBefore(newItem, next); + that._view.items.setItemAt(elementInfo.newIndex, { + element: newItem, + display: elementInfo.display, + wrapper: elementInfo.wrapper, + itemsManagerRecord: elementInfo.itemsManagerRecord + }); + delete that._updater.elements[oldItem.uniqueID]; + that._updater.elements[newItem.uniqueID] = { + item: newItem, + wrapper: elementInfo.wrapper, + index: elementInfo.index, + newIndex: elementInfo.newIndex, + display: elementInfo.display, + itemsManagerRecord: elementInfo.itemsManagerRecord + }; + } else { + that._renderSelection(elementInfo.wrapper, newItem, selected, true); + } + that._updater.changed = true; + that._groups.purgeDecorator(elementInfo.index); + } else { + var placeholder = that._updater.placeholders[oldItem.uniqueID]; + if (placeholder) { + that._updater.placeholders[oldItem.uniqueID].element = newItem; + that._updater.changed = true; + } + } + for (var i = 0, len = that._notificationHandlers.length; i < len; i++) { + that._notificationHandlers[i].changed(newItem, oldItem); + } + }, + + removed: function ListView_removed(item, mirage, handle) { + if (that._ifZombieDispose()) { return; } + + that._createUpdater(); + + function removeFromSelection(index) { + 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; + } + } + + if (item) { + var index, + elementInfo = that._updater.elements[item.uniqueID], + itemObject = that._itemsManager.itemObject(item); + + if (elementInfo) { + index = elementInfo.index; + + // We track removed elements for animation purposes (layout + // component consumes this). + // + that._updater.removedElements.push(elementInfo.wrapper); + + // 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; + + /*#DBG + delete elementInfo.itemsManagerRecord.updater; + #DBG*/ + delete that._updater.elements[item.uniqueID]; + } else { + index = itemObject && itemObject.index; + } + if (+index === index) { + that._groups.purgeDecorator(index); + } + + var placeholder = that._updater.placeholders[item.uniqueID]; + if (placeholder) { + delete that._updater.placeholders[item.uniqueID]; + } + + if (that._updater.oldFocus === index) { + that._updater.newFocus = index; // If index is too high, it'll be fixed in endNotifications + that._updater.focusedItemRemoved = true; + } + + removeFromSelection(index); + } else { + var index = that._updater.selectionHandles[handle]; + if (index === +index) { + removeFromSelection(index); + } + } + + if (that._groups.pinnedItem === index) { + that._groups.pinItem(); + } + 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 elementInfo = that._updater.elements[item.uniqueID]; + if (elementInfo) { + elementInfo.newIndex = newIndex; + that._updater.changed = true; + } + that._updater.itemsMoved = true; + var placeholder = that._updater.placeholders[item.uniqueID]; + if (placeholder) { + placeholder.newIndex = newIndex; + that._updater.changed = true; + } + } + if (that._updater.oldFocus === oldIndex) { + that._updater.newFocus = 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; + } + range = that._updater.selectionLast[oldIndex]; + if (range) { + range.newLastIndex = newIndex; + that._updater.changed = true; + } + if (that._groups.pinnedItem === oldIndex) { + that._groups.pinItem(); + } + that._groups.purgeDecorator(oldIndex); + }, + + endNotifications: function ListView_endNotifications() { + that._update(); + }, + + itemAvailable: function ListView_itemAvailable(item, placeholder) { + var index = that._itemsManager.itemObject(item).index; + + if (that._view.items.placeholders[index]) { + delete that._view.items.placeholders[index]; + } + + if (that._updater && that._updater.placeholders[placeholder.uniqueID]) { + delete that._updater.placeholders[placeholder.uniqueID]; + } + }, + + inserted: function ListView_inserted(itemPromise) { + if (that._ifZombieDispose()) { return; } + + that._createUpdater(); + that._updater.changed = true; + itemPromise.retain(); + that._insertedItems[itemPromise.handle] = itemPromise; + }, + + moved: function ListView_moved(item, previous, next, itemPromise) { + if (that._ifZombieDispose()) { return; } + + that._createUpdater(); + + if (item) { + that._updater.itemsMoved = true; + } + + var index = that._updater.selectionHandles[itemPromise.handle]; + if (index === +index) { + 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; + } + } + }, + + countChanged: function ListView_countChanged(newCount, oldCount) { + if (that._ifZombieDispose()) { return; } + + //#DBG _ASSERT(newCount !== undefined); + that._cachedCount = newCount; + if (newCount < oldCount) { + // no change, but the creation of the updater signals that things need to get refreshed + that._createUpdater(); + that._updater.updateScrollbar = true; + } else if ((that._view.lastIndexDisplayed + 1) === oldCount) { + that._createUpdater(); + that._updater.changed = true; + } + }, + + reload: function () { + if (that._ifZombieDispose()) { return; } + + // Inform scroll view that a realization pass is coming so that it doesn't restart the + // realization pass itself. + that._cancelRealize(); + that._raiseViewLoading(); + that._fadeOutViewport(function () { + that._selection._reset(); + that._updateItemsManager(); + that._view.reset(); + that._view.reload(that.scrollPosition); + }); + } + }; + + function statusChanged(eventObject) { + if (eventObject.detail === thisWinUI.DataSourceStatus.failure) { + that.itemDataSource = null; + that.groupDataSource = null; + } + } + + if (this._versionManager) { + this._versionManager._dispose(); + } + + this._versionManager = new WinJS.UI._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 = WinJS.UI._UNINITIALIZED; + + this._itemsManager = thisWinUI._createItemsManager( + this._dataSource, + function (itemPromise, oldElement) { return that._itemsPool.renderItemAsync(itemPromise, oldElement); }, + notificationHandler, + { + ownerElement: this._element, + versionManager: this._versionManager, + resetItem: function (item, element) { + if (that.resetItem) { + return that.resetItem(item, element); + } + return null; + }, + indexInView: function(index) { + return (index >= that.indexOfFirstVisible && index <= that.indexOfLastVisible); + } + }); + + if (this._dataSource.addEventListener) { + this._dataSource.addEventListener("statuschanged", statusChanged, false); + } + + this._selection._selected.set(ranges); + }, + + _createUpdater: function ListView_createUpdater() { + if (!this._updater) { + this._versionManager.beginUpdating(); + + // Inform scroll view that a realization pass is coming so that it doesn't restart the + // realization pass itself. + this._cancelRealize(); + + var updater = { + changed: false, + elements: {}, + placeholders: {}, + selectionFirst: {}, + selectionLast: {}, + selectionHandles: {}, + oldSelectionPivot: WinJS.UI._INVALID_INDEX, + newSelectionPivot: WinJS.UI._INVALID_INDEX, + removedElements: [], + selectionChanged: false, + oldFocus: WinJS.UI._INVALID_INDEX, + newFocus: WinJS.UI._INVALID_INDEX, + hadKeyboardFocus: this._hasKeyboardFocus, + itemsMoved: false, + lastVisible: this.indexOfLastVisible + }; + + this._view.items.each(function (index, item, itemData) { + /*#DBG + if (itemData.itemsManagerRecord.released) { + throw "ACK! found released data in items collection"; + } + itemData.itemsManagerRecord.updater = updater; + #DBG*/ + updater.elements[item.uniqueID] = { + item: item, + wrapper: itemData.wrapper, + index: index, + newIndex: index, + display: itemData.display, + itemsManagerRecord: itemData.itemsManagerRecord + }; + }); + + var placeholders = this._view.items.placeholders, + keys = Object.keys(placeholders); + for (var i = 0, len = keys.length; i < len; i++) { + var index = parseInt(keys[i], 10), + item = placeholders[index]; + updater.placeholders[item.uniqueID] = { + element: item, + index: index, + newIndex: index + }; + } + + var selection = this._selection._selected._ranges; + for (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 = updater.oldFocus; + + this._updater = updater; + } + }, + + _correctScrollbarPos: function () { + var current = this.scrollPosition; + var viewportLength = this._getViewportLength(); + var canvasMargins = this._getCanvasMargins(); + var maxScrollbarPos = this._canvasLength - viewportLength + + (this._layout.horizontal ? (canvasMargins.right + canvasMargins.left) : (canvasMargins.top + canvasMargins.bottom)); + + if (current > maxScrollbarPos) { + return Math.max(0, maxScrollbarPos); + } else { + return current; + } + }, + + _synchronize: function ListView_synchronize() { + var updater = this._updater; + this._updater = null; + var groupsChanged = this._groupsChanged; + this._groupsChanged = false; + + /*#DBG + if (updater) { + for (i in updater.elements) { + if (updater.elements.hasOwnProperty(i)) { + var elementInfo = updater.elements[i]; + delete elementInfo.itemsManagerRecord.updater; + } + } + } + #DBG*/ + + if (updater && updater.changed) { + this._resizeViewport(); + if (updater.itemsMoved) { + this._layout.itemsMoved(); + } + if (updater.removedElements.length) { + this._layout.itemsRemoved(updater.removedElements); + } + this._view.items.setLayoutIndices({}); + + 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 WinJS.UI._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 : WinJS.UI._INVALID_INDEX); + updater.newFocus = Math.max(0, Math.min(this._cachedCount - 1, updater.newFocus)); + this._selection._setFocused(updater.newFocus, this._selection._keyboardFocused()); + + var newItems = {}; + for (i in updater.elements) { + if (updater.elements.hasOwnProperty(i)) { + var elementInfo = updater.elements[i]; + /*#DBG + if (elementInfo.itemsManagerRecord.released) { + throw "ACK! attempt to put released record into list of items for ScrollView"; + } + #DBG*/ + newItems[elementInfo.newIndex] = { + element: elementInfo.item, + display: elementInfo.display, + wrapper: elementInfo.wrapper, + itemsManagerRecord: elementInfo.itemsManagerRecord + }; + } + } + this._view.items._itemData = newItems; + + var newPlaceholders = {}; + for (i in updater.placeholders) { + if (updater.placeholders.hasOwnProperty(i)) { + var placeholder = updater.placeholders[i]; + newPlaceholders[placeholder.newIndex] = placeholder.element; + } + } + this._view.items.placeholders = newPlaceholders; + + if (updater.focusedItemRemoved) { + 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 () { + // Once the groups have been synchronized, we call layout.getItemPosition on the last visible item so that multisize layout + // computes a new occupancy map before calling updateScrollbar. + var lastVisible = Math.min(that._cachedCount - 1, updater.lastVisible), + layoutPromise = (lastVisible > 0 ? that._layout.getItemPosition(lastVisible) : Promise.wrap()); + return layoutPromise.then(function () { + return (updater.updateScrollbar ? that._view.updateScrollbar(that._cachedCount, true) : Promise.wrap()); + }); + }).then(function () { + var newScrollbarPos = that._correctScrollbarPos(); + newScrollbarPos = Math.max(0, newScrollbarPos); + if (that._lastScrollPosition !== newScrollbarPos) { + + that._lastScrollPosition = newScrollbarPos; + that.scrollPosition = newScrollbarPos; + } + that._versionManager.endUpdating(); + return newScrollbarPos; + }); + } else if (groupsChanged) { + var that = this; + return this._groups.synchronizeGroups().then(function () { + updater && that._versionManager.endUpdating(); + return Promise.wrap(that.scrollPosition); + }); + } else { + updater && this._versionManager.endUpdating(); + + return Promise.wrap(this.scrollPosition); + } + }, + + _update: function ListView_update() { + if (this._ifZombieDispose()) { return; } + + var that = this; + if (this._versionManager.noOutstandingNotifications) { + if (this._forceReload) { + this._forceReload = false; + this._cancelRealize(); + this._synchronize().then(function (scrollbarPos) { + that._view.reload(scrollbarPos) + }); + } else if (this._updater || this._groupsChanged) { + this._cancelRealize(); + this._synchronize().then(function (scrollbarPos) { + that._view.refresh( + scrollbarPos, + that._viewport[that._scrollLength], + that._getViewportSize(), + that._cachedCount + ); + }); + } + } + }, + + _scheduleUpdate: function ListView_scheduleUpdate() { + if (!this._updateTimer) { + this._updateTimer = true; + + this._raiseViewLoading(); + + var that = this; + setImmediate(function () { + that._updateTimer = false; + that._update(); + }); + } + }, + + _registerNotificationHandler: function (handler) { + this._notificationHandlers.push(handler); + }, + + _unregisterNotificationHandler: function (handler) { + for (var i = 0, len = this._notificationHandlers.length; i < len; i++) { + if (this._notificationHandlers[i] === handler) { + this._notificationHandlers.splice(i, 1); + break; + } + } + }, + + _setLayoutSite: function () { + var that = this, + layoutSite = Object.create({ + invalidateLayout: function () { + that._refresh(that.scrollPosition); + }, + _isZombie: function () { + return that._isZombie(); + } + }, { + _itemsManager: { + enumerable: true, + get: function () { + return that._itemsManager; + } + }, + rtl: { + enumerable: true, + get: function () { + return that._rtl(); + } + }, + surface: { + enumerable: true, + get: function () { + return that._canvas; + } + }, + itemSurface: { + enumerable: true, + get: function () { + return that._itemCanvas; + } + }, + viewport: { + enumerable: true, + get: function () { + return that._viewport; + } + }, + _groupOf: { + enumerable: true, + get: function () { + return that._groups.groupOf.bind(that._groups); + } + }, + _groupHeaderTemplate: { + enumerable: true, + get: function () { + return that.groupHeaderTemplate; + } + }, + _groups: { + enumerable: true, + get: function () { + return that._groups; + } + }, + scrollbarPos: { + enumerable: true, + get: function () { + return that.scrollPosition; + } + }, + _surfaceLength: { + enumerable: true, + get: function () { + return that._getCanvasLength(); + } + }, + _surfaceScrollLimitMin: { + enumerable: true, + get: function () { + return that._getScrollLimitMin(); + } + }, + _surfaceMargins: { + enumerable: true, + get: function () { + return that._getCanvasMargins(); + } + }, + viewportSize: { + enumerable: true, + get: function () { + return that._getViewportSize(); + } + }, + loadingBehavior: { + enumerable: true, + get: function () { + return that.loadingBehavior; + } + }, + animationsDisabled: { + enumerable: true, + get: function () { + return that._animationsDisabled(); + } + } + }); + + this._layout.setSite(layoutSite); + }, + + _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._cancelRealize(); + this._layout.reset(); + hadPreviousLayout = true; + } + + if (layoutObject && typeof layoutObject.type === "function") { + var LayoutCtor = requireSupportedForProcessing(layoutObject.type); + this._layout = new LayoutCtor(layoutObject); + } else if (layoutObject && layoutObject.setSite) { + this._layout = layoutObject; + } else { + this._layout = new WinJS.UI.GridLayout(layoutObject); + } + + this._unsetFocusOnItem(); + this._setFocusOnItem(0); + this._selection._setFocused(0); + this._setLayoutSite(); + + if (this._layout.horizontal) { + this._startProperty = "left"; + this._scrollProperty = "scrollLeft"; + this._scrollLength = "scrollWidth"; + utilities.addClass(this._viewport, WinJS.UI._horizontalClass); + utilities.removeClass(this._viewport, WinJS.UI._verticalClass); + if (hadPreviousLayout) { + this._viewport.scrollTop = 0; + } + } else { + this._startProperty = "top"; + this._scrollProperty = "scrollTop"; + this._scrollLength = "scrollHeight"; + utilities.addClass(this._viewport, WinJS.UI._verticalClass); + utilities.removeClass(this._viewport, WinJS.UI._horizontalClass); + if (hadPreviousLayout) { + this._viewport.scrollLeft = 0; + } + } + }, + + _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. + if (this._currentMode() instanceof WinJS.UI._SelectionMode && this._selectionAllowed() && this._swipeBehavior === WinJS.UI.SwipeBehavior.select) { + utilities.addClass(this._element, WinJS.UI._swipeableClass); + } else { + utilities.removeClass(this._element, WinJS.UI._swipeableClass); + } + }, + + _resizeViewport: function ListView_resizeViewport() { + this._viewportWidth = WinJS.UI._UNINITIALIZED; + this._viewportHeight = WinJS.UI._UNINITIALIZED; + }, + + _onResize: function ListView_onResize() { + setImmediate((function() { + if (this._isZombie()) { return; } + // If these values are uninitialized there is already a realization pass pending. + if (this._viewportWidth !== WinJS.UI._UNINITIALIZED && this._viewportHeight !== WinJS.UI._UNINITIALIZED) { + if ((this._previousWidth !== this._element.offsetWidth) || + (this._previousHeight !== this._element.offsetHeight)) { + this._previousWidth = this._element.offsetWidth; + this._previousHeight = this._element.offsetHeight; + + this._resizeViewport(); + + this._groups.pinItem(); + this._groups.purgeDecorators(); + this._raiseViewLoading(); + this._view.onResize(this.scrollPosition, this._getViewportSize()); + } + } + }).bind(this)); + }, + + _onFocus: function ListView_onFocus(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.srcElement === this._keyboardEventsHelper) { + if (!this._keyboardEventsHelper._shouldHaveFocus && this._keyboardFocusInbound) { + moveFocusToItem(true); + } else { + this._keyboardEventsHelper._shouldHaveFocus = false; + } + } else if (event.srcElement === 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 { + this._tabEventsHelper.childFocus = null; + // 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, + itemRootElement = items.wrapperFrom(event.srcElement); + + // itemRootElement will be null when the element that is getting focus isn't part of a list item (e.g. this._canvas, this._viewport) + if (this._keyboardFocusInbound && itemRootElement && event.srcElement.parentNode === itemRootElement) { + this._drawFocusRectangle(itemRootElement); + } + + if (itemRootElement && this._tabManager.childFocus !== itemRootElement) { + var index = items.index(itemRootElement); + + //#DBG _ASSERT(index !== WinJS.UI._INVALID_INDEX); + this._selection._setFocused(index, this._keyboardFocusInbound); + this._keyboardFocusInbound = false; + var item = items.itemAt(index); + this._tabManager.childFocus = item; + + if (that._updater) { + var elementInfo = that._updater.elements[item.uniqueID], + focusIndex = index; + if (elementInfo && elementInfo.newIndex) { + focusIndex = elementInfo.newIndex; + } + + that._updater.oldFocus = focusIndex; + that._updater.newFocus = focusIndex; + } + } + } + }, + + _onBlur: function ListView_onBlur(event) { + this._hasKeyboardFocus = false; + this._itemFocused = false; + var itemRootElement = this._view.items.wrapperFrom(event.srcElement); + if (itemRootElement) { + this._clearFocusRectangle(itemRootElement); + } + if (this._focusRequest) { + // If we're losing focus and we had an outstanding focus request, that means the focused item isn't realized. To enable the user to tab back + // into the listview, we'll make the keyboardEventsHelper tabbable for this scenario. + this._tabEventsHelper.childFocus = this._keyboardEventsHelper; + } + }, + + _onMSManipulationStateChanged: function ListView_onMSManipulationStateChanged(ev) { + this._manipulationState = ev.currentState; + }, + + _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 = setTimeout(this._checkScroller.bind(this), 16); + + var direction = (currentScrollPosition < this._lastScrollPosition) ? "left" : "right"; + currentScrollPosition = Math.max(0, currentScrollPosition); + this._lastScrollPosition = currentScrollPosition; + this._raiseViewLoading(true); + this._view.onScroll( + this._lastScrollPosition, + this._viewport[this._scrollLength], + direction, + this._getViewportSize()); + } else { + this._pendingScroll = null; + } + }, + + _onTabEnter: function ListView_onTabEnter() { + this._keyboardFocusInbound = true; + }, + + _onTabExit: function ListView_onTabExit() { + this._keyboardFocusInbound = false; + }, + + _onPropertyChange: function ListView_onPropertyChange() { + if ((event.propertyName === "dir") || (event.propertyName === "style.direction")) { + this._cachedRTL = null; + utilities[this._rtl() ? "addClass" : "removeClass"](this._element, WinJS.UI._rtlListViewClass); + + this._lastScrollPosition = 0; + this.scrollPosition = 0; + + this.forceLayout(); + } + + if (event.propertyName === "tabIndex") { + var newTabIndex = this._element.tabIndex; + if (newTabIndex >= 0) { + this._view.items.each(function (index, item, itemData) { + item.tabIndex = newTabIndex; + }); + this._tabIndex = newTabIndex; + this._tabManager.tabIndex = newTabIndex; + this._tabEventsHelper.tabIndex = newTabIndex; + this._element.tabIndex = -1; + } + } + }, + + _getCanvasMargins: function ListView_getCanvasMargins() { + if (!this._canvasMargins) { + this._canvasMargins = WinJS.UI._getMargins(this._canvas); + } + return this._canvasMargins; + }, + + _convertFromCanvasCoordinates: function ListView_convertFromCanvasCoordinates(coordinates) { + function fix(field, offset) { + if (coordinates[field] !== undefined) { + 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; + }, + + // Methods in the site interface used by ScrollView + _getViewportSize: function ListView_getViewportSize() { + if (this._viewportWidth === WinJS.UI._UNINITIALIZED || this._viewportHeight === WinJS.UI._UNINITIALIZED) { + this._viewportWidth = Math.max(0, utilities.getContentWidth(this._element)); + this._viewportHeight = Math.max(0, utilities.getContentHeight(this._element)); + + this._previousWidth = this._element.offsetWidth; + this._previousHeight = this._element.offsetHeight; + } + return { + width: this._viewportWidth, + height: this._viewportHeight + }; + }, + + _itemsCount: function ListView_itemsCount() { + if (this._cachedCount !== WinJS.UI._UNINITIALIZED) { + return Promise.wrap(this._cachedCount); + } else { + var that = this; + this._itemsCountPromise = this._itemsManager.dataSource.getCount().then( + function (count) { + if (count === thisWinUI.CountResult.unknown) { + count = 0; + } + that._cachedCount = count; + that._selection._updateCount(that._cachedCount); + return count; + }, + function () { + return WinJS.Promise.cancel; + } + ); + return this._itemsCountPromise; + } + }, + + _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, null); + }, + + _raiseViewPortLoaded: function ListView_raiseViewPortLoaded() { + if (!this._scheduledForDispose) { + scheduleForDispose(this); + this._scheduledForDispose = true; + } + this._setViewState(this._LoadingState.viewPortLoaded, null); + }, + + _raiseViewLoaded: function ListView_raiseViewLoaded() { + var detail = { + scrolling: this._scrolling + }; + this._setViewState(this._LoadingState.itemsLoaded, detail); + }, + + _raiseViewComplete: function ListView_raiseViewComplete() { + if (!this._view.animating) { + /*#DBG + this._checkForOverlap(); + #DBG*/ + this._setViewState(this._LoadingState.complete, null); + } + }, + + _setViewState: function ListView_setViewState(state, detail) { + if (state !== this._loadingState) { + // 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: + this._setViewState(this._LoadingState.itemsLoading); + break; + + case this._LoadingState.itemsLoaded: + this._setViewState(this._LoadingState.viewPortLoaded); + break; + + case this._LoadingState.complete: + this._setViewState(this._LoadingState.itemsLoaded); + break; + } + + if (this.loadingBehavior === "incremental" && + (state === this._LoadingState.itemsLoaded || state == this._LoadingState.complete)) { + detail = detail || {}; + detail.numItemsLoaded = this._view.lastItem + 1; + } + + msWriteProfilerMark("WinJS.UI.ListView:loadingStateChanged:" + state + ",info"); + this._loadingState = state; + var eventObject = document.createEvent("CustomEvent"); + eventObject.initCustomEvent("loadingstatechanged", true, false, detail); + this._element.dispatchEvent(eventObject); + } + }, + /*#DBG + _checkForOverlap: function ListView_checkForOverlap(){ + if (WinJS.UI.ListView._internalValidation){ + // This checks against overlapping tiles in list view. It is slow (N^2) and is only used when we want + // extra validation. + var finishedCheckingHeaders = false; + var existing = []; + var elementToTest = this._canvas.firstChild; + while (elementToTest) { + var computedStyle = window.getComputedStyle(elementToTest); + if (WinJS.Utilities.hasClass(elementToTest, WinJS.UI._wrapperClass) || WinJS.Utilities.hasClass(elementToTest, WinJS.UI._headerClass)) { + if (computedStyle.position === "absolute" && computedStyle.opacity === "1") { + var clientRect = elementToTest.getBoundingClientRect(); + var left = clientRect.left; + var top = clientRect.top; + var right = clientRect.right; + var bottom = clientRect.bottom + + for (var i = 0; i < existing.length; i++) { + var previous = existing[i]; + var prevLeft = previous.left; + var prevTop = previous.top; + var prevRight = previous.right; + var prevBottom = previous.bottom; + + var overlapX = (left < prevRight && right > prevLeft); + var overlapY = (top < prevBottom && bottom > prevTop); + + _ASSERT(!(overlapX && overlapY)); + } + + existing.push({ + element: elementToTest, + left: left, + top: top, + right: right, + bottom: bottom + }); + } + } + elementToTest = elementToTest.nextSibling; + if (!elementToTest && !finishedCheckingHeaders) { + elementToTest = this._itemCanvas.firstChild; + finishedCheckingHeaders = true; + } + } + } + }, + #DBG*/ + _createTemplates: function ListView_createTemplates() { + + function createNodeWithClass(className, skipAriaHidden) { + var element = document.createElement("div"); + element.className = className; + if (!skipAriaHidden) { + element.setAttribute("aria-hidden", true); + } + return element; + } + + var selectionBorder = createNodeWithClass(WinJS.UI._selectionBorderContainerClass); + selectionBorder.appendChild(createNodeWithClass(WinJS.UI._selectionBorderClass + " " + WinJS.UI._selectionBorderTopClass)); + selectionBorder.appendChild(createNodeWithClass(WinJS.UI._selectionBorderClass + " " + WinJS.UI._selectionBorderRightClass)); + selectionBorder.appendChild(createNodeWithClass(WinJS.UI._selectionBorderClass + " " + WinJS.UI._selectionBorderBottomClass)); + selectionBorder.appendChild(createNodeWithClass(WinJS.UI._selectionBorderClass + " " + WinJS.UI._selectionBorderLeftClass)); + + this._selectionTemplate = []; + this._selectionTemplate.push(createNodeWithClass(WinJS.UI._selectionBackgroundClass)); + this._selectionTemplate.push(selectionBorder); + this._selectionTemplate.push(createNodeWithClass(WinJS.UI._selectionCheckmarkBackgroundClass)); + var checkmark = createNodeWithClass(WinJS.UI._selectionCheckmarkClass); + checkmark.innerText = WinJS.UI._SELECTION_CHECKMARK; + this._selectionTemplate.push(checkmark); + + this._wrapperTemplate = createNodeWithClass(WinJS.UI._wrapperClass, true); + }, + + // Methods used by SelectionManager + _renderSelection: function ListView_renderSelection(wrapper, element, selected, aria) { + // Update the selection rendering if necessary + if (selected !== WinJS.UI._isSelectionRenderer(wrapper)) { + if (selected) { + wrapper.insertBefore(this._selectionTemplate[0].cloneNode(true), wrapper.firstElementChild); + + for (var i = 1, len = this._selectionTemplate.length; i < len; i++) { + wrapper.appendChild(this._selectionTemplate[i].cloneNode(true)); + } + } else { + var nodes = wrapper.querySelectorAll(WinJS.UI._selectionPartsSelector); + for (var i = 0, len = nodes.length; i < len; i++) { + wrapper.removeChild(nodes[i]); + } + } + + utilities[selected ? "addClass" : "removeClass"](wrapper, WinJS.UI._selectedClass); + } + + // To allow itemPropertyChange to work properly, aria needs to be updated after the selection visuals are added to the wrapper + if (aria) { + this._setAriaSelected(element, selected); + } + }, + + _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; + } + } + + var that = this; + this._view.items.each(function (index, element, itemData) { + if (!utilities.hasClass(itemData.wrapper, thisWinUI._swipeClass)) { + var selected = selectAll || !!selectionMap[index]; + that._renderSelection(itemData.wrapper, element, selected, true); + } + }); + }, + + _getViewportLength: function ListView_getViewportLength() { + return this._getViewportSize()[this._horizontal() ? "width" : "height"]; + }, + + _getCanvasLength: function () { + return this._canvasLength; + }, + + _getScrollLimitMin: function() { + return this._scrollLimitMin; + }, + + _setCanvasLength: function (begin, end) { + var length = end; + begin = Math.max(0, begin); + + // Cache the canvas length before the right margin and scrollLimit extensions because getCanvasLength() + // is used to figure out how far items go. + this._canvasLength = length; + + // Margin right and bottom are not supportedChildren inside an overflow scroll parent. To make them work + // we just add extra length to win-surface. + var unsupportedMargin = this._horizontal() ? (this._rtl() ? "left" : "right") : "bottom"; + length += this._getCanvasMargins()[unsupportedMargin]; + + if (begin > 0) { + // The msScrollLimitXMin and msScrollLimitYMin styles only works if the + // content length (including margins) - scroll limit min >= viewport length. + var margin = this._horizontal() ? (this._rtl() ? "right" : "left") : "top"; + length = Math.max(length, (begin + this._getViewportLength() - this._getCanvasMargins()[margin])); + } + + this._canvas.style[this._horizontal() ? "width" : "height"] = length + "px"; + this._canvas.style[!this._horizontal() ? "width" : "height"] = ""; + + this._scrollLimitMin = begin; + this._viewport.style[this._horizontal() ? "msScrollLimitXMin" : "msScrollLimitYMin"] = begin + "px"; + }, + + _horizontal: function ListView_horizontal() { + return this._scrollProperty === "scrollLeft"; + }, + + _rtl: function ListView_rtl() { + if (typeof this._cachedRTL !== "boolean") { + this._cachedRTL = window.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(WinJS.UI._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, pagesToPrefetch) { + if (WinJS.validation) { + if (!this._view.realizePage || typeof this._view.begin !== "number") { + throw new WinJS.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._horizontal()) { + x += ( + this._rtl() ? + this._canvas.offsetWidth - this._viewportWidth - this.scrollPosition : + this.scrollPosition + ); + } else { + y += this.scrollPosition; + } + + var index = this._layout.hitTest(x, y); + + if (index >= 0) { + if (this._hasKeyboardFocus) { + this._changeFocus(index, true, false, true); + } else { + this._changeFocusPassively(index); + } + } + }, + + _getCurrentItem: function () { + var indexFocused = this._selection._getFocused(); + + if (typeof indexFocused !== "number") { + // Do a hit-test in the viewport center + this._setCurrentItem(0.5 * this._viewportWidth, 0.5 * this._viewportHeight); + + indexFocused = this._selection._getFocused(); + } + + var that = this; + var promisePosition = this._getItemOffsetPosition(indexFocused). + then(function (posCanvas) { + var scrollOffset = parseInt(that._canvasStart, 10); + + posCanvas[that._startProperty] += scrollOffset; + + return posCanvas; + }); + + return Promise.join({ + item: this._dataSource.itemFromIndex(indexFocused), + position: promisePosition + }); + }, + + _beginZoom: function () { + this._zooming = true; + + // Hide the scrollbar and extend the content beyond the ListView viewport + + var horizontal = this._horizontal(), + scrollOffset = -this.scrollPosition; + + utilities.addClass(this._viewport, horizontal ? WinJS.UI._zoomingXClass : WinJS.UI._zoomingYClass); + this._canvasStart = scrollOffset + "px"; + utilities.addClass(this._viewport, horizontal ? WinJS.UI._zoomingYClass : WinJS.UI._zoomingXClass); + }, + + _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._canvas[horizontal ? "offsetWidth" : "offsetHeight"], + viewportSize = (horizontal ? that._viewportWidth : that._viewportHeight), + scrollPosition; + + // Align the leading edge + var start = 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; + + // Since a zoom is in progress, adjust the div position + var scrollOffset = -scrollPosition; + that._canvasStart = scrollOffset + "px"; + + if (that._hasKeyboardFocus) { + that._changeFocus(index, true); + } else { + that._changeFocusPassively(index); + } + + that._raiseViewLoading(true); + that._view.realizePage(scrollPosition, true); + + 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 (WinJS.validation) { + if (description === undefined) { + throw new WinJS.ErrorFromName("WinJS.UI.ListView.InvalidItem", strings.listViewInvalidItem); + } + } + + itemPromise = this._dataSource.itemFromDescription(description); + } + + return itemPromise. + then(function (item) { + return Promise.timeout(). + then(function () { + return positionItemAtIndex(item.index); + }); + }); + } + }, + + _endZoom: function (isCurrentView) { + // Crop the content again and re-enable the scrollbar + + var horizontal = this._horizontal(), + scrollOffset = parseInt(this._canvasStart, 10); + + utilities.removeClass(this._viewport, WinJS.UI._zoomingYClass); + utilities.removeClass(this._viewport, WinJS.UI._zoomingXClass); + + this._canvasStart = "0px"; + + this.scrollPosition = -scrollOffset; + + this._disableEntranceAnimation = !isCurrentView; + + this._isCurrentZoomView = isCurrentView; + this._zooming = false; + + var that = this; + setImmediate(function () { + if (that._view.realizePage) { + that._view.realizePage(that.scrollPosition, false); + } + }); + }, + + _getItemOffsetPosition: function (index) { + var items = this._view.items, + elementAtIndex = items.itemAt(index); + + function elementOffsetPosition(element, index) { + var elementRoot = items.wrapperAt(index), + left = elementRoot.offsetLeft + element.offsetLeft, + width = element.offsetWidth; + + if (that._rtl()) { + left = that._canvasLength - left - width; //offsetRight equivalent + } + + return { + left: left, + top: elementRoot.offsetTop + element.offsetTop, + width: width, + height: element.offsetHeight + }; + } + + var that = this; + return new Promise(function (complete) { + // See if an element already exists for the item, in which case it has been positioned + if (elementAtIndex) { + complete(elementOffsetPosition(elementAtIndex, index)); + } else { + that._itemsCount().then(function (count) { + that._layout.startLayout(0, that._getViewportLength() - 1, count).then(function () { + var promisePosition = that._getItemOffset(index).then( + function () { + return that._layout.getItemPosition(index); + }, + function (error) { + if (error.name !== "WinJS.UI.LayoutNotInitialized") { + return WinJS.Promise.wrapError(error); + } else { + return WinJS.Promise.cancel; + } + }), + indexRealized = that._view.begin, + elementRealized = items.itemAt(indexRealized), + promiseResult; + + // Otherwise, see if there's at least one item that has been realized + if (elementRealized) { + // There is, so use the heuristic that its offsets relative to its computed position are likely to + // be the same as those of the item at the given index. + var offsetPosRealized = elementOffsetPosition(elementRealized, indexRealized); + + promiseResult = Promise.join([promisePosition, that._layout.getItemPosition(indexRealized)]).then(function (results) { + var pos = that._convertFromCanvasCoordinates(results[0]), + posRealized = results[1]; + + return { + left: pos.left + (offsetPosRealized.left - posRealized.left), + top: pos.top + (offsetPosRealized.top - posRealized.top), + width: offsetPosRealized.width, + height: offsetPosRealized.height + }; + }); + } else { + // There's no way to determine the offsets, just return the calculated position + promiseResult = promisePosition.then(function (pos) { + return { + left: pos.left, + top: pos.top, + width: pos.totalWidth, + height: pos.totalHeight + }; + }); + } + + promiseResult.then(function (result) { + that._layout.endLayout(); + complete(result); + }); + }); + }); + } + }); + }, + + _changeFocus: function (newFocus, skipSelection, ctrlKeyDown, skipEnsureVisible, keyboardFocused) { + //#DBG _ASSERT(newFocus !== -1); + if (this._isZombie()) { + return; + } + var targetItem = this._view.items.itemAt(newFocus); + 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); + } + 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) { + //#DBG _ASSERT(newFocus !== -1); + + var targetItem = this._view.items.itemAt(newFocus); + this._unsetFocusOnItem(!!targetItem); + this._selection._setFocused(newFocus); + this._setFocusOnItem(newFocus); + }, + + _drawFocusRectangle: function (item) { + var wrapper = utilities.hasClass(item, WinJS.UI._wrapperClass) ? item : item.parentNode; + //#DBG _ASSERT(utilities.hasClass(wrapper, WinJS.UI._wrapperClass)); + if (wrapper.querySelector("." + thisWinUI._itemFocusOutlineClass)) { + return; + } + utilities.addClass(wrapper, thisWinUI._itemFocusClass); + var outline = document.createElement("div"); + outline.className = thisWinUI._itemFocusOutlineClass; + wrapper.appendChild(outline); + }, + + _clearFocusRectangle: function (item) { + if (!item || this._isZombie()) { + return; + } + + if (!utilities.hasClass(item, WinJS.UI._wrapperClass) && !item.parentNode) { + return; + } + + var wrapper = utilities.hasClass(item, WinJS.UI._wrapperClass) ? item : item.parentNode; + //#DBG _ASSERT(utilities.hasClass(wrapper, WinJS.UI._wrapperClass)); + utilities.removeClass(wrapper, thisWinUI._itemFocusClass); + var outline = wrapper.querySelector("." + thisWinUI._itemFocusOutlineClass); + if (outline) { + outline.parentNode.removeChild(outline); + } + }, + + _defaultInvoke: function (itemIndex) { + if (this._isZoomedOut) { + this._changeFocusPassively(itemIndex); + this._triggerZoom(); + } + }, + + _selectionAllowed: function ListView_selectionAllowed() { + return this._selectionMode !== WinJS.UI.SelectionMode.none; + }, + + _multiSelection: function ListView_multiSelection() { + return this._selectionMode === WinJS.UI.SelectionMode.multi; + }, + + _selectOnTap: function ListView_selectOnTap() { + return this._tap === WinJS.UI.TapBehavior.toggleSelect || this._tap === WinJS.UI.TapBehavior.directSelect; + }, + + _selectFocused: function ListView_selectFocused(ctrlKeyDown) { + return this._tap === WinJS.UI.TapBehavior.directSelect && this._selectionMode === WinJS.UI.SelectionMode.multi && !ctrlKeyDown; + }, + + _dispose: function () { + if (!this._disposed) { + this._disposed = true; + var clear = function clear(e) { + e && (e.innerText = ""); + } + + 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._groupsPool && this._groupsPool.clear(); + this._wrappersPool && this._wrappersPool.clear(); + this._itemsPool && this._itemsPool.clear(); + this._headersPool && this._headersPool.clear(); + + this._itemsCountPromise && this._itemsCountPromise.cancel(); + this._versionManager && this._versionManager._dispose(); + this._clearInsertedItems(); + this._itemsManager && this._itemsManager.release(); + + clear(this._viewport); + clear(this._itemCanvas); + clear(this._canvas); + clear(this._canvasProxy); + + this._versionManager = null; + this._view = null; + this._wrappersPool = null; + this._itemsPool = null; + this._headersPool = null; + this._groupsPool = null; + this._mode = null; + this._element = null; + this._viewport = null; + this._itemsManager = null; + this._canvas = null; + this._canvasProxy = null; + this._itemCanvas = null; + this._itemsCountPromise = null; + } + }, + + _isZombie: function () { + // determines if this ListView is no longer in the DOM or has been cleared + // + return this._disposed || !(this.element.firstElementChild && 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 !WinJS.UI.isAnimationEnabled(); + }, + + _fadeOutViewport: function ListView_fadeOutViewport(complete) { + if (this._animationsDisabled()) { + complete(); + return; + } + + if (!this._fadingViewportOut) { + if (this._waitingEntranceAnimationPromise) { + this._waitingEntranceAnimationPromise.cancel(); + this._waitingEntranceAnimationPromise = null; + } + var eventDetails = this._fireAnimationEvent(WinJS.UI.ListViewAnimationType.contentTransition); + this._firedAnimationEvent = true; + if (!eventDetails.prevented) { + this._fadingViewportOut = true; + var that = this; + this._viewport.style["-ms-overflow-style"] = "none"; + AnimationHelper.fadeOutElement(this._viewport).then(function () { + if (that._isZombie()) { return; } + that._fadingViewportOut = false; + that._viewport.style.opacity = 1.0; + complete(); + }); + } else { + this._disableEntranceAnimation = true; + this._viewport.style.opacity = 1.0; + complete(); + } + } + }, + + _animateListEntrance: function (firstTime) { + var eventDetails = { + prevented: false, + animationPromise: Promise.wrap() + }; + var that = this; + function resetViewOpacity() { + that._canvas.style.opacity = 1; + that._viewport.style["-ms-overflow-style"] = ""; + } + + if (this._disableEntranceAnimation || this._animationsDisabled()) { + resetViewOpacity(); + if (this._waitingEntranceAnimationPromise) { + this._waitingEntranceAnimationPromise.cancel(); + this._waitingEntranceAnimationPromise = null; + } + return Promise.wrap(); + } + + if (!this._firedAnimationEvent) { + eventDetails = this._fireAnimationEvent(WinJS.UI.ListViewAnimationType.entrance); + } else { + this._firedAnimationEvent = false; + } + + if (eventDetails.prevented) { + resetViewOpacity(); + return Promise.wrap(); + } else { + if (this._waitingEntranceAnimationPromise) { + this._waitingEntranceAnimationPromise.cancel(); + } + this._canvas.style.opacity = 0; + this._viewport.style["-ms-overflow-style"] = "none"; + this._waitingEntranceAnimationPromise = eventDetails.animationPromise.then(function () { + if (!that._isZombie()) { + that._canvas.style.opacity = 1; + return AnimationHelper.animateEntrance(that._viewport, firstTime).then(function () { + if (!that._isZombie()) { + that._viewport.style["-ms-overflow-style"] = ""; + that._waitingEntranceAnimationPromise = null; + } + }); + } + }); + return this._waitingEntranceAnimationPromise; + } + }, + + _fireAnimationEvent: function (type) { + var animationEvent = document.createEvent("CustomEvent"), + animationPromise = Promise.wrap(); + + animationEvent.initCustomEvent("contentanimating", true, true, { + type: type + }); + if (type === WinJS.UI.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._ariaStartMarker) { + this._ariaStartMarker = document.createElement("div"); + this._ariaStartMarker.id = this._ariaStartMarker.uniqueID; + this._viewport.insertBefore(this._ariaStartMarker, this._canvas); + } + if (!this._ariaEndMarker) { + this._ariaEndMarker = document.createElement("div"); + this._ariaEndMarker.id = this._ariaEndMarker.uniqueID; + this._viewport.insertBefore(this._ariaEndMarker, this._canvas.nextSibling); + } + }, + + // 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. + _updateAriaRoles: function ListView_updateAriaRoles() { + 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, itemData) { + itemElement.setAttribute("role", that._itemRole); + }); + } + }, + + // 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); + } + }, + + _groupsEnabled: function () { + return this._groups.groupDataSource; + }, + + _getItemOffset: function ListView_getItemOffset(itemIndex) { + var that = this; + return this._layout.getItemPosition(itemIndex).then(function (pos) { + that._groups.pinItem(itemIndex, pos); + var margins = that._getItemMargins(); + if (that._layout.horizontal) { + var rtl = that._rtl(); + return { + begin: pos.left - margins[rtl ? "left" : "right"], + end: pos.left + pos.totalWidth + margins[rtl ? "right" : "left"] + }; + + } else { + return { + begin: pos.top - margins.bottom, + end: pos.top + pos.totalHeight + margins.top + }; + } + }); + }, + + _getItemMargins: function ListView_getItemMargins() { + if (!this._itemMargins) { + var item = this._itemCanvas.querySelector("." + WinJS.UI._wrapperClass), + cleanup; + + if (!item) { + item = document.createElement("div"), + utilities.addClass(item, WinJS.UI._wrapperClass); + this._viewport.appendChild(item); + + cleanup = true; + } + + this._itemMargins = WinJS.UI._getMargins(item); + + if (cleanup) { + this._viewport.removeChild(item); + } + } + return this._itemMargins; + }, + + _correctRangeInFirstColumn: function ListView_correctRangeInFirstColumn(range) { + var that = this; + return this._getItemOffset(0).then(function (firstRange) { + if (firstRange.begin === range.begin) { + if (that._horizontal()) { + range.begin = -that._getCanvasMargins()[that._rtl() ? "right" : "left"]; + } else { + range.begin = -that._getCanvasMargins().top; + } + } + return range; + }); + } + }, { + // 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. + /// + /// + WinJS.UI._disposeControls(); + }, + + _ScrollToPriority: { + uninitialized: 0, + low: 1, + medium: 2, + high: 3 + } + }), + + _isSelectionRenderer: function ListView_isSelectionRenderer(wrapper) { + // 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 wrapper.querySelectorAll(WinJS.UI._selectionPartsSelector).length > 0; + } +}); + +WinJS.Class.mix(thisWinUI.ListView, WinJS.Utilities.createEventProperties( + "iteminvoked", + "selectionchanging", + "selectionchanged", + "loadingstatechanged", + "keyboardnavigating", + "contentanimating")); +WinJS.Class.mix(thisWinUI.ListView, WinJS.UI.DOMEventMixin); + + +})(this, WinJS); + + +(function scrollViewInit(global, WinJS, undefined) { + "use strict"; + +var utilities = WinJS.Utilities, + Promise = WinJS.Promise; + +// Virtualized scroll view +WinJS.Namespace.define("WinJS.UI", { + _ScrollView: function (listView) { + this.listView = listView; + this.items = new WinJS.UI._ItemsContainer(listView); + this.pagesToPrefetch = WinJS.UI._ScrollView._pagesToPreload; + this.begin = 0; + this.end = 0; + this.realizePass = 1; + this.firstLayoutPass = true; + this.firstIndexDisplayed = -1; + this.lastIndexDisplayed = -1; + this.runningAnimations = null; + this.animating = false; + this.loadingItems = false; + this.renderCompletePromise = Promise.wrap(); + this._insertedElements = []; + + // These variables help determine if we can skip updateItem calls onScroll. + this.cleanStartIndex = -1; + this.cleanEndIndex = -1; + } +}); + +// OK to skip a frame, but don't skip two... +// +WinJS.UI._ScrollView._maxTimePerRealizeFrame = 40; +WinJS.UI._ScrollView._maxTimePerRealizeFrameWhenPanning = 50; +WinJS.UI._ScrollView._maxTimePerRealizeFrameInit = 250; +WinJS.UI._ScrollView._pagesToPreload = 2; +WinJS.UI._ScrollView._preloadEdges = true; +WinJS.UI._ScrollView._retryTimeForAriaSetupDuringZoom = 500; + +WinJS.UI._TimeBasedQueue = WinJS.Class.define( + function _TimeBasedQueue_ctor(options) { + this._work = []; + this._lowPriorityWork = []; + this._paused = 0; + options = options || {}; + this._duration = options.duration || WinJS.UI._ScrollView._maxTimePerRealizeFrame; + this.isBusy = options.isBusy; + }, + { + duration: { + get: function () { + return this._duration; + }, + set: function (val) { + if (val !== this._duration) { + this._duration = val; + if (this._running && !this._paused) { + this.pause(); + setImmediate(this.resume.bind(this)); + } + } + } + }, + cancel: function TimeBasedQueue_cancel() { + this._work = []; + this._lowPriorityWork = []; + }, + push: function TimeBasedQueue_push(workUnit, lowPriority) { + if (lowPriority) { + this._lowPriorityWork.push(workUnit); + } else { + this._work.push(workUnit); + } + if (this._started && !this._running) { + this.start(); + } + }, + start: function TimeBasedQueue_start(startDuration) { + if (!this._running && !this._paused) { + this._running = true; + this._started = true; + if (startDuration) { + // Do initial run synchronously to avoid any yielding to the browser in the case of small amounts + // of work. + this._run(startDuration); + } else { + setImmediate(this._run.bind(this)); + } + } + }, + pause: function TimeBasedQueue_pause() { + this._paused++; + }, + resume: function TimeBasedQueue_resume() { + //#DBG _ASSERT(this._paused); + this._paused--; + this.start(); + }, + _run: function TimeBasedQueue_run(duration) { + // If we are busy doing other things wait for a bit and try again + if (this.isBusy && this.isBusy() && (this._work.length > 0 || this._lowPriorityWork.length > 0)) { + setTimeout(this._run.bind(this), 16); + return; + } + + duration = duration || this.duration; + var start = new Date(); + do { + if (!this._paused && (this._work.length > 0 || this._lowPriorityWork.length > 0)) { + var workUnit = (this._work.length > 0 ? this._work.shift() : this._lowPriorityWork.shift()); + workUnit(); + } else { + this._running = false; + return; + } + } while (!(this.isBusy && this.isBusy()) && (new Date() - start) < duration); + + setImmediate(this._run.bind(this)); + } + }, { + supportedForProcessing: false, + } +); + +WinJS.UI._ScrollView.prototype = { + + _dispose: function ScrollView_dispose() { + this.cleanUp(); + this.items = null; + this.renderCompletePromise && this.renderCompletePromise.cancel(); + this.renderCompletePromise = null; + }, + + createItem: function ScrollView_createItem(itemIndex, available, unavailable) { + var that = this; + var itemPromise = that.listView._itemsManager._itemPromiseAtIndex(itemIndex); + var elementPromise = that.listView._groups.addItem(itemIndex, itemPromise). + then(function () { + return that.listView._itemsManager._itemFromItemPromise(itemPromise); + }).then( + function (element) { + if (element) { + return available(itemIndex, element, that.listView._itemsManager._recordFromElement(element)); + } else { + unavailable(itemIndex); + } + }, + function (err) { + unavailable(itemIndex); + return WinJS.Promise.wrapError(err); + } + ); + + return elementPromise; + }, + + addItem: function ScrollView_addItem(fragment, itemIndex, element, currentPass) { + /*#DBG + if (!WinJS.Utilities.data(element).itemsManagerRecord || WinJS.Utilities.data(element).removeElementMapRecord) { + throw "ACK! Attempt to add an item to the scrollview which hasn't yet been realized"; + } + #DBG*/ + + if (this.realizePass === currentPass) { + var layoutIndex = this.items.getLayoutIndex(itemIndex); + if (layoutIndex !== WinJS.UI._INVALID_INDEX) { + + var record = this.listView._itemsManager._recordFromElement(element); + var wrapper = element.parentNode; + + if (this.listView._isSelected(itemIndex)) { + this.listView._renderSelection(wrapper, element, true, true); + } + + this.items.setItemAt(itemIndex, { + wrapper: wrapper, + element: element, + detached: true, + itemsManagerRecord: record, + visible: true + }); + + this.listView._layout.layoutItem(layoutIndex, wrapper); + } + } + }, + + finalItem: function ScrollView_finalItem() { + return this.listView._itemsCount().then(function (count) { + return count - 1; + }); + }, + + hideItem: function ScrollView_hideItem(itemData, wrapper) { + if (itemData.display === undefined) { + itemData.display = wrapper.style.display; + wrapper.style.display = "none"; + } + }, + + updateItem: function ScrollView_updateItem(itemIndex, itemIsReadyCallback) { + var that = this, + itemPromise = that.listView._itemsManager._itemPromiseAtIndex(itemIndex); + + return that.listView._groups.addItem(itemIndex, itemPromise).then( + function () { + // Update can happen before an item promise completes when there are placeholders + itemPromise.cancel(); + + var layoutIndex = that.items.getLayoutIndex(itemIndex), + itemData = that.items.itemDataAt(itemIndex), + wrapper = itemData.wrapper; + + if (layoutIndex !== WinJS.UI._INVALID_INDEX) { + that.listView._layout.layoutItem(layoutIndex, wrapper); + if (itemData.display !== undefined) { + wrapper.style.display = itemData.display; + itemData.display = undefined; + } + } + else { + that.hideItem(itemData, wrapper); + } + + itemIsReadyCallback(itemIndex, itemData.element, itemData.itemsManagerRecord); + }, + function (e) { + itemPromise.cancel(); + return WinJS.Promise.wrapError(e); + } + ); + }, + + updateCleanIndexRange: function ScrollView_updateCleanIndexRange(itemIndex) { + // This function allows us to remember which elements we have called UpdateIndex on + // after a forceLayout so we do not have to do it again. + if (this.cleanStartIndex - 1 === itemIndex) { + this.cleanStartIndex = itemIndex; + } else if (this.cleanEndIndex + 1 === itemIndex) { + this.cleanEndIndex = itemIndex; + } else if (itemIndex < this.cleanStartIndex || itemIndex > this.cleanEndIndex) { + this.cleanStartIndex = this.cleanEndIndex = itemIndex; + } + }, + + realizeItems: function ScrollView_realizeItems(fragment, begin, end, count, currentPass, scrollbarPos, direction) { + var perfId = "WinJS.UI.ListView:realizeItems(" + begin + "-" + end + ")"; + msWriteProfilerMark(perfId + ",StartTM"); + direction = direction || "right"; + + var counter = end - begin; + var firstInView; + var lastInView; + var inViewCounter; + var renderCompletePromises = []; + var loadingSignal = new WinJS._Signal(); + var viewportItemsRealized = new WinJS._Signal(); + var allItemsRealized = new WinJS._Signal(); + + var that = this; + + var work; + var pinnedItemIndex = this.listView._groups.pinnedItem; + var waitForStart = (+pinnedItemIndex === pinnedItemIndex ? pinnedItemIndex : 0); + var waitForEnd = waitForStart; + var readyWaitForStart = waitForStart; + var readyWaitForEnd = waitForEnd; + + if (waitForStart < that.cleanStartIndex || waitForEnd > that.cleanEndIndex) { + this._resetLayoutState(); + } + + var newElements = {}; + var updatedElements = {}; + + this.loadingItems = true; + + var initComplete = Promise.join([ + this.listView.layout.calculateFirstVisible(scrollbarPos, false), + this.listView.layout.calculateLastVisible(scrollbarPos + this.listView._getViewportLength() - 1, false) + ]); + initComplete.then(function (indices) { + var showProgress = true; + for (var i = indices[0]; i <= indices[1]; i++) { + if (that.items.itemAt(i)) { + showProgress = false; + break; + } + } + + if (that.firstLayoutPass) { + that.listView._canvas.style.opacity = 0; + } else { + if (showProgress) { + that.listView._showProgressBar(that.listView._element, "50%", "50%"); + } else { + that.listView._hideProgressBar(); + } + } + }); + + function itemIsReady(itemIndex, element, itemsManagerRecord) { + renderCompletePromises.push(itemsManagerRecord.renderComplete); + + return delivered(itemIndex); + } + + function appendItemsToDom(startIndex, endIndex) { + if (that.listView._isZombie()) { return; } + + var firstIndexInDom = -1; + var lastIndexInDom = -1; + var itemIndex; + for (itemIndex = startIndex; itemIndex <= endIndex; itemIndex++) { + var itemData = that.items.itemDataAt(itemIndex); + if (itemData) { + var element = itemData.element; + var wrapper = element.parentNode; + if (wrapper.parentNode !== fragment) { + fragment.appendChild(wrapper); + } + + if (firstIndexInDom === -1) { + firstIndexInDom = lastIndexInDom = itemIndex; + } else { + firstIndexInDom = Math.min(firstIndexInDom, itemIndex); + lastIndexInDom = Math.max(lastIndexInDom, itemIndex); + } + + // 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); + } + } + + if (firstIndexInDom !== -1) { + // Update the headers in batches as well + that.updateHeaders(that.listView._canvas, firstIndexInDom, lastIndexInDom + 1, true); + + } + } + + function removeGaps(first, last, begin, end) { + // 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 element = itemData.element; + var wrapper = element.parentNode; + if (wrapper.parentNode !== fragment) { + return true; + } else if (foundMissing) { + wrapper.parentNode.removeChild(wrapper); + return true; + } else { + return false; + } + } else { + return true; + } + } + } + + function delivered(index) { + var retVal = null; + if (index >= firstInView && index <= lastInView) { + if (--inViewCounter === 0) { + if (that.realizePass === currentPass) { + appendItemsToDom(firstInView, lastInView); + removeGaps(firstInView, lastInView, begin, end); + that.updateBackdrop(count); + // Set the scroll bar to an initial guess + that.updateScrollbar(count); + + if (that.firstLayoutPass) { + work.duration = WinJS.UI._ScrollView._maxTimePerRealizeFrame; + + // Entrance animation does not work unless it is in a timeout + var entranceAnimation = Promise.timeout().then(function () { + if (that.listView._isZombie()) { return; } + + return that.listView._animateListEntrance(!that.firstEntranceAnimated); + }); + that.runningAnimations = Promise.join([that.runningAnimations, entranceAnimation]); + that.firstLayoutPass = false; + that.firstEntranceAnimated = true; + that.animating = true; + } + + viewportItemsRealized.complete(); + } + } + } + + counter--; + + if (that.realizePass === currentPass && inViewCounter <= 0 && (counter % (lastInView - firstInView + 1) === 0)) { + // We have another page worth of data. + appendItemsToDom(begin, end - 1); + that.updateBackdrop(count); + } + + 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); + } + }); + + if (that.realizePass === currentPass) { + //#DBG _ASSERT(Object.keys(newElements).length === 0); + //#DBG _ASSERT(Object.keys(updatedElements).length === 0); + + if (that._insertedElements.length) { + that.listView._layout.itemsAdded(that._insertedElements); + } + var endLayoutResult = that.listView._layout.endLayout(), + animationPromise; + that._insertedElements = []; + that.listView._clearInsertedItems(); + + if (endLayoutResult) { + if (endLayoutResult.newEndIndex) { + retVal = that.realizeItems(fragment, end, Math.min(count, endLayoutResult.newEndIndex), end, currentPass, scrollbarPos).allItemsRealized; + } + + animationPromise = endLayoutResult.animationPromise; + } + + if (animationPromise) { + that.animating = true; + } + that.renderCompletePromise.then(function () { + if (that.realizePass === currentPass) { + that.loadingItems = false; + } + }); + that.runningAnimations = Promise.join([that.runningAnimations, animationPromise]).then(function () { + return Promise.timeout(); + }); + // Delay the animation completed code until endLayout is finished. If endLayout triggers another animation, then + // realizePass will be updated and this promise will be invalid. + that.runningAnimations.then(function () { + if (that.realizePass === currentPass) { + that.animating = false; + that.runningAnimations = null; + } + }); + + Promise.join([that.renderCompletePromise, that.runningAnimations]).then(function () { + if (that.listView._isZombie()) { return WinJS.Promise.cancel; } + + if (that.realizePass === currentPass && !that.loadingItems && !that.animating) { + loadingSignal.complete(); + } + }); + + allItemsRealized.complete(); + } + } + return retVal; + } + + function newItemIsReady(itemIndex, element, itemsManagerRecord) { + var wrapper = element.parentNode; + + if (that.realizePass === currentPass) { + if (that.listView._isInsertedItem(itemsManagerRecord.itemPromise)) { + // Start element at opacity 0 so it can fade in. + wrapper.style.opacity = 0; + that._insertedElements.push(wrapper); + } + + //#DBG _ASSERT(!itemsManagerRecord.released); + newElements[itemIndex] = itemsManagerRecord; + processWaitingElements(itemIndex); + itemsManagerRecord.itemPromise.then(function () { + processReadyWaiting(); + }); + } + } + + function processReadyWaiting() { + function tryQueueReadyWork(itemIndex) { + var itemData = that.items.itemDataAt(itemIndex); + var record = itemData.itemsManagerRecord; + if (record.pendingReady) { + work.push(function () { + if (that.listView._isZombie()) { + return; + } + if (record.pendingReady) { + record.pendingReady(); + } + }, (itemIndex < that.firstIndexDisplayed || itemIndex > that.lastIndexDisplayed)); + return true; + } else if (record.readyComplete) { + return true; + } + return false; + } + + if (currentPass === that.realizePass) { + while (waitForEnd > readyWaitForEnd && tryQueueReadyWork(readyWaitForEnd)) { + if (readyWaitForEnd === readyWaitForStart) { + readyWaitForStart--; + } + + readyWaitForEnd++; + } + + while (waitForStart < readyWaitForStart && tryQueueReadyWork(readyWaitForStart)) { + readyWaitForStart--; + } + } + } + + function processWaitingElements(itemIndex) { + var processedOne = false; + if (itemIndex === waitForStart || itemIndex === waitForEnd) { + + var tryProcessingElement = function (itemIndex) { + if (newElements[itemIndex]) { + var itemsManagerRecord = newElements[itemIndex]; + //#DBG _ASSERT(!itemsManagerRecord.released); + var element = itemsManagerRecord.element; + delete newElements[itemIndex]; + that.addItem(fragment, itemIndex, element, currentPass); + itemIsReady(itemIndex, element, itemsManagerRecord); + that.updateCleanIndexRange(itemIndex); + } else if (updatedElements[itemIndex]) { + var updateFunction = updatedElements[itemIndex]; + delete updatedElements[itemIndex]; + updateFunction(); + } else { + return false; + } + return true; + } + + if (tryProcessingElement(waitForEnd)) { + if (waitForStart === waitForEnd) { + waitForStart--; + queueProcessWaiting(waitForStart); + } + + processedOne = true; + waitForEnd++; + queueProcessWaiting(waitForEnd); + } else if (tryProcessingElement(waitForStart)) { + processedOne = true; + waitForStart--; + queueProcessWaiting(waitForStart); + } + + if (processedOne) { + that.listView._hideProgressBar(); + } + } + + function queueProcessWaiting(itemIndex) { + work.push(function () { + processWaitingElements(itemIndex); + }); + } + } + + if (counter > 0) { + var itemsRealizedPromise = initComplete.then(function (v) { + var buffer = []; + var createCount = 0; + var updateCount = 0; + var cleanCount = 0; + firstInView = v[0] || begin; + lastInView = v[1] || end - 1; + //#DBG _ASSERT(lastInView < end); + inViewCounter = lastInView - firstInView + 1; + that.firstIndexDisplayed = firstInView; + that.lastIndexDisplayed = lastInView; + + var queueWork = function (itemIndex) { + return function ScrollView_realizeItemsWork() { + if (that.listView._isZombie()) { + return; + } + + var itemData = that.items.itemDataAt(itemIndex); + if (!itemData) { + createCount++; + buffer.push(that.createItem(itemIndex, newItemIsReady, delivered)); + } else { + updatedElements[itemIndex] = function () { + if (itemIndex > that.cleanEndIndex || itemIndex < that.cleanStartIndex) { + updateCount++; + // Skip calling updateItem if there was no reason to update since the last full + // realizepage or if the item has already been updated. + that.updateItem(itemIndex, itemIsReady); + that.updateCleanIndexRange(itemIndex); + } else { + cleanCount++; + itemIsReady(itemIndex, itemData.element, itemData.itemsManagerRecord); + } + + itemData.itemsManagerRecord.itemPromise.then(function () { + processReadyWaiting(); + }); + }; + processWaitingElements(itemIndex); + } + }; + }; + + var isCurrentZoomView = that.listView._isCurrentZoomView; + WinJS.UI._ScrollView._inViewWorking = WinJS.UI._ScrollView._inViewWorking || 0; + + var startDuration = 0; + var duration; + if (isCurrentZoomView) { + WinJS.UI._ScrollView._inViewWorking++; + + if (that.firstLayoutPass) { + // Use long duration during initialization since it does not impact scrolling + duration = WinJS.UI._ScrollView._maxTimePerRealizeFrameInit; + startDuration = WinJS.UI._ScrollView._maxTimePerRealizeFrameInit; + } else if (that.listView._manipulationState === MSManipulationEvent.MS_MANIPULATION_STATE_STOPPED) { + startDuration = WinJS.UI._ScrollView._maxTimePerRealizeFrame / 4; + } else { + startDuration = WinJS.UI._ScrollView._maxTimePerRealizeFrameWhenPanning; + duration = WinJS.UI._ScrollView._maxTimePerRealizeFrameWhenPanning; + } + } + + work = new WinJS.UI._TimeBasedQueue({ + isBusy: function () { + // when listview is disposed, we just let the queue drain + if (that.listView._disposed) { return false; } + + // Do not work on the hidden listviews when there is work to be done for the visible listviews + return that.listView._versionManager.locked || (!isCurrentZoomView && WinJS.UI._ScrollView._inViewWorking > 0); + }, + duration: duration + }); + + //#DBG work._startVersion = that.listView._versionManager.version; + + // Pause the itemsManager from performing ready work while the queue is running + that.listView._itemsManager._workQueue.pause(); + + // 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 queueWorkComplete; + var workPromise = new WinJS.Promise(function (c, e, p) { + queueWorkComplete = function () { + work.push(function () { + if (that.listView._isZombie()) { + c(WinJS.Promise.cancel); + } + c(); + }); + }; + }).then(function () { + if (isCurrentZoomView) { + WinJS.UI._ScrollView._inViewWorking--; + } + that.listView._versionManager.clearCancelOnNotification(cancelToken); + that.listView._itemsManager._workQueue.resume(); + }, function (err) { + if (isCurrentZoomView) { + WinJS.UI._ScrollView._inViewWorking--; + } + + that.listView._versionManager.clearCancelOnNotification(cancelToken); + work.cancel(); + that.listView._itemsManager && that.listView._itemsManager._workQueue.resume(); + + loadingSignal.cancel(); + viewportItemsRealized.cancel(); + allItemsRealized.cancel(); + + return WinJS.Promise.wrapError(err); + }); + + var promiseToCancel = new WinJS.Promise(function () { }); + promiseToCancel.then(null, function () { + workPromise.cancel(); + if (currentPass === that.realizePass) { + setImmediate(function () { + if (that.listView._isZombie()) { return; } + + // The work can be canceled by a beginNotification in ItemsManager or GroupsContainer. The + // ListView will call cancelRealize if the notification requires synchronization work. In + // that case the realization pass will be started by the ListView. However if cancelRealize + // was not called we need to resume the realization pass ourselves. + that.listView._versionManager.unlocked.then(function () { + if (currentPass === that.realizePass) { + that.realizePage(scrollbarPos, true, direction); + } + }); + }); + } + }); + + var cancelToken = that.listView._versionManager.cancelOnNotification(promiseToCancel); + + // Always build in the direction we are moving + if (direction === "left") { + for (var itemIndex = lastInView; itemIndex >= firstInView; itemIndex--) { + work.push(queueWork(itemIndex)); + } + } else { + for (var itemIndex = firstInView; itemIndex <= lastInView; itemIndex++) { + work.push(queueWork(itemIndex)); + } + } + + var startPromise; + if (waitForStart < firstInView || waitForEnd > lastInView) { + if (direction === "left") { + waitForStart = waitForEnd = lastInView; + } else { + waitForStart = waitForEnd = firstInView; + } + readyWaitForEnd = readyWaitForStart = waitForStart; + + startPromise = that.listView._layout.getItemPosition(waitForStart).then(function (position) { + that.listView._groups.pinItem(waitForStart, position); + work.start(startDuration); + }); + } else { + work.start(startDuration); + startPromise = WinJS.Promise.as(); + } + + viewportItemsRealized.promise.then(function () { + function queueRight() { + for (var itemIndex = lastInView + 1; itemIndex < end; itemIndex++) { + work.push(queueWork(itemIndex), true); + } + } + + function queueLeft() { + // Always build the left side in the direction away from the center. + for (var itemIndex = firstInView - 1; itemIndex >= begin; itemIndex--) { + work.push(queueWork(itemIndex), true); + } + } + + if (direction === "left") { + queueLeft(); + queueRight(); + allItemsRealized.promise.then(function () { + queueWorkComplete(); + }); + } + else { + queueRight(); + queueLeft(); + allItemsRealized.promise.then(function () { + queueWorkComplete(); + }); + } + }); + + return workPromise.then(function () { + msWriteProfilerMark(perfId + ":complete(created:" + createCount + " updated:" + updateCount + "),info"); + return WinJS.Promise.join(buffer); + }, function realizePageCanceled(err) { + msWriteProfilerMark(perfId + ":canceled(created:" + createCount + " updated:" + updateCount + " clean:" + cleanCount + "),info"); + startPromise.cancel(); + //#DBG _ASSERT(err.name === "Canceled"); + var newElementsKeys = Object.keys(newElements); + for (var i = 0, len = newElementsKeys.length; i < len; i++) { + var newElement = newElements[newElementsKeys[i]].element; + if (that.listView._itemsPool.release) { + var data = that.listView._itemsManager._recordFromElement(newElement).item; + that.listView._itemsPool.release(data, newElement); + } + that.listView._itemsManager.releaseItem(newElement); + var wrapper = newElement.parentNode; + if (wrapper.parentNode) { + wrapper.parentNode.removeChild(wrapper); + } + var indexOfElement = that._insertedElements.indexOf(wrapper); + if (indexOfElement !== -1) { + wrapper.style.opacity = 1; + that._insertedElements.splice(indexOfElement, 1); + } + } + newElements = {}; + updatedElements = {}; + return WinJS.Promise.wrapError(err); + }); + }); + + msWriteProfilerMark(perfId + ",StopTM"); + return { + viewportItemsRealized: viewportItemsRealized.promise, + allItemsRealized: itemsRealizedPromise, + loadingCompleted: Promise.join([itemsRealizedPromise, loadingSignal.promise]) + }; + } + else { + msWriteProfilerMark(perfId + ",StopTM"); + return { + viewportItemsRealized: Promise.wrap(), + allItemsRealized: Promise.wrap(), + loadingCompleted: Promise.wrap() + }; + } + }, + + addHeader: function ScrollView_addHeader(fragment, groupIndex) { + var that = this; + return this.listView._groups.renderGroup(groupIndex).then(function (results) { + if (results) { + var header = results.header, + group = results.group; + + //#DBG _ASSERT(header && header.element && group && group.element); + if (header.element.parentNode !== fragment) { + fragment.appendChild(header.element); + } + + if (group.element.parentNode !== fragment) { + fragment.appendChild(group.element); + } + + that.listView._groups.setDomElements(groupIndex, header.element, group.element); + + that.listView._layout.layoutHeader(groupIndex, header.element); + } + }); + }, + + updateHeader: function ScrollView_updateHeader(group, groupIndex) { + this.listView._layout.layoutHeader(groupIndex, group.elements.header); + }, + + updateHeaders: function ScrollView_updateHeaders(fragment, begin, end, hideOthers) { + var that = this; + + function updateGroup(index) { + var group = that.listView._groups.group(index); + if (group) { + if (group.elements) { + that.updateHeader(group, index); + // Show the header if it has been hidden. + group.elements.header.style.opacity = 1; + } else { + that.addHeader(fragment, index); + } + } + } + + var groupStart = this.listView._groups.groupFromItem(begin), + groupIndex = groupStart, + groupEnd = this.listView._groups.groupFromItem(end - 1); + if (groupIndex !== null) { + //#DBG _ASSERT(groupEnd !== null); + for (; groupIndex <= groupEnd; groupIndex++) { + updateGroup(groupIndex); + } + + if (hideOthers) { + // We call update headers at the same time as realizing a page of items. This allows the headers on the + // current page to be shown when the items on the current page are shown instead of after all the pages + // are done. We do not currently animate the headers to new locations on insert/remove events. To avoid + // overlapping the headers we need to hide the group headers that fall outside of our index range. These + // groups may have moved or been removed. + var listView = this.listView; + for (var i = 0, len = listView._groups.groups.length; i < len; i++) { + var group = listView._groups.groups[i]; + if (group.elements && (i < groupStart || i > groupEnd)) { + group.elements.header.style.opacity = 0; + } + } + + // Some groups may have "changed" because their count/start index changed. We need to remove those so + // they do not overlap as well. + listView._groups.removeElements(); + } + } + }, + + purgePlaceholders: function ScrollView_purgePlaceholders() { + var im = this.listView._itemsManager, + placeholders = this.items.placeholders; + + var keys = Object.keys(placeholders); + for (var i = 0, len = keys.length; i < len; i++) { + var index = parseInt(keys[i], 10); + if (index < this.begin || index >= this.end) { + var item = placeholders[index]; + delete placeholders[index]; + im.releaseItem(item); + } + } + }, + + updateBackdrop: function ScrollView_updateBackdrop(count) { + if (this.listView._layout.updateBackdrop) { + this.listView._layout.updateBackdrop(count); + } + }, + + addItemsToPool: function ScrollView_addItemsToPool(count) { + var listView = this.listView, + im = listView._itemsManager, + items = this.items, + keys = Object.keys(items), + focusedItemPurged = false, + len, + i, + mode = listView._currentMode(); + var removedItem = false; + var that = this; + + items.eachIndex(function (index) { + if (index < that.begin || index >= that.end) { + if (listView._selection._getFocused() === index) { + listView._unsetFocusOnItem(); + focusedItemPurged = true; + } + var itemData = items.itemDataAt(index), + item = itemData.element, + wrapper = itemData.wrapper; + + if (wrapper.parentNode) { + wrapper.parentNode.removeChild(wrapper); + removedItem = true; + } + + if (mode.itemUnrealized) { + mode.itemUnrealized(index); + } + + listView._layout.releaseItem(wrapper); + + var indexOfElement = that._insertedElements.indexOf(wrapper); + if (indexOfElement !== -1) { + wrapper.style.opacity = 1; + that._insertedElements.splice(indexOfElement, 1); + } + + items.removeItem(index); + var data = itemData.itemsManagerRecord.item; + + // 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) { + im.releaseItem(item); + } + + listView._itemsPool.add(data, item); + listView._wrappersPool.add(null, wrapper, itemData.display); + } + }); + + 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()); + } + + if (removedItem) { + this.updateBackdrop(count); + } + }, + + addHeadersToPool: function ScrollView_addHeadersToPool() { + var listView = this.listView, + beginGroup = listView._groups.groupFromItem(this.begin); + + if (beginGroup !== null) { + var endGroup = listView._groups.groupFromItem(this.end - 1); + for (var i = 0, len = listView._groups.groups.length; i < len; i++) { + var group = listView._groups.groups[i]; + if (group.elements && (i < beginGroup || i > endGroup)) { + var headerElement = group.elements.header, + groupElement = group.elements.group; + + if (headerElement.parentNode) { + headerElement.parentNode.removeChild(headerElement); + } + if (groupElement.parentNode) { + groupElement.parentNode.removeChild(groupElement); + } + listView._headersPool.add(group.userData, headerElement); + listView._groupsPool.add(null, groupElement); + delete group.elements; + delete group.left; + delete group.top; + } + } + } + }, + + // This function removes items which are outside of current viewport and prefetched area + purgeItems: function ScrollView_purgeItems() { + if (this.listView._disposed) { return; } + + this.listView._itemsPool.clear(); + this.listView._wrappersPool.clear(); + this.listView._headersPool.clear(); + this.listView._groupsPool.clear(); + }, + + clearDeferTimeout: function ScrollView_clearDeferTimeout() { + if (this.deferTimeout) { + this.deferTimeout.cancel(); + this.deferTimeout = null; + } + if (this.deferAriaCancelToken !== -1) { + this.listView._versionManager.clearCancelOnNotification(this.deferAriaCancelToken); + this.deferAriaCancelToken = -1; + } + }, + + deferAriaSetup: function ScrollView_deferAriaSetup(count, begin, end) { + msWriteProfilerMark("WinJS.UI.ListView:aria initialize,StartTM"); + var that = this; + + function calcLastRealizedIndexInGroup(groupIndex) { + var groups = that.listView._groups, + nextGroup = groups.group(groupIndex + 1); + return (nextGroup ? Math.min(nextGroup.startIndex - 1, end - 1) : end - 1); + } + + function deferAriaWorker() { + if (that.listView._zooming || that.listView._pinching) { + //To improve SeZo's zoom animation and pinch detection perf, we want to ensure unimportant task + //is only run while zooming or pinching is in progress + return Promise.timeout(WinJS.UI._ScrollView._retryTimeForAriaSetupDuringZoom).then(deferAriaWorker); + } else { + deferAriaWorkerImpl(); + return Promise.wrap(); + } + } + + function deferAriaWorkerImpl() { + if (that.listView._isZombie()) { return; } + + msWriteProfilerMark("WinJS.UI.ListView:aria work,StartTM"); + that.listView._createAriaMarkers(); + var startMarker = that.listView._ariaStartMarker, + endMarker = that.listView._ariaEndMarker, + item = that.items.itemAt(begin), + // These are only used when the ListView is using groups + groups, + groupIndex, + group, + lastRealizedIndexInGroup; + + if (count > 0) { + item.detachEvent("onpropertychange", WinJS.UI._itemPropertyChange); + WinJS.UI._ensureId(item); + if (that.listView._groupsEnabled()) { + groups = that.listView._groups; + groupIndex = groups.groupFromItem(begin); + group = groups.group(groupIndex); + lastRealizedIndexInGroup = calcLastRealizedIndexInGroup(groupIndex); + WinJS.UI._ensureId(group.elements.header); + WinJS.UI._setAttribute(group.elements.header, "x-ms-aria-flowfrom", startMarker.id); + WinJS.UI._setFlow(group.elements.header, item); + } else { + WinJS.UI._setAttribute(item, "x-ms-aria-flowfrom", startMarker.id); + } + + that.deferTimeout = null; + that.listView._versionManager.clearCancelOnNotification(that.deferAriaCancelToken); + that.deferAriaCancelToken = -1; + + that.items.each(function (index, item, itemData) { + var nextItem = that.items.itemAt(index + 1); + + if (nextItem) { + nextItem.detachEvent("onpropertychange", WinJS.UI._itemPropertyChange); + WinJS.UI._ensureId(nextItem); + } + + WinJS.UI._setAttribute(item, "role", that.listView._itemRole); + WinJS.UI._setAttribute(item, "aria-setsize", count); + WinJS.UI._setAttribute(item, "aria-posinset", index + 1); + if (item.tabIndex !== that.listView._tabIndex) { + item.tabIndex = that.listView._tabIndex; + } + + if (that.listView._groupsEnabled()) { + if (index === lastRealizedIndexInGroup) { + var nextGroup = groups.group(groupIndex + 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. + //#DBG _ASSERT(nextGroup && nextGroup.elements || !nextItem); + if (nextGroup && nextGroup.elements) { + WinJS.UI._ensureId(nextGroup.elements.header); + WinJS.UI._setFlow(item, nextGroup.elements.header); + WinJS.UI._setFlow(nextGroup.elements.header, nextItem); + } else { + // We're at the last group so flow to the end marker + WinJS.UI._setAttribute(item, "aria-flowto", endMarker.id); + } + + WinJS.UI._setAttribute(group.elements.group, "aria-hidden", true); + + groupIndex++; + group = nextGroup; + lastRealizedIndexInGroup = calcLastRealizedIndexInGroup(groupIndex); + } else { + // This is not the last item in the group so flow to the next item + //#DBG _ASSERT(nextItem); + WinJS.UI._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 + WinJS.UI._setFlow(item, nextItem); + } else { + // Groups are disabled and we're at the last item, so flow to the end marker + WinJS.UI._setAttribute(item, "aria-flowto", endMarker.id); + } + item.attachEvent("onpropertychange", WinJS.UI._itemPropertyChange); + }); + } + msWriteProfilerMark("WinJS.UI.ListView:aria work,StopTM"); + } + + this.clearDeferTimeout(); + if (this.listView._isZombie()) { return; } + + this.deferTimeout = WinJS.Promise.timeout(WinJS.UI._DEFERRED_ACTION).then(deferAriaWorker); + this.deferAriaCancelToken = this.listView._versionManager.cancelOnNotification(this.deferTimeout); + msWriteProfilerMark("WinJS.UI.ListView:aria initialize,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 ScrollView_updateAriaMarkers(count, firstIndexDisplayed, lastIndexDisplayed) { + this.listView._createAriaMarkers(); + var startMarker = this.listView._ariaStartMarker, + endMarker = this.listView._ariaEndMarker; + + if (count === 0) { + WinJS.UI._setFlow(startMarker, endMarker); + } else if (firstIndexDisplayed !== -1 && lastIndexDisplayed !== -1) { + var firstVisibleItem = this.items.itemAt(firstIndexDisplayed), + lastVisibleItem = this.items.itemAt(lastIndexDisplayed); + + if (firstVisibleItem && lastVisibleItem) { + WinJS.UI._ensureId(firstVisibleItem); + WinJS.UI._ensureId(lastVisibleItem); + + // Set startMarker's flowto + if (this.listView._groupsEnabled()) { + var groups = this.listView._groups, + firstVisibleGroup = groups.group(groups.groupFromItem(firstIndexDisplayed)); + + if (firstVisibleGroup.elements) { + WinJS.UI._ensureId(firstVisibleGroup.elements.header); + + if (firstIndexDisplayed === firstVisibleGroup.startIndex) { + WinJS.UI._setAttribute(startMarker, "aria-flowto", firstVisibleGroup.elements.header.id); + } else { + WinJS.UI._setAttribute(startMarker, "aria-flowto", firstVisibleItem.id); + } + } + } else { + WinJS.UI._setAttribute(startMarker, "aria-flowto", firstVisibleItem.id); + } + + // Set endMarker's flowfrom + WinJS.UI._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 ScrollView_updateAriaForAnnouncement(item, count) { + var index = this.items.index(item); + //#DBG _ASSERT(index !== WinJS.UI._INVALID_INDEX); + + WinJS.UI._setAttribute(item, "role", this.listView._itemRole); + WinJS.UI._setAttribute(item, "aria-setsize", count); + WinJS.UI._setAttribute(item, "aria-posinset", index + 1); + }, + + _resetLayoutState: function ScrollView_resetLayoutState() { + this.cleanStartIndex = -1; + this.cleanEndIndex = -1; + }, + + realizePage: function ScrollView_realizePage(scrollbarPos, forceRelayout, direction) { + msWriteProfilerMark("WinJS.UI.ListView:realizePage(" + forceRelayout + "),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) { + var that = this; + this.listView._versionManager.unlocked.then(function () { + return WinJS.Promise.timeout(); + }).then(function () { + if (!that.listView._isZombie()) { + that.realizePage(scrollbarPos, forceRelayout, direction); + } + }); + msWriteProfilerMark("WinJS.UI.ListView:realizePage,StopTM"); + return; + } + this.listView._versionManager.onunlocked = null; + + var start_pageRealized = WinJS.log && new Date(); + + if (forceRelayout) { + this._resetLayoutState(); + } + + var that = this, + itemsManager = this.listView._itemsManager, + viewportLength = that.listView._getViewportLength(), + viewBoundsSignal = new WinJS._Signal(), + pageRealizedSignal = new WinJS._Signal(); + + function calculateDisplayedItems(count) { + if (count) { + Promise.join([ + that.listView.layout.calculateFirstVisible(scrollbarPos, false), + that.listView.layout.calculateLastVisible(scrollbarPos + viewportLength - 1, false) + ]).then( + function (indices) { + that.firstIndexDisplayed = indices[0]; + that.lastIndexDisplayed = indices[1]; + viewBoundsSignal.complete(); + }, + function () { + that.firstIndexDisplayed = -1; + that.lastIndexDisplayed = -1; + viewBoundsSignal.complete(); + } + ); + } else { + that.firstIndexDisplayed = -1; + that.lastIndexDisplayed = -1; + viewBoundsSignal.complete(); + } + } + + function viewPortPageRealized() { + that.listView._hideProgressBar(); + that.listView._raiseViewPortLoaded(); + + if (that.listView.scrollPosition <= 0) { + // If the user hit the beginning of the list apply the fix immediately + that.fixScrollbarRange(that.listView.scrollPosition); + } + + WinJS.log && WinJS.log((new Date()) - start_pageRealized, "winjs viewPortPageRealized", "perf"); + } + + function pageRealized(count) { + that.listView._hideProgressBar(); + viewBoundsSignal.promise.then(function () { + that.updateAriaMarkers(count, that.firstIndexDisplayed, that.lastIndexDisplayed); + + that.listView._raiseViewLoaded(); + + WinJS.log && WinJS.log((new Date()) - start_pageRealized, "winjs pageRealized", "perf"); + + pageRealizedSignal.complete(); + }); + } + + this.listView._raiseViewLoading(); + if (this.firstLayoutPass) { + this.listView._showProgressBar(this.listView._element, "50%", "50%"); + } + + this.listView._itemsCount().then(function (count) { + if (!that.destroyed) { + var loadingSignal = new WinJS._Signal(); + + that.listView._groups.dirty = true; + + // While the zoom animation is played we want to minimize the # of pages + // being fetched to improve TtFF for SeZo scenarios + var pagesToPrefetch = that.pagesToPrefetch; + if (that.listView._zooming) { + pagesToPrefetch = 0; + } + + var beginningOffset = scrollbarPos - pagesToPrefetch * viewportLength; + var endingOffset = scrollbarPos + (1 + pagesToPrefetch) * viewportLength; + + // If we are near the beginning of the scroll region or the end of the scroll region + // shift the beginning and ending offsets so we still always get 1 + (2 * pagesToPrefetch) items. + // This helps the initial scroll so that many items do not have to be created. + if (beginningOffset < 0) { + if (WinJS.UI._ScrollView._preloadEdges) { + endingOffset -= beginningOffset; + } + beginningOffset = 0; + } + else { + var maxOffset = that.listView._getCanvasLength(); + + //#DBG _ASSERT(typeof maxOffset === "number"); + + // Now deal with the case where we are at the end of the canvas. + if (endingOffset > maxOffset && WinJS.UI._ScrollView._preloadEdges) { + // We could set endingOffset equal to maxOffset but it is not needed. + beginningOffset = Math.max(0, beginningOffset - (endingOffset - maxOffset)); + } + } + + that.listView._layout.startLayout(beginningOffset, endingOffset - 1, count).then( + function (range) { + var currentPass = that.realizePass, + endLayoutResult, + checkRenderStatus = false; + + if (range) { + var begin = Math.max(0, range.beginIndex); + var end = Math.min(count, range.endIndex); + + var firstPromise; + var lastPromise; + if (!count) { + that.firstIndexDisplayed = -1; + that.lastIndexDisplayed = -1; + that.updateBackdrop(count); + firstPromise = WinJS.Promise.wrap(-1); + lastPromise = WinJS.Promise.wrap(-1); + } else { + firstPromise = that.listView.layout.calculateFirstVisible(scrollbarPos, false); + lastPromise = that.listView.layout.calculateLastVisible(scrollbarPos + viewportLength - 1, false); + } + + WinJS.Promise.join([firstPromise, lastPromise]).then(function (indices) { + var firstInView = indices[0]; + var lastInView = indices[1]; + + if ((forceRelayout || begin !== that.begin || end !== that.end || firstInView !== that._previousFirstInView || lastInView !== that._previousLastInView) && (begin < end) && (beginningOffset < endingOffset)) { + that._cancelRealize(); + // _cancelRealize changes the realizePass and resets begin/end + currentPass = that.realizePass; + that.begin = begin; + that.end = end; + that._previousFirstInView = firstInView; + that._previousLastInView = lastInView; + + that.purgePlaceholders(); + that.addItemsToPool(count); + + var realizeWork = that.realizeItems( + that.listView._itemCanvas, + that.begin, + that.end, + count, + currentPass, + scrollbarPos, + direction); + that.lastRealizePass = realizeWork.allItemsRealized; + + realizeWork.viewportItemsRealized.then(viewPortPageRealized); + + realizeWork.allItemsRealized.then( + function () { + if (that.realizePass === currentPass) { + that.listView._groups.removeElements(); + + // We couldn't call this earlier with addItemsToPool() because we had to wait for all of the + // groups to be realized before deciding which groups/headers are no longer needed. + that.addHeadersToPool(); + + that.updateHeaders(that.listView._canvas, that.begin, that.end); + + that.updateScrollbar(count); + + // Items outside of current viewport and prefetched area can be removed + that.purgeItems(); + + that.deferAriaSetup(count, that.begin, that.end); + + calculateDisplayedItems(count); + + pageRealized(count); + + that.lastRealizePass = null; + } + }, + function (e) { + if (that.realizePass === currentPass) { + that.lastRealizePass = null; + that.begin = -1; + that.end = -1; + } + return WinJS.Promise.wrapError(e); + }); + + realizeWork.loadingCompleted.then(function () { + loadingSignal.complete(); + }); + + } else if (that.lastRealizePass === null) { + // 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. + calculateDisplayedItems(count); + endLayoutResult = that.listView._layout.endLayout(); + that.listView._clearInsertedItems(); + that.updateScrollbar(count); + viewPortPageRealized(); + pageRealized(count); + checkRenderStatus = true; + } + }, function () { + calculateDisplayedItems(count); + endLayoutResult = that.listView._layout.endLayout(); + that.listView._clearInsertedItems(); + that.updateScrollbar(count); + viewPortPageRealized(); + pageRealized(count); + checkRenderStatus = true; + }); + } else { + calculateDisplayedItems(count); + endLayoutResult = that.listView._layout.endLayout(); + that.listView._clearInsertedItems(); + that.listView._groups.removeElements(); + that.updateBackdrop(count); + viewPortPageRealized(); + pageRealized(count); + checkRenderStatus = true; + } + + if (endLayoutResult && endLayoutResult.animationPromise) { + that.animating = true; + that.runningAnimations = Promise.join([that.runningAnimations, endLayoutResult.animationPromise]); + that.runningAnimations.then(function () { + if (that.realizePass === currentPass) { + that.animating = false; + that.runningAnimations = null; + if (!that.loadingItems && !that.animating) { + loadingSignal.complete(); + } + } + }); + } else if (checkRenderStatus) { + loadingSignal.complete(); + } + }, + function () { + calculateDisplayedItems(count); + that.listView._clearInsertedItems(); + viewPortPageRealized(); + pageRealized(count); + } + ); + + Promise.join([pageRealizedSignal.promise, loadingSignal.promise]). + then(function () { + that.listView._raiseViewComplete(); + }); + } + }); + msWriteProfilerMark("WinJS.UI.ListView:realizePage,StopTM"); + }, + + onScroll: function ScrollView_onScroll(scrollbarPos, scrollLength, direction, viewportSize) { + this.realizePage(scrollbarPos, false, direction); + }, + + onResize: function ScrollView_onResize(scrollbarPos, viewportSize) { + this.realizePage(scrollbarPos, true); + }, + + _cancelRealize: function ScrollView_cancelRealize() { + this.clearDeferTimeout(); + this.realizePass++; + + var last = this.lastRealizePass; + if (last) { + this.lastRealizePass = null; + this.begin = -1; + this.end = -1; + last.cancel(); + } + }, + + reset: function ScrollView_reset() { + this._cancelRealize(); + // when in the zombie state, we let disposal cleanup the ScrollView state + // + if (!this.listView._isZombie()) { + var itemData = this.items.itemDataAt(0); + if (itemData && itemData.display !== undefined) { + itemData.element.style.display = itemData.display; + } + this.firstIndexDisplayed = -1; + this.lastIndexDisplayed = -1; + this.runningAnimations = null; + this.animating = false; + this.loadingItems = false; + + var listView = this.listView; + this.firstLayoutPass = true; + listView._unsetFocusOnItem(); + if (listView._currentMode().onDataChanged) { + listView._currentMode().onDataChanged(); + } + + this.items.each(function (index, item) { + listView._itemsManager.releaseItem(item); + }); + + this.items.removeItems(); + listView._groups.resetGroups(); + listView._resetCanvas(); + + this._insertedElements = []; + listView._clearInsertedItems(); + } + }, + + reload: function ScrollView_reload(scrollbarPos) { + if (this.listView._isZombie()) { return; } + + this.realizePage(scrollbarPos, true); + }, + + refresh: function ScrollView_refresh(scrollbarPos, scrollLength, viewportSize, newCount) { + if (this.listView._isZombie()) { return; } + + this.realizePage(scrollbarPos, true); + }, + + updateScrollbar: function ScrollView_updateScrollbar(count) { + var that = this; + + if (count > 0) { + return this.listView._layout.getScrollbarRange(count).then( + function (range) { + that.listView._setCanvasLength(range.beginScrollPosition, range.endScrollPosition); + that.updateBackdrop(count); + }, + function () { + that.listView._setCanvasLength(0, 0); + that.updateBackdrop(0); + } + ); + } else { + this.listView._setCanvasLength(0, 0); + that.updateBackdrop(count); + return Promise.wrap(); + } + }, + + update: function ScrollView_update(count) { + this.listView._layout.update(count); + this.listView._groups.dirty = true; + }, + + fixScrollbarRange: function ScrollView_fixScrollbarRange(scrollbarPos) { + var that = this, + fixedPos = scrollbarPos; + if (this.listView._groups.length() && this.listView._groups.group(0).offset) { + + this.updateScrollbar(this.listView._cachedCount); + + this.listView._layout.calculateFirstVisible(scrollbarPos).then(function (firstDisplayed) { + that.listView._layout.getItemPosition(firstDisplayed).then(function (position) { + that.listView._layout.getScrollbarRange().then(function (scrollbarRange) { + if (scrollbarRange.beginScrollPosition < 0) { + var start = position[that.listView._startProperty], + itemPos = start - scrollbarRange.beginScrollPosition; + + that.listView._groups.pinItem(firstDisplayed, { offset: itemPos }); + + that.listView._lastScrollPosition = itemPos; + that.listView.scrollPosition = itemPos; + that.realizePage(itemPos, true); + } + }); + }); + }); + } + }, + + cleanUp: function ScrollView_cleanUp() { + this._cancelRealize(); + this.runningAnimations && this.runningAnimations.cancel(); + var itemsManager = this.listView._itemsManager; + this.items.each(function (index, item) { + itemsManager.releaseItem(item); + }); + this.listView._unsetFocusOnItem(); + this.items.removeItems(); + this.listView._groups.resetGroups(); + this.listView._resetCanvas(); + + this.destroyed = true; + } +}; +})(this, WinJS); + +(function selectionManagerInit(global, WinJS, undefined) { + "use strict"; + +var utilities = WinJS.Utilities, + Promise = WinJS.Promise; + +WinJS.Namespace.define("WinJS.UI", { + _ItemSet: WinJS.Class.define(function _ItemSet_ctor(listView, ranges, count) { + this._listView = listView; + this._ranges = ranges; + this._itemsCount = count; + }), + + 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 WinJS.Promise.join(promises).then(function (items) { + listBinding.release(); + return items; + }); + }); + } +}); + +WinJS.UI._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 WinJS.UI.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; + } +}; + +function isEverythingRange(ranges) { + return ranges && ranges.firstIndex === 0 && ranges.lastIndex === Number.MAX_VALUE; +} + +WinJS.Namespace.define("WinJS.UI", { + _Selection: WinJS.Class.derive(WinJS.UI._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 () { + var range = { + firstIndex: 0, + lastIndex: that._itemsCount - 1, + }; + that._retainRange(range); + that._releaseRanges(that._ranges); + that._ranges = [range]; + return that._ensureKeys(); + }); + }, + + /*#DBG + _assertValid: function () { + for (var i = 0, len = this._ranges.length; i < len; i++) { + var range = this._ranges[i]; + _ASSERT(range.firstIndex <= range.lastIndex); + _ASSERT(!i || this._ranges[i - 1].lastIndex < range.firstIndex); + } + return true; + }, + #DBG*/ + + _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(); + } + } + + for (var i = 0, len = this._ranges.length; i < len; i++) { + range = this._ranges[i]; + if (newRange.firstIndex < range.firstIndex) { + var 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 + //#DBG _ASSERT(range.firstIndex >= toRemove.firstIndex && range.lastIndex <= toRemove.lastIndex); + 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) { + //#DBG _ASSERT(!target.lastPromise && !source.lastPromise); + 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 +WinJS.Namespace.define("WinJS.UI", { + _SelectionManager: function (listView) { + this._listView = listView; + this._selected = new WinJS.UI._Selection(this._listView); + this._pivot = WinJS.UI._INVALID_INDEX; + this._focused = 0; + this._pendingChange = Promise.wrap(); + } +}, { + /* empty */ +}, { + supportedForProcessing: false, +}); + +WinJS.UI._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 WinJS._Signal(); + return this._synchronize(signal).then(function () { + var newSelection = new WinJS.UI._Selection(that._listView); + return newSelection.set(items).then( + function () { + that._set(newSelection); + signal.complete(); + }, + function (error) { + newSelection.clear(); + signal.complete(); + return WinJS.Promise.wrapError(error); + } + ); + }); + }, + + clear: function () { + /// + /// + /// Clears the selection. + /// + /// + /// A Promise that is fulfilled when the clear operation completes. + /// + /// + + var that = this, + signal = new WinJS._Signal(); + return this._synchronize(signal).then(function () { + var newSelection = new WinJS.UI._Selection(that._listView); + return newSelection.clear().then( + function () { + that._set(newSelection); + signal.complete(); + }, + function (error) { + newSelection.clear(); + signal.complete(); + return WinJS.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 WinJS._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 WinJS.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 WinJS._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 WinJS.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 WinJS._Signal(); + return this._synchronize(signal).then(function () { + var newSelection = new WinJS.UI._Selection(that._listView); + return newSelection.selectAll().then( + function () { + that._set(newSelection); + signal.complete(); + }, + function (error) { + newSelection.clear(); + signal.complete(); + return WinJS.Promise.wrapError(error); + } + ); + }); + }, + + _synchronize: function(signal) { + var that = this; + return this._listView._versionManager.unlocked.then(function () { + var currentPendingChange = that._pendingChange; + that._pendingChange = WinJS.Promise.join([currentPendingChange, signal.promise]).then(function () { }); + return currentPendingChange; + }); + }, + + _reset: function () { + this._pivot = WinJS.UI._INVALID_INDEX; + this._setFocused(0, this._keyboardFocused()); + this._pendingChange.cancel(); + this._pendingChange = Promise.wrap(); + this._selected.clear(); + this._selected = new WinJS.UI._Selection(this._listView); + }, + + _dispose: function () { + this._selected.clear(); + this._selected = null; + this._listView = null; + }, + + _set: function (newSelection) { + var that = this; + this._fireSelectionChanging(newSelection).then(function (approved) { + if (approved) { + that._selected.clear(); + that._selected = newSelection; + that._listView._updateSelection(); + that._fireSelectionChanged(); + } else { + newSelection.clear(); + } + }); + }, + + _fireSelectionChanging: function (newSelection) { + var eventObject = 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 = document.createEvent("CustomEvent"); + eventObject.initCustomEvent("selectionchanged", true, false, null); + this._listView._element.dispatchEvent(eventObject); + }, + + _getFocused: function () { + return this._focused; + }, + + _setFocused: function (index, keyboardFocused) { + this._focused = 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 WinJS.UI._Selection(this._listView); + newSelection._ranges = this._selected.getRanges(); + newSelection._itemsCount = this._selected._itemsCount; + newSelection._retainRanges(); + return newSelection; + } +}; + +})(this, WinJS); + +(function datePickerInit(WinJS, undefined) { + "use strict"; + + // Constants definition + var DEFAULT_DAY_PATTERN = 'day', + DEFAULT_MONTH_PATTERN = '{month.full}', + DEFAULT_YEAR_PATTERN = 'year.full'; + + var strings = { + get selectDay() { return WinJS.Resources._getWinJSString("ui/selectDay").value; }, + get selectMonth() { return WinJS.Resources._getWinJSString("ui/selectMonth").value; }, + get selectYear() { return WinJS.Resources._getWinJSString("ui/selectYear").value; }, + }; + + var yearFormatCache = {}; + + function newFormatter(pattern, calendar, defaultPattern) { + var dtf = 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 = 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; + } + + WinJS.Namespace.define("WinJS.UI", { + /// Allows users to pick a date value. + /// Date Picker + /// + /// + /// ]]> + /// Occurs when the current date changes. + /// + /// + /// + DatePicker: WinJS.Class.define(function DatePicker_ctor(element, options) { + /// + /// Initializes a new instance of the DatePicker control + /// + /// The DOM element associated with the DatePicker control. + /// + /// + /// The set of options to be applied initially to the DatePicker control. + /// + /// 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 || document.createElement("div"); + element.winControl = this; + + // Set options BEFORE setting the element, so they can influence things + WinJS.UI.setOptions(this, options); + this._init(element); + }, { + _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 u = WinJS.Utilities; + 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); + u.addClass(that._monthElement, "win-order" + (orderIndex++)); + break; + case "date": + e.appendChild(that._dateElement); + u.addClass(that._dateElement, "win-order" + (orderIndex++)); + break; + case "year": + e.appendChild(that._yearElement); + u.addClass(that._yearElement, "win-order" + (orderIndex++)); + break; + } + }); + }, + + _createControlElements: function () { + this._monthElement = document.createElement("select"); + this._monthElement.className = "win-datepicker-month"; + this._dateElement = document.createElement("select"); + this._dateElement.className = "win-datepicker-date"; + this._yearElement = document.createElement("select"); + this._yearElement.className = "win-datepicker-year"; + }, + + _createControls: function () { + var that = this; + + 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 WinJS.UI._Select(this._yearElement, { + dataSource: this._information.years, + disabled: this.disabled, + index: index.year + }); + + this._monthControl = new WinJS.UI._Select(this._monthElement, { + dataSource: this._information.months(index.year), + disabled: this.disabled, + index: index.month + }); + + this._dateControl = new WinJS.UI._Select(this._dateElement, { + dataSource: this._information.dates(index.year, index.month), + disabled: this.disabled, + index: index.date + }); + + this._wireupEvents(); + }, + + /// 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(); + } + } + }, + + /// Specifies whether 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; } + + WinJS.Utilities.empty(this._domElement); + WinJS.Utilities.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 = WinJS.UI.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 minYear = tempCal.year; + 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 + index; + return formatMonth(datePatterns.month, calendar, DEFAULT_MONTH_PATTERN, monthCal); + } + }; + }; + + var dateSource = function (yearIndex, monthIndex) { + dayCal.setDateTime(startDate); + dayCal.addYears(yearIndex); + dayCal.month = Math.max(Math.min(monthIndex + dayCal.firstMonthInThisYear, dayCal.firstMonthInThisYear + dayCal.numberOfMonthsInThisYear - 1), dayCal.firstMonthInThisYear); + 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 = Math.max(Math.min(index.month + c.firstMonthInThisYear, c.firstMonthInThisYear + c.numberOfMonthsInThisYear - 1), c.firstMonthInThisYear); + if (lastCal && lastCal.year !== c.year) { + guessMonth = Math.max(Math.min(lastCal.month, 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; + 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 (yearIndex) { + 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 (WinJS.Utilities.hasWinRT) { + WinJS.UI.DatePicker.getInformation = WinJS.UI.DatePicker._getInformationWinRT; + } + else { + WinJS.UI.DatePicker.getInformation = WinJS.UI.DatePicker._getInformationJS; + } + + + WinJS.Class.mix(WinJS.UI.DatePicker, WinJS.Utilities.createEventProperties("change")); + WinJS.Class.mix(WinJS.UI.DatePicker, WinJS.UI.DOMEventMixin); + +})(WinJS); + + +(function timePickerInit(WinJS, undefined) { + "use strict"; + + // Constants definition + var DEFAULT_MINUTE_PATTERN = "{minute.integer(2)}", + DEFAULT_HOUR_PATTERN = "{hour.integer(1)}", + DEFAULT_PERIOD_PATTERN = "{period.abbreviated(2)}"; + + var strings = { + get selectHour() { return WinJS.Resources._getWinJSString("ui/selectHour").value; }, + get selectMinute() { return WinJS.Resources._getWinJSString("ui/selectMinute").value; }, + get selectAMPM() { return WinJS.Resources._getWinJSString("ui/selectAMPM").value; }, + }; + + WinJS.Namespace.define("WinJS.UI", { + /// Allows users to select time values. + /// Time Picker + /// + /// + /// ]]> + /// Occurs when the time changes. + /// + /// + /// + TimePicker: WinJS.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 = WinJS.UI.TimePicker._sentinelDate(); + + element = element || document.createElement("div"); + element.winControl = this; + this._timePatterns = { + minute: null, + hour: null, + period: null + }; + WinJS.UI.setOptions(this, options); + this._init(element); + }, { + _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; + var u = WinJS.Utilities; + info.order.forEach(function (s, index) { + switch (s) { + case "hour": + that._domElement.appendChild(that._hourElement); + u.addClass(that._hourElement, "win-order" + index); + break; + case "minute": + that._domElement.appendChild(that._minuteElement); + u.addClass(that._minuteElement, "win-order" + index); + break; + case "period": + if (that._ampmElement) { + that._domElement.appendChild(that._ampmElement); + u.addClass(that._ampmElement, "win-order" + index); + } + break; + } + }); + }, + + /// 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 = WinJS.UI.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 that = this; + var newTime; + if (typeof (value) === "string") { + newTime = WinJS.UI.TimePicker._sentinelDate(); + newTime.setTime(Date.parse(newTime.toDateString() + " " + value)); + } + else { + newTime = value; + } + + var oldTime = this.currentTime; + if (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 = WinJS.UI.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"); + } + + WinJS.Utilities.empty(this._domElement); + WinJS.Utilities.addClass(this._domElement, "win-timepicker"); + + this._hourElement = document.createElement("select"); + WinJS.Utilities.addClass(this._hourElement, "win-timepicker-hour"); + + this._minuteElement = document.createElement("select"); + WinJS.Utilities.addClass(this._minuteElement, "win-timepicker-minute"); + + this._ampmElement = null; + if (info.clock === "12HourClock") { + this._ampmElement = document.createElement("select"); + WinJS.Utilities.addClass(this._ampmElement, "win-timepicker-period"); + } + + this._addControlsInOrder(info); + + var that = this; + + var hoursAmpm = this._getHoursAmpm(this.current); + this._hourControl = new WinJS.UI._Select(this._hourElement, { + dataSource: this._getInfoHours(), + disabled: this.disabled, + index: this._getHoursIndex(hoursAmpm.hours) + }); + this._minuteControl = new WinJS.UI._Select(this._minuteElement, { + dataSource: info.minutes, + disabled: this.disabled, + index: this._getMinutesIndex(this.current) + }); + this._ampmControl = null; + if (this._ampmElement) { + this._ampmControl = new WinJS.UI._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 = 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 = Windows.Globalization; + var calendar = new glob.Calendar(); + if (clock) { + calendar = new glob.Calendar(calendar.languages, calendar.getCalendarSystem(), clock); + } + calendar.setDateTime(WinJS.UI.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 = WinJS.UI.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 (index) { + var minuteFormatter = newFormatter(timePatterns.minute, DEFAULT_MINUTE_PATTERN); + var now = WinJS.UI.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 (index) { + var hourFormatter = newFormatter(timePatterns.hour, DEFAULT_HOUR_PATTERN); + var now = WinJS.UI.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 = Windows.Globalization.DateTimeFormatting.DateTimeFormatter; + var dtf = new DateTimeFormatter("month.full", 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 = new Object(); + 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 (WinJS.Utilities.hasWinRT) { + WinJS.UI.TimePicker.getInformation = WinJS.UI.TimePicker._getInformationWinRT; + } + else { + WinJS.UI.TimePicker.getInformation = WinJS.UI.TimePicker._getInformationJS; + } + + WinJS.Class.mix(WinJS.UI.TimePicker, WinJS.Utilities.createEventProperties("change")); + WinJS.Class.mix(WinJS.UI.TimePicker, WinJS.UI.DOMEventMixin); + +})(WinJS); + +(function selectInit(WinJS, undefined) { + "use strict"; + + var encodeHtmlRegEx = /[^\w .,-]/g; + var stringDirectionRegEx = /̴[67];/g; + function encodeHtml(str) { + return str.replace(encodeHtmlRegEx, function (m) { + return "&#" + m.charCodeAt(0) + ";"; + }); + }; + function stripDirectionMarker(str) { + return str.replace(stringDirectionRegEx, ""); + } + function stockGetValue(index) { return this[index]; } + function stockGetLength() { return this.length; } + function fixDataSource(dataSource) { + if (!dataSource.getValue) { + dataSource.getValue = stockGetValue + } + + if (!dataSource.getLength) { + dataSource.getLength = stockGetLength + } + return dataSource; + } + WinJS.Namespace.define("WinJS.UI", { + _Select: WinJS.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 (e) { + //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 += ""; + } + WinJS.Utilities.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); + } + } + }) + }) +})(WinJS); + +(function ratingInit(global) { + "use strict"; + + +var utilities = WinJS.Utilities; + +var strings = { + get averageRating() { return WinJS.Resources._getWinJSString("ui/averageRating").value; }, + get clearYourRating() { return WinJS.Resources._getWinJSString("ui/clearYourRating").value; }, + get tentativeRating() { return WinJS.Resources._getWinJSString("ui/tentativeRating").value; }, + get tooltipStringsIsInvalid() { return WinJS.Resources._getWinJSString("ui/tooltipStringsIsInvalid").value; }, + get unrated() { return WinJS.Resources._getWinJSString("ui/unrated").value; }, + get userRating() { return WinJS.Resources._getWinJSString("ui/userRating").value; }, +}; + +// Constants definition +var DEFAULT_MAX_RATING = 5, + DEFAULT_DISABLED = false, + CANCEL = "cancel", + CHANGE = "change", + PREVIEW_CHANGE = "previewchange", + MOUSE_LBUTTON = 0, // Event attribute to indicate a mouse left click + PT_TOUCH = 2, // Pointer type to indicate a touch event + PT_PEN = 3, // Pointer type to indicate a pen event + PT_MOUSE = 4; // Pointer type to indicate a mouse event + +var hiddenAverageRatingCss = "padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px; -ms-flex: none; display: none"; + +// CSS class names +var msRating = "win-rating", + msRatingEmpty = "win-star win-empty", + msRatingAverageEmpty = "win-star win-average win-empty", + msRatingAverageFull = "win-star win-average win-full", + msRatingUserEmpty = "win-star win-user win-empty", + msRatingUserFull = "win-star win-user win-full", + msRatingTentativeEmpty = "win-star win-tentative win-empty", + msRatingTentativeFull = "win-star win-tentative win-full", + msRatingDisabled = "win-disabled", + msAverage = "win-average", + msUser = "win-user"; + +// Rating control implementation +WinJS.Namespace.define("WinJS.UI", { + /// + /// The Rating control allows users to give a number on a scale of 1 to maxRating (5 is the default). + /// + /// + /// + /// ]]> + /// Raised when the user chooses a new tentative rating but hasn't commited the change. + /// Raised when the user finishes interacting with the rating control without committing a tentative rating. + /// Raised when the user commits a change to the userRating. + /// The entire Rating control. + /// The empty star when the Rating control shows the average rating. + /// The full star when the Rating control shows the average rating. + /// The empty star when the Rating control shows the user rating. + /// The full star when the Rating control shows the user rating. + /// The empty star when the Rating control shows the tentative rating. + /// The full star when the Rating control shows the tentative rating. + /// The empty star when the control is disabled. + /// The full star when the control is disabled. + /// + /// + /// + Rating: WinJS.Class.define(function Rating_ctor(element, options) { + /// + /// + /// Creates a new Rating. + /// + /// + /// The DOM element that hosts the new Rating. + /// + /// + /// 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 cancel event, + /// add a property named "oncancel" to the options object and set its value to the event handler. + /// This parameter is optional. + /// + /// + /// The new Rating. + /// + /// + element = element || document.createElement("div"); + options = options || {}; + this._element = element; + + //initialize properties with default value + this._userRating = 0; + this._averageRating = 0; + this._disabled = DEFAULT_DISABLED; + this._enableClear = true; + this._tooltipStrings = []; + + this._controlUpdateNeeded = false; + this._setControlSize(options.maxRating); + if (!options.tooltipStrings) { + this._updateTooltips(null); + } + WinJS.UI.setOptions(this, options); + this._controlUpdateNeeded = true; + this._forceLayout(); + + // Remember ourselves + element.winControl = this; + this._events(); + }, { + /// + /// Gets or sets the maximum possible rating value. The default is 5. + /// + maxRating: { + get: function () { + return this._maxRating; + }, + set: function (value) { + this._setControlSize(value); + this._forceLayout(); + } + }, + + /// + /// Gets or sets the user's rating. This value must be between greater than or equal to zero and less than or equal to the maxRating. + /// + userRating: { + get: function () { + return this._userRating; + }, + set: function (value) { + // Coerce value to a positive integer between 0 and maxRating + this._userRating = Math.max(0, Math.min(Number(value) >> 0, this._maxRating)); + this._updateControl(); + } + }, + + /// + /// Gets or sets the average rating as a float value. This value must be [equal to zero] OR [greater than or equal to 1 AND less than or equal to the maxRating]. + /// + averageRating: { + get: function () { + return this._averageRating; + }, + set: function (value) { + // Coerce value to either 0 or a positive float between 1 and maxRating + this._averageRating = (Number(value) < 1) ? 0 : Math.min(Number(value) || 0, this._maxRating); + if (this._averageRatingElement) { // After the control has been created.. + this._ensureAverageMSStarRating(); // ..ensure correct msStarRating is given to 'average-rating' star. + } + this._updateControl(); + } + }, + + /// + /// Gets or sets a value that specifies whether the control is disabled. When the control is disabled, the user can't specify a + /// new rating or modify an existing rating. + /// + disabled: { + get: function () { + return this._disabled; + }, + set: function (value) { + this._disabled = !!value; + if (this._disabled) { + this._clearTooltips(); + } + this._updateTabIndex(); + this._updateControl(); + } + }, + + /// + /// Gets or sets whether the control lets the user clear the rating. + /// + enableClear: { + get: function () { + return this._enableClear; + }, + set: function (value) { + this._enableClear = !!value; + this._setAriaValueMin(); + this._updateControl(); + } + }, + + /// + /// Gets or sets a set of descriptions to show for rating values in the tooltip. The array must + /// contain a string for each available rating value, and it can contain an optional string + /// (at the end of the array) for the clear rating option. + /// + tooltipStrings: { + get: function () { + return this._tooltipStrings; + }, + set: function (value) { + if (typeof value !== "object") { + throw new WinJS.ErrorFromName("WinJS.UI.Rating.TooltipStringsIsInvalid", strings.tooltipStringsIsInvalid); + } + this._updateTooltips(value); + this._updateAccessibilityRestState(); + } + }, + + /// + element: { + get: function () { + return this._element; + } + }, + + addEventListener: function (eventName, eventCallBack, capture) { + /// + /// + /// Registers an event handler for the specified event. + /// + /// The name of the event. + /// The event handler function to associate with this event. + /// Set to true to register the event handler for the capturing phase; set to false to register for the bubbling phase. + /// + + this._element.addEventListener(eventName, eventCallBack, capture); + }, + + removeEventListener: function (eventName, eventCallBack, capture) { + /// + /// + /// Unregisters an event handler for the specified event. + /// + /// The name of the event. + /// The event handler function to remove. + /// Set to true to unregister the event handler for the capturing phase; otherwise, set to false to unregister the event handler for the bubbling phase. + /// + + return this._element.removeEventListener(eventName, eventCallBack, capture); + }, + + _forceLayout: function () { + if (!this._controlUpdateNeeded) { + return; + } + + // Disable incremental update during redraw, postpone till all properties set + var updateNeeded = false; + this._updateControl = function () { + updateNeeded = true; + }; + + // Coerce userRating and averageRating to conform to maxRating + this.userRating = this._userRating; + this.averageRating = this._averageRating; + + // Reset properties + this._lastEventWasChange = false; + this._lastEventWasCancel = false; + this._tentativeRating = -1; + this._captured = false; + this._ariaChangeEnabled = true; + this._pointerDownFocus = false; + this._elements = []; + this._toolTips = []; + this._clearElement = null; + + // Element that is used for showing average rating + this._averageRatingElement = null; + this._elementWidth = null; + this._elementPadding = null; + this._elementBorder = null; + this._floatingValue = 0; + + this._createControl(); + this._setAccessibilityProperties(); + + delete this._updateControl; + if (updateNeeded) { + this._updateControl(); + } + + }, + + // Hide the help star if the control is not showing average rating + _hideAverageRating: function () { + if (!this._averageRatingHidden) { + this._averageRatingHidden = true; + this._averageRatingElement.style.cssText = hiddenAverageRatingCss; + } + }, + + _createControl: function () { + // rating control could have more than one class name + utilities.addClass(this._element, msRating); + + var html = ""; + this._averageRatingHidden = true; + // create control + for (var i = 0; i <= this._maxRating; i++) { + if (i === this._maxRating) { + html = html + "
      "; + } + else { + html = html + "
      "; + } + } + this._element.innerHTML = html; + var oneStar = this._element.firstElementChild; + var i = 0; + while (oneStar) { + this._elements[i] = oneStar; + if (i < this._maxRating){ + WinJS.Utilities.data(oneStar).msStarRating = i + 1; + } + oneStar = oneStar.nextElementSibling; + i++; + } + this._averageRatingElement = this._elements[this._maxRating]; + this._ensureAverageMSStarRating(); + + // add focus capability relative to element's position in the document + this._updateTabIndex(); + }, + + _setAriaValueMin: function () { + this._element.setAttribute("aria-valuemin", this._enableClear ? 0 : 1); + }, + + _setAccessibilityProperties: function () { + this._element.setAttribute("role", "slider"); + this._element.setAttribute("aria-valuemax", this._maxRating); + this._setAriaValueMin(); + this._updateAccessibilityRestState(); + }, + + _getText: function (number) { + var string = this._tooltipStrings[number]; + if (string) { + var tempDiv = document.createElement("div"); + tempDiv.innerHTML = string; + return tempDiv.innerText; + } else if (number === this._maxRating) { + return strings.clearYourRating; + } else { + return number + 1; + } + }, + + _updateAccessibilityRestState: function () { + var element = this._element; + this._ariaChangeEnabled = false; + element.setAttribute("aria-readOnly", this._disabled); + + if (this._userRating !== 0) { + element.setAttribute("aria-valuenow", this._userRating); + element.setAttribute("aria-label", strings.userRating); + element.setAttribute("aria-valuetext", this._getText(this._userRating - 1)); + } else if (this._averageRating !== 0) { + element.setAttribute("aria-valuenow", this._averageRating); + element.setAttribute("aria-label", strings.averageRating); + element.setAttribute("aria-valuetext", this._averageRating); + } else { + element.setAttribute("aria-valuenow", strings.unrated); + element.setAttribute("aria-label", strings.userRating); + element.setAttribute("aria-valuetext", strings.unrated); + } + this._ariaChangeEnabled = true; + }, + + _updateAccessibilityHoverState: function () { + var element = this._element; + this._ariaChangeEnabled = false; + element.setAttribute("aria-readOnly", this._disabled); + + if (this._tentativeRating > 0) { + element.setAttribute("aria-label", strings.tentativeRating); + element.setAttribute("aria-valuenow", this._tentativeRating); + element.setAttribute("aria-valuetext", this._getText(this._tentativeRating - 1)); + } else if (this._tentativeRating === 0) { + element.setAttribute("aria-valuenow", strings.unrated); + element.setAttribute("aria-label", strings.tentativeRating); + element.setAttribute("aria-valuetext", this._getText(this._maxRating)); + } else { + //shouldn't get here + element.setAttribute("aria-valuenow", strings.unrated); + element.setAttribute("aria-label", strings.tentativeRating); + element.setAttribute("aria-valuetext", strings.unrated); + } + this._ariaChangeEnabled = true; + }, + + _ensureTooltips: function () { + if (this.disabled) { + return; + } + + if (this._toolTips.length === 0) { + for (var i = 0; i < this._maxRating; i++) { + this._toolTips[i] = new WinJS.UI.Tooltip(this._elements[i]); + } + } + }, + + // decrement tentative rating by one + _decrementRating: function () { + this._closeTooltip(); + var firePreviewChange = true; + if ((this._tentativeRating === 0) || ((this._tentativeRating === -1) && (this._userRating === 0))) { + firePreviewChange = false; + } else { + if (this._tentativeRating > 0) { + this._tentativeRating--; + } else if (this._tentativeRating === -1) { + if (this._userRating !== 0) { + if (this._userRating > 0) { + this._tentativeRating = this._userRating - 1; + } else { + this._tentativeRating = 0; + } + } else { + this._tentativeRating = 0; + } + } + + if ((this._tentativeRating === 0) && !this._enableClear) { + this._tentativeRating = 1; + firePreviewChange = false; + } + } + + this._showTentativeRating(firePreviewChange, "keyboard"); + }, + + _events: function () { + var that = this; + function ratingHandler(eventName) { + return { + name: eventName, + lowerCaseName: eventName.toLowerCase(), + handler: function (event) { + var fn = that["_on" + eventName]; + if (fn) { + fn.apply(that, [event]); + } + } + }; + } + + var eventsRegisteredInLowerCase = [ + ratingHandler("KeyDown"), + ratingHandler("Blur"), + ratingHandler("Focus") + ]; + var events = [ + ratingHandler("MSPointerCancel"), + ratingHandler("MSPointerDown"), + ratingHandler("MSPointerMove"), + ratingHandler("MSPointerOver"), + ratingHandler("MSPointerUp"), + ratingHandler("MSPointerOut"), + ratingHandler("DOMNodeInserted"), + ratingHandler("DOMAttrModified") + ]; + + var i; + for (i = 0; i < eventsRegisteredInLowerCase.length; ++i) { + this._element.addEventListener(eventsRegisteredInLowerCase[i].lowerCaseName, eventsRegisteredInLowerCase[i].handler, false); + } + for (i = 0; i < events.length; ++i) { + this._element.addEventListener(events[i].name, events[i].handler, false); + } + }, + + _onDOMNodeInserted: function (eventObject) { + if (eventObject.target === this._element) { + this._recalculateStarProperties(); + this._updateControl(); + } + }, + + _recalculateStarProperties: function () { + var j = 0; + // If the average rating is 1 we do not have correct padding on the first star so we are reading it from the second star + // When we create average rating star we are creating it from 2 divs - stars. The first one is the average rating star the second one is the regular rating star. + // If the average rating is 1 we are creating that rating on the following way - The first part of star + // (without right padding, right border) is average rating star - the second part is regular star that does not have left padding and left border anymore + // (we set on 0 to create average rating star). In that situation the average rating star has correct left padding and left border. + if (this._averageRating === 1) { + j = 1; + } + var style = this._elements[j].currentStyle; + this._elementWidth = style.width; + if (this._element.currentStyle.direction === "rtl") { + this._elementPadding = style.paddingRight; + this._elementBorder = style.borderRight; + } else { + this._elementPadding = style.paddingLeft; + this._elementBorder = style.borderLeft; + } + }, + + // Hide the help star if the control is not showing average rating + _hideAverageStar: function () { + // check if this average rating control + if (this._averageRating !== 0) { + // hide the empty star + this._resetAverageStar(false); + } + }, + + // increase tentative rating by one + _incrementRating: function () { + this._closeTooltip(); + var firePreviewChange = true; + if ((this._tentativeRating === this._maxRating) || ((this._tentativeRating === -1) && (this._userRating === this._maxRating))) { + firePreviewChange = false; + } + + if (this._tentativeRating !== -1) { + if (this._tentativeRating < this._maxRating) { + this._tentativeRating++; + } + } else { + if (this._userRating !== 0) { + if (this._userRating < this._maxRating) { + this._tentativeRating = this._userRating + 1; + } else { + this._tentativeRating = this._maxRating; + } + } else { + this._tentativeRating = 1; + } + } + this._showTentativeRating(firePreviewChange, "keyboard"); + }, + + _onDOMAttrModified: function (eventObject) { + var attrName = eventObject.attrName; + if ((attrName === "dir") || (attrName === "style.direction")) { + this._resetAverageStar(true); + this._updateControl(); + // We need to update the control when aria-valuenow is set and we are not in the middle of an interaction. + } else if (!this._disabled && (attrName === "aria-valuenow") && this._ariaChangeEnabled) { + var attrNode = this._element.getAttributeNode("aria-valuenow"); + if (attrNode !== null) { + this.userRating = attrNode.nodeValue; + this._tentativeRating = this._userRating; + this._raiseEvent(CHANGE, this._userRating); + } + } + }, + + _onMSPointerCancel: function (eventObject) { + this._showCurrentRating(); + if (!this._lastEventWasChange) { + this._raiseEvent(CANCEL, null); + } + }, + + _onMSPointerDown: function (eventObject) { + if (eventObject.pointerType === PT_MOUSE && eventObject.button !== MOUSE_LBUTTON) { + return; // Ignore any mouse clicks that are not left clicks. + } + if (!this._captured) { // Rating Control does not support multi-touch, ignore mspointerdown messages if the control already has capture. + this._pointerDownAt = { x: eventObject.clientX, y: eventObject.clientY }; + this._pointerDownFocus = true; + if (!this._disabled) { + // Only capture the event when active to support block panning + this._element.msSetPointerCapture(eventObject.pointerId); + this._captured = true; + + if (eventObject.pointerType === PT_TOUCH) { + this._tentativeRating = WinJS.Utilities.data(eventObject.srcElement).msStarRating || 0; + // change states for all stars + this._setStarClasses(msRatingTentativeFull, this._tentativeRating, msRatingTentativeEmpty); + this._hideAverageStar(); + this._updateAccessibilityHoverState(); + this._openTooltip("touch"); + this._raiseEvent(PREVIEW_CHANGE, this._tentativeRating); + } else { + this._openTooltip("mousedown"); + } + } + } + }, + + _onPointerMove: function (eventObject, tooltipType) { + // Manual hit-test because we capture the pointer + // If the pointer is already down, we use its information. + var pointerAt = this._pointerDownAt || { x: eventObject.clientX, y: eventObject.clientY }; + + var star; + var hit = document.msElementsFromPoint(eventObject.clientX, pointerAt.y); + if (hit) { + for (var i = 0, len = hit.length; i < len; i++) { + var item = hit[i]; + if (item.getAttribute("role") === "tooltip") { + return; + } + if (WinJS.Utilities.hasClass(item, "win-star")) { + star = item; + break; + } + } + } + var starNum; + if (star && (star.parentElement === this._element)) { + starNum = WinJS.Utilities.data(star).msStarRating || 0; + } + else { + var left = 0, right = this.maxRating; + if (this._element.currentStyle.direction === "rtl") { + left = right; + right = 0; + } + if (eventObject.clientX < pointerAt.x) { + starNum = left; + } + else { + starNum = right; + } + } + + var firePreviewChange = false; + var newTentativeRating = Math.min(Math.ceil(starNum), this._maxRating); + if ((newTentativeRating === 0) && !this._enableClear) { + newTentativeRating = 1; + } + if (newTentativeRating !== this._tentativeRating) { + this._closeTooltip(); + firePreviewChange = true; + } + + this._tentativeRating = newTentativeRating; + this._showTentativeRating(firePreviewChange, tooltipType); + }, + + _onMSPointerMove: function (eventObject) { + if (this._captured) { + if (eventObject.pointerType === PT_TOUCH) { + this._onPointerMove(eventObject, "touch"); + } else { + this._onPointerMove(eventObject, "mousedown"); + } + } + }, + + _onMSPointerOver: function (eventObject) { + if (!this._disabled && (eventObject.pointerType === PT_PEN || eventObject.pointerType === PT_MOUSE)) { + this._onPointerMove(eventObject, "mouseover"); + } + }, + + _onMSPointerUp: function (eventObject) { + if (this._captured) { + this._element.msReleasePointerCapture(eventObject.pointerId); + this._captured = false; + this._onUserRatingChanged(); + } + this._pointerDownAt = null; + }, + + _onBlur: function () { + if (!this._captured) { + this._onUserRatingChanged(); + if (!this._lastEventWasChange && !this._lastEventWasCancel) { + this._raiseEvent(CANCEL, null); + } + } + }, + + _onFocus: function () { + if (!this._pointerDownFocus) { + // if the control is read only don't hover stars + if (!this._disabled) { + // change states for all previous stars + // but only if user didnt vote + if (this._userRating === 0) { + for (var i = 0; i < this._maxRating; i++) { + this._elements[i].className = msRatingTentativeEmpty; + } + } + // hide the help star + this._hideAverageStar(); + } + + if (this._userRating !== 0) { + this._raiseEvent(PREVIEW_CHANGE, this._userRating); + } else { + this._raiseEvent(PREVIEW_CHANGE, 0); + } + this._tentativeRating = this._userRating; + } + this._pointerDownFocus = false; + }, + + _onKeyDown: function (eventObject) { + var Key = utilities.Key; + var keyCode = eventObject.keyCode; + var rtlString = this._element.currentStyle.direction; + var handled = true; + switch (keyCode) { + case Key.enter: // Enter + this._onUserRatingChanged(); + break; + case Key.tab: //Tab + this._onUserRatingChanged(); + handled = false; + break; + case Key.escape: // escape + this._showCurrentRating(); + + if (!this._lastEventWasChange) { + this._raiseEvent(CANCEL, null); + } + + break; + case Key.leftArrow: // Arrow Left + if (rtlString === "rtl") { + this._incrementRating(); + } else { + this._decrementRating(); + } + break; + case Key.upArrow: // Arrow Up + this._incrementRating(); + break; + case Key.rightArrow: // Arrow Right + if (rtlString === "rtl") { + this._decrementRating(); + } else { + this._incrementRating(); + } + break; + case Key.downArrow: // Arrow Down + this._decrementRating(); + break; + default: + var number = 0; + if ((keyCode >= Key.num0) && (keyCode <= Key.num9)) { + number = Key.num0; + } else if ((keyCode >= Key.numPad0) && (keyCode <= Key.numPad9)) { + number = Key.numPad0; + } + + if (number > 0) { + var firePreviewChange = false; + var newTentativeRating = Math.min(keyCode - number, this._maxRating); + if ((newTentativeRating === 0) && !this._enableClear) { + newTentativeRating = 1; + } + if (newTentativeRating !== this._tentativeRating) { + this._closeTooltip(); + firePreviewChange = true; + } + this._tentativeRating = newTentativeRating; + this._showTentativeRating(firePreviewChange, "keyboard"); + } else { + handled = false; + } + } + + if (handled) { + eventObject.stopPropagation(); + eventObject.preventDefault(); + } + }, + + _onMSPointerOut: function (eventObject) { + if (!this._captured && !utilities.eventWithinElement(this._element, eventObject)) { + this._showCurrentRating(); + if (!this._lastEventWasChange) { + // only fire cancel event if we move out of the rating control, and if + // user did not change rating on the control + this._raiseEvent(CANCEL, null); + } + } + }, + + _onUserRatingChanged: function () { + if (!this._disabled) { + this._closeTooltip(); + // Only submit a change event if the user has altered the rating control value via PREVIEWCHANGE event. + if (this._userRating !== this._tentativeRating && !this._lastEventWasCancel && !this._lastEventWasChange) { + this.userRating = this._tentativeRating; + this._raiseEvent(CHANGE, this._userRating); + } else { + this._updateControl(); + } + } + }, + + _raiseEvent: function (eventName, tentativeRating) { + if (!this._disabled) { + this._lastEventWasChange = (eventName === CHANGE); + this._lastEventWasCancel = (eventName === CANCEL); + if (document.createEvent) { + var event = document.createEvent("CustomEvent"); + event.initCustomEvent(eventName, false, false, { tentativeRating: tentativeRating }); + this._element.dispatchEvent(event); + } + } + }, + + _resetNextElement: function (prevState) { + if (this._averageRatingElement.nextSibling !== null) { + var style = this._averageRatingElement.nextSibling.style; + style.msFlexPositive = 1; style.msFlexNegative = 1; + var direction = this._element.currentStyle.direction; + if (prevState) { + if (direction === "rtl") { + direction = "ltr"; + } else { + direction = "rtl"; + } + } + if (direction === "rtl") { + style.paddingRight = this._elementPadding; + style.borderRight = this._elementBorder; + style.direction = "rtl"; + } else { + style.paddingLeft = this._elementPadding; + style.borderLeft = this._elementBorder; + style.direction = "ltr"; + } + style.backgroundPosition = "left"; + style.backgroundSize = "100% 100%"; + style.width = this._resizeStringValue(this._elementWidth, 1, style.width); + } + }, + + _resetAverageStar: function (prevState) { + this._resetNextElement(prevState); + this._hideAverageRating(); + }, + + _resizeStringValue: function (string, factor, curString) { + var number = parseFloat(string); + if (isNaN(number)) { + if (curString !== null) { + return curString + } else { + return string; + } + } + var unit = string.substring(number.toString(10).length); + number = number * factor; + return (number + unit); + }, + + _setControlSize: function (value) { + // Coerce value to a positive integer between 0 and maxRating + // if negative default to DEFAULT_MAX_RATING + var maxRating = (Number(value) || DEFAULT_MAX_RATING) >> 0; + this._maxRating = maxRating > 0 ? maxRating : DEFAULT_MAX_RATING; + }, + + _updateTooltips: function (value) { + var i, max = 0; + if (value !== null) { + max = ((value.length <= this._maxRating + 1) ? value.length : this._maxRating + 1); + for (i = 0; i < max; i++) { + this._tooltipStrings[i] = value[i]; + } + } else { + for (i = 0; i < this._maxRating; i++) { + this._tooltipStrings[i] = i + 1; + } + this._tooltipStrings[this._maxRating] = strings.clearYourRating; + } + }, + + _updateTabIndex: function () { + this._element.tabIndex = (this._disabled ? "-1" : "0"); + }, + + _setStarClasses: function (classNameBeforeThreshold, threshold, classNameAfterThreshold) { + for (var i = 0; i < this._maxRating; i++) { + if (i < threshold) { + this._elements[i].className = classNameBeforeThreshold; + } else { + this._elements[i].className = classNameAfterThreshold; + } + } + }, + + // Average rating star is created from 2 divs: + // In the first div the glyph starts from the beginning in the direction of the control + // In the second div the glyph starts from the beginning in the opposite direction + // That way we are making the average star look like one glyph + _updateAverageStar: function () { + var style = this._averageRatingElement.style; + var nextStyle = this._averageRatingElement.nextSibling.style; + if (this._element.currentStyle.direction == "rtl") { + style.backgroundPosition = "right"; + style.paddingRight = this._elementPadding; + style.borderRight = this._elementBorder; + nextStyle.paddingRight = "0px"; + nextStyle.borderRight = "0px"; + nextStyle.direction = "ltr"; + } else { + style.backgroundPosition = "left"; + nextStyle.backgroundPosition = "right"; + style.paddingLeft = this._elementPadding; + style.borderLeft = this._elementBorder; + nextStyle.paddingLeft = "0px"; + nextStyle.borderLeft = "0px"; + nextStyle.direction = "rtl"; + } + style.width = this._resizeStringValue(this._elementWidth, this._floatingValue, style.width); + style.msFlexPositive = this._floatingValue; style.msFlexNegative = this._floatingValue; + style.backgroundSize = (100 / this._floatingValue) + "% 100%"; + style.display = this._averageRatingElement.nextSibling.currentStyle.display; + this._averageRatingHidden = false; + nextStyle.msFlexPositive = 1 - this._floatingValue; nextStyle.msFlexNegative = 1 - this._floatingValue; + nextStyle.width = this._resizeStringValue(this._elementWidth, 1 - this._floatingValue, nextStyle.width); + nextStyle.backgroundSize = (100 / (1 - this._floatingValue)) + "% 100%"; + }, + + // show current rating + _showCurrentRating: function () { + this._closeTooltip(); + // reset tentative rating + this._tentativeRating = -1; + // if the control is read only then we didn't change anything on hover + if (!this._disabled) { + this._updateControl(); + } + this._updateAccessibilityRestState(); + }, + + _showTentativeRating: function (firePreviewChange, tooltipType) { + // if the control is read only don't hover stars + if ((!this._disabled) && (this._tentativeRating >= 0)) { + this._setStarClasses(msRatingTentativeFull, this._tentativeRating, msRatingTentativeEmpty); + + // hide the empty star + this._hideAverageStar(); + } + + this._updateAccessibilityHoverState(); + + if (firePreviewChange) { + this._openTooltip(tooltipType); + this._raiseEvent(PREVIEW_CHANGE, this._tentativeRating); + } + }, + + _openTooltip: function (tooltipType) { + if (this.disabled) { + return; + } + + this._ensureTooltips(); + if (this._tentativeRating > 0) { + this._toolTips[this._tentativeRating - 1].innerHTML = this._tooltipStrings[this._tentativeRating - 1]; + this._toolTips[this._tentativeRating - 1].open(tooltipType); + } else if (this._tentativeRating === 0) { + this._clearElement = document.createElement("div"); + var distance = this._elements[0].offsetWidth + parseInt(this._elementPadding, 10); + if (this._element.currentStyle.direction === "ltr") { + distance *= -1; + } + this._clearElement.style.cssText ="visiblity:hidden; position:absolute; width:0px; height:100%; left:" + distance + "px; top:0px;"; + this._elements[0].appendChild(this._clearElement); + this._toolTips[this._maxRating] = new WinJS.UI.Tooltip(this._clearElement); + this._toolTips[this._maxRating].innerHTML = this._tooltipStrings[this._maxRating]; + this._toolTips[this._maxRating].open(tooltipType); + } + }, + + _closeTooltip: function (tooltipType) { + if (this._toolTips.length !== 0) { + if (this._tentativeRating > 0) { + this._toolTips[this._tentativeRating - 1].close(); + } else if (this._tentativeRating === 0) { + if (this._clearElement !== null) { + this._toolTips[this._maxRating].close(); + this._elements[0].removeChild(this._clearElement); + this._clearElement = null; + } + } + } + }, + + _clearTooltips: function () { + if (this._toolTips && this._toolTips.length !== 0) { + for (var i = 0; i < this._maxRating; i++) { + this._toolTips[i].innerHTML = null; + } + } + }, + + _appendClass: function (classNameToBeAdded) { + for (var i = 0; i <= this._maxRating; i++) { + utilities.addClass(this._elements[i], classNameToBeAdded); + } + }, + + _setClasses: function (classNameBeforeThreshold, threshold, classNameAfterThreshold) { + for (var i = 0; i < this._maxRating; i++) { + if (i < threshold) { + this._elements[i].className = classNameBeforeThreshold; + } else { + this._elements[i].className = classNameAfterThreshold; + } + } + }, + + _ensureAverageMSStarRating: function() { + WinJS.Utilities.data(this._averageRatingElement).msStarRating = Math.ceil(this._averageRating); + }, + + _updateControl: function () { + if (!this._controlUpdateNeeded) { + return; + } + + // check for average rating (if user rating is specified then we are not showing average rating) + if ((this._averageRating !== 0) && (this._userRating === 0)) { + if ((this._averageRating >= 1) && (this._averageRating <= this._maxRating)) { // Display average rating + this._setClasses(msRatingAverageFull, this._averageRating - 1, msRatingAverageEmpty); + this._averageRatingElement.className = msRatingAverageFull; + + for (var i = 0; i < this._maxRating; i++) { + // check if it is average star + if ((i < this._averageRating) && ((i + 1) >= this._averageRating)) { + this._resetNextElement(false); + + this._element.insertBefore(this._averageRatingElement, this._elements[i]); + + this._floatingValue = this._averageRating - i; + this._elementWidth = this._elements[i].currentStyle.width; + + if (this._element.currentStyle.direction == "rtl") { + this._elementPadding = this._elements[i].currentStyle.paddingRight; + this._elementBorder = this._elements[i].currentStyle.borderRight; + } else { + this._elementPadding = this._elements[i].currentStyle.paddingLeft; + this._elementBorder = this._elements[i].currentStyle.borderLeft; + } + + this._updateAverageStar(); + } + } + } + } + + // check if it is user rating control + if (this._userRating !== 0) { + if ((this._userRating >= 1) && (this._userRating <= this._maxRating)) { // Display user rating. + this._setClasses(msRatingUserFull, this._userRating, msRatingUserEmpty); + + // hide average star + this._resetAverageStar(false); + } + } + + // update stars if the rating is not set + if ((this._userRating === 0) && (this._averageRating === 0)) { // Display empty rating + this._setClasses(msRatingEmpty, this._maxRating); + + // hide average star + this._resetAverageStar(false); + } + + if (this.disabled) { // Display disabled rating. + this._appendClass(msRatingDisabled); + } + + // update classes to differentiate average rating vs user rating + // If the userRating is 0 and averageRating is 0 we would like to treat that rating control as user rating control (not as average rating control). + if ((this._averageRating !== 0) && (this._userRating === 0)) { + this._appendClass(msAverage); + } else { + this._appendClass(msUser); + } + + this._updateAccessibilityRestState(); + } + }) +}); + +// Rating support for "on" properties +WinJS.Class.mix(WinJS.UI.Rating, WinJS.Utilities.createEventProperties( + CANCEL, + CHANGE, + PREVIEW_CHANGE)); + +})(this, WinJS); +(function toggleInit(global) { + "use strict"; + + // Constants definition + var MOUSE_LBUTTON = 0; // left button of the mouse + + var strings = { + get on() { return WinJS.Resources._getWinJSString("ui/on").value; }, + get off() { return WinJS.Resources._getWinJSString("ui/off").value; }, + }; + + // CSS class names + var msToggle = "win-toggleswitch"; + var msToggleSwitch = "win-switch"; + var msToggleTitle = "win-title"; + var msToggleLabel = "win-label"; + var msToggleOn = "win-on"; + var msToggleOff = "win-off"; + var msToggleDisabled = "win-disabled"; + var msToggleHidden = "win-hidden"; + var msFocusHide = "win-focus-hide"; + + var Control = WinJS.Class.define(null, { + raiseEvent: function (type, eventProperties) { + this.dispatchEvent(type, eventProperties); + } + }); + + var utilities = WinJS.Utilities; + + function reloadChangeHandler(event) { + if (event.propertyName === "defaultValue") { + var that = event.srcElement.winControl; + that.checked = that._switchElement.valueAsNumber; + } + }; + + WinJS.Class.mix(Control, WinJS.UI.DOMEventMixin); + + WinJS.Namespace.define("WinJS.UI", { + /// + /// A control that lets the user switch an option on or off. + /// + /// + /// + /// ]]> + /// Raised when the switch is flipped to on (checked is set to true) or off (checked is set to false). + /// The entire ToggleSwitch control. + /// The slider that enables the user to switch the state of the ToggleSwitch. + /// The main text for the ToggleSwitch control. + /// The text for when the switch is on. + /// The text for when the switch is off. + /// + /// + /// + ToggleSwitch: WinJS.Class.derive(Control, function (element, options) { + /// + /// + /// Creates a new ToggleSwitch. + /// + /// + /// The DOM element that hosts the ToggleSwitch. + /// + /// + /// 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 change event, + /// add a property named "onchange" to the options object and set its value to the event handler. + /// This parameter is optional. + /// + /// + /// The new ToggleSwitch. + /// + /// + + element = element || document.createElement("div"); + + var toggle = utilities.data(element).toggle; + if (toggle) { + return toggle; + } + + // Elements + this._domElement = null; + this._switchElement = null; + this._titleElement = null; + this._labelGridElement = null; + this._labelOnElement = null; + this._labelOffElement = null; + + + // Strings + this._labelOn = strings.on; + this._labelOff = strings.off; + + // Variable + this._spaceKeyDown = false; + + this._gesture = new MSGesture(); // Add the gesture object before creating the listeners in _setElement for gesture events handling + this._shouldHideFocus = false; // This variable is needed to prevent focus rect from showing between the time during pointer down and focus happens. + this._pointerId = 0; + this._hasCapture = false; + + this._setElement(element); + this._setDefaultOptions(); + WinJS.UI.setOptions(this, options); + element.winControl = this; + utilities.data(element).toggle = this; + }, { + // Properties + + /// + /// Gets or sets whether the control is on (checked is set to true) or off (checked is set to false). + /// + checked: { + get: function () { + return this._checked; + }, + set: function (value) { + this._setChecked(value); + } + }, + /// + /// Gets or sets a value that specifies whether the control is disabled. + /// + disabled: { + get: function () { + return this._switchElement.disabled; + }, + set: function (value) { + var disabled = !!value; // Sanitize for a bool + this._switchElement.disabled = disabled; // This is necessary to apply the css to the toggle 'switch' + if (disabled == true) { // This is necessary to apply the css to the toggle 'label' and 'title' + utilities.addClass(this._labelOnElement, msToggleDisabled); + utilities.addClass(this._labelOffElement, msToggleDisabled); + utilities.addClass(this._titleElement, msToggleDisabled); + } else { + utilities.removeClass(this._labelOnElement, msToggleDisabled); + utilities.removeClass(this._labelOffElement, msToggleDisabled); + utilities.removeClass(this._titleElement, msToggleDisabled); + } + this._switchElement.setAttribute("aria-disabled", disabled); + } + }, + /// + element: { + get: function () { return this._domElement; } + }, + /// + /// Gets or sets the text that displays when the control is on (checked is set to true). The default value is "On". + /// + labelOn: { + get: function () { + return this._labelOn; + }, + set: function (value) { + this._labelOn = value; + this._labelOnElement.innerHTML = this._labelOn; + } + }, + /// + /// Gets or sets the text that displays when the control is off (checked is set to false). The default value is "Off". + /// + labelOff: { + get: function () { + return this._labelOff; + }, + set: function (value) { + this._labelOff = value; + this._labelOffElement.innerHTML = this._labelOff; + } + }, + + /// + /// Gets or sets the main text for the ToggleSwitch control. This text is always displayed, regardless of whether + /// the control is switched on or off. + /// + title: { + get: function () { + return this._titleElement.innerHTML; + }, + set: function (value) { + this._titleElement.innerHTML = value; + } + }, + + _addControlsInOrder: function () { + this._domElement.appendChild(this._titleElement); + this._labelGridElement.appendChild(this._labelOnElement); + this._labelGridElement.appendChild(this._labelOffElement); + this._labelGridElement.appendChild(this._switchElement); + this._domElement.appendChild(this._labelGridElement); + }, + + _setChecked: function (value) { + value = !!value; // Sanitize the value + if (value !== this._checked) { + this._checked = value; + if (this._checked) { // On state + utilities.removeClass(this._domElement, msToggleOff); + utilities.addClass(this._domElement, msToggleOn); + utilities.addClass(this._labelOffElement, msToggleHidden); + utilities.removeClass(this._labelOnElement, msToggleHidden); + this._switchElement.valueAsNumber = 1; // Update the slider visual + } else { // Off state + utilities.removeClass(this._domElement, msToggleOn); + utilities.addClass(this._domElement, msToggleOff); + utilities.addClass(this._labelOnElement, msToggleHidden); + utilities.removeClass(this._labelOffElement, msToggleHidden); + this._switchElement.valueAsNumber = 0; // Update the slider visual + } + this._switchElement.setAttribute("aria-checked", this._checked); // Update accessibility information + } + }, + + _setDefaultOptions: function () { + this.labelOn = strings.on; + this.labelOff = strings.off; + this.title = ""; + this.checked = false; + this.disabled = false; + }, + + _setElement: function (element) { + this._domElement = element; + utilities.addClass(this._domElement, msToggle); + utilities.addClass(this._domElement, msToggleOff); + + this._titleElement = document.createElement("div"); + this._titleElement.setAttribute("id", this._titleElement.uniqueID); + this._titleElement.setAttribute("role", "note"); + utilities.addClass(this._titleElement, msToggleTitle); + + this._switchElement = document.createElement("input"); + this._switchElement.type = "range"; + this._switchElement.max = 1; + this._switchElement.step = 1; + this._switchElement.setAttribute("role", "checkbox"); + this._switchElement.setAttribute("aria-labelledby", this._titleElement.id); + utilities.addClass(this._switchElement, msToggleSwitch); + + this._labelGridElement = document.createElement("div"); + this._labelGridElement.style.display = "-ms-grid"; + + this._labelOnElement = document.createElement("div"); + utilities.addClass(this._labelOnElement, msToggleLabel); + + this._labelOffElement = document.createElement("div"); + utilities.addClass(this._labelOffElement, msToggleLabel); + + this._addControlsInOrder(); + + this._wireupEvents(); + }, + + + _valueHandler: function (fTapped) { + var oldValue = this._checked; + if (fTapped) { + this.checked = !this.checked; + } else { + this.checked = this._switchElement.valueAsNumber; + } + + if (oldValue !== this._checked) { + this.raiseEvent("change"); + } + }, + + _wireupEvents: function () { + var that = this; + var pointerUpHandler = function (event) { + if (event.pointerId == that._pointerId) { + that._valueHandler(false); + } + }; + var spaceDownHandler = function (event) { + if (event.keyCode === utilities.Key.space) { // Spacebar + if (!that._spaceKeyDown) { + that._switchElement.valueAsNumber = (that._switchElement.valueAsNumber + 1) % 2; + that._spaceKeyDown = true; + } + event.preventDefault(); + } + }; + var keyUpHandler = function (event) { + if (event.keyCode === utilities.Key.space || (event.keyCode >= utilities.Key.end && event.keyCode <= utilities.Key.downArrow)) { // Spacebar and arrow, home/end key + that._valueHandler(false); + if (event.keyCode === utilities.Key.space) { // Additional step for spacebar + that._spaceKeyDown = false; + } + } + }; + var tapHandler = function () { + that._valueHandler(true); + }; + var cancelHandler = function () { + that._switchElement.valueAsNumber = that.checked; + that._spaceKeyDown = false; // Reset flag on spaceKey + }; + var dragHandler = function (event) { + if (!that._switchElement.disabled) { + // touch or the left button of mouse is down + if (!that._hasCapture && event.type == "MSGestureChange") { + try { + that._switchElement.msSetPointerCapture(that._pointerId); + that._hasCapture = true; + } catch (err) {} + } + else if (that._hasCapture && event.type == "MSGestureEnd") { + try { + that._hasCapture = false; + } catch (err) {} + } + } + }; + var trackTap = function (event) { + if (!that._switchElement.disabled && (event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === event.MSPOINTER_TYPE_PEN || event.button === MOUSE_LBUTTON) ) { + if (!that._hasCapture) { + try { + // since toggle only needs to track one finger, + // it's better to reset the old pointerId and only track one + that._gesture.stop(); + that._gesture.target = event.target; + that._gesture.addPointer(event.pointerId); + that._pointerId = event.pointerId; + } catch (err) {} + } + // Prevent default behavior for these events + event.preventDefault(); + switchFocus(); + } + }; + var onDOMAttrModified = function (event) { + if (event.attrName === "aria-checked") { + var attrNode = that._switchElement.getAttributeNode("aria-checked"); + if (attrNode !== null) { + var oldValue = that._checked; + + if (attrNode.nodeValue === "true") { // "nodeValue" is a string + that._setChecked(true); + } + else { + that._setChecked(false); + } + + if (oldValue !== that._checked) { + that.raiseEvent("change"); + } + } + } + }; + var switchFocus = function () { + that._switchElement.focus(); + that._shouldHideFocus = false; + }; + var dismissFocusRect = function () { + utilities.addClass(that._switchElement, msFocusHide); + that._shouldHideFocus = true; + }; + var enableFocusRect = function () { + if (!that._shouldHideFocus) { + utilities.removeClass(that._switchElement, msFocusHide); + } + }; + this._switchElement.addEventListener("MSPointerCancel", cancelHandler, false); + this._switchElement.addEventListener("MSLostPointerCapture", cancelHandler, false); + this._switchElement.addEventListener("MSPointerUp", pointerUpHandler, false); + this._switchElement.addEventListener("MSGestureChange", dragHandler, false); + this._switchElement.addEventListener("MSGestureEnd", dragHandler, false); + this._switchElement.addEventListener("MSGestureTap", tapHandler, false); + this._switchElement.addEventListener("keydown", spaceDownHandler, false); + this._switchElement.addEventListener("keyup", keyUpHandler, false); + this._switchElement.attachEvent("onpropertychange", reloadChangeHandler); + this._switchElement.addEventListener("change", function (ev) { ev.stopPropagation(); }, true); // Stop the change event from bubbling up and fire our own change event when the user interaction is done. + + this._switchElement.addEventListener("MSPointerDown", trackTap, false); // Use the gesture object so we could listen to tap events + this._switchElement.addEventListener("DOMAttrModified", onDOMAttrModified, false); // Listen to DOMAttrModified for aria-checked change + this._switchElement.addEventListener("blur", function() { enableFocusRect(); cancelHandler(); }, false); + this._domElement.addEventListener("focus", switchFocus, true); + this._domElement.addEventListener("MSPointerDown", dismissFocusRect, true); + }, + + addEventListener: function (eventName, eventCallBack, capture) { + /// + /// + /// Registers an event handler for the specified event. + /// + /// The name of the event. + /// The event handler function to associate with this event. + /// Set to true to register the event handler for the capturing phase; set to false to register for the bubbling phase. + /// + if (eventName == "change") { + // Set the capture to be false explicitly because we want the change events for Toggle to be listened only in bubble up phase + // Therefore, the change events would only happen when users have finished their actions. + capture = false; + } + this._domElement.addEventListener(eventName, eventCallBack, capture); + + }, + + removeEventListener: function (eventName, eventCallBack, capture) { + /// + /// + /// Unregisters an event handler for the specified event. + /// + /// The name of the event. + /// The event handler function to remove. + /// Set to true to unregister the event handler for the capturing phase; otherwise, set to false to unregister the event handler for the bubbling phase. + /// + if (eventName == "change") { + // Set the capture to be false explicitly because we only allow the user to add change events that are listened to in bubble up phase. + // Therefore it is not possible to remove a change event that is listened to in the capture phase. + capture = false; + } + return this._domElement.removeEventListener(eventName, eventCallBack, capture); + } + }) + }); + +WinJS.Class.mix(WinJS.UI.ToggleSwitch, WinJS.Utilities.createEventProperties("change")); + +})(WinJS); +// Semantic Zoom control +(function semanticZoomInit(global) { + "use strict"; + + + var Utilities = WinJS.Utilities, + UI = WinJS.UI; + + var strings = { + get invalidZoomFactor() { return WinJS.Resources._getWinJSString("ui/invalidZoomFactor").value; }, + }; + + // Private statics + + var sezoButtonClass = "win-semanticzoom-button"; + var sezoButtonLocationClass = "win-semanticzoom-button-location"; + var sezoButtonShowDuration = 3000; + var sezoButtonMouseMoveThreshold = 8; + + var semanticZoomClass = "win-semanticzoom"; + var semanticZoomActiveClass = "win-semanticzoomactive"; + + var zoomChangedEvent = "zoomchanged"; + + var bounceFactor = 1.05; + var defaultZoomFactor = 0.65; // Value used by the shell + // If we change these we need to update the metadata for the zoomFactor property as well. + var maxZoomFactor = 0.8; + var minZoomFactor = 0.2; + + var canvasSizeMax = 4096; + + var outgoingOpacityTransitionDuration = 0.333; + var incomingOpacityTransitionDuration = 0.333; + var outgoingScaleTransitionDuration = 0.333; + var incomingScaleTransitionDuration = 0.333; + var zoomAnimationDuration = outgoingOpacityTransitionDuration * 1000; + var zoomAnimationTTFFBuffer = 50; + // PS 846107 - TransitionEnd event not being fired occassionally if duration is not same + var bounceInDuration = 0.333; + var bounceBackDuration = 0.333; + var easeOutBezier = "cubic-bezier(0.1,0.9,0.2,1)"; + + function buildTransition(prop, duration, timing) { + return prop + " " + duration + "s " + timing + " " + WinJS.UI._libraryDelay + "ms"; + } + function outgoingElementTransition() { + return buildTransition("transform", outgoingScaleTransitionDuration, "ease-in-out") + ", " + + buildTransition("opacity", outgoingOpacityTransitionDuration, "ease-in-out"); + } + + function incomingElementTransition() { + return buildTransition("transform", incomingScaleTransitionDuration, "ease-in-out") + ", " + + buildTransition("opacity", incomingOpacityTransitionDuration, "ease-in-out"); + } + + function bounceInTransition() { + return buildTransition("transform", bounceInDuration, easeOutBezier); + } + + function bounceBackTransition() { + return buildTransition("transform", bounceBackDuration, easeOutBezier); + } + + var pinchDistanceCount = 2; + var zoomOutGestureDistanceChangeFactor = 0.2; + var zoomInGestureDistanceChangeFactor = 0.45; + + var zoomAnimationTimeout = 1000; + + // The semantic zoom has to piece together information from a variety of separate events to get an understanding of the current + // manipulation state. Since these events are altogether separate entities, we have to put a delay between the end of one event + // to allow time for another event to come around. For example, when we handle MSLostPointerCapture events, we need + // to wait because DManip might be taking over. If it is, we'll receive an MSManipulationStateChanged event soon, + // and so we don't want to reset our state back, and need give that event a chance to fire. + var eventTimeoutDelay = 50; + + var PinchDirection = { + none: 0, + zoomedIn: 1, + zoomedOut: 2 + }; + + var PT_TOUCH = 2; + var PT_PEN = 3; + var PT_MOUSE = 4; + + function getDimension(element, property) { + return WinJS.Utilities.convertToPixels(element, property); + } + + function scaleElement(element, scale) { + if (WinJS.UI.isAnimationEnabled()) { + element.style["transform"] = "scale(" + scale + ")"; + } + } + + var origin = { x: 0, y: 0 }; + + function translateElement(element, offset) { + if (WinJS.UI.isAnimationEnabled()) { + element.style["transform"] = "translate(" + offset.x + "px, " + offset.y + "px)"; + } + } + + function onSemanticZoomResize(ev) { + var control = ev.srcElement && ev.srcElement.winControl; + if (control && !control._resizing) { + control._onResize(); + } + } + + function onSemanticZoomPropertyChanged(ev) { + var control = ev.srcElement && ev.srcElement.winControl; + if (control && control instanceof WinJS.UI.SemanticZoom) { + control._onPropertyChanged(ev); + } + } + + WinJS.Namespace.define("WinJS.UI", { + + /// + /// Enables the user to zoom between two different views supplied by two child controls. + /// One child control supplies the zoomed-out view and the other provides the zoomed-in view. + /// + /// + /// + ///
      ]]>
      + /// The entire SemanticZoom control. + /// + /// + /// + SemanticZoom: WinJS.Class.define(function SemanticZoom_ctor(element, options) { + /// + /// + /// Creates a new SemanticZoom. + /// + /// + /// The DOM element that hosts the SemanticZoom. + /// + /// + /// 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. This parameter is optional. + /// + /// + /// The new SemanticZoom control. + /// + /// + + var that = this; + + this._element = element; + this._element.winControl = this; + Utilities.addClass(this._element, semanticZoomClass); + this._element.setAttribute("role", "ms-semanticzoomcontainer"); + var ariaLabel = this._element.getAttribute("aria-label"); + if (!ariaLabel) { + this._element.setAttribute("aria-label", ""); + } + + options = options || {}; + this._zoomedOut = !!options.zoomedOut || !!options.initiallyZoomedOut || false; + this._enableButton = true; + if (options.enableButton !== undefined) { + this._enableButton = !!options.enableButton; + } + + this._element.setAttribute("aria-checked", this._zoomedOut.toString()); + this._zoomFactor = Utilities._clamp(options.zoomFactor, minZoomFactor, maxZoomFactor, defaultZoomFactor); + + var identity = function (item) { return item; }; + this._zoomedInItem = options.zoomedInItem || identity; + this._zoomedOutItem = options.zoomedOutItem || identity; + + if (WinJS.validation) { + if (options._zoomFactor && options._zoomFactor !== this._zoomFactor) { + throw new WinJS.ErrorFromName("WinJS.UI.SemanticZoom.InvalidZoomFactor", strings.invalidZoomFactor); + } + } + + this._locked = false; + + this._zoomInProgress = false; + this._isBouncingIn = false; + this._isBouncing = false; + this._zooming = false; + this._aligning = false; + this._gesturing = false; + this._gestureEnding = false; + this._buttonShown = false; + + // Initialize the control + + this._initialize(); + this._configure(); + + // Register event handlers + + this._element.attachEvent("onresize", onSemanticZoomResize); + this._element.attachEvent("onpropertychange", onSemanticZoomPropertyChanged, false); + this._element.addEventListener("mousewheel", this._onMouseWheel.bind(this), true); + this._element.addEventListener("keydown", this._onKeyDown.bind(this), true); + this._element.addEventListener("MSPointerDown", this._onPointerDown.bind(this), this._isListView); + this._element.addEventListener("MSPointerMove", this._onPointerMove.bind(this), true); + this._element.addEventListener("MSPointerOut", this._onPointerOut.bind(this), true); + this._element.addEventListener("MSPointerCancel", this._onPointerCancel.bind(this), true); + this._element.addEventListener("MSPointerUp", this._onPointerUp.bind(this), false); + this._hiddenElement.addEventListener("MSGotPointerCapture", this._onGotPointerCapture.bind(this), false); + this._hiddenElement.addEventListener("MSLostPointerCapture", this._onLostPointerCapture.bind(this), false); + this._element.addEventListener("click", this._onClick.bind(this), true); + this._canvasIn.addEventListener("transitionend", this._onCanvasTransitionEnd.bind(this), false); + this._canvasOut.addEventListener("transitionend", this._onCanvasTransitionEnd.bind(this), false); + this._resetPointerRecords(); + + // Get going + this._onResizeImpl(); + + WinJS.UI._setOptions(this, options, true); + + // Present the initial view + setImmediate(function () { + that._setVisibility(); + }); + }, { + // Public members + + /// + element: { + get: function () { + return this._element; + } + }, + + /// + /// Gets or sets a value that specifies whether the semantic zoom button should be displayed or not + /// + enableButton: { + get: function () { + return this._enableButton; + }, + set: function (value) { + var newValue = !!value; + if (this._enableButton !== newValue) { + this._enableButton = newValue; + if (newValue) { + this._createSemanticZoomButton(); + } else { + this._removeSemanticZoomButton(); + } + } + } + }, + + /// + /// Gets or sets a value that specifies whether the zoomed out view is currently displayed. + /// + zoomedOut: { + get: function () { + return this._zoomedOut; + }, + set: function (value) { + this._zoom(!!value, { x: 0.5 * this._sezoClientWidth, y : 0.5 * this._sezoClientHeight }, false); + } + }, + + /// + /// Gets or sets a value between 0.2 and 0.85 that specifies the scale of the zoomed out view. The default is 0.65. + /// + zoomFactor: { + get: function () { + return this._zoomFactor; + }, + set: function (value) { + var oldValue = this._zoomFactor; + var newValue = Utilities._clamp(value, minZoomFactor, maxZoomFactor, defaultZoomFactor); + if (oldValue !== newValue) { + this._zoomFactor = newValue; + this._onResize(); + } + } + }, + + /// + /// Gets or sets a value that indicates whether SemanticZoom is locked and zooming between views is disabled. + /// + locked: { + get: function () { + return this._locked; + }, + set: function (value) { + this._locked = !!value; + } + }, + + forceLayout: function () { + /// + /// + /// Forces the SemanticZoom to update its layout. Use this function when making the SemanticZoom visible again + /// after its style.display property had been set to "none". + /// + /// + this._onResizeImpl(); + }, + + // Private members + + _initialize: function () { + // initialize the semantic zoom, parent the child controls + + // Zoomed in and zoomed out controls must be on the first two child elements + + var children = Utilities.children(this._element); + this._elementIn = children[0]; + this._elementOut = children[1]; + + // Ensure the child controls have the same height as the SemanticZoom element + + this._elementIn.style.height = this._elementOut.style.height = this._element.offsetHeight + "px"; + + // Create the child controls if they haven't been created already + + UI.processAll(this._elementIn); + UI.processAll(this._elementOut); + + this._viewIn = this._elementIn.winControl.zoomableView; + this._viewOut = this._elementOut.winControl.zoomableView; + this._elementInIsListView = this._elementIn.winControl instanceof WinJS.UI.ListView; + this._elementOutIsListView = this._elementOut.winControl instanceof WinJS.UI.ListView; + this._isListView = this._elementInIsListView && this._elementOutIsListView; + + // Remove the children and place them beneath new divs that will serve as canvases and viewports + + this._element.removeChild(this._elementOut); + this._element.removeChild(this._elementIn); + this._element.innerHTML = ""; + this._cropViewport = document.createElement("div"); + this._element.appendChild(this._cropViewport); + this._viewportIn = document.createElement("div"); + this._viewportOut = document.createElement("div"); + this._cropViewport.appendChild(this._viewportIn); + this._cropViewport.appendChild(this._viewportOut); + + this._canvasIn = document.createElement("div"); + this._canvasOut = document.createElement("div"); + this._viewportIn.appendChild(this._canvasIn); + this._viewportOut.appendChild(this._canvasOut); + this._canvasIn.appendChild(this._elementIn); + this._canvasOut.appendChild(this._elementOut); + + if (this._enableButton) { + this._createSemanticZoomButton(); + } + + this._hiddenElement = document.createElement("div"); + this._hiddenElement.tabIndex = -1; + this._hiddenElement.visibility = "hidden"; + this._hiddenElement.setAttribute("aria-hidden", "true"); + this._element.appendChild(this._hiddenElement); + + this._setLayout(this._element, "relative", "hidden"); + this._setLayout(this._cropViewport, "absolute", "hidden"); + this._setLayout(this._viewportIn, "absolute", "hidden"); + this._setLayout(this._viewportOut, "absolute", "hidden"); + this._setLayout(this._canvasIn, "absolute", "hidden"); + this._setLayout(this._canvasOut, "absolute", "hidden"); + + + this._elementIn.style.position = "absolute"; + this._elementOut.style.position = "absolute"; + }, + + _createSemanticZoomButton: function () { + this._sezoButton = document.createElement("button"); + this._sezoButton.className = sezoButtonClass + " " + sezoButtonLocationClass + " " + (this._rtl() ? "rtl" : "ltr"); + this._sezoButton.tabIndex = -1; + this._sezoButton.style.visibility = "hidden"; + this._element.appendChild(this._sezoButton); + + //register the appropriate events for display the sezo button + this._sezoButton.addEventListener("click", this._onSeZoButtonZoomOutClick.bind(this), false); + this._element.addEventListener("scroll", this._onSeZoChildrenScroll.bind(this), true); + this._element.addEventListener("MSPointerHover", this._onPenHover.bind(this), false); + }, + + _removeSemanticZoomButton: function () { + if (this._sezoButton) { + this._element.removeChild(this._sezoButton); + this._sezoButton = null; + } + }, + + _configure: function () { + // Configure the controls for zooming + var axisIn = this._viewIn.getPanAxis(), + axisOut = this._viewOut.getPanAxis(); + this._pansHorizontallyIn = (axisIn === "horizontal" || axisIn === "both"); + this._pansVerticallyIn = (axisIn === "vertical" || axisIn === "both"); + this._pansHorizontallyOut = (axisOut === "horizontal" || axisOut === "both"); + this._pansVerticallyOut = (axisOut === "vertical" || axisOut === "both"); + + if (this._zoomInProgress) { + return; + } + + var pagesToPrefetchIn = 1 / this._zoomFactor - 1, + pagesToPrefetchOut = bounceFactor - 1; + + this._setLayout(this._elementIn, "absolute", "visible"); + this._setLayout(this._elementOut, "absolute", "visible"); + this._viewIn.configureForZoom(false, !this._zoomedOut, this._zoomFromCurrent.bind(this, true), pagesToPrefetchIn); + this._viewOut.configureForZoom(true, this._zoomedOut, this._zoomFromCurrent.bind(this, false), pagesToPrefetchOut); + this._pinching = false; + this._pinchGesture = 0; + this._canvasLeftIn = 0; + this._canvasTopIn = 0; + this._canvasLeftOut = 0; + this._canvasTopOut = 0; + + // Set scales and opacity + if (this._zoomedOut) { + scaleElement(this._canvasIn, this._zoomFactor); + } else { + scaleElement(this._canvasOut, 1 / this._zoomFactor); + } + + var styleViewportIn = this._viewportIn.style, + styleViewportOut = this._viewportOut.style, + styleCanvasIn = this._canvasIn.style, + styleCanvasOut = this._canvasOut.style; + + styleCanvasIn.opacity = (this._zoomedOut ? 0 : 1); + styleCanvasOut.opacity = (this._zoomedOut ? 1 : 0); + + // Enable animation + if (WinJS.UI.isAnimationEnabled()) { + styleViewportIn["transition-property"] = "transform"; + styleViewportIn["transition-duration"] = "0s"; + styleViewportIn["transition-timing-function"] = "linear"; + + styleViewportOut["transition-property"] = "transform"; + styleViewportOut["transition-duration"] = "0s"; + styleViewportOut["transition-timing-function"] = "linear"; + } + }, + + _onPropertyChanged: function (ev) { + if (ev.propertyName === "aria-checked") { + var newValue = this._element.getAttribute("aria-checked"); + var zoomedOut = newValue === "true"; + if (this._zoomedOut !== zoomedOut) { + var that = this; + setImmediate(function () { + that.zoomedOut = zoomedOut; + }); + } + } + }, + + _onResizeImpl: function () { + this._resizing = this._resizing || 0; + this._resizing++; + try { + var positionElement = function (element, left, top, width, height) { + var style = element.style; + style.left = left + "px"; + style.top = top + "px"; + style.width = width + "px"; + style.height = height + "px"; + }; + + var sezoComputedStyle = window.getComputedStyle(this._element, null), + sezoPaddingLeft = getDimension(this._element, sezoComputedStyle["paddingLeft"]), + sezoPaddingRight = getDimension(this._element, sezoComputedStyle["paddingRight"]), + sezoPaddingTop = getDimension(this._element, sezoComputedStyle["paddingTop"]), + sezoPaddingBottom = getDimension(this._element, sezoComputedStyle["paddingBottom"]), + viewportWidth = this._element.clientWidth - sezoPaddingLeft - sezoPaddingRight, + viewportHeight = this._element.clientHeight - sezoPaddingTop - sezoPaddingBottom, + scaleFactor = 1 / this._zoomFactor; + + + if (this._viewportWidth === viewportWidth && this._viewportHeight === viewportHeight) { + return; + } + this._sezoClientHeight = this._element.clientHeight; + this._sezoClientWidth = this._element.clientWidth; + this._viewportWidth = viewportWidth; + this._viewportHeight = viewportHeight; + + this._configure(); + + var multiplierIn = 2 * scaleFactor - 1, + canvasInWidth = Math.min(canvasSizeMax, (this._pansHorizontallyIn ? multiplierIn : 1) * viewportWidth), + canvasInHeight = Math.min(canvasSizeMax, (this._pansVerticallyIn ? multiplierIn : 1) * viewportHeight); + + this._canvasLeftIn = 0.5 * (canvasInWidth - viewportWidth); + this._canvasTopIn = 0.5 * (canvasInHeight - viewportHeight); + positionElement(this._cropViewport, sezoPaddingLeft, sezoPaddingTop, viewportWidth, viewportHeight); + positionElement(this._viewportIn, 0, 0, viewportWidth, viewportHeight); + positionElement(this._canvasIn, -this._canvasLeftIn, -this._canvasTopIn, canvasInWidth, canvasInHeight); + positionElement(this._elementIn, this._canvasLeftIn, this._canvasTopIn, viewportWidth, viewportHeight); + + var multiplierOut = 2 * bounceFactor - 1, + canvasOutWidth = (this._pansHorizontallyOut ? multiplierOut : 1) * viewportWidth, + canvasOutHeight = (this._pansVerticallyOut ? multiplierOut : 1) * viewportHeight; + + this._canvasLeftOut = 0.5 * (canvasOutWidth - viewportWidth); + this._canvasTopOut = 0.5 * (canvasOutHeight - viewportHeight); + positionElement(this._viewportOut, 0, 0, viewportWidth, viewportHeight); + positionElement(this._canvasOut, -this._canvasLeftOut, -this._canvasTopOut, canvasOutWidth, canvasOutHeight); + positionElement(this._elementOut, this._canvasLeftOut, this._canvasTopOut, viewportWidth, viewportHeight); + } finally { + this._resizing--; + } + }, + + _onResize: function () { + this._onResizeImpl(); + }, + + _onMouseMove: function (ev) { + if (this._zooming || + (!this._lastMouseX && !this._lastMouseY) || + (ev.screenX === this._lastMouseX && ev.screenY === this._lastMouseY)) { + this._lastMouseX = ev.screenX; + this._lastMouseY = ev.screenY; + return; + } + + if (Math.abs(ev.screenX - this._lastMouseX) <= sezoButtonMouseMoveThreshold && + Math.abs(ev.screenY - this._lastMouseY) <= sezoButtonMouseMoveThreshold) { + return; + } + + this._lastMouseX = ev.screenX; + this._lastMouseY = ev.screenY; + + this._displayButton(); + }, + + _displayButton: function () { + clearTimeout(this._dismissButtonTimer); + this._showSemanticZoomButton(); + + var that = this; + this._dismissButtonTimer = setTimeout(function () { + that._hideSemanticZoomButton(); + }, sezoButtonShowDuration); + }, + + _showSemanticZoomButton: function () { + if (this._buttonShown) { + return; + } + + if (this._sezoButton && !this._zoomedOut) { + WinJS.UI.Animation.fadeIn(this._sezoButton); + this._sezoButton.style.visibility = "visible"; + } + this._buttonShown = true; + }, + + _hideSemanticZoomButton: function (immediately) { + if (!this._buttonShown) { + return; + } + + if (this._sezoButton) { + if (!immediately) { + WinJS.UI.Animation.fadeOut(this._sezoButton); + } + this._sezoButton.style.visibility = "hidden"; + } + this._buttonShown = false; + }, + + _onSeZoChildrenScroll: function (ev) { + if (ev.target !== this.element) { + this._hideSemanticZoomButton(true); + } + }, + + _onMouseWheel: function (ev) { + if (ev.ctrlKey) { + this._zoom(ev.wheelDelta < 0, this._getPointerLocation(ev)); + + ev.stopPropagation(); + ev.preventDefault(); + } + }, + + _onPenHover: function (ev) { + if (ev.pointerType === PT_PEN) { + this._displayButton(); + } + }, + + _onSeZoButtonZoomOutClick: function (ev) { + this._hideSemanticZoomButton(); + this._zoom(true, { x: 0.5 * this._sezoClientWidth, y : 0.5 * this._sezoClientHeight }, false); + }, + + _onKeyDown: function (ev) { + var handled = false; + + if (ev.ctrlKey) { + var Key = Utilities.Key; + + switch (ev.keyCode) { + case Key.add: + case Key.equal: + this._zoom(false); + handled = true; + break; + + case Key.subtract: + case Key.dash: + this._zoom(true); + handled = true; + break; + } + } + + if (handled) { + ev.stopPropagation(); + ev.preventDefault(); + } + }, + + _createPointerRecord: function (ev) { + var location = this._getPointerLocation(ev); + + var newRecord = {}; + newRecord.startX = newRecord.currentX = location.x; + newRecord.startY = newRecord.currentY = location.y; + + this._pointerRecords[ev.pointerId] = newRecord; + this._pointerCount = Object.keys(this._pointerRecords).length; + + return newRecord; + }, + + _deletePointerRecord: function (id) { + var record = this._pointerRecords[id]; + + delete this._pointerRecords[id]; + this._pointerCount = Object.keys(this._pointerRecords).length; + + if (this._pointerCount !== 2) { + this._pinching = false; + } + + return record; + }, + + _handlePointerDown: function (ev) { + this._createPointerRecord(ev); + + // When we get more than one pointer, we need to explicitly set msPointerCapture on every pointer we've got to the SemanticZoom. + // This will fire lostCapture events on any descendant elements that had called setCapture earlier (for example, ListView items), + // and let the hosted control know that the pointer is no longer under its control. + var targetSurface = this._element; + var contactKeys = Object.keys(this._pointerRecords); + + for (var i = 0, len = contactKeys.length; i < len; i++) { + try { + this._hiddenElement.msSetPointerCapture(contactKeys[i] | 0); + } catch (e) { + this._resetPointerRecords(); + return; + } + } + + + ev.stopImmediatePropagation(); + ev.cancelBubble = true; + }, + + _handleFirstPointerDown: function (ev) { + this._resetPointerRecords(); + this._createPointerRecord(ev); + this._startedZoomedOut = this._zoomedOut; + }, + + // SeZo wants to prevent clicks while it is playing the bounce animation + // This can happen when user try to pinch out on the zoomed out view + // and lift the finger up on the same item + _onClick: function (ev) { + if (ev.srcElement !== this._element) { + if (this._isBouncing) { + ev.stopImmediatePropagation(); + } + } + }, + + // To optimize perf for ListView and to support more than 2 contact points + // for custom control, we wire up pointerDown routine for listview during capture + // but during bubbling phase for everythign else + _onPointerDown: function (ev) { + if (ev.pointerType !== PT_TOUCH) { + return; + } + + if (this._pointerCount === 0) { + this._handleFirstPointerDown(ev); + } else { + this._handlePointerDown(ev); + } + }, + + // SemanticZoom uses MSPointerMove messages to recognize a pinch. It has to use pointer messages instead of GestureUpdate for a few reasons: + // 1 - MSGestureUpdate events' scale property (the property that determines pinches) is based on a scalar value. We want our pinch threshold to be pixel based + // 2 - MSGestureUpdate events' scale property doesn't work when multiple contacts are on multiple surfaces. When that happens .scale will always stay 1.0. + _onPointerMove: function (ev) { + if (ev.pointerType === PT_MOUSE || ev.pointerType === PT_PEN) { + this._onMouseMove(ev); + return; + } + + if (ev.pointerType !== PT_TOUCH) { + return; + } + + function distance(startX, startY, endX, endY) { + return Math.sqrt((endX - startX) * (endX - startX) + (endY - startY) * (endY - startY)); + } + + function midpoint(point1, point2) { + return { + x: (0.5 * (point1.currentX + point2.currentX)) | 0, + y: (0.5 * (point1.currentY + point2.currentY)) | 0 + }; + } + + var pointerRecord = this._pointerRecords[ev.pointerId], + location = this._getPointerLocation(ev); + + // We listen to MSPointerDown on the bubbling phase of its event, but listen to MSPointerMove on the capture phase. + // MSPointerDown can be stopped from bubbling if the underlying control doesn't want the SemanticZoom to interfere for whatever reason. + // When that happens, we won't have a pointer record for the event we just got, so there's no sense in doing additional processing. + if (!pointerRecord) { + return; + } + pointerRecord.currentX = location.x; + pointerRecord.currentY = location.y; + + if (this._pointerCount === 2) { + this._pinching = true; + + // The order in which these contacts are stored and retrieved from contactKeys is unimportant. Any two points will suffice." + var contactKeys = Object.keys(this._pointerRecords), + point1 = this._pointerRecords[contactKeys[0]], + point2 = this._pointerRecords[contactKeys[1]]; + this._currentMidPoint = midpoint(point1, point2); + var contactDistance = distance(point1.currentX, point1.currentY, point2.currentX, point2.currentY); + var that = this; + var processPinchGesture = function (zoomingOut) { + var pinchDirection = (zoomingOut ? PinchDirection.zoomedOut : PinchDirection.zoomedIn), + gestureReversed = (zoomingOut ? (that._pinchedDirection === PinchDirection.zoomedIn && !that._zoomingOut) : (that._pinchedDirection === PinchDirection.zoomedOut && that._zoomingOut)), + canZoomInGesturedDirection = (zoomingOut ? !that._zoomedOut : that._zoomedOut); + if (that._pinchedDirection === PinchDirection.none) { + if (canZoomInGesturedDirection) { + that._isBouncingIn = false; + that._zoom(zoomingOut, midpoint(point1, point2), true); + that._pinchedDirection = pinchDirection; + } else if (!that._isBouncingIn) { + that._playBounce(true, midpoint(point1, point2)); + } + } else if (gestureReversed) { + var deltaFromStart = that._lastPinchDistance / that._lastPinchStartDistance; + var deltaFromLast = that._lastLastPinchDistance / that._lastPinchDistance; + if ((zoomingOut && deltaFromStart > zoomOutGestureDistanceChangeFactor) || + (!zoomingOut && deltaFromLast > zoomInGestureDistanceChangeFactor)) { + that._zoom(zoomingOut, midpoint(point1, point2), true); + that._pinchedDirection = pinchDirection; + } + } + + }; + this._updatePinchDistanceRecords(contactDistance); + if (this._pinchDistanceCount >= pinchDistanceCount) { + if (!this._zooming && !this._isBouncing) { + msWriteProfilerMark("WinJS.UI.SemanticZoom:EndPinchDetection,info"); + processPinchGesture(this._lastPinchDirection === PinchDirection.zoomedOut); + } + } + + // When two or more pointers are down, we want to hide all of their move events from the underlying view. + ev.stopImmediatePropagation(); + } else if (this._pointerCount > 2) { + // When more than two pointers are down, we're not going to interpret that as a pinch, so we reset the distance we'd recorded when it was + // just two pointers down. + this._resetPinchDistanceRecords(); + ev.stopImmediatePropagation(); + } + + // If the pointerCount isn't 2, we're no longer making a pinch. This generally happens if you try pinching, find you can't zoom in the pinched direction, + // then release one finger. When that happens we need to animate back to normal state. + if (this._pointerCount !== 2 && this._isBouncingIn) { + this._playBounce(false); + } + }, + + _onPointerOut: function (ev) { + if (ev.pointerType !== PT_TOUCH || ev.srcElement !== this._element) { + return; + } + + this._completePointerUp(ev); + }, + + _onPointerUp: function (ev) { + this._releasePointerCapture(ev); + this._completePointerUp(ev); + this._completeZoomingIfTimeout(); + }, + + _onPointerCancel: function (ev) { + this._releasePointerCapture(ev); + this._completePointerUp(ev); + this._completeZoomingIfTimeout(); + }, + + _onGotPointerCapture: function (ev) { + var pointerRecord = this._pointerRecords[ev.pointerId]; + if (pointerRecord) { + pointerRecord.dirty = false; + } + }, + + _onLostPointerCapture: function (ev) { + var pointerRecord = this._pointerRecords[ev.pointerId]; + if (pointerRecord) { + // If we lose capture on an element, there are three things that could be happening: + // 1 - Independent Manipulations are taking over. If that's the case, we should be getting an MSManipulationStateChanged event soon. + // 2 - Capture is just moving around inside of the semantic zoom region. We should get a got capture event soon, so we'll want to preserve this record. + // 3 - Capture got moved outside of the semantic zoom region. We'll destroy the pointer record if this happens. + pointerRecord.dirty = true; + var that = this; + WinJS.Promise.timeout(eventTimeoutDelay).then(function () { + if (pointerRecord.dirty) { + // If the timeout completed and the record is still dirty, we can discard it + that._completePointerUp(ev); + } + }); + } + }, + + _updatePinchDistanceRecords: function (contactDistance) { + var that = this; + function updatePinchDirection(direction) { + if (that._lastPinchDirection === direction) { + that._pinchDistanceCount++; + } else { + that._pinchGesture++; + that._pinchDistanceCount = 0; + that._lastPinchStartDistance = contactDistance; + } + that._lastPinchDirection = direction; + that._lastPinchDistance = contactDistance; + that._lastLastPinchDistance = that._lastPinchDistance; + } + + if (this._lastPinchDistance === -1) { + msWriteProfilerMark("WinJS.UI.SemanticZoom:StartPinchDetection,info"); + this._lastPinchDistance = contactDistance; + } else { + if (this._lastPinchDistance !== contactDistance) { + if (this._lastPinchDistance > contactDistance) { + updatePinchDirection(PinchDirection.zoomedOut); + } else { + updatePinchDirection(PinchDirection.zoomedIn); + } + } + } + }, + + _zoomFromCurrent: function (zoomOut) { + this._zoom(zoomOut, null, false, true); + }, + + _zoom: function (zoomOut, zoomCenter, gesture, centerOnCurrent) { + msWriteProfilerMark("WinJS.UI.SemanticZoom:StartZoom(zoomOut=" + zoomOut + "),info"); + + this._clearTimeout(this._completeZoomTimer); + this._clearTimeout(this._TTFFTimer); + + this._hideSemanticZoomButton(); + this._resetPinchDistanceRecords(); + + if (this._locked || this._gestureEnding) { + return; + } + + if (this._zoomInProgress) { + if (this._gesturing === !gesture) { + return; + } + + if (zoomOut !== this._zoomingOut) { + // Reverse the zoom that's currently in progress + this._startAnimations(zoomOut); + } + } else if (zoomOut !== this._zoomedOut) { + this._zooming = true; + this._aligning = true; + this._gesturing = !!gesture; + + if (zoomCenter) { + (zoomOut ? this._viewIn : this._viewOut).setCurrentItem(zoomCenter.x, zoomCenter.y); + } + + this._zoomInProgress = true; + + (zoomOut ? this._viewportOut : this._viewportIn).style.visibility = "visible"; + + this._viewIn.beginZoom(); + this._viewOut.beginZoom(); + + // To simplify zoomableView implementations, only call getCurrentItem between beginZoom and endZoom + if (centerOnCurrent) { + var that = this; + (zoomOut ? this._viewIn : this._viewOut).getCurrentItem().then(function (current) { + var position = current.position; + + // Pass in current item to avoid calling getCurrentItem again + that._prepareForZoom(zoomOut, { + x: that._rtl() ? (that._sezoClientWidth - position.left - 0.5 * position.width) : position.left + 0.5 * position.width, + y: position.top + 0.5 * position.height + }, WinJS.Promise.wrap(current)); + }); + } else { + this._prepareForZoom(zoomOut, zoomCenter || {}); + } + } + }, + + _prepareForZoom: function (zoomOut, zoomCenter, completedCurrentItem) { + msWriteProfilerMark("WinJS.UI.SemanticZoom:prepareForZoom,StartTM"); + var that = this; + var centerX = zoomCenter.x, + centerY = zoomCenter.y; + + + if (typeof centerX !== "number" || !this._pansHorizontallyIn || !this._pansHorizontallyOut) { + centerX = 0.5 * this._sezoClientWidth; + } + + if (typeof centerY !== "number" || !this._pansVerticallyIn || !this._pansVerticallyOut) { + centerY = 0.5 * this._sezoClientHeight; + } + + function setZoomCenters(adjustmentIn, adjustmentOut) { + that._canvasIn.style["transform-origin"] = (that._canvasLeftIn + centerX - adjustmentIn.x) + "px " + (that._canvasTopIn + centerY - adjustmentIn.y) + "px"; + that._canvasOut.style["transform-origin"] = (that._canvasLeftOut + centerX - adjustmentOut.x) + "px " + (that._canvasTopOut + centerY - adjustmentOut.y) + "px"; + } + + setZoomCenters(origin, origin); + + this._alignViews(zoomOut, centerX, centerY, completedCurrentItem).then(function (adjustment) { + that._aligning = false; + that._gestureEnding = false; + if (!that._zooming && !that._gesturing) { + that._completeZoom(); + } + }); + + this._zoomingOut = zoomOut; + msWriteProfilerMark("WinJS.UI.SemanticZoom:prepareForZoom,StopTM"); + setImmediate(function () { + that._startAnimations(zoomOut); + }); + }, + + _alignViews: function (zoomOut, centerX, centerY, completedCurrentItem) { + var multiplier = (1 - this._zoomFactor), + rtl = this._rtl(), + offsetLeft = multiplier * (rtl ? this._viewportWidth - centerX: centerX), + offsetTop = multiplier * centerY; + + var that = this; + if (zoomOut) { + var item = completedCurrentItem || this._viewIn.getCurrentItem(); + if (item) { + return item.then(function (current) { + var positionIn = current.position, + positionOut = { + left: positionIn.left * that._zoomFactor + offsetLeft, + top: positionIn.top * that._zoomFactor + offsetTop, + width: positionIn.width * that._zoomFactor, + height: positionIn.height * that._zoomFactor + }; + + return that._viewOut.positionItem(that._zoomedOutItem(current.item), positionOut); + }); + } + } else { + var item2 = completedCurrentItem || this._viewOut.getCurrentItem(); + if (item2) { + return item2.then(function (current) { + var positionOut = current.position, + positionIn = { + left: (positionOut.left - offsetLeft) / that._zoomFactor, + top: (positionOut.top - offsetTop) / that._zoomFactor, + width: positionOut.width / that._zoomFactor, + height: positionOut.height / that._zoomFactor + }; + + return that._viewIn.positionItem(that._zoomedInItem(current.item), positionIn); + }); + } + } + + return new WinJS.Promise(function () { return { x: 0, y: 0 }; }); + }, + + _startAnimations: function (zoomOut) { + this._zoomingOut = zoomOut; + + if (WinJS.UI.isAnimationEnabled()) { + msWriteProfilerMark("WinJS.UI.SemanticZoom:ZoomAnimation,StartTM"); + this._canvasIn.style["transition"] = (zoomOut ? outgoingElementTransition() : incomingElementTransition()); + this._canvasOut.style["transition"] = (zoomOut ? incomingElementTransition() : outgoingElementTransition()); + } + + scaleElement(this._canvasIn, (zoomOut ? this._zoomFactor : 1)); + scaleElement(this._canvasOut, (zoomOut ? 1 : 1 / this._zoomFactor)); + this._canvasIn.style.opacity = (zoomOut ? 0 : 1); + this._canvasOut.style.opacity = (zoomOut ? 1 : 0); + + if (!WinJS.UI.isAnimationEnabled()) { + this._zooming = false; + this._completeZoom(); + } else { + this.setTimeoutAfterTTFF(this._onZoomAnimationComplete.bind(this), zoomAnimationDuration); + } + }, + + _onBounceAnimationComplete: function () { + if (!this._isBouncingIn) { + this._completeZoom(); + } + }, + + _onZoomAnimationComplete: function () { + msWriteProfilerMark("WinJS.UI.SemanticZoom:ZoomAnimation,StopTM"); + this._zooming = false; + if (!this._aligning && !this._gesturing && !this._gestureEnding) { + this._completeZoom(); + } + }, + + _onCanvasTransitionEnd: function (ev) { + if ((ev.srcElement === this._canvasOut || ev.srcElement === this._canvasIn) && this._isBouncing) { + this._onBounceAnimationComplete(); + return; + } + + if (ev.srcElement === this._canvasIn && ev.propertyName === "transform") { + this._onZoomAnimationComplete(); + } + }, + + _clearTimeout: function (timer) { + if (timer) { + clearTimeout(timer); + } + }, + + _completePointerUp: function (ev) { + var id = ev.pointerId; + var pointerRecord = this._pointerRecords[id]; + if (pointerRecord) { + this._deletePointerRecord(id); + if (this._isBouncingIn) { + this._playBounce(false); + } + + + if (this._pointerCount === 0) { + // if we are not zooming and if there's any single pending pinch gesture detected that's not being triggered (fast pinch), process them now + if (this._pinchGesture === 1 && !this._zooming && this._lastPinchDirection !== PinchDirection.none && this._pinchDistanceCount < pinchDistanceCount) { + this._zoom(this._lastPinchDirection === PinchDirection.zoomedOut, this._currentMidPoint, false); + this._pinchGesture = 0; + this._attemptRecordReset(); + return; + } + + if (this._pinchedDirection !== PinchDirection.none) { + this._gesturing = false; + if (!this._aligning && !this._zooming) { + this._completeZoom(); + } + } + this._pinchGesture = 0; + this._attemptRecordReset(); + } + } + }, + + setTimeoutAfterTTFF: function (callback, delay) { + var that = this; + that._TTFFTimer = setTimeout(function () { + that._TTFFTimer = setTimeout(callback, delay); + }, zoomAnimationTTFFBuffer); + }, + + _completeZoomingIfTimeout: function () { + if (this._pointerCount !== 0) { + return; + } + + var that = this; + if (this._zoomInProgress || this._isBouncing) { + that._completeZoomTimer = setTimeout(function () { + that._completeZoom(); + }, zoomAnimationTimeout); + } + }, + + _completeZoom: function () { + if (this._isBouncing) { + if (this._zoomedOut) { + this._viewOut.endZoom(true); + } else { + this._viewIn.endZoom(true); + } + this._isBouncing = false; + return; + } + + + if (!this._zoomInProgress) { + return; + } + + msWriteProfilerMark("WinJS.UI.SemanticZoom:CompleteZoom,info"); + + this._clearTimeout(this._completeZoomTimer); + this._clearTimeout(this._TTFFTimer); + + this._gestureEnding = false; + + this._viewIn.endZoom(!this._zoomingOut); + this._viewOut.endZoom(this._zoomingOut); + + this._zoomInProgress = false; + + var zoomChanged = false; + if (this._zoomingOut !== this._zoomedOut) { + this._zoomedOut = !!this._zoomingOut; + this._element.setAttribute("aria-checked", this._zoomedOut.toString()); + zoomChanged = true; + } + + this._setVisibility(); + + if (zoomChanged) { + // Dispatch the zoomChanged event + var ev = document.createEvent("CustomEvent"); + ev.initCustomEvent(zoomChangedEvent, true, true, this._zoomedOut); + this._element.dispatchEvent(ev); + + if (this._isActive) { + // If the element is no longer a valid focus target, it will throw, we + // simply won't do anything in this case + try { + (this._zoomedOut ? this._elementOut : this._elementIn).setActive(); + } catch (e) {} + } + } + + msWriteProfilerMark("WinJS.UI.SemanticZoom:CompleteZoom_Custom,info"); + }, + + _isActive: function () { + var element = document.activeElement; + while ((element = element.parentElement)) { + if (element === this._element) { + return true; + } + } + return false; + }, + + _setLayout: function (element, position, overflow) { + var style = element.style; + style.position = position; + style.overflow = overflow; + }, + + _setVisibility: function () { + function setVisibility(element, isVisible) { + element.style.visibility = (isVisible ? "visible" : "hidden"); + } + setVisibility(this._viewportIn, !this._zoomedOut); + setVisibility(this._viewportOut, this._zoomedOut); + }, + + _resetPointerRecords: function () { + this._pinchedDirection = PinchDirection.none; + this._pointerCount = 0; + this._pointerRecords = {}; + this._resetPinchDistanceRecords(); + }, + + _releasePointerCapture: function (ev) { + var id = ev.pointerId; + try { + // Release the pointer capture since they are going away, to allow in air touch pointers + // to be reused for multiple interactions + this._hiddenElement.msReleasePointerCapture(id); + } catch (e) { + // This can throw if the pointer was not already captured + } + }, + + _attemptRecordReset: function () { + if (this._recordResetPromise) { + this._recordResetPromise.cancel(); + } + + var that = this; + this._recordResetPromise = WinJS.Promise.timeout(eventTimeoutDelay).then(function () { + if (that._pointerCount === 0) { + that._resetPointerRecords(); + that._recordResetPromise = null; + } + }); + }, + + _resetPinchDistanceRecords: function () { + this._lastPinchDirection = PinchDirection.none; + this._lastPinchDistance = -1; + this._lastLastPinchDistance = -1; + this._pinchDistanceCount = 0; + this._currentMidPoint = null; + }, + + _getPointerLocation: function (ev) { + // Get pointer location returns co-ordinate in the sezo control co-ordinate space + var sezoBox = { left: 0, top: 0 }; + try { + sezoBox = this._element.getBoundingClientRect(); + } + catch (err) { } // an exception can be thrown if SeZoDiv is no longer available + + var sezoComputedStyle = window.getComputedStyle(this._element, null), + sezoPaddingLeft = getDimension(this._element, sezoComputedStyle["paddingLeft"]), + sezoPaddingTop = getDimension(this._element, sezoComputedStyle["paddingTop"]), + sezoBorderLeft = getDimension(this._element, sezoComputedStyle["borderLeftWidth"]); + + return { + x: +ev.clientX === ev.clientX ? (ev.clientX - sezoBox.left - sezoPaddingLeft - sezoBorderLeft) : 0, + y: +ev.clientY === ev.clientY ? (ev.clientY - sezoBox.top - sezoPaddingTop - sezoPaddingTop) : 0 + }; + }, + + _playBounce: function (beginBounce, center) { + if (!WinJS.UI.isAnimationEnabled()) { + return; + } + + if (this._isBouncingIn === beginBounce) { + return; + } + + this._clearTimeout(this._completeZoomTimer); + this._clearTimeout(this._TTFFTimer); + this._isBouncing = true; + this._isBouncingIn = beginBounce; + if (beginBounce) { + this._bounceCenter = center; + } else { + this._aligned = true; + } + + var targetElement = (this._zoomedOut ? this._canvasOut : this._canvasIn); + var adjustmentX = (this._zoomedOut ? this._canvasLeftOut : this._canvasLeftIn); + var adjustmentY = (this._zoomedOut ? this._canvasTopOut : this._canvasTopIn); + targetElement.style["transform-origin"] = (adjustmentX + this._bounceCenter.x) + "px " + (adjustmentY + this._bounceCenter.y) + "px"; + targetElement.style["transition"] = beginBounce ? bounceInTransition() : bounceBackTransition(); + + if (!this._zoomedOut) { + this._viewIn.beginZoom(); + } else { + this._viewOut.beginZoom(); + } + + var scale = (beginBounce ? (this._zoomedOut ? 2 - bounceFactor : bounceFactor) : 1); + + scaleElement(targetElement, scale); + + this.setTimeoutAfterTTFF(this._onBounceAnimationComplete.bind(this), zoomAnimationDuration); + }, + + _rtl: function () { + return window.getComputedStyle(this._element, null).direction === "rtl"; + }, + + _pinching: { + set: function (value) { + if (this._elementInIsListView) { + this._elementIn.winControl._pinching = value; + } + + if (this._elementOutIsListView) { + this._elementOut.winControl._pinching = value; + } + } + } + }) + }); + + // The Semantic Zoom processes its own descendents + UI.SemanticZoom.isDeclarativeControlContainer = true; + + WinJS.Class.mix(WinJS.UI.SemanticZoom, WinJS.Utilities.createEventProperties("zoomchanged")); + WinJS.Class.mix(WinJS.UI.SemanticZoom, WinJS.UI.DOMEventMixin); + + +})(this); + +/// animatable,appbar,appbars,divs,Flyout,Flyouts,iframe,Statics,unfocus,unselectable +(function overlayInit(WinJS) { + "use strict"; + + var thisWinUI = WinJS.UI; + var utilities = thisWinUI.Utilities; + + // Class Names + var overlayClass = "win-overlay"; + var hideFocusClass = "win-hidefocus"; // Prevents the element from showing a focus rect + + // 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 = 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 = document.querySelectorAll("." + overlayClass); + if (elements) { + var len = elements.length; + for (var i = 0; i < len; i++) { + var element = elements[i]; + var control = element.winControl; + if (control) { + control[command](event); + } + } + } + } + + WinJS.Namespace.define("WinJS.UI", { + _Overlay: WinJS.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); + }, { + // Command Animations to Queue + _queuedToShow: [], + _queuedToHide: [], + _queuedCommandAnimation: false, + + // Functions/properties + _baseOverlayConstructor: function (element, options) { + // Make sure there's an input element + if (!element) { + element = document.createElement("div"); + } + + // Check to make sure we weren't duplicated + var overlay = element.winControl; + if (overlay) { + throw new WinJS.ErrorFromName("WinJS.UI._Overlay.DuplicateConstruction", strings.duplicateConstruction); + } + + 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 + WinJS.Utilities.addClass(this._element, overlayClass); + + // 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; + + if (options) { + WinJS.UI.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(); + } + } + } + }, + + show: function () { + /// + /// + /// Shows the Overlay, if hidden, regardless of other state + /// + /// + // call private show to distinguish it from public version + this._show(); + }, + + _show: function() { + // 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() { + // 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" || + this._fakeHide); + } + }, + + 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 () { + // If we are already animating, just remember this for later + if (this._animating || this._keyboardShowing || this._keyboardHiding) { + this._doNext = "show"; + return false; + } + + // "hiding" would need to cancel. + if (this._element.style.visibility !== "visible" || this._fakeHide) { + // Let us know we're showing. + this._element.winAnimating = "showing"; + + // Hiding, but not none + this._element.style.display = ""; + if (!this._fakeHide) { + 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(thisWinUI._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._currentAnimateIn(). + then(function () { + that._baseEndShow(); + }, function (err) { + that._baseEndShow(); + }); + this._fakeHide = false; + return true; + } + return false; + }, + + // Flyout in particular will need to measure our positioning. + _findPosition: function() { + }, + + _baseEndShow: function () { + // 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(thisWinUI._Overlay.afterShow); + + // If we had something queued, do that + var that = this; + setImmediate(function () { that._checkDoNext(); }); + }, + + _endShow: function () { + // Nothing by default + }, + + _baseHide: function () { + // If we are already animating, just remember this for later + if (this._animating || this._keyboardShowing) { + 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._keyboardHiding) { + // 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(thisWinUI._Overlay.beforeHide); + + // If we 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._currentAnimateOut(). + then(function () { + that._baseEndHide(); + }, function (err) { + that._baseEndHide(); + }); + } + return true; + } + this._fakeHide = false; + + return false; + }, + + _baseEndHide: function () { + // 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(thisWinUI._Overlay.afterHide); + + // 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. + var that = this; + setImmediate(function () { that._checkDoNext(); }); + }, + + _checkDoNext: function () { + // Do nothing if we're still animating + if (this._animating || this._keyboardShowing || this._keyboardHiding) { + 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 () { + this._element.style.opacity = 0; + this._element.style.visibility = "visible"; + // touch opacity so that IE fades from the 0 we just set to 1 + window.getComputedStyle(this._element, null).opacity; + return WinJS.UI.Animation.fadeIn(this._element); + }, + + _baseAnimateOut: function () { + this._element.style.opacity = 1; + // touch opacity so that IE fades from the 1 we just set to 0 + window.getComputedStyle(this._element, null).opacity; + return WinJS.UI.Animation.fadeOut(this._element); + }, + + _animating: { + get: function () { + // 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 (eventName) { + var event = document.createEvent("CustomEvent"); + event.initEvent(eventName, true, true, {}); + this._element.dispatchEvent(event); + }, + + // Show commands + _showCommands: function (commands, immediate) { + var showHide = this._resolveCommands(commands); + this._showAndHideCommands(showHide.commands, [], immediate); + }, + + // Hide commands + _hideCommands: function (commands, immediate) { + var showHide = this._resolveCommands(commands); + this._showAndHideCommands([], showHide.commands, immediate); + }, + + // Hide commands + _showOnlyCommands: function (commands, immediate) { + var showHide = this._resolveCommands(commands); + this._showAndHideCommands(showHide.commands, showHide.others, immediate); + }, + + _showAndHideCommands: function (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 (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 (addCommands, toQueue, fromQueue) { + // 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) { + var that = this; + setImmediate(function () { that._checkDoNext(); }); + } + this._queuedCommandAnimation = true; + } + }, + + // show/hide commands without doing any animation. + _showAndHideFast: function (showCommands, hideCommands) { + var count; + for (count = 0; count < showCommands.length; count++) { + if (showCommands[count] && showCommands[count].style) { + showCommands[count].style.visibility = ""; + showCommands[count].style.display = ""; + } + } + for (count = 0; count < hideCommands.length; count++) { + if (hideCommands[count] && hideCommands[count].style) { + hideCommands[count].style.visibility = "hidden"; + hideCommands[count].style.display = "none"; + } + } + }, + + // show and hide the queued commands, perhaps animating if overlay isn't hidden. + _showAndHideQueue: function () { + // 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 + var that = this; + setImmediate(function () { that._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)); + + // Don't animate ones that don't need 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 || + !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 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 || + !document.body.contains(hideCommands[count]) || + hideCommands[count].style.visibility === "hidden" || + hideCommands[count].style.opacity === "0") { + // Don't need to animate this one, not real, or it's hidden, + // so don't even need it as a sibling. + hideCommands.splice(count, 1); + count--; + } + } + + // Now we have the show, hide & siblings lists + 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 = WinJS.UI.Animation.createDeleteFromListAnimation(hideCommands, showCommands.length === 0 ? siblings : undefined); + } + if (showCommands.length > 0) { + showAnimated = WinJS.UI.Animation.createAddToListAnimation(showCommands, siblings); + } + + // Update hiding commands + for (count = 0; count < hideCommands.length; count++) { + // Need to fix our position + var rectangle = hideCommands[count].getBoundingClientRect(), + style = window.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 = WinJS.Promise.join([promise, newPromise]); + } else { + promise = newPromise; + } + } + + // Hook end animations + var that = this; + if (promise) { + // Needed to animate + promise.done(function () { that._endAnimateCommands(hideCommands); }, + function () { that._endAnimateCommands(hideCommands); }); + } else { + // Already positioned correctly + setImmediate(function () { that._endAnimateCommands([]); }); + } + } + + // Done, clear queues + this._queuedToShow = []; + this._queuedToHide = []; + }, + + // Once animation is complete, ensure that the commands are display:none + // and check if there's another animation to start. + _endAnimateCommands: function(hideCommands) { + // 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 = ""; + + // Might be something else to do + this._checkDoNext(); + }, + + // Resolves our commands + _resolveCommands: function (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. + // This is all .win-commands that are NOT in our commands array. + _findSiblings: function (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 (event) { + // Could be an orientation change + if (WinJS.Utilities.hasWinRT) { + var newState = Windows.UI.ViewManagement.ApplicationView.value; + if (this._currentViewState !== newState) { + this._currentViewState = newState; + if (!this._sticky) { + this._hideOrDismiss(); + } + } + } + + // Call specific resize + this._resize(event); + }, + + _hideOrDismiss: function () { + var element = this._element; + if (element && WinJS.Utilities.hasClass(element, "win-settingsflyout")) { + this._dismiss(); + } else { + this.hide(); + } + }, + + _resize: function (event) { + // Nothing by default + }, + + _checkScrollPosition: function (event) { + // Nothing by default + }, + + _showingKeyboard: function (event) { + // Nothing by default + }, + + _hidingKeyboard: function (event) { + // Nothing by default + }, + + // Verify that this HTML AppBar only has AppBar/MenuCommands. + _verifyCommandsOnly: function (element, type) { + if (element) { + var commands = element.children; + for (var i = 0; i < commands.length; i++) { + // If constructed they have win-command class, otherwise they have data-win-control + if (!WinJS.Utilities.hasClass(commands[i], "win-command") && + commands[i].getAttribute("data-win-control") !== type) { + // Wasn't tagged with class or AppBar/MenuCommand, not an AppBar/MenuCommand + throw new WinJS.ErrorFromName("WinJS.UI._Overlay.MustContainCommands", strings.mustContainCommands); + } + } + } + }, + + // Sets focus on what we think is the last tab stop. If nothing is focusable will + // try to set focus on itself. + _focusOnLastFocusableElementOrThis: function () { + if (!this._focusOnLastFocusableElement()) { + // Nothing is focusable. Set focus to this. + thisWinUI._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 () { + var _elms = this._element.getElementsByTagName("*"); + + // There should be at least the firstDiv & finalDiv + if (_elms.length < 2) { + return false; + } + + // Get the tabIndex set to the finalDiv (which is the highest) + var _highestTabIndex = thisWinUI._Overlay._getHighestTabIndexInList(_elms); + var _nextHighestTabIndex = 0; + + // Try all tabIndex 0 first. After this conditional the _highestTabIndex + // should be equal to the highest positive tabIndex. + if (_highestTabIndex === 0) { + for (var i = _elms.length - 2; i > 0; i--) { + if (_elms[i].tabIndex === _highestTabIndex) { + _elms[i].focus(); + + if (_elms[i] === document.activeElement) { + thisWinUI._Overlay._trySelect(_elms[i]); + 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 - 2; i > 0; i--) { + if (_elms[i].tabIndex === _highestTabIndex) { + _elms[i].focus(); + + if (_elms[i] === document.activeElement) { + thisWinUI._Overlay._trySelect(_elms[i]); + 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--) { + _elms[i].focus(); + + if (_elms[i] === document.activeElement) { + thisWinUI._Overlay._trySelect(_elms[i]); + return true; + } + } + + 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 () { + if (!this._focusOnFirstFocusableElement()) { + // Nothing is focusable. Set focus to this. + thisWinUI._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 () { + var _elms = this._element.getElementsByTagName("*"); + + // There should be at least the firstDiv & finalDiv + if (_elms.length < 2) { + return false; + } + + // Get the tabIndex set to the firstDiv (which is the lowest) + var _lowestTabIndex = thisWinUI._Overlay._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. + while (_lowestTabIndex) { + for (var i = 1; i < _elms.length - 1; i++) { + if (_elms[i].tabIndex === _lowestTabIndex) { + _elms[i].focus(); + + if (_elms[i] === document.activeElement) { + thisWinUI._Overlay._trySelect(_elms[i]); + 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 = 1; i < _elms.length - 1; i++) { + _elms[i].focus(); + + if (_elms[i] === document.activeElement) { + thisWinUI._Overlay._trySelect(_elms[i]); + return true; + } + } + + return false; + }, + + _addFlyoutEventHandlers: function (isFlyoutOrSettings) { + // Set up global event handlers for all overlays + if (!thisWinUI._Overlay._flyoutEdgeLightDismissEvent) { + // Dismiss on blur & resize + window.addEventListener("blur", thisWinUI._Overlay._checkBlur, false); + + // Be careful so it behaves in designer as well. + if (WinJS.Utilities.hasWinRT) { + // Catch edgy events too + var commandUI = Windows.UI.Input.EdgeGesture.getForCurrentView(); + commandUI.addEventListener("starting", thisWinUI._Overlay._hideAllFlyouts); + commandUI.addEventListener("completed", function edgyMayHideFlyouts() { + if (!thisWinUI._Overlay._rightMouseMightEdgy) { + thisWinUI._Overlay._hideAllFlyouts(); + } + }); + + // Also need to react to keyboard + var inputPane = Windows.UI.ViewManagement.InputPane.getForCurrentView(); + inputPane.addEventListener("showing", function (event) { _allOverlaysCallback(event, "_showingKeyboard"); }); + inputPane.addEventListener("hiding", function (event) { _allOverlaysCallback(event, "_hidingKeyboard"); }); + window.addEventListener("resize", function (event) { _allOverlaysCallback(event, "_baseResize"); }); + document.addEventListener("scroll", function (event) { _allOverlaysCallback(event, "_checkScrollPosition"); }); + } + + thisWinUI._Overlay._flyoutEdgeLightDismissEvent = true; + } + + // Each overlay tracks it's own view state + if (WinJS.Utilities.hasWinRT) { + // Cache current state + this._currentViewState = Windows.UI.ViewManagement.ApplicationView.value; + } + + // Individual handlers for Flyouts only + if (isFlyoutOrSettings) { + // Need to hide ourselves if we lose focus + var that = this; + this._element.addEventListener("focusout", function (e) { thisWinUI._Overlay._hideIfLostFocus(that, e); }, false); + + // Attempt to flag right clicks that may turn into edgy + this._element.addEventListener("MSPointerDown", thisWinUI._Overlay._checkRightClickDown, true); + this._element.addEventListener("MSPointerUp", thisWinUI._Overlay._checkRightClickUp, true); + } + } + }) + }); + + // Statics + thisWinUI._Overlay._clickEatingAppBarDiv = false; + thisWinUI._Overlay._clickEatingFlyoutDiv = false; + thisWinUI._Overlay._flyoutEdgeLightDismissEvent = false; + + thisWinUI._Overlay._hideFlyouts = function (testElement, notSticky) { + var elements = testElement.querySelectorAll(".win-flyout,.win-settingsflyout"); + 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(); + } + } + } + }; + + thisWinUI._Overlay._hideAllFlyouts = function () { + thisWinUI._Overlay._hideFlyouts(document, true); + }; + + thisWinUI._Overlay._createClickEatingDivTemplate = function (divClass, hideClickEatingDivFunction) { + var clickEatingDiv = document.createElement("section"); + WinJS.Utilities.addClass(clickEatingDiv, divClass); + clickEatingDiv.addEventListener("MSPointerUp", function (event) { thisWinUI._Overlay._checkSameClickEatingPointerUp(event, true); }, true); + clickEatingDiv.addEventListener("MSPointerDown", function (event) { thisWinUI._Overlay._checkClickEatingPointerDown(event, true); }, true); + clickEatingDiv.addEventListener("click", hideClickEatingDivFunction, 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"); + document.body.appendChild(clickEatingDiv); + return clickEatingDiv; + }; + + // Used by AppBar, and Settings Pane + thisWinUI._Overlay._createClickEatingDivAppBar = function () { + if (!thisWinUI._Overlay._clickEatingAppBarDiv) { + thisWinUI._Overlay._clickEatingAppBarDiv = thisWinUI._Overlay._createClickEatingDivTemplate(thisWinUI._Overlay._clickEatingAppBarClass, thisWinUI._Overlay._handleAppBarClickEatingClick); + } + }; + + // Used by Flyout and Menu + thisWinUI._Overlay._createClickEatingDivFlyout = function () { + if (!thisWinUI._Overlay._clickEatingFlyoutDiv) { + thisWinUI._Overlay._clickEatingFlyoutDiv = thisWinUI._Overlay._createClickEatingDivTemplate(thisWinUI._Overlay._clickEatingFlyoutClass, thisWinUI._Overlay._handleFlyoutClickEatingClick); + } + }; + + // All click-eaters eat "down" clicks so that we can still eat + // the "up" click that'll come later. + thisWinUI._Overlay._checkClickEatingPointerDown = function (event, stopPropogation) { + 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 (stopPropogation && !target._winRightMouse) { + event.stopPropagation(); + event.preventDefault(); + } + }; + + // Make sure that if we have an up we had an earlier down of the same kind + thisWinUI._Overlay._checkSameClickEatingPointerUp = function (event, stopPropogation) { + 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 && stopPropogation) { + result = false; + } + } + } catch (e) { } + + + if (stopPropogation && !rightMouse) { + event.stopPropagation(); + event.preventDefault(); + } + + 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. + thisWinUI._Overlay._handleAppBarClickEatingClick = function (event) { + event.stopPropagation(); + event.preventDefault(); + + thisWinUI.AppBar._hideLightDismissAppBars(null, false); + thisWinUI._Overlay._hideClickEatingDivAppBar(); + thisWinUI._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. + thisWinUI._Overlay._handleFlyoutClickEatingClick = function (event) { + event.stopPropagation(); + event.preventDefault(); + + // Don't light dismiss AppBars because edgy will do that as needed, + // so flyouts only. + thisWinUI._Overlay._hideClickEatingDivFlyout(); + thisWinUI._Overlay._hideAllFlyouts(); + }; + + thisWinUI._Overlay._checkRightClickDown = function (event) { + thisWinUI._Overlay._checkClickEatingPointerDown(event, false); + }; + + thisWinUI._Overlay._checkRightClickUp = function (event) { + if (thisWinUI._Overlay._checkSameClickEatingPointerUp(event, false)) { + // It was a right click we may want to eat. + thisWinUI._Overlay._rightMouseMightEdgy = true; + setImmediate(function () { thisWinUI._Overlay._rightMouseMightEdgy = false; }); + } + }; + + thisWinUI._Overlay._showClickEatingDivAppBar = function () { + setImmediate(function() { thisWinUI._Overlay._clickEatingAppBarDiv.style.display = "block"; }); + }; + + thisWinUI._Overlay._hideClickEatingDivAppBar = function () { + setImmediate(function() { thisWinUI._Overlay._clickEatingAppBarDiv.style.display = "none"; }); + }; + + thisWinUI._Overlay._showClickEatingDivFlyout = function () { + setImmediate(function() { thisWinUI._Overlay._clickEatingFlyoutDiv.style.display = "block"; }); + }; + + thisWinUI._Overlay._hideClickEatingDivFlyout = function () { + setImmediate(function() { thisWinUI._Overlay._clickEatingFlyoutDiv.style.display = "none"; }); + }; + + thisWinUI._Overlay._isFlyoutVisible = function() { + if (!thisWinUI._Overlay._clickEatingFlyoutDiv) { + return false; + } + return (thisWinUI._Overlay._clickEatingFlyoutDiv.style.display === "block"); + }; + + thisWinUI._Overlay._hideIfLostFocus = function (overlay, focusEvent) { + // 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 = document.activeElement; + if (overlay._element && overlay._element.contains(active)) { + return; + } + // Settings don't dismiss if they spawned a flyout + if (WinJS.Utilities.hasClass(overlay._element, "win-settingsflyout")) { + // If the active thing is a flyout, then don't dismiss us. + var control = thisWinUI._Overlay._getParentControlUsingClassName(active, "win-flyout"); + if (control) { + 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 && + (WinJS.Utilities.hasClass(active, thisWinUI._Overlay._clickEatingFlyoutClass) || + WinJS.Utilities.hasClass(active, thisWinUI._Overlay._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. + thisWinUI._Overlay._checkBlur = function (focusEvent) { + if (!document.hasFocus()) { + // The document doesn't have focus, so they clicked off the app, so light dismiss. + thisWinUI._Overlay._hideAllFlyouts(); + thisWinUI.AppBar._hideLightDismissAppBars(null, false); + } else { + if ((thisWinUI._Overlay._clickEatingFlyoutDiv && + thisWinUI._Overlay._clickEatingFlyoutDiv.style.display === "block") || + (thisWinUI._Overlay._clickEatingAppBarDiv && + thisWinUI._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 = document.activeElement; + if (active && active.tagName === "IFRAME" && !active.msLightDismissBlur) { + // This will go away when the IFRAME goes away, and we only create one + active.msLightDismissBlur = active.addEventListener("blur", thisWinUI._Overlay._checkBlur, false); + } + } + } + }; + + // Try to set us as active + thisWinUI._Overlay._trySetActive = function (element) { + if (!element || !element.setActive || !document.body || !document.body.contains(element)) { + return false; + } + try { + element.setActive(); + } catch (err) { + return false; + } + return (element === document.activeElement); + }; + + // Try to select the text so keyboard can be used. + thisWinUI._Overlay._trySelect = function (element) { + try { + if (element && element.select) { + element.select(); + } + } catch (e) { } + }; + + // Prevent the document.activeElement from showing focus + thisWinUI._Overlay._addHideFocusClass = function (element) { + if (element) { + WinJS.Utilities.addClass(element, hideFocusClass); + element.addEventListener("focusout", thisWinUI._Overlay._removeHideFocusClass, false); + } + }; + + // Allow the event.target (element that is losing focus) to show focus next time it gains focus + thisWinUI._Overlay._removeHideFocusClass = function (event) { + // Make sure we really lost focus and was not just an App switch + var target = event.target; + if (target && target !== document.activeElement) { + WinJS.Utilities.removeClass(target, hideFocusClass); + event.target.removeEventListener("focusout", thisWinUI._Overlay._removeHideFocusClass, false); + } + }; + + // Returns the lowest positive tabIndex in a list of elements. + // Returns 0 if there are no positive tabIndices. + thisWinUI._Overlay._getLowestTabIndexInList = function (elements) { + 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; + }; + + // 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. + thisWinUI._Overlay._getHighestTabIndexInList = function (elements) { + 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; + }; + + thisWinUI._Overlay._getParentControlUsingClassName = function (element, className) { + while (element && element !== document.body) { + if (WinJS.Utilities.hasClass(element, className)) { + return element.winControl; + } + element = element.parentNode; + } + return null; + }; + + // Global keyboard hiding offset + thisWinUI._Overlay._keyboardInfo = { + // Determine if the keyboard is visible or not. + get _visible() { + return (WinJS.Utilities.hasWinRT && Windows.UI.ViewManagement.InputPane.getForCurrentView().occludedRect.height > 0); + }, + + // See if we have to reserve extra space for the IHM + get _extraOccluded() { + var occluded = WinJS.Utilities.hasWinRT ? Windows.UI.ViewManagement.InputPane.getForCurrentView().occludedRect.height : 0; + + // Nothing occluded if not visible. + if (occluded && !thisWinUI._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 = document.documentElement.clientHeight / window.innerHeight, + widthRatio = document.documentElement.clientWidth / window.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 top of our visible area in terms of document.documentElement. + get _visibleDocTop() { + return window.pageYOffset - document.documentElement.scrollTop; + }, + + // Get the bottom of our visible area. + get _visibleDocBottom() { + return thisWinUI._Overlay._keyboardInfo._visibleDocTop + thisWinUI._Overlay._keyboardInfo._visibleDocHeight; + }, + + // Get the visible height, minus any needed keyboard occlusion. + get _visibleDocHeight() { + return window.innerHeight - thisWinUI._Overlay._keyboardInfo._extraOccluded; + }, + + // Get offset of visible window from bottom. + get _visibleDocBottomOffset() { + return document.documentElement.clientHeight - thisWinUI._Overlay._keyboardInfo._visibleDocBottom; + }, + + // Get total length of the IHM showPanel animation + get _animationShowLength() { + if (!WinJS.Utilities.hasWinRT) { + return 0; + } + + var a = 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; + } + }; + + // Classes other objects use + thisWinUI._Overlay._clickEatingAppBarClass = "win-appbarclickeater"; + thisWinUI._Overlay._clickEatingFlyoutClass = "win-flyoutmenuclickeater"; + + // Padding for IHM timer to allow for first scroll event + thisWinUI._Overlay._scrollTimeout = 150; + + // Events + thisWinUI._Overlay.beforeShow = "beforeshow"; + thisWinUI._Overlay.beforeHide = "beforehide"; + thisWinUI._Overlay.afterShow = "aftershow"; + thisWinUI._Overlay.afterHide = "afterhide"; + + thisWinUI._Overlay.commonstrings = { + get cannotChangeCommandsWhenVisible() { return WinJS.Resources._getWinJSString("ui/cannotChangeCommandsWhenVisible").value; }, + get cannotChangeHiddenProperty() { return WinJS.Resources._getWinJSString("ui/cannotChangeHiddenProperty").value; } + }; + + var strings = { + get duplicateConstruction() { return WinJS.Resources._getWinJSString("ui/duplicateConstruction").value; }, + get mustContainCommands() { return WinJS.Resources._getWinJSString("ui/mustContainCommands").value; }, + get closeOverlay() { return WinJS.Resources._getWinJSString("ui/closeOverlay").value; }, + }; + + WinJS.Class.mix(WinJS.UI._Overlay, WinJS.Utilities.createEventProperties("beforeshow", "aftershow", "beforehide", "afterhide")); + WinJS.Class.mix(WinJS.UI._Overlay, WinJS.UI.DOMEventMixin); +})(WinJS); + + +// Glyph Enumeration +/// Segoe +(function appBarIconInit(WinJS) { + "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"]; + + // 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. + /// + WinJS.Namespace.define("WinJS.UI.AppBarIcon", + glyphs.reduce(function (fixedIcons, item) { + fixedIcons[item] = { get: function () { return WinJS.Resources._getWinJSString("ui/appBarIcons/" + item).value; } }; + return fixedIcons; + }, {})); +})(WinJS); +// AppBarCommand +/// appbar,appbars,Flyout,Flyouts,onclick,Statics +(function appBarCommandInit(WinJS) { + "use strict"; + + var thisWinUI = WinJS.UI; + + // Class Names + var appBarCommandClass = "win-command", + appBarCommandGlobalClass = "win-global", + appBarCommandSelectionClass = "win-selection", + typeSeparator = "separator", + typeButton = "button", + typeToggle = "toggle", + typeFlyout = "flyout", + sectionSelection = "selection", + sectionGlobal = "global", + wideSize = 1024; + + function _handleClick(event) { + var command = this.winControl; + if (command) { + if (command._type === typeToggle) { + command.selected = !command.selected; + } else if (command._type === typeFlyout && command._flyout) { + var parentAppBar = thisWinUI._Overlay._getParentControlUsingClassName(this, "win-appbar"); + 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 = document.getElementById(flyout); + } + if (!flyout.show) { + flyout = flyout.winControl; + } + if (flyout && flyout.show) { + flyout.show(this, placement); + } + } + if (command.onclick) { + command.onclick(event); + } + } + } + + WinJS.Namespace.define("WinJS.UI", { + /// + /// AppBarCommands provide button, toggle button, flyout button, or separator functionality for AppBars. + /// + /// + /// + /// ]]> + /// 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: WinJS.Class.define(function AppBarCommand_ctor(element, options) { + /// + /// + /// Constructs the AppBarCommand control + /// + /// + /// The DOM element to be associated with the AppBarCommand control. AppBarCommand will create one if null. + /// + /// + /// The set of options to be applied initially to the AppBarCommand control. + /// + /// + /// A AppBarCommand control. + /// + /// + + // Check to make sure we weren't duplicated + if (element && element.winControl) { + throw new WinJS.ErrorFromName("WinJS.UI.AppBarCommand.DuplicateConstruction", strings.duplicateConstruction); + } + + // 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 = typeButton; + } + + // Go ahead and create it, separators look different than buttons + // Don't forget to use passed in element if one was provided. + this._element = element; + + if (options.type === typeSeparator) { + this._createSeparator(); + } else { + // This will also set the icon & label + this._createButton(); + } + + // Remember ourselves + this._element.winControl = this; + + // Attach our css class + WinJS.Utilities.addClass(this._element, appBarCommandClass); + + if (options.onclick) { + this.onclick = options.onclick; + } + // We want to handle some clicks + options.onclick = _handleClick; + + WinJS.UI.setOptions(this, options); + + if (!options.section) { + this._setSection(options.section); + } + + if (this._type === typeToggle && !options.selected) { + this.selected = false; + } + + // Clean up ARIA if needed + if (this._type !== 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 === typeToggle) { + role = "menuitemcheckbox"; + } + this._element.setAttribute("role", role); + if (this._type === 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); + } + } + }, { + /// 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; + } + } + }, + + /// The Type of the AppBarCommand, 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 !== typeButton && value !== typeFlyout && value !== typeToggle && value !== typeSeparator) { + this._type = typeButton; + } else { + this._type = value; + } + } + } + }, + + /// The label of the AppBarCommand + label: { + get: function () { + return this._label; + }, + set: function (value) { + this._label = value; + if (this._labelSpan) { + this._labelSpan.innerText = 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(); + } + }, + + /// The icon of the AppBarCommand + icon: { + get: function () { + return this._icon; + }, + set: function (value) { + if (value) { + this._icon = (WinJS.UI.AppBarIcon[value] || value); + } else { + this._icon = 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.innerText = this._icon; + this._imageSpan.style.backgroundImage = ""; + this._imageSpan.style.msHighContrastAdjust = ""; + } else { + // Must be an image, set that + this._imageSpan.innerText = ""; + this._imageSpan.style.backgroundImage = this._icon; + this._imageSpan.style.msHighContrastAdjust = "none"; + } + } + } + }, + + /// The click event to call when the AppBarCommand is invoked + onclick: { + get: function () { + return this._onclick; + }, + set: function (value) { + if (value && typeof value !== "function") { + throw new WinJS.ErrorFromName("WinJS.UI.AppBarCommand.BadClick", WinJS.Resources._formatString(strings.badClick, "AppBarCommand")); + } + this._onclick = value; + } + }, + + /// For flyout type AppBarCommands, get returns the WinJS.UI.Flyout that this command invokes. Set may also use the String id of the flyout to invoke, the DOM object of the flyout, or the flyout object itself + flyout: { + get: function () { + // Resolve it to the flyout + var flyout = this._flyout; + if (typeof flyout === "string") { + flyout = 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 = id.uniqueID; + id = id.id; + } + } + } + if (typeof id === "string") { + this._element.setAttribute("aria-owns", id); + } + + // Remember it + this._flyout = value; + } + }, + + /// Set or get the section to place the button in, either "global" or "selection" + section: { + get: function () { + return this._section; + }, + set: function (value) { + // we allow settings section only one time + if (!this._section || (window.Windows && Windows.ApplicationModel && Windows.ApplicationModel.DesignMode.designModeEnabled)) { + this._setSection(value); + } + } + }, + + /// The tooltip text of the AppBarCommand + tooltip: { + get: function () { + return this._tooltip; + }, + set: function (value) { + this._tooltip = value; + + // Update already-constructed tooltips. Separators 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; + } + }, + + /// Disable a command. It will get or set the HTML disabled attribute. + 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 = thisWinUI._Overlay._getParentControlUsingClassName(this._element, "win-appbar"); + if (appbarControl && !appbarControl.hidden) { + throw new WinJS.ErrorFromName("WinJS.UI.AppBarCommand.CannotChangeHiddenProperty", WinJS.Resources._formatString(thisWinUI._Overlay.commonstrings.cannotChangeHiddenProperty, "AppBar")); + } + + var style = this._element.style; + if (value) { + style.visibility = "hidden"; + style.display = "none"; + } else { + style.visibility = ""; + style.display = "inline-block"; + } + } + }, + + addEventListener: function (type, listener, useCapture) { + /// + /// + /// Add an event listener to the DOM element for this command + /// + /// Required. Event type to add. + /// 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 command + /// + /// Required. Event type to remove. + /// 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); + }, + + /// Adds an extra CSS class during construction. + extraClass: { + get: function () { + return this._extraClass; + }, + set: function (value) { + if (this._extraClass) { + WinJS.Utilities.removeClass(this._element, this._extraClass); + } + this._extraClass = value; + WinJS.Utilities.addClass(this._element, this._extraClass); + } + }, + + // Private + _testIdenticalTooltip: function () { + this._hideIfWide = (this._label === this._tooltip); + }, + + _createSeparator: function () { + // Make sure there's an input element + if (!this._element) { + this._element = document.createElement("hr"); + } else { + // Verify the input was an hr + if (this._element.tagName !== "HR") { + throw new WinJS.ErrorFromName("WinJS.UI.AppBarCommand.BadHrElement", strings.badHrElement); + } + } + }, + + _createButton: function () { + // Make sure there's an input element + if (!this._element) { + this._element = document.createElement("button"); + } else { + // Verify the input was a button + if (this._element.tagName !== "BUTTON") { + throw new WinJS.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 = 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 = 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 = 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 WinJS.UI.Tooltip(this._element); + var that = this; + this._tooltipControl.addEventListener("beforeopen", function () { + if (that._hideIfWide && window.innerWidth >= wideSize) { + that._tooltipControl.close(); + } + }, false); + + // Hide the modern focus rect on click or touch + var that = this; + this._element.addEventListener("MSPointerDown", function () { thisWinUI._Overlay._addHideFocusClass(that._element); }, false); + }, + + _setSection: function (section) { + if (!section) { + section = sectionGlobal; + } + if (this._section) { + // Remove the old section class + if (this._section === sectionGlobal) { + WinJS.Utilities.removeClass(this._element, appBarCommandGlobalClass); + } else if (this.section === sectionSelection) { + WinJS.Utilities.removeClass(this._element, appBarCommandSelectionClass); + } + } + // Add the new section class + this._section = section; + if (section === sectionGlobal) { + WinJS.Utilities.addClass(this._element, appBarCommandGlobalClass); + } else if (section === sectionSelection) { + WinJS.Utilities.addClass(this._element, appBarCommandSelectionClass); + } + } + }) + }); + + // Statics + + var strings = { + get ariaLabel() { return WinJS.Resources._getWinJSString("ui/appBarCommandAriaLabel").value; }, + get duplicateConstruction() { return WinJS.Resources._getWinJSString("ui/duplicateConstruction").value; }, + get badClick() { return WinJS.Resources._getWinJSString("ui/badClick").value; }, + get badHrElement() { return WinJS.Resources._getWinJSString("ui/badHrElement").value; }, + get badButtonElement() { return WinJS.Resources._getWinJSString("ui/badButtonElement").value; } + }; + +})(WinJS); + +// AppBar +/// appbar,appBars,Flyout,Flyouts,iframe,Statics,unfocus,WinJS +(function appBarInit(WinJS) { + "use strict"; + + var thisWinUI = WinJS.UI; + + // Class Names + var commandClass = "win-commandlayout", + appBarClass = "win-appbar", + settingsFlyoutClass = "win-settingsflyout", + topClass = "win-top", + bottomClass = "win-bottom"; + + var firstDivClass = "win-firstdiv", + finalDivClass = "win-finaldiv"; + + // Constants for placement + var appBarPlacementTop = "top", + appBarPlacementBottom = "bottom"; + + // Constants for layout + var appBarLayoutCustom = "custom", + appBarLayoutCommands = "commands"; + + // 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 (thisWinUI._Overlay._rightMouseMightEdgy && + e.kind === 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 === Windows.UI.Input.EdgeGestureKind.keyboard; + WinJS.UI.AppBar._toggleAppBarEdgy(keyboardInvoked); + } + } + + function _startingEdgy() { + if (!edgyHappening) { + // Edgy wasn't happening, so toggle & start it + edgyHappening = WinJS.UI.AppBar._toggleAppBarEdgy(false); + } + } + + function _canceledEdgy() { + // Shouldn't get here unless edgy was happening. + // Undo whatever we were doing. + var bars = _getDynamicBarsForEdgy(); + if (edgyHappening === "showing") { + _hideAllBars(bars, false); + } else if (edgyHappening === "hiding") { + _showAllBars(bars, false); + } + edgyHappening = null; + } + + function _allManipulationChanged(event) { + var elements = document.querySelectorAll("." + 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 _visible set if ANY are hidden of visible. + function _getDynamicBarsForEdgy() { + var elements = document.querySelectorAll("." + appBarClass); + var len = elements.length; + var AppBars = []; + AppBars._visible = 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); + // Middle of animation is different than animated + if (AppBar._element.winAnimating) { + // If animating, look at showing/hiding + if (AppBar._element.winAnimating === "hiding") { + AppBars._hidden = true; + } else { + AppBars._visible = true; + } + } else { + // Not animating, so use "hidden" + if (AppBar._element.style.visibility === "hidden") { + AppBars._hidden = true; + } else { + AppBars._visible = true; + } + } + } + } + + return AppBars; + } + + // Show or hide all bars + function _hideAllBars(bars, keyboardInvoked) { + var len = bars.length; + for (var i = 0; i < len; i++) { + bars[i]._keyboardInvoked = keyboardInvoked; + bars[i].hide(); + } + } + + function _showAllBars(bars, keyboardInvoked) { + var len = bars.length; + for (var i = 0; i < len; i++) { + bars[i]._keyboardInvoked = keyboardInvoked; + bars[i]._doNotFocus = false; + bars[i]._show(); + } + } + + // On "Esc" key press hide all flyouts and light dismiss AppBars. + function _handleKeyDown(event) { + if (event.key === "Esc") { + event.preventDefault(); + event.stopPropagation(); + thisWinUI._Overlay._hideAllFlyouts(); + thisWinUI.AppBar._hideLightDismissAppBars(null, true); + } + } + + // 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) { + for (var i = startIndex; i >= 0; i--) { + if (appBars[i].winControl + && appBars[i].winControl.placement === appBarPlacement + && !appBars[i].winControl.hidden + && appBars[i].winControl._focusOnLastFocusableElement + && appBars[i].winControl._focusOnLastFocusableElement()) { + return true; + } + } + return false; + } + + // Sets focus to the last AppBar in the provided appBars array with other placement. + // Returns true if focus was set. False otherwise. + function _setFocusToPreviousAppBarHelperNeither(startIndex, appBars) { + for (var i = startIndex; i >= 0; i--) { + if (appBars[i].winControl + && appBars[i].winControl.placement != appBarPlacementBottom + && appBars[i].winControl.placement != appBarPlacementTop + && !appBars[i].winControl.hidden + && appBars[i].winControl._focusOnLastFocusableElement + && appBars[i].winControl._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 + // 3) Other AppBars + // DOM order is respected, because an AppBar should not have a defined tabIndex + function _setFocusToPreviousAppBar() { + var appBars = document.querySelectorAll("." + 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 === appBarPlacementBottom) { + // Bottom appBar: Focus order: (1)previous bottom appBars (2)other appBars (3)top appBars (4)bottom appBars + if (thisAppBarIndex && _setFocusToPreviousAppBarHelper(thisAppBarIndex - 1, appBarPlacementBottom, appBars)) { return; } + if (_setFocusToPreviousAppBarHelperNeither(appBars.length - 1, appBars)) { return; } + if (_setFocusToPreviousAppBarHelper(appBars.length - 1, appBarPlacementTop, appBars)) { return; } + if (_setFocusToPreviousAppBarHelper(appBars.length - 1, appBarPlacementBottom, appBars)) { return; } + } else if (appBarControl.placement === appBarPlacementTop) { + // Top appBar: Focus order: (1)previous top appBars (2)bottom appBars (3)other appBars (4)top appBars + if (thisAppBarIndex && _setFocusToPreviousAppBarHelper(thisAppBarIndex - 1, appBarPlacementTop, appBars)) { return; } + if (_setFocusToPreviousAppBarHelper(appBars.length - 1, appBarPlacementBottom, appBars)) { return; } + if (_setFocusToPreviousAppBarHelperNeither(appBars.length - 1, appBars)) { return; } + if (_setFocusToPreviousAppBarHelper(appBars.length - 1, appBarPlacementTop, appBars)) { return; } + } else { + // Other appBar: Focus order: (1)previous other appBars (2)top appBars (3)bottom appBars (4)other appBars + if (thisAppBarIndex && _setFocusToPreviousAppBarHelperNeither(thisAppBarIndex - 1, appBars)) { return; } + if (_setFocusToPreviousAppBarHelper(appBars.length - 1, appBarPlacementTop, appBars)) { return; } + if (_setFocusToPreviousAppBarHelper(appBars.length - 1, appBarPlacementBottom, appBars)) { return; } + if (_setFocusToPreviousAppBarHelperNeither(appBars.length - 1, 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) { + for (var i = startIndex; i < appBars.length; i++) { + if (appBars[i].winControl + && appBars[i].winControl.placement === appBarPlacement + && !appBars[i].winControl.hidden + && appBars[i].winControl._focusOnFirstFocusableElement + && appBars[i].winControl._focusOnFirstFocusableElement()) { + return true; + } + } + return false; + } + + // Sets focus to the first AppBar in the provided appBars array with other placement. + // Returns true if focus was set. False otherwise. + function _setFocusToNextAppBarHelperNeither(startIndex, appBars) { + for (var i = startIndex; i < appBars.length; i++) { + if (appBars[i].winControl + && appBars[i].winControl.placement != appBarPlacementBottom + && appBars[i].winControl.placement != appBarPlacementTop + && !appBars[i].winControl.hidden + && appBars[i].winControl._focusOnFirstFocusableElement + && appBars[i].winControl._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 + // 3) Other AppBars + // DOM order is respected, because an AppBar should not have a defined tabIndex + function _setFocusToNextAppBar() { + var appBars = document.querySelectorAll("." + appBarClass); + + 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 (this.parentElement.winControl.placement === appBarPlacementBottom) { + // Bottom appBar: Focus order: (1)next bottom appBars (2)top appBars (3)other appBars (4)bottom appBars + if (_setFocusToNextAppBarHelper(thisAppBarIndex + 1, appBarPlacementBottom, appBars)) { return; } + if (_setFocusToNextAppBarHelper(0, appBarPlacementTop, appBars)) { return; } + if (_setFocusToNextAppBarHelperNeither(0, appBars)) { return; } + if (_setFocusToNextAppBarHelper(0, appBarPlacementBottom, appBars)) { return; } + } else if (this.parentElement.winControl.placement === appBarPlacementTop) { + // Top appBar: Focus order: (1)next top appBars (2)other appBars (3)bottom appBars (4)top appBars + if (_setFocusToNextAppBarHelper(thisAppBarIndex + 1, appBarPlacementTop, appBars)) { return; } + if (_setFocusToNextAppBarHelperNeither(0, appBars)) { return; } + if (_setFocusToNextAppBarHelper(0, appBarPlacementBottom, appBars)) { return; } + if (_setFocusToNextAppBarHelper(0, appBarPlacementTop, appBars)) { return; } + } else { + // Other appBar: Focus order: (1)next other appBars (2)bottom appBars (3)top appBars (4)other appBars + if (_setFocusToNextAppBarHelperNeither(thisAppBarIndex + 1, appBars)) { return; } + if (_setFocusToNextAppBarHelper(0, appBarPlacementBottom, appBars)) { return; } + if (_setFocusToNextAppBarHelper(0, appBarPlacementTop, appBars)) { return; } + if (_setFocusToNextAppBarHelperNeither(0, appBars)) { return; } + } + } + + // Updates the firstDiv & finalDiv of all visible AppBars + function _updateAllAppBarsFirstAndFinalDiv() { + var appBars = document.querySelectorAll("." + appBarClass); + + for(var i = 0; i < appBars.length; i++) { + if (appBars[i].winControl + && !appBars[i].winControl.hidden + && appBars[i].winControl._updateFirstAndFinalDiv) { + appBars[i].winControl._updateFirstAndFinalDiv(); + } + } + } + + // Returns true if a visible non-sticky (light dismiss) AppBar is found in the document + function _isThereVisibleNonStickyBar() { + var appBars = document.querySelectorAll("." + appBarClass); + for (var i = 0; i < appBars.length; i++) { + var appBarControl = appBars[i].winControl; + if (appBarControl && !appBarControl.sticky && + (!appBarControl.hidden || appBarControl._element.winAnimating === "showing")) { + return true; + } + } + + return false; + } + + // Hide all light dismiss AppBars if what has focus is not part of a AppBar or flyout. + function _hideIfAllAppBarsLostFocus() { + if (!thisWinUI.AppBar._isAppBarOrChild(document.activeElement)) { + thisWinUI.AppBar._hideLightDismissAppBars(null, false); + // Ensure that sticky appbars clear cached focus after light dismiss are dismissed, which moved focus. + thisWinUI.AppBar._ElementWithFocusPreviousToAppBar = null; + } + } + + // 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 + && !thisWinUI.AppBar._isAppBarOrChild(focusEvent.relatedTarget)) { + _storePreviousFocus(focusEvent.relatedTarget); + } + } + + // Cache the previous focus information + function _storePreviousFocus(element) { + if (element) { + thisWinUI.AppBar._ElementWithFocusPreviousToAppBar = element; + } + } + + // Try to return focus to what had focus before. + // If successfully return focus to a textbox, restore the selection too. + function _restorePreviousFocus() { + thisWinUI._Overlay._trySetActive(thisWinUI.AppBar._ElementWithFocusPreviousToAppBar); + } + + WinJS.Namespace.define("WinJS.UI", { + /// + /// The AppBar control provides a surface for AppBarCommands or other information that the user can show or hide. + /// + /// + /// + /// + /// + /// ]]> + /// Raised just before showing an AppBar. + /// Raised immediately after an AppBar is fully shown. + /// Raised just before hiding an AppBar. + /// Raised immediately after an AppBar is fully hidden. + /// The AppBar control itself + /// Style for a custom layout appbar + /// + /// + /// + AppBar: WinJS.Class.derive(WinJS.UI._Overlay, function (element, options) { + /// + /// + /// Constructs the AppBar control + /// + /// + /// The DOM element to be associated with the AppBar control. + /// + /// + /// The set of options to be applied initially to the AppBar control. + /// + /// + /// A constructed AppBar control. + /// + /// + + // Simplify checking later + if (!options) { + options = {}; + } + + // 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 !== appBarLayoutCustom && !options.commands) { + this._verifyCommandsOnly(element, "WinJS.UI.AppBarCommand"); + } + + // Call the base overlay constructor helper + this._baseOverlayConstructor(element, options); + + // Make a click eating div + thisWinUI._Overlay._createClickEatingDivAppBar(); + + // Attach our css class, + WinJS.Utilities.addClass(this._element, appBarClass); + // Also may need a command class if not a custom layout appbar + if (options.layout !== appBarLayoutCustom) { + WinJS.Utilities.addClass(this._element, commandClass); + } + + if (!options.placement) { + // Make sure we have default placement + this.placement = appBarPlacementBottom; + } + + // 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); + } + + // Handle key presses (esc) + this._element.addEventListener("keydown", _handleKeyDown, 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 (WinJS.Utilities.hasWinRT) { + var commandUI = 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 + document.addEventListener("MSManipulationStateChanged", _allManipulationChanged, false); + + appBarCommandEvent = true; + } + + // Make sure flyout event handlers are hooked up (this aids light dismiss) + this._addFlyoutEventHandlers(false); + + // Need to store what had focus before + this._element.addEventListener("focusin", function (event) { _checkStorePreviousFocus(event); }, false); + + // Need to hide ourselves if we lose focus + this._element.addEventListener("focusout", function (event) { _hideIfAllAppBarsLostFocus(); }, false); + + return this; + }, { + // Public Properties + + /// The placement of the AppBar on the display. Values are "top" or "bottom". + placement: { + get: function () { + return this._placement; + }, + set: function (value) { + // In designer we may have to move it + var wasShown = false; + if (window.Windows && Windows.ApplicationModel && Windows.ApplicationModel.DesignMode.designModeEnabled && !this.hidden) { + this._hide(); + wasShown = true; + } + + if (!this.hidden) { + throw new WinJS.ErrorFromName("WinJS.UI.AppBar.CannotChangePlacementWhenVisible", strings.cannotChangePlacementWhenVisible); + } + + // Set placement + this._placement = value; + + // Clean up win-top, win-bottom styles + if (this._placement === appBarPlacementTop) { + WinJS.Utilities.addClass(this._element, topClass); + WinJS.Utilities.removeClass(this._element, bottomClass); + } else if (this._placement === appBarPlacementBottom) { + WinJS.Utilities.removeClass(this._element, topClass); + WinJS.Utilities.addClass(this._element, bottomClass); + } else { + WinJS.Utilities.removeClass(this._element, topClass); + WinJS.Utilities.removeClass(this._element, bottomClass); + } + + // Make sure our animations are correct + this._assignAnimations(); + + // Show again if we hid ourselves for the designer + if (wasShown) { + this._show(); + } + } + }, + + /// The layout of the AppBar contents, either "commands" or "custom". + layout: { + get: function () { + // Defaults to commands if not set + return this._layout ? this._layout : appBarLayoutCommands; + }, + set: function (value) { + if (value !== appBarLayoutCommands && value !== appBarLayoutCustom) { + throw new WinJS.ErrorFromName("WinJS.UI.AppBar.BadLayout", strings.badLayout); + } + + // In designer we may have to redraw it + var wasShown = false; + if (window.Windows && Windows.ApplicationModel && Windows.ApplicationModel.DesignMode.designModeEnabled && !this.hidden) { + this._hide(); + wasShown = true; + } + + if (!this.hidden) { + throw new WinJS.ErrorFromName("WinJS.UI.AppBar.CannotChangeLayoutWhenVisible", strings.cannotChangeLayoutWhenVisible); + } + + // Set layout + this._layout = value; + + // Update our classes + if (this._layout === appBarLayoutCommands) { + // Add the appbar css class back + WinJS.Utilities.addClass(this._element, commandClass); + } else { + // Remove the appbar css class + WinJS.Utilities.removeClass(this._element, commandClass); + } + + // Show again if we hid ourselves for the designer + if (wasShown) { + this._show(); + } + } + }, + + /// Whether the AppBar is sticky. + sticky: { + get: function () { + return this._sticky; + }, + set: function (value) { + // If it doesn't change, do nothing + if (this._sticky === !!value) { + return; + } + + this._sticky = !!value; + + // Note: caller has to call .show() if they also want it visible + + // 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()) { + thisWinUI._Overlay._hideClickEatingDivAppBar(); + } + } else { + thisWinUI._Overlay._showClickEatingDivAppBar(); + + if (this._shouldStealFocus()) { + _storePreviousFocus(document.activeElement); + this._setFocusToAppBar(); + } + } + } + } + }, + + /// The Commands for the AppBar, contains an array of AppBarCommand options. + commands: { + set: function (value) { + // Fail if trying to set when visible + if (!this.hidden) { + throw new WinJS.ErrorFromName("WinJS.UI.AppBar.CannotChangeCommandsWhenVisible", WinJS.Resources._formatString(thisWinUI._Overlay.commonstrings.cannotChangeCommandsWhenVisible, "AppBar")); + } + + // Start from scratch + this._element.innerHTML = ""; + + // 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 AppBar. If more than one command is found, all are returned. + /// + /// 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 with that id. + /// + var commands = this.element.querySelectorAll("#" + id); + for (var count = 0; count < commands.length; count++) { + // Any elements we generated this should succeed for, + // but remove missing ones just to be safe. + commands[count] = commands[count].winControl; + if (!commands[count]) { + commands.splice(count, 1); + } + } + + if (commands.length === 1) { + return commands[0]; + } else if (commands.length === 0) { + return null; + } + + return commands; + }, + + + showCommands: function (commands) { + /// + /// + /// Show commands within this AppBar + /// + /// Required. Command or Commands to show, either String, DOM elements, or WinJS objects. + /// + if (!commands) { + throw new WinJS.ErrorFromName("WinJS.UI.AppBar.RequiresCommands", strings.requiresCommands); + } + + this._showCommands(commands); + }, + + + hideCommands: function (commands) { + /// + /// + /// Hide commands within this AppBar + /// + /// Required. Command or Commands to hide, either String, DOM elements, or WinJS objects. + /// + if (!commands) { + throw new WinJS.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. + /// + /// Required. Command or Commands to show, either String, DOM elements, or WinJS objects. + /// + if (!commands) { + throw new WinJS.ErrorFromName("WinJS.UI.AppBar.RequiresCommands", strings.requiresCommands); + } + + this._showOnlyCommands(commands); + }, + + + show: function () { + /// + /// + /// Shows the AppBar, if hidden, regardless of other state + /// + /// + // Just wrap the private one, turning off keyboard invoked flag + this._keyboardInvoked = false; + this._doNotFocus = !!this.sticky; + this._show(); + }, + + _show: function () { + // Don't do anything if disabled + if (this.disabled) { + return; + } + + // If we're covered by a keyboard we look hidden, so we may have to jump up + if (this._keyboardObscured) { + // just make us look hidden so that show() gets called. + this._fakeHide = true; + this._keyboardObscured = false; + } + + // Regardless we're going to be in a CED state + if (!this.sticky) { + // Need click-eating div to be visible ASAP. + thisWinUI._Overlay._showClickEatingDivAppBar(); + } + + // If we are already animating, just remember this for later + if (this._element.winAnimating) { + this._doNext = "show"; + return false; + } + + // We call our base _baseShow because AppBar may need to override show + // "hiding" would need to cancel. + this._baseShow(); + + // Clean up after showing + if (!this.sticky && _isThereVisibleNonStickyBar()) { + this._needToUpdateAllAppBarsInEndShow = true; + } else { + this._needToUpdateAllAppBarsInEndShow = false; + } + }, + + _endShow: function () { + // Make sure first & final divs are right + if (this._needToUpdateAllAppBarsInEndShow) { + _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 (!thisWinUI.AppBar._ElementWithFocusPreviousToAppBar) { + _storePreviousFocus(document.activeElement); + } + + this._setFocusToAppBar(); + } + }, + + hide: function () { + /// + /// + /// Hides the AppBar, if visible, regardless of other state + /// + /// + // Just wrap the private one + this._hide(); + }, + + _hide: function () { + // If we're covered by a keyboard we already look hidden + if (this._keyboardObscured && !this._animating) { + this._keyboardObscured = false; + this._baseEndHide(); + } else { + // We call our base _baseHide because AppBar may need to override hide + this._baseHide(); + } + + // Determine if there are any AppBars that are visible. + // Set the focus to the next visible 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 = document.querySelectorAll("." + 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 = document.querySelectorAll("." + 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 + thisWinUI._Overlay._hideClickEatingDivAppBar(); + } + + var that = this; + if (!areOtherAppBars) { + // Set focus to what had focus before showing the AppBar + if (thisWinUI.AppBar._ElementWithFocusPreviousToAppBar && + (!document.activeElement || thisWinUI.AppBar._isAppBarOrChild(document.activeElement))) { + _restorePreviousFocus(); + } + // Always clear the previous focus (to prevent temporary leaking of element) + thisWinUI.AppBar._ElementWithFocusPreviousToAppBar = null; + } else if (thisWinUI.AppBar._isWithinAppBarOrChild(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; + }, + + _assignAnimations: function () { + // Make sure the animations are correct for our current placement + if (this._placement === appBarPlacementTop || this._placement === appBarPlacementBottom) { + // Top or Bottom + this._currentAnimateIn = this._animateSlideIn; + this._currentAnimateOut = this._animateSlideOut; + } else { + // Default for in the middle of nowhere + this._currentAnimateIn = this._baseAnimateIn; + this._currentAnimateOut = this._baseAnimateOut; + } + }, + + // AppBar animations + _animateSlideIn: function () { + var where, + height = this._element.offsetHeight; + // Get top/bottoms + this._checkPosition(); + // Get animation direction and clear other value + if (this._placement === appBarPlacementTop) { + // Top Bar + where = { top: "-" + height + "px", left: "0px" }; + this._element.style.bottom = "auto"; + } else { + // Bottom Bar + where = { top: height + "px", left: "0px" }; + this._element.style.top = "auto"; + } + + this._element.style.opacity = 1; + this._element.style.visibility = "visible"; + return WinJS.UI.Animation.showEdgeUI(this._element, where); + }, + + _animateSlideOut: function () { + var where, + height = this._element.offsetHeight; + if (this._placement === appBarPlacementTop) { + // Top Bar + where = { top: height + "px", left: "0px" }; + // Adjust for scrolling or soft keyboard positioning + this._element.style.top = (this._getAdjustedTop() - height) + "px"; + } else { + // Bottom Bar + where = { top: "-" + height + "px", left: "0px" }; + // Adjust for scrolling or soft keyboard positioning + this._element.style.bottom = (this._getAdjustedBottom() - height) + "px"; + } + return WinJS.UI.Animation.showEdgeUI(this._element, where); + }, + + _isABottomAppBarInTheProcessOfShowing : function () { + var appbars = document.querySelectorAll("." + appBarClass + "." + bottomClass); + for (var i = 0; i < appbars.length; i++) { + if (appbars[i].winAnimating === "showing") { + 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 () { + var activeElementAppBar = thisWinUI.AppBar._isAppBarOrChild(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 === 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 !== appBarPlacementBottom) + && (activeElementAppBar.winControl._placement !== 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 () { + if (this._focusOnFirstFocusableElement()) { + // Prevent what is gaining focus from showing that it has focus, + // but only in the non-keyboard scenario. + if (!this._keyboardInvoked) { + thisWinUI._Overlay._addHideFocusClass(document.activeElement); + } + } else { + // No first element, set it to appbar itself + thisWinUI._Overlay._trySetActive(this._element); + } + }, + + _addCommand: function (command) { + // See if it's a command already + if (command && !command._element) { + // Not a command, so assume it's options for a command + command = new WinJS.UI.AppBarCommand(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); + }, + + // Get the top of the top appbars, would be 0 if not for viewport changes. + // "normally", top is just 0, if there's no IHM yet, + // When keyboard first shows up, also still 0, + // When resize happens, then we need to consider page offset - base offset + _getAdjustedTop: function() { + // Top is always at the top of the viewport + return thisWinUI._Overlay._keyboardInfo._visibleDocTop; + }, + + // Get the bottom of the bottom appbars, would be 0 if not for viewport changes + // "normally", bottom is just 0, if there's no IHM yet, + // When keyboard first shows, need to adjust by distance of keyboard + // When resize happens, then we need to consider page offset - base offset. + // If the keyboard obscures the appbar we need to make sure it stays obscured. + _getAdjustedBottom: function() { + // Need the distance the IHM moved as well. + return thisWinUI._Overlay._keyboardInfo._visibleDocBottomOffset; + }, + + _showingKeyboard: function (event) { + // Remember keyboard showing state. + this._keyboardObscured = false; + this._keyboardHiding = false; + + // If we're already moved, then ignore the whole thing + if (thisWinUI._Overlay._keyboardInfo._visible && this._alreadyInPlace()) { + return; + } + + this._keyboardShowing = true; + // If focus is in the appbar, don't cause scrolling. + if (!this.hidden && this._element.contains(document.activeElement)) { + event.ensuredFocusedElementInView = true; + } + + // Check if appbar moves or is obscured + if (!this.hidden && this._placement !== appBarPlacementTop && thisWinUI._Overlay._isFlyoutVisible()) { + // Remember that we're obscured + this._keyboardObscured = true; + } else { + // If not obscured, tag as showing and set timeout to restore us. + this._scrollHappened = false; + } + + // Also set timeout regardless, so we can clean up our keyboard showing flag. + var that = this; + setTimeout(function (e) { that._checkKeyboardTimer(e); }, thisWinUI._Overlay._keyboardInfo._animationShowLength + thisWinUI._Overlay._scrollTimeout); + }, + + _hidingKeyboard: function (event) { + // We won't be obscured + this._keyboardObscured = false; + this._keyboardShowing = false; + this._keyboardHiding = true; + + // We'll either just reveal the current space or resize the window + if (!thisWinUI._Overlay._keyboardInfo._isResized) { + // If we're visible or only fake hiding under keyboard, or already animating, + // then snap us to our final position. + if (!this.hidden || this._fakeHide || this._animating) { + // Not resized, update our final position immediately + this._checkScrollPosition(); + this._element.style.display = ""; + this._fakeHide = false; + } + this._keyboardHiding = false; + } + // Else resize should clear keyboardHiding. + }, + + _resize: function (event) { + // If we're hidden by the keyboard, then hide bottom appbar so it doesn't pop up twice when it scrolls + if (this._keyboardShowing) { + // 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.hidden) { + if (this._placement !== 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 thie 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._keyboardHiding) { + this._keyboardHiding = false; + if (!this.hidden || this._animating) { + // Snap to final position + this._checkScrollPosition(); + this._element.style.display = ""; + this._fakeHide = false; + } + } + }, + + _checkKeyboardTimer: function () { + if (!this._scrollHappened) { + this._mayEdgeBackIn(); + } + }, + + _manipulationChanged: function (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 (event) { + // May need to react to IHM being resized event + if (this._keyboardShowing) { + // If not top appbar or viewport isn't still at top, then need to show again + this._keyboardShowing = false; + // If obscured (flyout showing), don't change. + // If hidden, may be because _fakeHide was set in _resize. + // If bottom we have to move, or if top scrolled off screen. + if (!this._keyboardObscured && (!this.hidden || this._fakeHide) && + (this._placement !== appBarPlacementTop || thisWinUI._Overlay._keyboardInfo._visibleDocTop !== 0)) { + this._doNotFocus = true; + this._fakeHide = true; + this._show(); + } else { + // Ensure any animation dropped during the showing keyboard are caught up. + this._checkDoNext(); + } + } + this._scrollHappened = false; + }, + + // _checkPosition repositions the AppBar when the soft keyboard shows up + _checkPosition: function () { + // Bottom's the only one needing movement + if (this._placement === appBarPlacementBottom) { + this._element.style.bottom = this._getAdjustedBottom() + "px"; + } else if (this._placement === appBarPlacementTop) { + this._element.style.top = this._getAdjustedTop() + "px"; + } + // else we don't touch custom positions + }, + + _checkScrollPosition: function (event) { + // If keyboard's animating, then remember we may come in + if (this._keyboardShowing) { + // Tag that it's OK to edge back in. + this._scrollHappened = true; + return; + } + + // We only need to update if we're visible + if (!this.hidden || this._animating) { + this._checkPosition(); + // Ensure any animation dropped during the showing keyboard are caught up. + this._checkDoNext(); + } + }, + + _alreadyInPlace: function () { + // See if we're already where we're supposed to be. + if (this._placement === appBarPlacementBottom) { + if (parseInt(this._element.style.bottom) === this._getAdjustedBottom()) { + return true; + } + } else if (this._placement === appBarPlacementTop) { + if (parseInt(this._element.style.top) === this._getAdjustedTop()) { + return true; + } + } + // else we don't understand custom positioning + return false; + }, + + // If there is a visible 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 () { + var appBarFirstDiv = this._element.querySelectorAll("." + firstDivClass); + appBarFirstDiv = appBarFirstDiv.length >= 1 ? appBarFirstDiv[0] : null; + + var appBarFinalDiv = this._element.querySelectorAll("." + 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 visible non-sticky AppBars. + appBarFirstDiv = 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 = firstDivClass; + appBarFirstDiv.tabIndex = -1; + appBarFirstDiv.setAttribute("aria-hidden", "true"); + appBarFirstDiv.addEventListener("focus", _setFocusToPreviousAppBar, true); + this._element.insertAdjacentElement("AfterBegin", 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 visible non-sticky AppBars. + appBarFinalDiv = 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 = finalDivClass; + appBarFinalDiv.tabIndex = -1; + appBarFinalDiv.setAttribute("aria-hidden", "true"); + appBarFinalDiv.addEventListener("focus", _setFocusToNextAppBar, true); + this._element.appendChild(appBarFinalDiv); + } + + // Update the tabIndex of the firstDiv & finalDiv + if (_isThereVisibleNonStickyBar()) { + var elms = this._element.getElementsByTagName("*"); + + if (appBarFirstDiv) { + appBarFirstDiv.tabIndex = thisWinUI._Overlay._getLowestTabIndexInList(elms); + } + if (appBarFinalDiv) { + appBarFinalDiv.tabIndex = thisWinUI._Overlay._getHighestTabIndexInList(elms); + } + } else { + if (appBarFirstDiv) { + appBarFirstDiv.tabIndex = -1; + } + if (appBarFinalDiv) { + appBarFinalDiv.tabIndex = -1; + } + } + } + }) + }); + + + // Statics + thisWinUI.AppBar._ElementWithFocusPreviousToAppBar = null; + + // 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. + thisWinUI.AppBar._isAppBarOrChild = function (element) { + // If it's null, we can't do this + if (!element) { + return null; + } + + // click eating divs and sentinals should not have children + if (WinJS.Utilities.hasClass(element, thisWinUI._Overlay._clickEatingAppBarClass) || + WinJS.Utilities.hasClass(element, thisWinUI._Overlay._clickEatingFlyoutClass) || + WinJS.Utilities.hasClass(element, firstDivClass) || + WinJS.Utilities.hasClass(element, finalDivClass)) { + return element; + } + + while (element && element !== document) { + if (WinJS.Utilities.hasClass(element, appBarClass)) { + return element; + } + if (WinJS.Utilities.hasClass(element, "win-flyout") + && element != element.winControl._previousFocus) { + return thisWinUI.AppBar._isAppBarOrChild(element.winControl._previousFocus); + } + + element = element.parentNode; + } + + return null; + }; + + // 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. + thisWinUI.AppBar._isWithinAppBarOrChild = function (element, appBar) { + if (!element || !appBar) { + return false; + } + if (appBar.contains(element)) { + return true; + } + var flyout = thisWinUI._Overlay._getParentControlUsingClassName(element, "win-flyout"); + return (flyout && appBar.contains(flyout._previousFocus)); + }; + + // Overlay class calls this for global light dismiss events + thisWinUI.AppBar._hideLightDismissAppBars = function (event, keyboardInvoked) { + var elements = document.querySelectorAll("." + 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); + } + } + + _hideAllBars(AppBars, keyboardInvoked); + }; + + // Callback for AppBar Edgy Event Command + thisWinUI.AppBar._toggleAppBarEdgy = function(keyboardInvoked) { + var bars = _getDynamicBarsForEdgy(); + + // If they're all visible hide them, otherwise show them all + if (bars._visible && !bars._hidden) { + _hideAllBars(bars, keyboardInvoked); + return "hiding"; + } else { + _showAllBars(bars, keyboardInvoked); + return "showing"; + } + }; + + var strings = { + get ariaLabel() { return WinJS.Resources._getWinJSString("ui/appBarAriaLabel").value; }, + get requiresCommands() { return WinJS.Resources._getWinJSString("ui/requiresCommands").value; }, + get cannotChangePlacementWhenVisible() { return WinJS.Resources._getWinJSString("ui/cannotChangePlacementWhenVisible").value; }, + get badLayout() { return WinJS.Resources._getWinJSString("ui/badLayout").value; }, + get cannotChangeLayoutWhenVisible() { return WinJS.Resources._getWinJSString("ui/cannotChangeLayoutWhenVisible").value; } + }; +})(WinJS); + + +/// appbar,Flyout,Flyouts,Statics +(function flyoutInit(WinJS) { + "use strict"; + + var thisWinUI = WinJS.UI; + + // Class Names + var appBarCommandClass = "win-command"; + var flyoutClass = "win-flyout"; + var flyoutLightClass = "win-ui-light"; + var menuClass = "win-menu"; + var scrollsClass = "win-scrolls"; + + var finalDivClass = "win-finaldiv"; + var firstDivClass = "win-firstdiv"; + + function getDimension(element, property) { + return parseFloat(element, window.getComputedStyle(element, null)[property]); + } + + WinJS.Namespace.define("WinJS.UI", { + /// Flyout controls provide temporary surfaces for small amounts of content. + /// 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: WinJS.Class.derive(WinJS.UI._Overlay, function (element, options) { + /// + /// + /// Constructs the Flyout control and associates it with the underlying DOM element. + /// + /// + /// The DOM element to be associated with the Flyout control. + /// + /// + /// The set of options to be applied initially to the Flyout control. + /// + /// A fully constructed Flyout control. + /// + + this._baseFlyoutConstructor(element, options); + + var _elms = this._element.getElementsByTagName("*"); + var firstDiv = this._addFirstDiv(); + firstDiv.tabIndex = thisWinUI._Overlay._getLowestTabIndexInList(_elms); + var finalDiv = this._addFinalDiv(); + finalDiv.tabIndex = thisWinUI._Overlay._getHighestTabIndexInList(_elms); + + // Handle "esc" & "tab" key presses + this._element.addEventListener("keydown", this._handleKeyDown, true); + + return this; + }, { + _lastMaxHeight: null, + + _baseFlyoutConstructor: function (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 + thisWinUI._Overlay._createClickEatingDivFlyout(); + + // Start flyouts hidden + this._element.style.visibilty = "hidden"; + this._element.style.display = "none"; + + // Attach our css class + WinJS.Utilities.addClass(this._element, flyoutClass); + WinJS.Utilities.addClass(this._element, flyoutLightClass); + + // Make sure we have an ARIA role + var role = this._element.getAttribute("role"); + if (role === null || role === "" || role === undefined) { + if (WinJS.Utilities.hasClass(this._element, 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 flyout event handlers are hooked up + this._addFlyoutEventHandlers(true); + }, + + /// The anchor element is the HTML element which the flyout originates from and is positioned relative to. You can override it with the anchor that you pass in the show() method. + anchor: { + get: function () { + return this._anchor; + }, + set: function (value) { + this._anchor = value; + } + }, + + /// Default placement to be used for this flyout, overridden by placement passed in show(). + 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 WinJS.ErrorFromName("WinJS.UI.Flyout.BadPlacement", strings.badPlacement); + } + this._placement = value; + } + }, + + /// Default alignment to be used for this flyout, overridden by alignment passed in show(). + alignment: { + get: function () { + return this._alignment; + }, + set: function (value) { + if (value !== "right" && value !== "left" && value !== "center") { + // Not a legal alignment value + throw new WinJS.ErrorFromName("WinJS.UI.Flyout.BadAlignment", strings.badAlignment); + } + this._alignment = value; + } + }, + + show: function (anchor, placement, alignment) { + /// + /// + /// Shows the Flyout, if hidden, regardless of other state + /// + /// + /// 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. + /// + /// + // Just call private version to make appbar flags happy + this._show(anchor, placement, alignment); + }, + + _show: function (anchor, placement, alignment) { + this._baseFlyoutShow(anchor, placement, alignment); + }, + + hide: function () { + /// + /// + /// Hides the Flyout, if visible, regardless of other state + /// + /// + // Just wrap the private one, turning off keyboard invoked flag + this._keyboardInvoked = false; + this._hide(); + }, + + _hide: function () { + if (this._baseHide()) { + // Return focus if this or the flyout CED has focus + var active = document.activeElement; + if (this._previousFocus + && active + && (this._element.contains(active) + || WinJS.Utilities.hasClass(active, thisWinUI._Overlay._clickEatingFlyoutClass)) + && this._previousFocus.focus !== undefined) { + + // _isAppBarOrChild may return a CED or sentinal + var appBar = thisWinUI.AppBar._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 = thisWinUI._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 { + thisWinUI._Overlay._trySetActive(this._previousFocus); + } + active = document.activeElement; + + if (fHideRole) { + // Restore the role so that css is applied correctly + var that = this; + setImmediate(function () { + // We clear the previousFocus so make sure it still exists + if (that._previousFocus) { + that._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) { + thisWinUI._Overlay._addHideFocusClass(active); + } + } + + // Clear the previousFocus after allowing role to be restored if needed + var that = this; + setImmediate(function () { + that._previousFocus = null; + }); + + // Need click-eating div to be hidden if there are no other visible flyouts + if (!this._isThereVisibleFlyout()) { + thisWinUI._Overlay._hideClickEatingDivFlyout(); + } + } + }, + + _baseFlyoutShow: function (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 = 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 WinJS.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) { + thisWinUI._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 (!WinJS.Utilities.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 && !WinJS.Utilities.hasClass(this.element.children[0], firstDivClass)) { + if (firstDiv && firstDiv.length > 0) { + firstDiv.item(0).parentNode.removeChild(firstDiv.item(0)); + } + + firstDiv = this._addFirstDiv(); + } + firstDiv.tabIndex = thisWinUI._Overlay._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 (!WinJS.Utilities.hasClass(this.element.children[this.element.children.length - 1], finalDivClass)) { + if (finalDiv && finalDiv.length > 0) { + finalDiv.item(0).parentNode.removeChild(finalDiv.item(0)); + } + + finalDiv = this._addFinalDiv(); + } + finalDiv.tabIndex = thisWinUI._Overlay._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 = document.activeElement; + } + }, + + _endShow: function () { + // 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 = thisWinUI._Overlay._keyboardInfo._visible; + + if (!WinJS.Utilities.hasClass(this.element, "win-menu")) { + // Put focus on the first child in the Flyout + this._focusOnFirstFocusableElementOrThis(); + + // Prevent what is gaining focus from showing that it has focus + thisWinUI._Overlay._addHideFocusClass(document.activeElement); + } else { + // Make sure the menu has focus, but don't show a focus rect + thisWinUI._Overlay._trySetActive(this._element); + } + }, + + // Find our new flyout position. + _findPosition: function() { + 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 - current scroll adjustment + this._scrollTop = this._nextTop - thisWinUI._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) { + WinJS.Utilities.addClass(this._element, 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 (thisWinUI._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 () { + 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 = WinJS.Utilities.getTotalWidth(this._element); + flyout.height = WinJS.Utilities.getTotalHeight(this._element); + flyout.innerWidth = WinJS.Utilities.getContentWidth(this._element); + flyout.innerHeight = WinJS.Utilities.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 = thisWinUI._Overlay._keyboardInfo._visibleDocTop; + this._nextHeight = anchor.top - thisWinUI._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 = thisWinUI._Overlay._keyboardInfo._visibleDocHeight - (anchor.bottom - thisWinUI._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 = thisWinUI._Overlay._keyboardInfo._visibleDocTop; + this._nextHeight = anchor.top - thisWinUI._Overlay._keyboardInfo._visibleDocTop - this._nextMarginPadding; + } else { + // Bottom, won't fit, needs scrollbar + this._nextTop = -1; + this._nextHeight = thisWinUI._Overlay._keyboardInfo._visibleDocHeight - (anchor.bottom - thisWinUI._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 WinJS.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(anchor, flyout) { + return ((thisWinUI._Overlay._keyboardInfo._visibleDocHeight - anchor.height) / 2) >= flyout.height; + }, + + _topHasMoreRoom: function(anchor) { + return anchor.top > thisWinUI._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 (anchor, flyout) { + this._nextTop = anchor.top - flyout.height; + this._nextAnimOffset = { top: "40px", left: "0px" }; + return (this._nextTop >= thisWinUI._Overlay._keyboardInfo._visibleDocTop && + this._nextTop + flyout.height <= thisWinUI._Overlay._keyboardInfo._visibleDocBottom); + }, + + _fitBottom: function (anchor, flyout) { + this._nextTop = anchor.bottom; + this._nextAnimOffset = { top: "-40px", left: "0px" }; + return (this._nextTop >= thisWinUI._Overlay._keyboardInfo._visibleDocTop && + this._nextTop + flyout.height <= thisWinUI._Overlay._keyboardInfo._visibleDocBottom); + }, + + _fitLeft: function (anchor, flyout) { + this._nextLeft = anchor.left - flyout.width; + this._nextAnimOffset = { top: "0px", left: "40px" }; + return (this._nextLeft >= 0 && this._nextLeft + flyout.width <= document.documentElement.clientWidth); + }, + + _fitRight: function (anchor, flyout) { + this._nextLeft = anchor.right; + this._nextAnimOffset = { top: "0px", left: "-40px" }; + return (this._nextLeft >= 0 && this._nextLeft + flyout.width <= document.documentElement.clientWidth); + }, + + _centerVertically: function (anchor, flyout) { + this._nextTop = anchor.top + anchor.height / 2 - flyout.height / 2; + if (this._nextTop < thisWinUI._Overlay._keyboardInfo._visibleDocTop) { + this._nextTop = thisWinUI._Overlay._keyboardInfo._visibleDocTop; + } else if (this._nextTop + flyout.height >= thisWinUI._Overlay._keyboardInfo._visibleDocBottom) { + // Flag to put on bottom + this._nextTop = -1; + } + }, + + _centerHorizontally: function (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 WinJS.ErrorFromName("WinJS.UI.Flyout.BadAlignment", strings.badAlignment); + } + if (this._nextLeft < 0) { + this._nextLeft = 0; + } else if (this._nextLeft + flyout.width >= document.documentElement.clientWidth) { + // flag to put on right + this._nextLeft = -1; + } + }, + + _updateAdjustments: function (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 + WinJS.Utilities.removeClass(this._element, scrollsClass); + if (this._lastMaxHeight !== null) { + this._element.style.maxHeight = this._lastMaxHeight; + this._lastMaxHeight = null; + } + // Alignment + if (alignment === "center") { + WinJS.Utilities.removeClass(this._element, "win-leftalign"); + WinJS.Utilities.removeClass(this._element, "win-rightalign"); + } else if (alignment === "left") { + WinJS.Utilities.addClass(this._element, "win-leftalign"); + WinJS.Utilities.removeClass(this._element, "win-rightalign"); + } else if (alignment === "right") { + WinJS.Utilities.addClass(this._element, "win-rightalign"); + WinJS.Utilities.removeClass(this._element, "win-leftalign"); + } + }, + + _showingKeyboard: function (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; + setTimeout(function () { that._adjustForKeyboard(); that._baseAnimateIn(); }, thisWinUI._Overlay._keyboardInfo._animationShowLength); + } + }, + + _resize: function (event) { + // 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._keyboardHiding) { + // Hiding keyboard, update our position, giving the anchor a chance to update first. + var that = this; + setImmediate(function () { that._findPosition(); }); + this._keyboardHiding = false; + } + }, + + _checkKeyboardFit: function () { + // Check for moving to fit keyboard: + // - Too Tall, above top, or below bottom. + var height = WinJS.Utilities.getTotalHeight(this._element); + var viewportHeight = thisWinUI._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 < thisWinUI._Overlay._keyboardInfo._visibleDocTop) { + // Above the top of the viewport + this._scrollTop = 0; + this._keyboardMovedUs = true; + } else if (this._nextBottom > thisWinUI._Overlay._keyboardInfo._visibleDocBottom) { + // Below the bottom of the viewport + this._scrollTop = -1; + this._keyboardMovedUs = true; + } + }, + + _adjustForKeyboard: function () { + // Keyboard moved us, update our metrics as needed + if (this._keyboardSquishedUs) { + // Add scrollbar if we didn't already have scrollsClass + if (!this._hasScrolls) { + WinJS.Utilities.addClass(this._element, scrollsClass); + this._lastMaxHeight = this._element.style.maxHeight; + } + // Adjust height + this._element.style.maxHeight = this._keyboardSquishedUs + "px"; + } + + // Update top/bottom + this._checkScrollPosition(true); + }, + + _hidingKeyboard: function (event) { + // 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 (thisWinUI._Overlay._keyboardInfo._isResized) { + // Flag resize that we'll need an updated position + this._keyboardHiding = true; + } else { + // Not resized, update our final position, giving the anchor a chance to update first. + var that = this; + setImmediate(function () { that._findPosition(); }); + } + }, + + _checkScrollPosition: function (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 = thisWinUI._Overlay._keyboardInfo._visibleDocBottomOffset + "px"; + this._element.style.top = "auto"; + } else { + // Normal, attach to top + this._element.style.top = (this._scrollTop + thisWinUI._Overlay._keyboardInfo._visibleDocTop) + "px"; + this._element.style.bottom = "auto"; + } + }, + + // AppBar flyout animations + _flyoutAnimateIn: function () { + if (this._keyboardMovedUs){ + return this._baseAnimateIn(); + } else { + this._element.style.opacity = 1; + this._element.style.visibility = "visible"; + return WinJS.UI.Animation.showPopup(this._element, this._nextAnimOffset); + } + }, + + _flyoutAnimateOut: function () { + if (this._keyboardMovedUs) { + return this._baseAnimateOut(); + } else { + this._element.style.opacity = 0; + return WinJS.UI.Animation.hidePopup(this._element, this._nextAnimOffset); + } + }, + + // Hide all other flyouts besides this one + _hideAllOtherFlyouts: function (thisFlyout) { + var flyouts = document.querySelectorAll(".win-flyout"); + 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 () { + var flyouts = document.querySelectorAll(".win-flyout"); + for (var i = 0; i < flyouts.length; i++) { + var flyoutControl = flyouts[i].winControl; + if (flyoutControl && !flyoutControl.hidden) { + return true; + } + } + + return false; + }, + + _handleKeyDown: function (event) { + if (event.key === "Esc") { + // Show a focus rect on what we move focus to + event.preventDefault(); + event.stopPropagation(); + this.winControl._keyboardInvoked = true; + this.winControl._hide(); + } else if ((event.key === "Spacebar" || event.key === "Enter") + && (this === document.activeElement)) { + event.preventDefault(); + event.stopPropagation(); + this.winControl.hide(); + } else if (event.shiftKey && event.key === "Tab" + && this === 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 () { + var firstDiv = document.createElement("div"); + firstDiv.className = firstDivClass; + firstDiv.style.display = "inline"; + firstDiv.setAttribute("role", "menuitem"); + firstDiv.setAttribute("aria-hidden", "true"); + + this._element.insertAdjacentElement("AfterBegin", firstDiv); + var that = this; + firstDiv.addEventListener("focus", function () { that._focusOnLastFocusableElementOrThis(); }, false); + + return firstDiv; + }, + + // Create and add a new final div as the last child + _addFinalDiv: function () { + var finalDiv = document.createElement("div"); + finalDiv.className = finalDivClass; + finalDiv.style.display = "inline"; + finalDiv.setAttribute("role", "menuitem"); + finalDiv.setAttribute("aria-hidden", "true"); + + this._element.appendChild(finalDiv); + var that = this; + finalDiv.addEventListener("focus", function () { that._focusOnFirstFocusableElementOrThis(); }, false); + + return finalDiv; + } + }) + }); + + // Statics + + var strings = { + get ariaLabel() { return WinJS.Resources._getWinJSString("ui/flyoutAriaLabel").value; }, + get noAnchor() { return WinJS.Resources._getWinJSString("ui/noAnchor").value; }, + get badPlacement() { return WinJS.Resources._getWinJSString("ui/badPlacement").value; }, + get badAlignment() { return WinJS.Resources._getWinJSString("ui/badAlignment").value; } + }; + + //// Labels + + //// Errors + +})(WinJS); + +// Menu +/// Menu,Menus,Flyout,Flyouts,Statics +(function menuInit(WinJS) { + "use strict"; + + var thisWinUI = WinJS.UI; + + // Class Names + var menuClass = "win-menu"; + var menuToggleClass = "win-menu-toggle"; + + WinJS.Namespace.define("WinJS.UI", { + /// The Menu control provides MenuCommands to the user. + /// 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: WinJS.Class.derive(WinJS.UI.Flyout, function (element, options) { + /// + /// + /// Constructs the Menu control and associates it with the underlying DOM element. + /// + /// + /// The DOM element to be associated with the Menu control. + /// + /// + /// The set of options to be applied initially to the Menu control. + /// + /// A fully constructed Menu control. + /// + + // We need to be built on top of a Flyout, so stomp on the user's input + if (!options) { + options = {}; + } + if (!options.commands) { + // 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. + this._verifyCommandsOnly(element, "WinJS.UI.MenuCommand"); + } + + // Remember aria role in case base constructor changes it + var role = element ? element.getAttribute("role") : null; + var label = element ? element.getAttribute("aria-label") : null; + + // Call the base overlay constructor helper + this._baseFlyoutConstructor(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 + WinJS.Utilities.addClass(this._element, menuClass); + + // Need to set our commands, making sure we're hidden first + this.hide(); + }, { + // Public Properties + + /// The Commands for the Menu, contains an array of MenuCommand options. + commands: { + set: function (value) { + // Fail if trying to set when visible + if (!this.hidden) { + throw new WinJS.ErrorFromName("WinJS.UI.Menu.CannotChangeCommandsWhenVisible", WinJS.Resources._formatString(thisWinUI._Overlay.commonstrings.cannotChangeCommandsWhenVisible, "Menu")); + } + + // Start from scratch + this._element.innerHTML = ""; + + // 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. + /// + /// 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 with that id. + /// + var commands = this.element.querySelectorAll("#" + id); + for (var count = 0; count < commands.length; count++) { + // Any elements we generated this should succeed for, + // but remove missing ones just to be safe. + commands[count] = commands[count].winControl; + if (!commands[count]) { + commands.splice(count, 1); + } + } + + if (commands.length === 1) { + return commands[0]; + } else if (commands.length === 0) { + return null; + } + + return commands; + }, + + + showCommands: function (commands) { + /// + /// + /// Show commands within this Menu + /// + /// Required. Command or Commands to show, either String, DOM elements, or WinJS objects. + /// + if (!commands) { + throw new WinJS.ErrorFromName("WinJS.UI.Menu.RequiresCommands", strings.requiresCommands); + } + + this._showCommands(commands, true); + }, + + hideCommands: function (commands) { + /// + /// + /// Hide commands within this Menu + /// + /// Required. Command or Commands to hide, either String, DOM elements, or WinJS objects. + /// + if (!commands) { + throw new WinJS.ErrorFromName("WinJS.UI.Menu.RequiresCommands", strings.requiresCommands); + } + + this._hideCommands(commands, true); + }, + + showOnlyCommands: function (commands) { + /// + /// + /// Show the specified commands, hiding all of the others in the Menu. + /// + /// Required. Command or Commands to show, either String, DOM elements, or WinJS objects. + /// + if (!commands) { + throw new WinJS.ErrorFromName("WinJS.UI.Menu.RequiresCommands", strings.requiresCommands); + } + + this._showOnlyCommands(commands, true); + }, + + show: function (anchor, placement, alignment) { + /// + /// + /// Shows the Menu, if hidden, regardless of other state + /// + /// + /// The DOM element, or ID of a DOM element to anchor the Menu, overriding the anchor property for this time only. + /// + /// + /// The placement of the Menu 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 Menu to the anchor's edge: 'center' (default), 'left', or 'right'. This parameter overrides the alignment property for this show only. + /// + /// + // Just call private version to make appbar flags happy + this._show(anchor, placement, alignment); + }, + + _show: function (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 (command) { + // See if its a command already + if (command && !command._element) { + // Not a command, so assume it's options for a command + command = new WinJS.UI.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 () { + 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) { + WinJS.Utilities.addClass(this._element, menuToggleClass); + } else { + WinJS.Utilities.removeClass(this._element, menuToggleClass); + } + }, + + _checkForFlyoutCommands: function () { + 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 (event) { + if (event.key === "Esc") { + // Show a focus rect on what we move focus to + this.winControl._keyboardInvoked = true; + this.winControl._hide(); + } else if ((event.key === "Spacebar" || event.key === "Enter") + && (this === document.activeElement)) { + event.preventDefault(); + this.winControl.hide(); + } else if (event.key === "Up") { + var that = this; + thisWinUI.Menu._focusOnPreviousElement(that); + + // Prevent the page from scrolling + event.preventDefault(); + } else if (event.key === "Down") { + that = this; + thisWinUI.Menu._focusOnNextElement(that); + + // Prevent the page from scrolling + event.preventDefault(); + } else if (event.key === "Tab") { + event.preventDefault(); + } + } + }) + }); + // 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. + thisWinUI.Menu._focusOnNextElement = function (menu) { + var _currentElement = document.activeElement; + + do { + if (_currentElement === menu) { + _currentElement = _currentElement.firstElementChild; + } else { + _currentElement = _currentElement.nextElementSibling; + } + + if (_currentElement) { + _currentElement.focus(); + } else { + _currentElement = menu; + } + + } while (_currentElement !== 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. + thisWinUI.Menu._focusOnPreviousElement = function (menu) { + var _currentElement = document.activeElement; + + do { + if (_currentElement === menu) { + _currentElement = _currentElement.lastElementChild; + } else { + _currentElement = _currentElement.previousElementSibling; + } + + if (_currentElement) { + _currentElement.focus(); + } else { + _currentElement = menu; + } + + } while (_currentElement !== document.activeElement) + }; + + var strings = { + get ariaLabel() { return WinJS.Resources._getWinJSString("ui/menuAriaLabel").value; }, + get requiresCommands() { return WinJS.Resources._getWinJSString("ui/requiresCommands").value; } + }; +})(WinJS); + + +// Menu Command +/// appbar,appbars,Flyout,Flyouts,onclick,Statics +(function menuCommandInit(WinJS) { + "use strict"; + + var thisWinUI = WinJS.UI; + + // Class Names + var menuCommandClass = "win-command"; + var typeSeparator = "separator"; + var typeButton = "button"; + var typeToggle = "toggle"; + var typeFlyout = "flyout"; + + function _handleMenuClick(event) { + var command = this.winControl; + if (command) { + var hideParent = true; + if (command._type === typeToggle) { + command.selected = !command.selected; + } else if (command._type === 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 = 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(event) { + if (this && this.focus) { + this.focus(); + + this.addEventListener("mousemove", _handleMouseMove, false); + } + } + + function _handleMouseMove(event) { + if (this && this.focus && this !== document.activeElement) { + this.focus(); + } + } + + function _handleMouseOut(event) { + var that = this; + var parentFlyout = _getParentFlyout(that); + if (parentFlyout + && this === document.activeElement + && WinJS.Utilities.hasClass(parentFlyout, "win-menu") + && parentFlyout.focus) { + // Menu gives focus to the menu itself + parentFlyout.focus(); + } else if (parentFlyout + && this === document.activeElement + && parentFlyout.children + && parentFlyout.children.length > 0 + && parentFlyout.children[0] + && WinJS.Utilities.hasClass(parentFlyout.children[0], "win-firstdiv") + && parentFlyout.children[0].focus) { + // Flyout gives focus to firstDiv + parentFlyout.children[0].focus(); + } + + this.removeEventListener("mousemove", _handleMouseMove, false); + } + + function _getParentFlyout(element) { + while (element && !WinJS.Utilities.hasClass(element, "win-flyout")) { + element = element.parentElement; + } + + return element; + } + + WinJS.Namespace.define("WinJS.UI", { + /// + /// MenuCommands provide button, toggle button, flyout button, or separator functionality for Menus. + /// + /// + /// + /// ]]> + /// The MenuCommand control itself + /// + /// + /// + MenuCommand: WinJS.Class.define(function MenuCommand_ctor(element, options) { + /// + /// + /// Constructs the MenuCommand control + /// + /// + /// The DOM element to be associated with the MenuCommand control. MenuCommand will create one if null. + /// + /// + /// The set of options to be applied initially to the MenuCommand control. + /// + /// + /// A MenuCommand control. + /// + /// + + // Check to make sure we weren't duplicated + if (element && element.winControl) { + throw new WinJS.ErrorFromName("WinJS.UI.MenuCommand.DuplicateConstruction", strings.duplicateConstruction); + } + + // 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 = typeButton; + } + + // Go ahead and create it, separators look different than buttons + // Don't forget to use passed in element if one was provided. + this._element = element; + if (options.type === typeSeparator) { + this._createSeparator(); + } else { + // This will also set the icon & label + this._createButton(); + } + + // Remember ourselves + this._element.winControl = this; + + // Attach our css class + WinJS.Utilities.addClass(this._element, menuCommandClass); + + if (!options.selected && options.type === typeToggle) { + // Make sure toggle's have selected false for CSS + this.selected = false; + } + if (options.onclick) { + this.onclick = options.onclick; + } + options.onclick = _handleMenuClick; + + WinJS.UI.setOptions(this, options); + + // Set our options + if (this._type !== 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 === typeToggle) { + role = "menuitemcheckbox"; + } + this._element.setAttribute("role", role); + if (this._type === 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); + }, { + /// 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; + } + } + }, + + /// 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 !== typeButton && value !== typeFlyout && value !== typeToggle && value !== typeSeparator) { + this._type = typeButton; + } else { + this._type = value; + } + } + } + }, + + /// The label of the MenuCommand + label: { + get: function () { + return this._label; + }, + set: function (value) { + this._label = value; + this._element.innerText = this.label; + + // Update aria-label + this._element.setAttribute("aria-label", this.label); + } + }, + + /// The click event to call when the MenuCommand is invoked + onclick: { + get: function () { + return this._onclick; + }, + set: function (value) { + if (value && typeof value !== "function") { + throw new WinJS.ErrorFromName("WinJS.UI.MenuCommand.BadClick", WinJS.Resources._formatString(strings.badClick, "MenuCommand")); + } + this._onclick = value; + } + }, + + /// For flyout type MenuCommands, get returns the WinJS.UI.Flyout that this command invokes. Set may also use the String id of the flyout to invoke, the DOM object of the flyout, or the flyout object itself + flyout: { + get: function () { + // Resolve it to the flyout + var flyout = this._flyout; + if (typeof flyout === "string") { + flyout = 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 = id.uniqueID; + id = id.id; + } + } + } + if (typeof id === "string") { + this._element.setAttribute("aria-owns", id); + } + + // Remember it + this._flyout = value; + } + }, + + /// 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; + } + }, + + /// Disable a command. It will get or set the HTML disabled attribute. + 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 = thisWinUI._Overlay._getParentControlUsingClassName(this._element, "win-menu"); + if (menuControl && !menuControl.hidden) { + throw new WinJS.ErrorFromName("WinJS.UI.MenuCommand.CannotChangeHiddenProperty", WinJS.Resources._formatString(thisWinUI._Overlay.commonstrings.cannotChangeHiddenProperty, "Menu")); + } + + var style = this._element.style; + if (value) { + style.visibility = "hidden"; + style.display = "none"; + } else { + style.visibility = ""; + style.display = "block"; + } + } + }, + + /// Adds an extra CSS class during construction. + extraClass: { + get: function () { + return this._extraClass; + }, + set: function (value) { + if (this._extraClass) { + WinJS.Utilities.removeClass(this._element, this._extraClass); + } + this._extraClass = value; + WinJS.Utilities.addClass(this._element, this._extraClass); + } + }, + + addEventListener: function (type, listener, useCapture) { + /// + /// + /// Add an event listener to the DOM element for this command + /// + /// Required. Event type to add. + /// 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 command + /// + /// Required. Event type to remove. + /// 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); + }, + + // Private properties + _createSeparator: function () { + // Make sure there's an input element + if (!this._element) { + this._element = document.createElement("hr"); + } else { + // Verify the input was an hr + if (this._element.tagName !== "HR") { + throw new WinJS.ErrorFromName("WinJS.UI.MenuCommand.BadHrElement", strings.badHrElement); + } + } + }, + + _createButton: function () { + // Make sure there's an input element + if (!this._element) { + this._element = document.createElement("button"); + } else { + // Verify the input was a button + if (this._element.tagName !== "BUTTON") { + throw new WinJS.ErrorFromName("WinJS.UI.MenuCommand.BadButtonElement", strings.badButtonElement); + } + this._element.innerHtml = ""; + } + + // MenuCommand buttons need to look like this: + //// + this._element.type = "button"; + + // 'innertext' label is added later by caller + } + }) + }); + + // Statics + + var strings = { + get ariaLabel() { return WinJS.Resources._getWinJSString("ui/menuCommandAriaLabel").value; }, + get duplicateConstruction() { return WinJS.Resources._getWinJSString("ui/duplicateConstruction").value; }, + get badClick() { return WinJS.Resources._getWinJSString("ui/badClick").value; }, + get badHrElement() { return WinJS.Resources._getWinJSString("ui/badHrElement").value; }, + get badButtonElement() { return WinJS.Resources._getWinJSString("ui/badButtonElement").value; } + }; + +})(WinJS); + + +/// appbar,Flyout,Flyouts,registeredforsettings,SettingsFlyout,Statics,Syriac +(function settingsFlyoutInit(WinJS) { + "use strict"; + + var thisWinUI = WinJS.UI; + + var focusHasHitSettingsPaneOnce; + + // Class Names + var settingsFlyoutClass = "win-settingsflyout", + fullSettingsFlyoutClassName = "." + settingsFlyoutClass, + settingsFlyoutLightClass = "win-ui-light", + narrowClass = "win-narrow", + wideClass = "win-wide"; + + var firstDivClass = "win-firstdiv"; + var finalDivClass = "win-finaldiv"; + + // Constants for width + var settingsNarrow = "narrow", + settingsWide = "wide"; + + // Determine if the settings pane (system language) is RTL or not. + function _shouldAnimateFromLeft() { + if (WinJS.Utilities.hasWinRT && Windows.UI.ApplicationSettings.SettingsEdgeLocation) { + var appSettings = 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(fullSettingsFlyoutClassName); + var retValue, + control; + for (var i=0; iThe SettingsFlyout provides access to information from the Settings pane. + /// 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: WinJS.Class.derive(WinJS.UI._Overlay, function (element, options) { + /// + /// Constructs a SettingsFlyout control. + /// + /// The DOM element to be associated with the SettingsFlyout control. + /// + /// + /// The set of options to be applied initially to the SettingsFlyout control. + /// + /// A constructed SettingsFlyout control. + /// + + // Call the base overlay constructor helper + this._baseOverlayConstructor(element, options); + + this._addFirstDiv(); + this._addFinalDiv(); + + // Handle "esc" & "tab" key presses + this._element.addEventListener("keydown", this._handleKeyDown, true); + + // Make a click eating div + thisWinUI._Overlay._createClickEatingDivAppBar(); + + // Start settings hidden + this._element.style.visibilty = "hidden"; + this._element.style.display = "none"; + + // Attach our css class + WinJS.Utilities.addClass(this._element, settingsFlyoutClass); + + // apply the light theme styling to the win-content elements inside the SettingsFlyout + WinJS.Utilities.query("div.win-content", this._element). + forEach(function (e) { + if (!e.msMatchesSelector('.win-ui-dark, .win-ui-dark *')) { + WinJS.Utilities.addClass(e, settingsFlyoutLightClass); + } + }); + + // 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 Flyout event handlers are hooked up + this._addFlyoutEventHandlers(true); + + // Make sure animations are hooked up + this._currentAnimateIn = this._animateSlideIn; + this._currentAnimateOut = this._animateSlideOut; + }, { + // Public Properties + + /// Width of the SettingsFlyout, "narrow", or "wide". + width: { + get: function () { + return this._width; + }, + + set: function (value) { + if (value === this._width) { + return; + } + // Get rid of old class + if (this._width === settingsNarrow) { + WinJS.Utilities.removeClass(this._element, narrowClass); + } else if (this._width === settingsWide) { + WinJS.Utilities.removeClass(this._element, wideClass); + } + this._width = value; + + // Attach our new css class + if (this._width === settingsNarrow) { + WinJS.Utilities.addClass(this._element, narrowClass); + } else if (this._width === settingsWide) { + WinJS.Utilities.addClass(this._element, 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._show(); + }, + + _show: function() { + // We call our base _baseShow because AppBar may need to override show + this._baseShow(); + // Need click-eating div to be visible, + // (even if now hiding, we'll show and need click eater) + thisWinUI._Overlay._showClickEatingDivAppBar(); + }, + + _endShow: function () { + // Clean up after showing + this._initAfterAnimation(); + }, + + _initAfterAnimation: function() { + focusHasHitSettingsPaneOnce = 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 (!WinJS.Utilities.hasClass(this.element.children[0], 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]) { + this.element.children[0].addEventListener("focusout", function () { focusHasHitSettingsPaneOnce = 1; }, false); + this.element.children[0].focus(); + } + + if (!WinJS.Utilities.hasClass(this.element.children[this.element.children.length - 1], 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() { + var backbuttons = this.element.querySelectorAll(".win-backbutton"); + var label; + for (var i=0; i + /// + /// Hides the SettingsFlyout, if visible, regardless of other state. + /// + /// + // Just call private version to make appbar flags happy + this._hide(); + }, + + _hide: function () { + if (this._baseHide()) { + // Need click-eating div to be hidden + thisWinUI._Overlay._hideClickEatingDivAppBar(); + } + }, + + // SettingsFlyout animations + _animateSlideIn: function () { + var animateFromLeft = _shouldAnimateFromLeft(); + var offset = animateFromLeft ? "-100px" : "100px"; + WinJS.Utilities.query("div.win-content", this._element). + forEach(function(e) { WinJS.UI.Animation.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 WinJS.UI.Animation.showPanel(this._element, where); + }, + + _animateSlideOut: function () { + 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 WinJS.UI.Animation.showPanel(this._element, where); + }, + + _fragmentDiv: { + get: function () { + return this._fragDiv; + }, + + set: function (value) { + this._fragDiv = value; + } + }, + + _unloadPage: function (event) { + var settingsControl = event.currentTarget.winControl; + settingsControl.removeEventListener(thisWinUI._Overlay.afterHide, this._unloadPage, false); + + WinJS.Promise.as().then( function () { + if (settingsControl._fragmentDiv) { + document.body.removeChild(settingsControl._fragmentDiv); + settingsControl._fragmentDiv = null; + } + }); + }, + + _dismiss: function () { + this.addEventListener(thisWinUI._Overlay.afterHide, this._unloadPage, false); + this._hide(); + }, + + _handleKeyDown: function (event) { + if (event.key === "Esc") { + event.preventDefault(); + event.stopPropagation(); + this.winControl._dismiss(); + } else if ((event.key === "Spacebar" || event.key === "Enter") + && (this.children[0] === document.activeElement)) { + event.preventDefault(); + event.stopPropagation(); + this.winControl._dismiss(); + } else if (event.shiftKey && event.key === "Tab" + && this.children[0] === 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] === document.activeElement) { + break; + } + } + } + }, + + _focusOnLastFocusableElementFromParent: function () { + var active = document.activeElement; + if (!focusHasHitSettingsPaneOnce || !active || !WinJS.Utilities.hasClass(active, 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. + if (_highestTabIndex) { + for (var 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] === document.activeElement) { + break; + } + } + } + } + }, + + _focusOnFirstFocusableElementFromParent: function () { + var active = document.activeElement; + if (!active || !WinJS.Utilities.hasClass(active, 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. + if (_lowestTabIndex) { + for (var 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] === document.activeElement) { + break; + } + } + } + } + }, + + // Create and add a new first div to the beginning of the list + _addFirstDiv: function () { + 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 = document.createElement("div"); + firstDiv.className = firstDivClass; + firstDiv.style.display = "inline"; + firstDiv.setAttribute("role", "menuitem"); + firstDiv.setAttribute("aria-hidden", "true"); + firstDiv.tabIndex = _minTab; + firstDiv.addEventListener("focus", this._focusOnLastFocusableElementFromParent, false); + + this._element.insertAdjacentElement("AfterBegin", firstDiv); + }, + + // Create and add a new final div to the end of the list + _addFinalDiv: function () { + 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 = document.createElement("div"); + finalDiv.className = finalDivClass; + finalDiv.style.display = "inline"; + finalDiv.setAttribute("role", "menuitem"); + finalDiv.setAttribute("aria-hidden", "true"); + finalDiv.tabIndex = _maxTab; + finalDiv.addEventListener("focus", this._focusOnFirstFocusableElementFromParent, false); + + this._element.appendChild(finalDiv); + } + }) + }); + + // Statics + thisWinUI.SettingsFlyout.show = function () { + /// + /// + /// Activate the settings charm window. + /// + /// + // Show the main settings pane + if (WinJS.Utilities.hasWinRT) { + Windows.UI.ApplicationSettings.SettingsPane.show(); + } + // And hide the WWA one + var elements = 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 }; + thisWinUI.SettingsFlyout.populateSettings = function (e) { + /// + /// + /// Populate the settings commands to be shown in the settings charm window. + /// + /// + /// The event object holding the settings commands array to be shown in the settings charm window. + /// + /// + _settingsEvent.event = e.detail; + + if (_settingsEvent.event.applicationcommands) { + var n = 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, thisWinUI.SettingsFlyout._onSettingsCommand); + _settingsEvent.event.e.request.applicationCommands.append(command); + }); + } + }; + + thisWinUI.SettingsFlyout._onSettingsCommand = function (command) { + var id = command.id; + if (_settingsEvent.event.applicationcommands && _settingsEvent.event.applicationcommands[id]) { + thisWinUI.SettingsFlyout.showSettings(id, _settingsEvent.event.applicationcommands[id].href); + } + }; + + thisWinUI.SettingsFlyout.showSettings = function (id, path) { + /// + /// + /// Show the SettingsFlyout using the path of the page contains the settings element and the settings element Id. + /// + /// + var control = _getChildSettingsControl(document, id); + if (control) { + control.show(); + } else if (path) { + var divElement = document.createElement("div"); + divElement = document.body.appendChild(divElement); + WinJS.UI.Pages.render(path, divElement).then(function () { + control = _getChildSettingsControl(divElement, id); + if (control) { + control._fragmentDiv = divElement; + control.show(); + } else { + document.body.removeChild(divElement); + } + }); + } else { + throw new WinJS.ErrorFromName("WinJS.UI.SettingsFlyout.BadReference", strings.badReference); + } + }; + // Application Settings Handling End + + var strings = { + get ariaLabel() { return WinJS.Resources._getWinJSString("ui/settingsFlyoutAriaLabel").value; }, + get badReference() { return WinJS.Resources._getWinJSString("ui/badReference").value; }, + get backbuttonAriaLabel() { return WinJS.Resources._getWinJSString("ui/backbuttonarialabel").value; }, + }; + + //// Labels + + //// Errors + +})(WinJS); + + +(function tooltipInit(global) { + "use strict"; + + + var lastCloseTime = 0; + var utilities = WinJS.Utilities; + var animation = WinJS.UI.Animation; + + // Constants definition + var DEFAULT_PLACEMENT = "top"; + var DELAY_INITIAL_TOUCH_SHORT = 400; + var DELAY_INITIAL_TOUCH_LONG = 1200; + var DEFAULT_MOUSE_HOVER_TIME = 400; // 0.4 second + var DEFAULT_MESSAGE_DURATION = 5000; // 5 secs + var DELAY_RESHOW_NONINFOTIP_TOUCH = 0; + var DELAY_RESHOW_NONINFOTIP_NONTOUCH = 600; + var DELAY_RESHOW_INFOTIP_TOUCH = 400; + var DELAY_RESHOW_INFOTIP_NONTOUCH = 600; + var RESHOW_THRESHOLD = 200; + var HIDE_DELAY_MAX = 300000; // 5 mins + var OFFSET_KEYBOARD = 12; + var OFFSET_MOUSE = 20; + var OFFSET_TOUCH = 45; + var OFFSET_PROGRAMMATIC_TOUCH = 20; + var OFFSET_PROGRAMMATIC_NONTOUCH = 12; + var SAFETY_NET_GAP = 1; // We set a 1-pixel gap between the right or bottom edge of the tooltip and the viewport to avoid possible re-layout + var PT_TOUCH = 2; // pointer type to indicate a touch event + + var EVENTS_INVOKE = { "keyup": "", "MSPointerOver": "" }, + EVENTS_UPDATE = { "MSPointerMove": "" }, + EVENTS_DISMISS = { "MSPointerDown": "", "keydown": "", "blur": "", "MSPointerOut": "", "MSPointerCancel": "", "MSPointerUp": "" }, + EVENTS_BY_CHILD = { "MSPointerOver": "", "MSPointerOut": "" }; + + // CSS class names + var msTooltip = "win-tooltip", + msTooltipPhantom = "win-tooltip-phantom"; + + // Global attributes + var mouseHoverTime = DEFAULT_MOUSE_HOVER_TIME, + nonInfoTooltipNonTouchShowDelay = 2 * mouseHoverTime, + infoTooltipNonTouchShowDelay = 2.5 * mouseHoverTime, + messageDuration = DEFAULT_MESSAGE_DURATION, + isLeftHanded = false; + + var hasInitWinRTSettings = false; + + // Tooltip control implementation + WinJS.Namespace.define("WinJS.UI", { + /// + /// Displays a tooltip that can contain images and formatting. + /// + /// + /// + ///
      ]]> + /// Raised when the tooltip is about to appear. + /// Raised when the tooltip is showing. + /// Raised when the tooltip is about to become hidden. + /// Raised when the tooltip is hidden. + /// The entire Tooltip control. + /// + /// + /// + Tooltip: WinJS.Class.define(function Tooltip_ctor(anchorElement, options) { + /// + /// + /// Creates a new Tooltip. + /// + /// + /// The DOM element that hosts the Tooltip. + /// + /// + /// 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 opened event, + /// add a property named "onopened" to the options object and set its value to the event handler. + /// This parameter is optional. + /// + /// + /// The new Tooltip. + /// + /// + anchorElement = anchorElement || document.createElement("div"); + + var tooltip = utilities.data(anchorElement).tooltip; + if (tooltip) { + return tooltip; + } + + // Set system attributes if it is in WWA, otherwise, use the default values + if (!hasInitWinRTSettings && WinJS.Utilities.hasWinRT) { // in WWA + var uiSettings = new Windows.UI.ViewManagement.UISettings(); + mouseHoverTime = uiSettings.mouseHoverTime; + nonInfoTooltipNonTouchShowDelay = 2 * mouseHoverTime; + infoTooltipNonTouchShowDelay = 2.5 * mouseHoverTime; + messageDuration = uiSettings.messageDuration * 1000; // uiSettings.messageDuration is in seconds. + var handedness = uiSettings.handPreference; + isLeftHanded = (handedness == Windows.UI.ViewManagement.HandPreference.leftHanded); + } + hasInitWinRTSettings = true; + + // Need to initialize properties + this._placement = DEFAULT_PLACEMENT; + this._infotip = false; + this._innerHTML = null; + this._contentElement = null; + this._extraClass = null; + this._lastContentType = "html"; + this._anchorElement = anchorElement; + this._domElement = null; + this._phantomDiv = null; + this._triggerByOpen = false; + + // To handle keyboard navigation + this._lastKeyOrBlurEvent = null; + this._currentKeyOrBlurEvent = null; + + // Remember ourselves + anchorElement.winControl = this; + + // If anchor element's title is defined, set as the default tooltip content + if (anchorElement.title) { + this._innerHTML = this._anchorElement.title; + this._anchorElement.removeAttribute("title"); + } + + WinJS.UI.setOptions(this, options); + this._events(); + utilities.data(anchorElement).tooltip = this; + }, { + /// + /// Gets or sets the HTML content of the Tooltip. + /// + innerHTML: { + get: function () { + return this._innerHTML; + }, + set: function (value) { + this._innerHTML = value; + if (this._domElement) { + // If we set the innerHTML to null or "" while tooltip is up, we should close it + if (!this._innerHTML || this._innerHTML === "") { + this._onDismiss(); + return; + } + this._domElement.innerHTML = value; + this._position(); + } + this._lastContentType = "html"; + } + }, + + /// + element: { + get: function () { + return this._anchorElement; + } + }, + + /// + /// Gets or sets the DOM element that is the content for the ToolTip. + /// + contentElement: { + get: function () { + return this._contentElement; + }, + set: function (value) { + this._contentElement = value; + if (this._domElement) { + // If we set the contentElement to null while tooltip is up, we should close it + if (!this._contentElement) { + this._onDismiss(); + return; + } + this._domElement.innerHTML = ""; + this._domElement.appendChild(this._contentElement); + this._position(); + } + this._lastContentType = "element"; + } + }, + + /// + /// Gets or sets the position for the Tooltip relative to its target element: top, bottom, left or right. + /// + placement: { + get: function () { + return this._placement; + }, + set: function (value) { + if (value !== "top" && value !== "bottom" && value !== "left" && value !== "right") { + value = DEFAULT_PLACEMENT; + } + this._placement = value; + if (this._domElement) { + this._position(); + } + } + }, + + /// + /// Gets or sets a value that specifies whether the Tooltip is an infotip, a tooltip that contains + /// a lot of info and should be displayed for longer than a typical Tooltip. + /// The default value is false. + /// + infotip: { + get: function () { + return this._infotip; + }, + set: function (value) { + this._infotip = !!value; //convert the value to boolean + } + }, + + /// + /// Gets or sets additional CSS classes to apply to the Tooltip control's host element. + /// + extraClass: { + get: function () { + return this._extraClass; + }, + set: function (value) { + this._extraClass = value; + } + }, + + addEventListener: function (eventName, eventCallBack, capture) { + /// + /// + /// Registers an event handler for the specified event. + /// + /// The name of the event. + /// The event handler function to associate with this event. + /// Set to true to register the event handler for the capturing phase; set to false to register for the bubbling phase. + /// + + if (this._anchorElement) { + this._anchorElement.addEventListener(eventName, eventCallBack, capture); + } + }, + + removeEventListener: function (eventName, eventCallBack, capture) { + /// + /// + /// Unregisters an event handler for the specified event. + /// + /// The name of the event. + /// The event handler function to remove. + /// Set to true to unregister the event handler for the capturing phase; otherwise, set to false to unregister the event handler for the bubbling phase. + /// + + if (this._anchorElement) { + this._anchorElement.removeEventListener(eventName, eventCallBack, capture); + } + }, + + open: function (type) { + /// + /// + /// Shows the Tooltip. + /// + /// The type of tooltip to show: "touch", "mouseover", "mousedown", or "keyboard". The default value is "mousedown". + /// + + // Open takes precedence over other triggering events + // Once tooltip is opened using open(), it can only be closed by time out(mouseover or keyboard) or explicitly by close(). + this._triggerByOpen = true; + + if (type !== "touch" && type !== "mouseover" && type !== "mousedown" && type !== "keyboard") { + type = "default"; + } + + switch (type) { + case "touch": + this._onInvoke("touch", "never"); + break; + case "mouseover": + this._onInvoke("mouse", "auto"); + break; + case "keyboard": + this._onInvoke("keyboard", "auto"); + break; + case "mousedown": + case "default": + this._onInvoke("nodelay", "never"); + break; + } + + }, + + close: function () { + /// + /// + /// Hids the Tooltip. + /// + /// + + this._onDismiss(); + }, + + _cleanUpDOM: function () { + if (this._domElement) { + document.body.removeChild(this._phantomDiv); + document.body.removeChild(this._domElement); + this._domElement = null; + this._phantomDiv = null; + } + }, + + _createTooltipDOM: function () { + this._cleanUpDOM(); + + this._domElement = document.createElement("div"); + + var id = this._domElement.uniqueID; + this._domElement.setAttribute("id", id); + + // Set the direction of tooltip according to anchor element's + var computedStyle = document.defaultView.getComputedStyle(this._anchorElement, null); + var elemStyle = this._domElement.style; + elemStyle.direction = computedStyle.direction; + elemStyle.writingMode = computedStyle["writing-mode"]; // must use CSS name, not JS name + + // Make the tooltip non-focusable + this._domElement.setAttribute("tabindex", -1); + + // Set the aria tags for accessibility + this._domElement.setAttribute("role", "tooltip"); + this._anchorElement.setAttribute("aria-describedby", id); + + // Set the tooltip content + if (this._lastContentType === "element") { // Last update through contentElement option + this._domElement.appendChild(this._contentElement); + } else { // Last update through innerHTML option + this._domElement.innerHTML = this._innerHTML; + } + + document.body.appendChild(this._domElement); + utilities.addClass(this._domElement, msTooltip); + + // In the event of user-assigned classes, add those too + if (this._extraClass) { + utilities.addClass(this._domElement, this._extraClass); + } + + // Create a phantom div on top of the tooltip div to block all interactions + this._phantomDiv = document.createElement("div"); + this._phantomDiv.setAttribute("tabindex", -1); + document.body.appendChild(this._phantomDiv); + utilities.addClass(this._phantomDiv, msTooltipPhantom); + var zIndex = document.defaultView.getComputedStyle(this._domElement, null).zIndex + 1; + this._phantomDiv.style.zIndex = zIndex; + }, + + _raiseEvent: function (type, eventProperties) { + if (this._anchorElement) { + var customEvent = document.createEvent("CustomEvent"); + customEvent.initCustomEvent(type, false, false, eventProperties); + this._anchorElement.dispatchEvent(customEvent); + } + }, + + // Support for keyboard navigation + _captureLastKeyBlurOrPointerOverEvent: function (event, listener) { + listener._lastKeyOrBlurEvent = listener._currentKeyOrBlurEvent; + switch (event.type) { + case "keyup": + if (event.key === "Shift") { + listener._currentKeyOrBlurEvent = null; + } else { + listener._currentKeyOrBlurEvent = "keyboard"; + } + break; + case "blur": + //anchor elment no longer in focus, clear up the stack + listener._currentKeyOrBlurEvent = null; + break; + default: + break; + + } + }, + + _registerEventToListener: function (element, eventType, listener) { + element.addEventListener(eventType, + function (event) { + listener._captureLastKeyBlurOrPointerOverEvent(event, listener); + listener._handleEvent(event); + }, + false); + }, + + _events: function () { + for (var eventType in EVENTS_INVOKE) { + this._registerEventToListener(this._anchorElement, eventType, this); + } + for (var eventType in EVENTS_UPDATE) { + this._registerEventToListener(this._anchorElement, eventType, this); + } + for (eventType in EVENTS_DISMISS) { + this._registerEventToListener(this._anchorElement, eventType, this); + } + + + }, + + _handleEvent: function (event) { + var eventType = event.type; + if (!this._triggerByOpen) { + // If the anchor element has children, we should ignore events that are caused within the anchor element + // Please note that we are not using event.target here as in bubbling phases from the child, the event target + // is usually the child + if (eventType in EVENTS_BY_CHILD) { + var elem = event.relatedTarget; + + while (elem && elem !== this._anchorElement && elem !== document.body) { + try { + elem = elem.parentNode; + } + catch (e) { + if (e instanceof Error && e.message === 'Permission denied') { + //Permission denied error, if we can't access the node's + //information, we should not handle the event + //Put this guard prior Bug 484666 is fixed + return; + } + else { + throw e; + } + } + } + if (elem === this._anchorElement) { + return; + } + } + if (eventType in EVENTS_INVOKE) { + if (event.pointerType == PT_TOUCH) { + this._onInvoke("touch", "never", event); + this._showTrigger = "touch"; + } else { + var type = eventType.substring(0, 3) === "key" ? "keyboard" : "mouse"; + this._onInvoke(type, "auto", event); + this._showTrigger = type; + } + } else if (eventType in EVENTS_UPDATE) { + this._contactPoint = { x: event.clientX, y: event.clientY }; + } else if (eventType in EVENTS_DISMISS) { + var eventTrigger; + if (event.pointerType == PT_TOUCH) { + if (eventType == "MSPointerDown") { + return; + } + eventTrigger = "touch"; + } + else { + eventTrigger = eventType.substring(0, 3) === "key" ? "keyboard" : "mouse"; + } + if (eventType != "blur" && eventTrigger != this._showTrigger) { + return; + } + this._onDismiss(); + } + } + }, + + _onShowAnimationEnd: function () { + if (this._shouldDismiss) { + return; + } + this._raiseEvent("opened"); + if (this._domElement) { + if (this._hideDelay !== "never") { + var that = this; + var delay = this._infotip ? Math.min(3 * messageDuration, HIDE_DELAY_MAX) : messageDuration; + this._hideDelayTimer = setTimeout(function () { + that._onDismiss(); + }, delay); + } + } + }, + + + _onHideAnimationEnd: function () { + document.body.removeEventListener("DOMNodeRemoved", this._removeTooltip, false); + this._cleanUpDOM(); + // Once we remove the tooltip from the DOM, we should remove the aria tag from the anchor + if (this._anchorElement) { + this._anchorElement.removeAttribute("aria-describedby"); + } + lastCloseTime = (new Date()).getTime(); + this._triggerByOpen = false; + this._raiseEvent("closed"); + }, + + _decideOnDelay: function (type) { + var value; + this._useAnimation = true; + + if (type == "nodelay") { + value = 0; + this._useAnimation = false; + } + else { + var curTime = (new Date()).getTime(); + // If the mouse is moved immediately from another anchor that has + // tooltip open, we should use a shorter delay + if (curTime - lastCloseTime <= RESHOW_THRESHOLD) { + if (type == "touch") { + value = this._infotip ? DELAY_RESHOW_INFOTIP_TOUCH : DELAY_RESHOW_NONINFOTIP_TOUCH; + } + else { + value = this._infotip ? DELAY_RESHOW_INFOTIP_NONTOUCH : DELAY_RESHOW_NONINFOTIP_NONTOUCH; + } + this._useAnimation = false; + } else if (type == "touch") { + value = this._infotip ? DELAY_INITIAL_TOUCH_LONG : DELAY_INITIAL_TOUCH_SHORT; + } else { + value = this._infotip ? infoTooltipNonTouchShowDelay : nonInfoTooltipNonTouchShowDelay; + } + } + return value; + }, + + // This function returns the anchor element's position in the Window coordinates. + _getAnchorPositionFromElementWindowCoord: function () { + var rect = this._anchorElement.getBoundingClientRect(); + + return { + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height + }; + }, + + _getAnchorPositionFromPointerWindowCoord: function (contactPoint) { + return { + x: contactPoint.x, + y: contactPoint.y, + width: 1, + height: 1 + }; + }, + + _canPositionOnSide: function (placement, viewport, anchor, tip) { + var availWidth = 0, availHeight = 0; + + switch (placement) { + case "top": + availWidth = tip.width + this._offset; + availHeight = anchor.y; + break; + case "bottom": + availWidth = tip.width + this._offset; + availHeight = viewport.height - anchor.y - anchor.height; + break; + case "left": + availWidth = anchor.x; + availHeight = tip.height + this._offset; + break; + case "right": + availWidth = viewport.width - anchor.x - anchor.width; + availHeight = tip.height + this._offset; + break; + } + return ((availWidth >= tip.width + this._offset) && (availHeight >= tip.height + this._offset)); + }, + + _positionOnSide: function (placement, viewport, anchor, tip) { + var left = 0, top = 0; + + switch (placement) { + case "top": + case "bottom": + // Align the tooltip to the anchor's center horizontally + left = anchor.x + anchor.width / 2 - tip.width / 2; + + // If the left boundary is outside the window, set it to 0 + // If the right boundary is outside the window, set it to align with the window right boundary + left = Math.min(Math.max(left, 0), viewport.width - tip.width - SAFETY_NET_GAP); + + top = (placement == "top") ? anchor.y - tip.height - this._offset : anchor.y + anchor.height + this._offset; + break; + case "left": + case "right": + // Align the tooltip to the anchor's center vertically + top = anchor.y + anchor.height / 2 - tip.height / 2; + + // If the top boundary is outside the window, set it to 0 + // If the bottom boundary is outside the window, set it to align with the window bottom boundary + top = Math.min(Math.max(top, 0), viewport.height - tip.height - SAFETY_NET_GAP); + + left = (placement == "left") ? anchor.x - tip.width - this._offset : anchor.x + anchor.width + this._offset; + break; + } + + // Actually set the position + this._domElement.style.left = left + "px"; + this._domElement.style.top = top + "px"; + + // Set the phantom's position and size + this._phantomDiv.style.left = left + "px"; + this._phantomDiv.style.top = top + "px"; + this._phantomDiv.style.width = tip.width + "px"; + this._phantomDiv.style.height = tip.height + "px"; + }, + + _position: function (contactType) { + var viewport = { width: 0, height: 0 }; + var anchor = { x: 0, y: 0, width: 0, height: 0 }; + var tip = { width: 0, height: 0 }; + + viewport.width = document.documentElement.clientWidth; + viewport.height = document.documentElement.clientHeight; + if (document.defaultView.getComputedStyle(document.body, null)["writing-mode"] === "tb-rl") { + viewport.width = document.documentElement.clientHeight; + viewport.height = document.documentElement.clientWidth; + } + + if (this._contactPoint && (contactType === "touch" || contactType === "mouse")) { + anchor = this._getAnchorPositionFromPointerWindowCoord(this._contactPoint); + } + else { + // keyboard or programmatic is relative to element + anchor = this._getAnchorPositionFromElementWindowCoord(); + } + tip.width = this._domElement.offsetWidth; + tip.height = this._domElement.offsetHeight; + var fallback_order = { + "top": ["top", "bottom", "left", "right"], + "bottom": ["bottom", "top", "left", "right"], + "left": ["left", "right", "top", "bottom"], + "right": ["right", "left", "top", "bottom"] + }; + if (isLeftHanded) { + fallback_order.top[2] = "right"; + fallback_order.top[3] = "left"; + fallback_order.bottom[2] = "right"; + fallback_order.bottom[3] = "left"; + } + + // Try to position the tooltip according to the placement preference + // We use this order: + // 1. Try the preferred placement + // 2. Try the opposite placement + // 3. If the preferred placement is top or bottom, we should try left + // and right (or right and left if left handed) + // If the preferred placement is left or right, we should try top and bottom + var order = fallback_order[this._placement]; + var length = order.length; + for (var i = 0; i < length; i++) { + if (i == length - 1 || this._canPositionOnSide(order[i], viewport, anchor, tip)) { + this._positionOnSide(order[i], viewport, anchor, tip); + break; + } + } + return order[i]; + }, + + _showTooltip: function (contactType) { + // Give a chance to dismiss the tooltip before it starts to show + if (this._shouldDismiss) { + return; + } + this._isShown = true; + this._raiseEvent("beforeopen"); + + // If the anchor is not in the DOM tree, we don't create the tooltip + if (!this._anchorElement.parentNode) { + return; + } + if (this._shouldDismiss) { + return; + } + + // If the contentElement is set to null or innerHTML set to null or "", we should NOT show the tooltip + if (this._lastContentType === "element") { // Last update through contentElement option + if (!this._contentElement) { + this._isShown = false; + return; + } + } else { // Last update through innerHTML option + if (!this._innerHTML || this._innerHTML === "") { + this._isShown = false; + return; + } + } + + var that = this; + this._removeTooltip = function (event) { + var current = that._anchorElement; + while (current) { + if (event.target == current) { + document.body.removeEventListener("DOMNodeRemoved", that._removeTooltip, false); + that._cleanUpDOM(); + break; + } + current = current.parentNode; + } + }; + + document.body.addEventListener("DOMNodeRemoved", this._removeTooltip, false); + this._createTooltipDOM(); + var pos = this._position(contactType); + var that = this; + if (this._useAnimation) { + animation.fadeIn(this._domElement) + .then(this._onShowAnimationEnd.bind(this)); + } else { + this._onShowAnimationEnd(); + } + }, + + _onInvoke: function (type, hide, event) { + // Reset the dismiss flag + this._shouldDismiss = false; + + // If the tooltip is already shown, ignore the current event + if (this._isShown) { + return; + } + + // To handle keyboard support, we only want to display tooltip on the first tab key event only + if (event && event.type === "keyup") { + if (this._lastKeyOrBlurEvent == "keyboard" || + !this._lastKeyOrBlurEvent && event.key !== "Tab") { + return; + } + } + + // Set the hide delay, + this._hideDelay = hide; + + this._contactPoint = null; + if (event) { // Open through interaction + this._contactPoint = { x: event.clientX, y: event.clientY }; + // Tooltip display offset differently for touch events and non-touch events + if (type == "touch") { + this._offset = OFFSET_TOUCH; + } else if (type === "keyboard") { + this._offset = OFFSET_KEYBOARD; + } else { + this._offset = OFFSET_MOUSE; + } + } else { // Open Programmatically + if (type == "touch") { + this._offset = OFFSET_PROGRAMMATIC_TOUCH; + } else { + this._offset = OFFSET_PROGRAMMATIC_NONTOUCH; + } + } + + clearTimeout(this._delayTimer); + clearTimeout(this._hideDelayTimer); + + // Set the delay time + var delay = this._decideOnDelay(type); + if (delay > 0) { + var that = this; + this._delayTimer = setTimeout(function () { + that._showTooltip(type); + }, delay); + } else { + this._showTooltip(type); + } + }, + + _onDismiss: function () { + // Set the dismiss flag so that we don't miss dismiss events + this._shouldDismiss = true; + + // If the tooltip is already dismissed, ignore the current event + if (!this._isShown) { + return; + } + + this._isShown = false; + + // Reset tooltip state + this._showTrigger = "mouse"; + + if (this._domElement) { + this._raiseEvent("beforeclose"); + if (this._useAnimation) { + animation.fadeOut(this._domElement) + .then(this._onHideAnimationEnd.bind(this)); + } else { + this._onHideAnimationEnd(); + } + } else { + this._raiseEvent("beforeclose"); + this._raiseEvent("closed"); + } + } + }) + }); + +// Tooltip support for "on" properties +WinJS.Class.mix(WinJS.UI.Tooltip, WinJS.Utilities.createEventProperties( + "beforeopen", + "opened", + "beforeclose", + "closed")); + +})(this, WinJS); +// ViewBox control +(function viewboxInit(global, undefined) { + "use strict"; + + var strings = { + get invalidViewBoxChildren() { return WinJS.Resources._getWinJSString("ui/invalidViewBoxChildren").value; }, + }; + + 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.srcElement) { + onresize(ev.srcElement.winControl); + } + } + + function onresizeSizer(ev) { + if (ev.srcElement) { + onresize(ev.srcElement.parentElement.winControl); + } + } + + WinJS.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: WinJS.Class.define( + function ViewBox_ctor(element, options) { + /// + /// 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._element = element || document.createElement("div"); + var box = this.element; + box.winControl = this; + WinJS.Utilities.addClass(box, "win-viewbox"); + this.forceLayout(); + }, + { + _sizer: null, + _element: null, + + /// + element: { + get: function () { return this._element; } + }, + + _rtl: { + get: function () { + return window.getComputedStyle(this.element).direction === "rtl"; + } + }, + + _initialize: function () { + var box = this.element; + if (box.firstElementChild !== this._sizer) { + if (WinJS.validation) { + if (box.childElementCount != 1) { + throw new WinJS.ErrorFromName("WinJS.UI.ViewBox.InvalidChildren", strings.invalidViewBoxChildren); + } + } + if (this._sizer) { + this._sizer.onresize = null; + } + var sizer = box.firstElementChild; + this._sizer = sizer; + if (sizer) { + box.attachEvent("onresize", onresizeBox); + sizer.attachEvent("onresize", onresizeSizer); + } + if (box.clientWidth === 0 && box.clientHeight === 0) { + var that = this; + setImmediate(function () { + that._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; + this._sizer.style["transform"] = "translate(" + transX + "px," + transY + "px) scale(" + mRatio + ")"; + this._sizer.style["transform-origin"] = this._rtl ? "top right" : "top left"; + } + }, + + forceLayout: function () { + this._initialize(); + this._updateLayout(); + } + } + ) + }); + + WinJS.Class.mix(WinJS.UI.ViewBox, WinJS.UI.DOMEventMixin); + +}(this)); + diff --git a/shared/html/theme/dark/default.css b/shared/html/theme/dark/default.css new file mode 100644 index 0000000..6e93c18 --- /dev/null +++ b/shared/html/theme/dark/default.css @@ -0,0 +1,35 @@ +* { + transition: all 0.5s cubic-bezier(0.1, 0.9, 0.2, 1); +} + +.page { + background-color: #1d1d1d; +} + +.page.splash { + background-color: #001629; +} + +.page.splash .content, +.page.splash .content * { + transition: none; +} + +.page .content.loading, +.page .content.main, +.page .progress, +.page .reason p, +.page .controls .checkbox { + color: white; +} + +.page .content.main .pkgapplabel { + color: #299fff; +} + +.page .reason textarea { + background-color: rgba (255, 255, 255, 0); + color: white; + border: 2px solid white; + box-sizing: border-box; +} \ No newline at end of file diff --git a/shared/html/theme/light/default.css b/shared/html/theme/light/default.css new file mode 100644 index 0000000..a1d5a35 --- /dev/null +++ b/shared/html/theme/light/default.css @@ -0,0 +1,19 @@ +.page { + background-color: #F3F3F3; +} + +.page.splash { + background-color: #0078d7; +} + +.page .content.loading, +.page .content.main, +.page .progress, +.page .reason p, +.page .controls .checkbox { + color: black; +} + +.page .content.main .pkgapplabel { + color: #0078d7; +} \ No newline at end of file